Skip to content

Commit 2fd3252

Browse files
committed
added: enhance landmark handling by including landmark selector and improving highlight functionality
1 parent fb4d1e7 commit 2fd3252

3 files changed

Lines changed: 293 additions & 11 deletions

File tree

admin/class-ajax.php

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -305,7 +305,7 @@ function ( $a, $b ) {
305305

306306
foreach ( $rules as $rule ) {
307307
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Using direct query for interacting with custom database, safe variable used for table name, caching not required for one time operation.
308-
$results = $wpdb->get_results( $wpdb->prepare( 'SELECT id, postid, object, ruletype, ignre, ignre_user, ignre_date, ignre_comment, ignre_global, landmark FROM %i where postid = %d and rule = %s and siteid = %d', $table_name, $postid, $rule['slug'], $siteid ), ARRAY_A );
308+
$results = $wpdb->get_results( $wpdb->prepare( 'SELECT id, postid, object, ruletype, ignre, ignre_user, ignre_date, ignre_comment, ignre_global, landmark, landmark_selector FROM %i where postid = %d and rule = %s and siteid = %d', $table_name, $postid, $rule['slug'], $siteid ), ARRAY_A );
309309
$count_classes = ( 'error' === $rule['rule_type'] ) ? ' edac-details-rule-count-error' : ' edac-details-rule-count-warning';
310310
$count_classes .= ( 0 !== $rule['count'] ) ? ' active' : '';
311311

@@ -468,8 +468,27 @@ function ( $a, $b ) {
468468

469469
$html .= '<div class="edac-details-rule-records-record-cell edac-details-rule-records-record-landmark">';
470470

471-
$landmark = isset( $row['landmark'] ) ? esc_html( $row['landmark'] ) : '';
472-
$html .= $landmark ? $landmark : '<span class="edac-no-landmark">—</span>';
471+
$landmark = isset( $row['landmark'] ) ? esc_html( $row['landmark'] ) : '';
472+
$landmark_selector = isset( $row['landmark_selector'] ) ? $row['landmark_selector'] : '';
473+
474+
if ( $landmark && $landmark_selector ) {
475+
$landmark_url = add_query_arg(
476+
[
477+
'edac_landmark' => base64_encode( $landmark_selector ),
478+
'edac_nonce' => wp_create_nonce( 'edac_highlight' ),
479+
],
480+
get_the_permalink( $postid )
481+
);
482+
483+
// translators: %s is the landmark type (e.g., "Header", "Navigation", "Main").
484+
$landmark_aria_label = sprintf( __( 'View %s landmark on website, opens a new window', 'accessibility-checker' ), $landmark );
485+
// translators: %s is the landmark type (e.g., "Header", "Navigation", "Main").
486+
$html .= '<a href="' . $landmark_url . '" class="edac-details-rule-records-record-landmark-link" target="_blank" aria-label="' . esc_attr( $landmark_aria_label ) . '" title="' . esc_attr( sprintf( __( 'Click to highlight the %s landmark on the page', 'accessibility-checker' ), $landmark ) ) . '">' . $landmark . '</a>';
487+
} elseif ( $landmark ) {
488+
$html .= $landmark;
489+
} else {
490+
$html .= '<span class="edac-no-landmark">—</span>';
491+
}
473492

474493
$html .= '</div>';
475494

src/frontendHighlighterApp/index.js

Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ class AccessibilityCheckerHighlight {
3232
this.fixes = null;
3333
this.currentButtonIndex = null;
3434
this.urlParameter = this.get_url_parameter( 'edac' );
35+
this.landmarkParameter = this.get_url_parameter( 'edac_landmark' );
3536
this.currentIssueStatus = null;
3637
this.tooltips = [];
3738
this.panelControlsFocusTrap = createFocusTrap( '#' + this.panelControls.id, {
@@ -97,6 +98,8 @@ class AccessibilityCheckerHighlight {
9798
// Open panel if a URL parameter exists
9899
if ( this.urlParameter ) {
99100
this.panelOpen( this.urlParameter );
101+
} else if ( this.landmarkParameter ) {
102+
this.highlightLandmark( this.landmarkParameter );
100103
}
101104
}
102105

@@ -242,6 +245,9 @@ class AccessibilityCheckerHighlight {
242245
buttons.forEach( ( button ) => {
243246
button.remove();
244247
} );
248+
249+
// Clean up any landmark labels
250+
this.removeLandmarkLabels();
245251
}
246252

247253
/**
@@ -577,6 +583,9 @@ class AccessibilityCheckerHighlight {
577583
selectedElement.removeAttribute( 'class' );
578584
}
579585
} );
586+
587+
// Clean up any landmark labels when highlights are removed
588+
this.removeLandmarkLabels();
580589
};
581590

582591
/**
@@ -889,6 +898,189 @@ class AccessibilityCheckerHighlight {
889898

890899
div.textContent = textContent;
891900
}
901+
902+
/**
903+
* This function highlights a landmark based on the selector.
904+
* @param {string} encodedSelector Base64-encoded CSS selector for the landmark
905+
*/
906+
highlightLandmark( encodedSelector ) {
907+
try {
908+
// Decode the base64 selector
909+
const selector = atob( encodedSelector );
910+
911+
// Find the landmark element using multiple strategies
912+
let landmarkElement = null;
913+
914+
try {
915+
// Try the original selector first
916+
landmarkElement = document.querySelector( selector );
917+
} catch ( error ) {
918+
// Selector might be invalid, try fallbacks
919+
}
920+
921+
// If original selector failed, try some fallback strategies
922+
if ( ! landmarkElement ) {
923+
// Try common landmark selectors as fallbacks
924+
const fallbackSelectors = [
925+
// Remove complex pseudo-selectors and try simpler versions
926+
selector.replace( /:nth-child\(\d+\)/g, '' ).replace( /\s+>\s+/g, ' ' ),
927+
// Try just the last part of the selector
928+
selector.split( ' > ' ).pop(),
929+
// Try without classes
930+
selector.replace( /\.[^:\s>]+/g, '' ),
931+
];
932+
933+
for ( const fallback of fallbackSelectors ) {
934+
if ( fallback && fallback.trim() ) {
935+
try {
936+
landmarkElement = document.querySelector( fallback.trim() );
937+
if ( landmarkElement ) {
938+
break;
939+
}
940+
} catch ( e ) {
941+
// Continue to next fallback
942+
}
943+
}
944+
}
945+
}
946+
947+
if ( landmarkElement ) {
948+
// Clean up any existing landmark labels first
949+
this.removeLandmarkLabels();
950+
951+
// Add highlighting styles
952+
landmarkElement.classList.add( 'edac-highlight-element-selected' );
953+
landmarkElement.classList.add( 'edac-landmark-highlight' );
954+
955+
// Create and add landmark type label
956+
const landmarkType = this.getLandmarkType( landmarkElement );
957+
const landmarkLabel = document.createElement( 'div' );
958+
landmarkLabel.classList.add( 'edac-landmark-label' );
959+
landmarkLabel.textContent = `Landmark: ${ landmarkType }`;
960+
landmarkLabel.setAttribute( 'aria-hidden', 'true' );
961+
landmarkLabel.style.cssText = `
962+
position: absolute;
963+
background: #072446;
964+
color: white;
965+
padding: 4px 8px;
966+
font-size: 12px;
967+
font-weight: bold;
968+
border-radius: 3px;
969+
z-index: 999999;
970+
pointer-events: none;
971+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
972+
line-height: 1;
973+
box-shadow: 0 2px 4px rgba(0,0,0,0.2);
974+
`;
975+
976+
// Position the label inside the top-left corner of the landmark
977+
const rect = landmarkElement.getBoundingClientRect();
978+
landmarkLabel.style.left = ( rect.left + window.scrollX - 0 ) + 'px'; // 15px inside from left edge
979+
landmarkLabel.style.top = ( rect.top + window.scrollY - 0 ) + 'px'; // 15px inside from top edge
980+
981+
// Add label to the page
982+
document.body.appendChild( landmarkLabel );
983+
984+
// Store reference for cleanup
985+
landmarkElement.setAttribute( 'data-edac-landmark-label-id', Date.now() );
986+
landmarkLabel.setAttribute( 'data-edac-landmark-for', landmarkElement.getAttribute( 'data-edac-landmark-label-id' ) );
987+
988+
// Adjust for small elements
989+
if ( landmarkElement.offsetWidth < 20 ) {
990+
landmarkElement.classList.add( 'edac-highlight-element-selected-min-width' );
991+
}
992+
993+
if ( landmarkElement.offsetHeight < 5 ) {
994+
landmarkElement.classList.add( 'edac-highlight-element-selected-min-height' );
995+
}
996+
997+
// Scroll to the landmark
998+
landmarkElement.scrollIntoView( { block: 'center', behavior: 'smooth' } );
999+
1000+
} else {
1001+
// Landmark element not found - silently fail
1002+
}
1003+
} catch ( error ) {
1004+
// Error highlighting landmark - silently fail
1005+
}
1006+
}
1007+
1008+
/**
1009+
* Determines the landmark type of an element
1010+
* @param {HTMLElement} element The element to check
1011+
* @return {string} The landmark type (e.g., "Header", "Navigation", "Main")
1012+
*/
1013+
getLandmarkType( element ) {
1014+
// Check explicit ARIA role first
1015+
const role = element.getAttribute( 'role' );
1016+
if ( role ) {
1017+
switch ( role.toLowerCase() ) {
1018+
case 'banner':
1019+
return 'Header';
1020+
case 'navigation':
1021+
return 'Navigation';
1022+
case 'main':
1023+
return 'Main';
1024+
case 'complementary':
1025+
return 'Complementary';
1026+
case 'contentinfo':
1027+
return 'Footer';
1028+
case 'search':
1029+
return 'Search';
1030+
case 'form':
1031+
return 'Form';
1032+
case 'region':
1033+
return 'Region';
1034+
default:
1035+
return role.charAt( 0 ).toUpperCase() + role.slice( 1 );
1036+
}
1037+
}
1038+
1039+
// Check semantic HTML elements
1040+
const tagName = element.tagName.toLowerCase();
1041+
switch ( tagName ) {
1042+
case 'header':
1043+
return 'Header';
1044+
case 'nav':
1045+
return 'Navigation';
1046+
case 'main':
1047+
return 'Main';
1048+
case 'aside':
1049+
return 'Complementary';
1050+
case 'footer':
1051+
return 'Footer';
1052+
case 'section':
1053+
// Check if section has accessible name
1054+
const hasAccessibleName = element.getAttribute( 'aria-label' ) ||
1055+
element.getAttribute( 'aria-labelledby' ) ||
1056+
element.querySelector( 'h1, h2, h3, h4, h5, h6' );
1057+
return hasAccessibleName ? 'Region' : 'Section';
1058+
case 'form':
1059+
// Check if form has accessible name
1060+
const formHasAccessibleName = element.getAttribute( 'aria-label' ) ||
1061+
element.getAttribute( 'aria-labelledby' );
1062+
return formHasAccessibleName ? 'Form' : 'Form (unlabeled)';
1063+
default:
1064+
return 'Landmark';
1065+
}
1066+
}
1067+
1068+
/**
1069+
* Remove all landmark labels from the page
1070+
*/
1071+
removeLandmarkLabels() {
1072+
const landmarkLabels = document.querySelectorAll( '.edac-landmark-label' );
1073+
landmarkLabels.forEach( ( label ) => {
1074+
label.remove();
1075+
} );
1076+
1077+
// Remove landmark highlight classes
1078+
const landmarkHighlights = document.querySelectorAll( '.edac-landmark-highlight' );
1079+
landmarkHighlights.forEach( ( element ) => {
1080+
element.classList.remove( 'edac-landmark-highlight' );
1081+
element.removeAttribute( 'data-edac-landmark-label-id' );
1082+
} );
1083+
}
8921084
}
8931085

8941086
// Some systems (Cloudflare Rocket Loader) defers scripts for performance but that can

src/pageScanner/index.js

Lines changed: 79 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -72,20 +72,91 @@ function getElementSelector( element ) {
7272
if ( ! element ) {
7373
return null;
7474
}
75+
76+
// Use ID if available (most reliable)
7577
if ( element.id ) {
7678
return `#${ element.id }`;
7779
}
80+
81+
// For landmark elements, try to use semantic selectors first
82+
const tagName = element.tagName.toLowerCase();
83+
84+
// For main element, use tag selector if it's unique
85+
if ( tagName === 'main' ) {
86+
const mainElements = document.querySelectorAll( 'main' );
87+
if ( mainElements.length === 1 ) {
88+
return 'main';
89+
}
90+
}
91+
92+
// For header/footer, check if they're direct children of body
93+
if ( ( tagName === 'header' || tagName === 'footer' ) && element.parentElement === document.body ) {
94+
return tagName;
95+
}
96+
97+
// For nav elements, try role-based selector first
98+
if ( tagName === 'nav' || element.getAttribute( 'role' ) === 'navigation' ) {
99+
const navElements = document.querySelectorAll( 'nav, [role="navigation"]' );
100+
if ( navElements.length === 1 ) {
101+
return tagName === 'nav' ? 'nav' : '[role="navigation"]';
102+
}
103+
// If multiple, try to use aria-label or other identifying attributes
104+
if ( element.hasAttribute( 'aria-label' ) ) {
105+
const ariaLabel = element.getAttribute( 'aria-label' );
106+
return `${ tagName === 'nav' ? 'nav' : '[role="navigation"]' }[aria-label="${ ariaLabel }"]`;
107+
}
108+
}
109+
110+
// For other landmark roles, use role selector if unique
111+
const role = element.getAttribute( 'role' );
112+
if ( role && LANDMARK_ROLES.includes( role ) ) {
113+
const roleElements = document.querySelectorAll( `[role="${ role }"]` );
114+
if ( roleElements.length === 1 ) {
115+
return `[role="${ role }"]`;
116+
}
117+
// If multiple, try to use aria-label
118+
if ( element.hasAttribute( 'aria-label' ) ) {
119+
const ariaLabel = element.getAttribute( 'aria-label' );
120+
return `[role="${ role }"][aria-label="${ ariaLabel }"]`;
121+
}
122+
}
123+
124+
// Fallback to path-based selector (simplified)
78125
const path = [];
79-
while ( element && element.nodeType === Node.ELEMENT_NODE && element !== document.body ) {
80-
let selector = element.nodeName.toLowerCase();
81-
if ( element.className ) {
82-
const classes = element.className.trim().split( /\s+/ ).join( '.' );
83-
selector += `.${ classes }`;
126+
let current = element;
127+
while ( current && current.nodeType === Node.ELEMENT_NODE && current !== document.body ) {
128+
let selector = current.nodeName.toLowerCase();
129+
130+
// Add ID if available
131+
if ( current.id ) {
132+
selector = `#${ current.id }`;
133+
path.unshift( selector );
134+
break; // Stop here since ID is unique
135+
}
136+
137+
// Add stable classes (avoid dynamic/generated classes)
138+
if ( current.className ) {
139+
const classes = current.className.trim().split( /\s+/ )
140+
.filter( ( cls ) => ! cls.match( /^(wp-|js-|css-|generated-|dynamic-)/ ) ) // Filter out common dynamic classes
141+
.slice( 0, 2 ); // Limit to first 2 classes for stability
142+
if ( classes.length > 0 ) {
143+
selector += `.${ classes.join( '.' ) }`;
144+
}
84145
}
85-
const siblingIndex = Array.from( element.parentNode.children ).indexOf( element ) + 1;
86-
selector += `:nth-child(${ siblingIndex })`;
146+
147+
// Only add nth-child as last resort and only if element has no other identifying features
148+
if ( ! current.id && ! current.className ) {
149+
const siblingIndex = Array.from( current.parentNode.children ).indexOf( current ) + 1;
150+
selector += `:nth-child(${ siblingIndex })`;
151+
}
152+
87153
path.unshift( selector );
88-
element = element.parentElement;
154+
current = current.parentElement;
155+
156+
// Limit path depth to avoid overly complex selectors
157+
if ( path.length >= 4 ) {
158+
break;
159+
}
89160
}
90161
return path.length ? path.join( ' > ' ) : null;
91162
}

0 commit comments

Comments
 (0)