@surea11y/core 1.4.1 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +47 -7
- package/README.md +19 -3
- package/docs/API_STABILITY.md +2 -2
- package/docs/ENGINE_OPTIONS.md +14 -10
- package/docs/I18N.md +176 -20
- package/docs/INTEGRATION.md +28 -6
- package/docs/LIMITATIONS.md +1 -1
- package/docs/OUTPUT_SCHEMA.md +13 -3
- package/docs/REPORT.md +2 -0
- package/docs/RULE_AUTHORING.md +53 -0
- package/docs/TROUBLESHOOTING.md +2 -2
- package/package.json +6 -1
- package/src/checks/automatic/aria-allowed-attr.js +27 -30
- package/src/checks/automatic/aria-allowed-role.js +14 -16
- package/src/checks/automatic/aria-braille-equivalent.js +17 -19
- package/src/checks/automatic/aria-conditional-attr.js +17 -19
- package/src/checks/automatic/aria-deprecated-role.js +62 -49
- package/src/checks/automatic/aria-hidden-body.js +2 -9
- package/src/checks/automatic/aria-hidden-focus.js +99 -18
- package/src/checks/automatic/aria-prohibited-attr.js +54 -55
- package/src/checks/automatic/aria-prohibited-children.js +26 -26
- package/src/checks/automatic/aria-required-attr.js +14 -17
- package/src/checks/automatic/aria-required-children.js +17 -20
- package/src/checks/automatic/aria-required-parent.js +17 -20
- package/src/checks/automatic/aria-roles-valid.js +37 -23
- package/src/checks/automatic/aria-valid-attr-value.js +18 -21
- package/src/checks/automatic/aria-valid-attr.js +14 -17
- package/src/checks/automatic/autocomplete-valid.js +15 -17
- package/src/checks/automatic/avoid-inline-spacing.js +14 -16
- package/src/checks/automatic/binary-control-name-present.js +19 -21
- package/src/checks/automatic/button-name-present.js +23 -28
- package/src/checks/automatic/combobox-name-present.js +15 -17
- package/src/checks/automatic/css-orientation-lock.js +22 -22
- package/src/checks/automatic/definition-list-children-valid.js +18 -21
- package/src/checks/automatic/deprecated-elements-not-used.js +14 -16
- package/src/checks/automatic/dialog-name-present.js +16 -18
- package/src/checks/automatic/dlitem-parent-valid.js +15 -17
- package/src/checks/automatic/duplicate-id-aria.js +45 -37
- package/src/checks/automatic/form-control-single-label.js +38 -40
- package/src/checks/automatic/html-xml-lang-mismatch.js +2 -9
- package/src/checks/automatic/iframe-focusable-content.js +29 -31
- package/src/checks/automatic/iframe-name-present.js +15 -17
- package/src/checks/automatic/iframe-title-unique.js +18 -23
- package/src/checks/automatic/label-in-name.js +31 -36
- package/src/checks/automatic/link-in-text-block.js +19 -21
- package/src/checks/automatic/link-name-present.js +25 -30
- package/src/checks/automatic/list-children-valid.js +15 -17
- package/src/checks/automatic/listbox-name-present.js +15 -17
- package/src/checks/automatic/listitem-parent-valid.js +14 -17
- package/src/checks/automatic/menuitem-name-present.js +16 -18
- package/src/checks/automatic/meta-refresh-no-exceptions.js +15 -18
- package/src/checks/automatic/meta-refresh-timing-absent.js +14 -17
- package/src/checks/automatic/meta-viewport-zoom-enabled.js +14 -17
- package/src/checks/automatic/meter-name-present.js +16 -18
- package/src/checks/automatic/nested-interactive-controls-absent.js +17 -19
- package/src/checks/automatic/option-name-present.js +16 -18
- package/src/checks/automatic/progressbar-name-present.js +19 -21
- package/src/checks/automatic/searchbox-name-present.js +19 -17
- package/src/checks/automatic/server-side-image-map-absent.js +15 -18
- package/src/checks/automatic/slider-name-present.js +15 -17
- package/src/checks/automatic/spinbutton-name-present.js +19 -17
- package/src/checks/automatic/summary-name-present.js +16 -18
- package/src/checks/automatic/tab-name-present.js +16 -18
- package/src/checks/automatic/table-headers-attr-valid.js +15 -17
- package/src/checks/automatic/table-th-has-data-cells.js +15 -19
- package/src/checks/automatic/target-size-minimum.js +104 -81
- package/src/checks/automatic/td-has-header.js +15 -20
- package/src/checks/automatic/textbox-name-present.js +15 -17
- package/src/checks/automatic/tooltip-name-present.js +16 -18
- package/src/checks/automatic/treeitem-name-present.js +16 -18
- package/src/checks/automatic/valid-lang.js +15 -17
- package/src/checks/manual/accesskeys-manual.js +18 -19
- package/src/checks/manual/aria-checked-state-mismatch-manual.js +19 -22
- package/src/checks/manual/bypass-blocks-present-manual.js +4 -12
- package/src/checks/manual/empty-heading-manual.js +15 -17
- package/src/checks/manual/empty-table-header-manual.js +27 -30
- package/src/checks/manual/form-control-programmatic-label-quality-manual.js +2 -9
- package/src/checks/manual/heading-order-manual.js +17 -22
- package/src/checks/manual/image-redundant-alt-manual.js +14 -17
- package/src/checks/manual/label-title-only-manual.js +15 -17
- package/src/checks/manual/landmark-banner-is-top-level-manual.js +14 -17
- package/src/checks/manual/landmark-contentinfo-is-top-level-manual.js +14 -17
- package/src/checks/manual/landmark-main-is-top-level-manual.js +14 -17
- package/src/checks/manual/landmark-no-duplicate-banner-manual.js +2 -7
- package/src/checks/manual/landmark-no-duplicate-contentinfo-manual.js +2 -7
- package/src/checks/manual/landmark-no-duplicate-main-manual.js +2 -7
- package/src/checks/manual/landmark-one-main-manual.js +2 -9
- package/src/checks/manual/landmark-unique-manual.js +22 -27
- package/src/checks/manual/link-name-quality-manual.js +15 -17
- package/src/checks/manual/meta-viewport-large-manual.js +14 -17
- package/src/checks/manual/mouse-only-event-handlers-manual.js +17 -19
- package/src/checks/manual/page-has-heading-one-manual.js +2 -9
- package/src/checks/manual/presentation-role-conflict-manual.js +19 -21
- package/src/checks/manual/region-manual.js +13 -6
- package/src/checks/manual/scope-attr-valid-manual.js +14 -17
- package/src/checks/manual/skip-link-manual.js +39 -47
- package/src/checks/manual/tabindex-manual.js +14 -17
- package/src/checks/manual/table-duplicate-name-manual.js +14 -17
- package/src/core.js +4203 -3604
- package/src/report.js +14 -0
- package/surea11y.browser.js +1960 -3671
- package/surea11y.i18n.de.js +22 -0
- package/surea11y.i18n.es.js +22 -0
- package/surea11y.i18n.fr.js +22 -0
|
@@ -146,6 +146,54 @@ function runInPage(ctx) {
|
|
|
146
146
|
return false;
|
|
147
147
|
}
|
|
148
148
|
|
|
149
|
+
// An open modal is expected to trap focus, so a focusable background behind
|
|
150
|
+
// it may be unreachable; that can't be proven statically, so the finding is
|
|
151
|
+
// downgraded to cantTell instead of fail. A modal that is display:none is
|
|
152
|
+
// not open, so it does not count.
|
|
153
|
+
function isRenderedForModal(node) {
|
|
154
|
+
if (!node) return false;
|
|
155
|
+
if (!isDomVisibleEligible) return true;
|
|
156
|
+
try {
|
|
157
|
+
const vis = isDomVisibleEligible(node, ctx, {
|
|
158
|
+
visibilityMode: 'styleOnly',
|
|
159
|
+
disableGeometry: true
|
|
160
|
+
});
|
|
161
|
+
if (vis && vis.eligible === false) return false;
|
|
162
|
+
} catch {
|
|
163
|
+
// treat as rendered
|
|
164
|
+
}
|
|
165
|
+
return true;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Native <dialog>, aria-modal="true", or a dialog/alertdialog role, so
|
|
169
|
+
// libraries that leave aria-modal off (e.g. Angular Material defaults to
|
|
170
|
+
// aria-modal="false") still count as an open modal.
|
|
171
|
+
function collectOpenModalCandidates() {
|
|
172
|
+
const nodes = qAll('dialog[open],[aria-modal="true"],[role="dialog"],[role="alertdialog"]');
|
|
173
|
+
const out = [];
|
|
174
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
175
|
+
const n = nodes[i];
|
|
176
|
+
if (!n || !n.getAttribute) continue;
|
|
177
|
+
if (!isRenderedForModal(n)) continue;
|
|
178
|
+
out.push(n);
|
|
179
|
+
}
|
|
180
|
+
return out;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Only a modal in a separate subtree is the "hidden background behind an
|
|
184
|
+
// open dialog" case. A modal inside the aria-hidden subtree (or an
|
|
185
|
+
// aria-hidden root inside the modal) is a genuine defect, so keep it a fail.
|
|
186
|
+
function hasSeparateOpenModal(rootEl, candidates) {
|
|
187
|
+
for (let i = 0; i < candidates.length; i++) {
|
|
188
|
+
const m = candidates[i];
|
|
189
|
+
if (!m || m === rootEl) continue;
|
|
190
|
+
if (isWithinComposedSubtree(m, rootEl)) continue;
|
|
191
|
+
if (isWithinComposedSubtree(rootEl, m)) continue;
|
|
192
|
+
return true;
|
|
193
|
+
}
|
|
194
|
+
return false;
|
|
195
|
+
}
|
|
196
|
+
|
|
149
197
|
function getDeepActiveElement() {
|
|
150
198
|
let cur = document && document.activeElement ? document.activeElement : null;
|
|
151
199
|
let guard = 0;
|
|
@@ -713,6 +761,8 @@ function runInPage(ctx) {
|
|
|
713
761
|
entry.rootIsFocusable = isActuallyFocusable(el);
|
|
714
762
|
}
|
|
715
763
|
|
|
764
|
+
const modalCandidates = collectOpenModalCandidates();
|
|
765
|
+
|
|
716
766
|
// 3) Report one occurrence per aria-hidden root that contains focusable content.
|
|
717
767
|
const failOccurrences = [];
|
|
718
768
|
const uncertainOccurrences = [];
|
|
@@ -765,24 +815,56 @@ function runInPage(ctx) {
|
|
|
765
815
|
: `aria-hidden ${tagName} is focusable (${totalFocusable} focusable element(s)).`
|
|
766
816
|
: `aria-hidden ${tagName} contains ${totalFocusable} focusable element(s).`;
|
|
767
817
|
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
818
|
+
// A modal takes precedence over the redirect probe; skip the probe when a
|
|
819
|
+
// modal already explains the background.
|
|
820
|
+
const modalOpenOutside = modalCandidates.length
|
|
821
|
+
? hasSeparateOpenModal(el, modalCandidates)
|
|
822
|
+
: false;
|
|
823
|
+
|
|
824
|
+
const runtimeProbe = modalOpenOutside ? null : probeImmediateFocusRedirect(entry);
|
|
825
|
+
const downgradedByRedirect = !!(runtimeProbe && runtimeProbe.redirected);
|
|
826
|
+
const downgradedByModal = modalOpenOutside;
|
|
827
|
+
const downgradedToCantTell = downgradedByModal || downgradedByRedirect;
|
|
828
|
+
|
|
829
|
+
const cantTellRedirectSummary = `aria-hidden ${tagName} received focus but focus moved immediately to another element. Verify sentinel/focus-trap behavior.`;
|
|
830
|
+
const cantTellModalSummary = `aria-hidden ${tagName} contains ${totalFocusable} focusable element(s) while a modal dialog is open. If the modal keeps keyboard focus trapped they may be unreachable; verify focus cannot land on them.`;
|
|
831
|
+
|
|
832
|
+
let occSummary;
|
|
833
|
+
let occHint;
|
|
834
|
+
let occSummaryKey;
|
|
835
|
+
let occHintKey;
|
|
836
|
+
let occReasonCode;
|
|
837
|
+
|
|
838
|
+
if (downgradedByModal) {
|
|
839
|
+
occSummary = cantTellModalSummary;
|
|
840
|
+
occHint =
|
|
841
|
+
'A modal dialog appears to be open. Prefer making the background inert (or a native <dialog> opened with showModal()) so it leaves the tab order, then verify keyboard focus stays within the dialog.';
|
|
842
|
+
occSummaryKey = 'ariaHidden_focus_summary_cantTell_modal';
|
|
843
|
+
occHintKey = 'ariaHidden_focus_hint_cantTell_modal';
|
|
844
|
+
occReasonCode = 'ariaHiddenFocusable_modalOpen_needsReview';
|
|
845
|
+
} else if (downgradedByRedirect) {
|
|
846
|
+
occSummary = cantTellRedirectSummary;
|
|
847
|
+
occHint =
|
|
848
|
+
'Verify this is an intentional focus sentinel/focus-trap handoff and that keyboard users never remain on hidden focus targets.';
|
|
849
|
+
occSummaryKey = 'ariaHidden_focus_summary_cantTell_redirect';
|
|
850
|
+
occHintKey = 'ariaHidden_focus_hint_cantTell_redirect';
|
|
851
|
+
occReasonCode = 'ariaHiddenFocusable_runtimeRedirect_needsReview';
|
|
852
|
+
} else {
|
|
853
|
+
occSummary = summaryText;
|
|
854
|
+
occHint =
|
|
855
|
+
'Remove focusability from descendants or remove aria-hidden; ensure focus and accessibility trees stay aligned.';
|
|
856
|
+
occSummaryKey = summaryKey;
|
|
857
|
+
occHintKey = 'ariaHidden_focus_hint_fail';
|
|
858
|
+
occReasonCode = reasonCode;
|
|
859
|
+
}
|
|
772
860
|
|
|
773
861
|
const baseOccurrence = {
|
|
774
|
-
summary:
|
|
775
|
-
hint:
|
|
776
|
-
? 'Verify this is an intentional focus sentinel/focus-trap handoff and that keyboard users never remain on hidden focus targets.'
|
|
777
|
-
: 'Remove focusability from descendants or remove aria-hidden; ensure focus and accessibility trees stay aligned.',
|
|
862
|
+
summary: occSummary,
|
|
863
|
+
hint: occHint,
|
|
778
864
|
occurrenceOutcome: downgradedToCantTell ? 'cantTell' : 'fail',
|
|
779
865
|
i18n: {
|
|
780
|
-
summaryKey:
|
|
781
|
-
|
|
782
|
-
: summaryKey,
|
|
783
|
-
hintKey: downgradedToCantTell
|
|
784
|
-
? 'ariaHidden_focus_hint_cantTell_redirect'
|
|
785
|
-
: 'ariaHidden_focus_hint_fail',
|
|
866
|
+
summaryKey: occSummaryKey,
|
|
867
|
+
hintKey: occHintKey,
|
|
786
868
|
params: {
|
|
787
869
|
element: tagName,
|
|
788
870
|
focusableCount: String(totalFocusable),
|
|
@@ -795,15 +877,14 @@ function runInPage(ctx) {
|
|
|
795
877
|
},
|
|
796
878
|
data: {
|
|
797
879
|
details: {
|
|
798
|
-
reasonCode:
|
|
799
|
-
? 'ariaHiddenFocusable_runtimeRedirect_needsReview'
|
|
800
|
-
: reasonCode,
|
|
880
|
+
reasonCode: occReasonCode,
|
|
801
881
|
metrics: {
|
|
802
882
|
focusableTotal: totalFocusable,
|
|
803
883
|
focusableDescendants: descendantFocusable,
|
|
804
884
|
rootIsFocusable: selfFocusable,
|
|
805
885
|
offendersCaptured: entry.offenders.length,
|
|
806
|
-
visibilityHints: hintsArr.slice(0)
|
|
886
|
+
visibilityHints: hintsArr.slice(0),
|
|
887
|
+
modalOpen: !!downgradedByModal
|
|
807
888
|
},
|
|
808
889
|
runtimeProbe: runtimeProbe || null,
|
|
809
890
|
offenders: entry.offenders.slice(0)
|
|
@@ -167,25 +167,22 @@ function runInPage(ctx) {
|
|
|
167
167
|
|
|
168
168
|
if (!present.length) continue;
|
|
169
169
|
|
|
170
|
-
const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
|
|
171
|
-
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
|
|
172
|
-
|
|
173
170
|
for (const attr of present) {
|
|
174
|
-
failOccurrences.push(
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
}
|
|
188
|
-
|
|
171
|
+
failOccurrences.push(
|
|
172
|
+
helpers.reportOccurrence(el, {
|
|
173
|
+
occurrenceOutcome: 'fail',
|
|
174
|
+
summary: 'This attribute is prohibited on this element’s role.',
|
|
175
|
+
hint: 'Remove this attribute; this role must not carry an accessible name.',
|
|
176
|
+
i18n: {
|
|
177
|
+
summaryKey: 'ariaProhibitedAttr_summary_fail',
|
|
178
|
+
hintKey: 'ariaProhibitedAttr_hint_fail',
|
|
179
|
+
params: { attr, role }
|
|
180
|
+
},
|
|
181
|
+
data: {
|
|
182
|
+
details: { reasonCode: 'ARIA_ATTR_PROHIBITED', attr, role }
|
|
183
|
+
}
|
|
184
|
+
})
|
|
185
|
+
);
|
|
189
186
|
}
|
|
190
187
|
}
|
|
191
188
|
|
|
@@ -361,47 +358,49 @@ function runInPage(ctx) {
|
|
|
361
358
|
String(nameInfo.value || '').trim() !== ''
|
|
362
359
|
);
|
|
363
360
|
|
|
364
|
-
const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
|
|
365
|
-
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
|
|
366
|
-
|
|
367
361
|
for (const attr of present) {
|
|
368
362
|
if (hasContentFallback) {
|
|
369
|
-
cantTellOccurrences.push(
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
363
|
+
cantTellOccurrences.push(
|
|
364
|
+
helpers.reportOccurrence(el, {
|
|
365
|
+
occurrenceOutcome: 'cantTell',
|
|
366
|
+
summary: `This ${tag} has no role, so ${attr} may not be exposed as its accessible name by assistive technology — but the element's own content already provides one.`,
|
|
367
|
+
hint: 'Verify whether the existing text content already serves as this element’s label; if so the naming attribute is redundant, otherwise give the element a role that supports naming (e.g. role="img").',
|
|
368
|
+
i18n: {
|
|
369
|
+
summaryKey: 'ariaProhibitedAttr_summary_cantTell_roleless',
|
|
370
|
+
hintKey: 'ariaProhibitedAttr_hint_cantTell_roleless',
|
|
371
|
+
params: { attr, element: tag }
|
|
372
|
+
},
|
|
373
|
+
data: {
|
|
374
|
+
details: {
|
|
375
|
+
reasonCode: 'ARIA_ATTR_PROHIBITED_ROLELESS_NEEDS_REVIEW',
|
|
376
|
+
attr,
|
|
377
|
+
role: null,
|
|
378
|
+
element: tag
|
|
379
|
+
}
|
|
386
380
|
}
|
|
387
|
-
}
|
|
388
|
-
|
|
381
|
+
})
|
|
382
|
+
);
|
|
389
383
|
} else {
|
|
390
|
-
failOccurrences.push(
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
384
|
+
failOccurrences.push(
|
|
385
|
+
helpers.reportOccurrence(el, {
|
|
386
|
+
occurrenceOutcome: 'fail',
|
|
387
|
+
summary: `This ${tag} has no role and no other accessible-name source, so ${attr} is not reliably exposed to assistive technology.`,
|
|
388
|
+
hint: 'Give this element a role that supports an accessible name (e.g. role="img"/"button"), or remove this attribute if it serves no purpose without one.',
|
|
389
|
+
i18n: {
|
|
390
|
+
summaryKey: 'ariaProhibitedAttr_summary_fail_roleless',
|
|
391
|
+
hintKey: 'ariaProhibitedAttr_hint_fail_roleless',
|
|
392
|
+
params: { attr, element: tag }
|
|
393
|
+
},
|
|
394
|
+
data: {
|
|
395
|
+
details: {
|
|
396
|
+
reasonCode: 'ARIA_ATTR_PROHIBITED_ROLELESS',
|
|
397
|
+
attr,
|
|
398
|
+
role: null,
|
|
399
|
+
element: tag
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
})
|
|
403
|
+
);
|
|
405
404
|
}
|
|
406
405
|
}
|
|
407
406
|
}
|
|
@@ -256,10 +256,6 @@ function runInPage(ctx) {
|
|
|
256
256
|
for (const entry of owned) {
|
|
257
257
|
if (entry.role && requiredSet.has(entry.role)) continue;
|
|
258
258
|
|
|
259
|
-
const stableSelector = helpers.buildSelector ? helpers.buildSelector(entry.el) : 'html';
|
|
260
|
-
const html = helpers.getOuterHtmlSnippet
|
|
261
|
-
? helpers.getOuterHtmlSnippet(entry.el)
|
|
262
|
-
: entry.el.outerHTML || '';
|
|
263
259
|
const containerSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
|
|
264
260
|
|
|
265
261
|
const isRoleless = !entry.role;
|
|
@@ -286,29 +282,33 @@ function runInPage(ctx) {
|
|
|
286
282
|
hintKey = 'ariaProhibitedChildren_hint_fail';
|
|
287
283
|
}
|
|
288
284
|
|
|
289
|
-
occurrences.push(
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
285
|
+
occurrences.push(
|
|
286
|
+
helpers.reportOccurrence(entry.el, {
|
|
287
|
+
summary,
|
|
288
|
+
hint,
|
|
289
|
+
i18n: {
|
|
290
|
+
summaryKey,
|
|
291
|
+
hintKey,
|
|
292
|
+
params: isRoleless
|
|
293
|
+
? { attr: entry.attr, containerRole: role }
|
|
294
|
+
: {
|
|
295
|
+
childRole: entry.role,
|
|
296
|
+
containerRole: role,
|
|
297
|
+
allowedRoles: requiredOwned.join(', ')
|
|
298
|
+
}
|
|
299
|
+
},
|
|
300
|
+
data: {
|
|
301
|
+
details: {
|
|
302
|
+
reasonCode: isRoleless ? 'ARIA_PROHIBITED_CHILD_ROLELESS' : 'ARIA_PROHIBITED_CHILD',
|
|
303
|
+
childRole: entry.role,
|
|
304
|
+
attr: entry.attr,
|
|
305
|
+
containerRole: role,
|
|
306
|
+
containerSelector,
|
|
307
|
+
allowedOwnedRoles: requiredOwned
|
|
308
|
+
}
|
|
309
309
|
}
|
|
310
|
-
}
|
|
311
|
-
|
|
310
|
+
})
|
|
311
|
+
);
|
|
312
312
|
}
|
|
313
313
|
}
|
|
314
314
|
|
|
@@ -129,24 +129,21 @@ function runInPage(ctx) {
|
|
|
129
129
|
|
|
130
130
|
if (!missing.length) continue;
|
|
131
131
|
|
|
132
|
-
const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
|
|
133
|
-
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
|
|
134
|
-
|
|
135
132
|
for (const attr of missing) {
|
|
136
|
-
occurrences.push(
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
}
|
|
149
|
-
|
|
133
|
+
occurrences.push(
|
|
134
|
+
helpers.reportOccurrence(el, {
|
|
135
|
+
summary: 'This attribute is required for this element’s role, but is missing.',
|
|
136
|
+
hint: 'Add this attribute with a valid value for this role.',
|
|
137
|
+
i18n: {
|
|
138
|
+
summaryKey: 'ariaRequiredAttr_summary_fail',
|
|
139
|
+
hintKey: 'ariaRequiredAttr_hint_fail',
|
|
140
|
+
params: { attr, role }
|
|
141
|
+
},
|
|
142
|
+
data: {
|
|
143
|
+
details: { reasonCode: 'ARIA_ATTR_REQUIRED_MISSING', attr, role }
|
|
144
|
+
}
|
|
145
|
+
})
|
|
146
|
+
);
|
|
150
147
|
}
|
|
151
148
|
}
|
|
152
149
|
|
|
@@ -253,27 +253,24 @@ function runInPage(ctx) {
|
|
|
253
253
|
|
|
254
254
|
if (found) continue;
|
|
255
255
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
reasonCode: 'ARIA_REQUIRED_CHILD_MISSING',
|
|
272
|
-
role,
|
|
273
|
-
requiredOwnedRoles: requiredOwned
|
|
256
|
+
occurrences.push(
|
|
257
|
+
helpers.reportOccurrence(el, {
|
|
258
|
+
summary: 'This container role has no owned child with a required role.',
|
|
259
|
+
hint: 'Add a descendant (or aria-owns-referenced element) with one of the required owned roles.',
|
|
260
|
+
i18n: {
|
|
261
|
+
summaryKey: 'ariaRequiredChildren_summary_fail',
|
|
262
|
+
hintKey: 'ariaRequiredChildren_hint_fail',
|
|
263
|
+
params: { role, requiredRoles: requiredOwned.join(', ') }
|
|
264
|
+
},
|
|
265
|
+
data: {
|
|
266
|
+
details: {
|
|
267
|
+
reasonCode: 'ARIA_REQUIRED_CHILD_MISSING',
|
|
268
|
+
role,
|
|
269
|
+
requiredOwnedRoles: requiredOwned
|
|
270
|
+
}
|
|
274
271
|
}
|
|
275
|
-
}
|
|
276
|
-
|
|
272
|
+
})
|
|
273
|
+
);
|
|
277
274
|
}
|
|
278
275
|
|
|
279
276
|
if (applicableCount === 0) {
|
|
@@ -210,27 +210,24 @@ function runInPage(ctx) {
|
|
|
210
210
|
|
|
211
211
|
if (hasContext) continue;
|
|
212
212
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
reasonCode: 'ARIA_REQUIRED_PARENT_MISSING',
|
|
229
|
-
role,
|
|
230
|
-
requiredContextRoles: requiredContext
|
|
213
|
+
occurrences.push(
|
|
214
|
+
helpers.reportOccurrence(el, {
|
|
215
|
+
summary: 'This role requires a specific ancestor/owner context role, which was not found.',
|
|
216
|
+
hint: 'Place this element inside (or aria-owns-reference it from) an element with an acceptable context role.',
|
|
217
|
+
i18n: {
|
|
218
|
+
summaryKey: 'ariaRequiredParent_summary_fail',
|
|
219
|
+
hintKey: 'ariaRequiredParent_hint_fail',
|
|
220
|
+
params: { role, requiredRoles: requiredContext.join(', ') }
|
|
221
|
+
},
|
|
222
|
+
data: {
|
|
223
|
+
details: {
|
|
224
|
+
reasonCode: 'ARIA_REQUIRED_PARENT_MISSING',
|
|
225
|
+
role,
|
|
226
|
+
requiredContextRoles: requiredContext
|
|
227
|
+
}
|
|
231
228
|
}
|
|
232
|
-
}
|
|
233
|
-
|
|
229
|
+
})
|
|
230
|
+
);
|
|
234
231
|
}
|
|
235
232
|
|
|
236
233
|
if (applicableCount === 0) {
|
|
@@ -68,14 +68,28 @@ function runInPage(ctx) {
|
|
|
68
68
|
let applicableCount = 0;
|
|
69
69
|
|
|
70
70
|
// Programmatically hidden per the ACT glossary: display:none, visibility not
|
|
71
|
-
// visible, or aria-hidden on the element or an ancestor.
|
|
71
|
+
// visible, or aria-hidden on the element or an ancestor. inert is treated
|
|
72
|
+
// the same although the glossary predates it -- an inert subtree is out of
|
|
73
|
+
// the accessibility tree entirely, so a role on it reaches no one.
|
|
72
74
|
function isHidden(el) {
|
|
73
75
|
try {
|
|
74
76
|
if (typeof helpers.isDomVisibleEligible === 'function') {
|
|
75
77
|
if (!helpers.isDomVisibleEligible(el, ctx)) return true;
|
|
76
78
|
}
|
|
77
|
-
|
|
79
|
+
// Walk the composed tree, not parentElement: that stops at a shadow
|
|
80
|
+
// root, so a host carrying aria-hidden or inert would never be seen
|
|
81
|
+
// from inside its own shadow content.
|
|
82
|
+
const up =
|
|
83
|
+
typeof helpers.composedParent === 'function'
|
|
84
|
+
? helpers.composedParent
|
|
85
|
+
: (n) => n.parentElement;
|
|
86
|
+
|
|
87
|
+
// A shadow root has no getAttribute, so skip past it rather than
|
|
88
|
+
// stopping: the host one step further up is the node that matters.
|
|
89
|
+
for (let n = el; n; n = up(n)) {
|
|
90
|
+
if (!n.getAttribute) continue;
|
|
78
91
|
if (String(n.getAttribute('aria-hidden') || '').toLowerCase() === 'true') return true;
|
|
92
|
+
if (n.hasAttribute && n.hasAttribute('inert')) return true;
|
|
79
93
|
}
|
|
80
94
|
} catch {
|
|
81
95
|
return false;
|
|
@@ -106,27 +120,27 @@ function runInPage(ctx) {
|
|
|
106
120
|
const role = tokens[0];
|
|
107
121
|
const isKnown = tokens.some((t) => ariaHelpers.isKnownRole(t));
|
|
108
122
|
const reasonCode = !isKnown ? 'ARIA_ROLE_INVALID' : 'ARIA_ROLE_ABSTRACT';
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
}
|
|
129
|
-
|
|
123
|
+
|
|
124
|
+
occurrences.push(
|
|
125
|
+
helpers.reportOccurrence(el, {
|
|
126
|
+
summary: !isKnown
|
|
127
|
+
? 'The role attribute value is not a recognized ARIA role.'
|
|
128
|
+
: 'The role attribute value is an abstract ARIA role, which must not be used directly.',
|
|
129
|
+
hint: !isKnown
|
|
130
|
+
? 'Use a valid ARIA role token, or remove the role attribute if none applies.'
|
|
131
|
+
: 'Replace this abstract role with a concrete role appropriate for the widget/structure.',
|
|
132
|
+
i18n: {
|
|
133
|
+
summaryKey: !isKnown
|
|
134
|
+
? 'ariaRolesValid_summary_invalid'
|
|
135
|
+
: 'ariaRolesValid_summary_abstract',
|
|
136
|
+
hintKey: !isKnown ? 'ariaRolesValid_hint_invalid' : 'ariaRolesValid_hint_abstract',
|
|
137
|
+
params: { role }
|
|
138
|
+
},
|
|
139
|
+
data: {
|
|
140
|
+
details: { reasonCode, role }
|
|
141
|
+
}
|
|
142
|
+
})
|
|
143
|
+
);
|
|
130
144
|
}
|
|
131
145
|
|
|
132
146
|
if (applicableCount === 0) {
|
|
@@ -105,29 +105,26 @@ function runInPage(ctx) {
|
|
|
105
105
|
|
|
106
106
|
if (!invalid || !invalid.length) continue;
|
|
107
107
|
|
|
108
|
-
const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
|
|
109
|
-
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
|
|
110
|
-
|
|
111
108
|
for (const item of invalid) {
|
|
112
|
-
occurrences.push(
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
109
|
+
occurrences.push(
|
|
110
|
+
helpers.reportOccurrence(el, {
|
|
111
|
+
summary: 'This element has an ARIA attribute with an invalid value.',
|
|
112
|
+
hint: 'Use a value that matches the attribute’s expected type (see the WAI-ARIA specification for this attribute).',
|
|
113
|
+
i18n: {
|
|
114
|
+
summaryKey: 'ariaValidAttrValue_summary_fail',
|
|
115
|
+
hintKey: 'ariaValidAttrValue_hint_fail',
|
|
116
|
+
params: { attr: item.name, value: item.value }
|
|
117
|
+
},
|
|
118
|
+
data: {
|
|
119
|
+
details: {
|
|
120
|
+
reasonCode: 'ARIA_ATTR_VALUE_INVALID',
|
|
121
|
+
attr: item.name,
|
|
122
|
+
value: item.value,
|
|
123
|
+
valueReason: item.reason
|
|
124
|
+
}
|
|
128
125
|
}
|
|
129
|
-
}
|
|
130
|
-
|
|
126
|
+
})
|
|
127
|
+
);
|
|
131
128
|
}
|
|
132
129
|
}
|
|
133
130
|
|