@surea11y/core 1.4.0 → 1.4.1

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.
Files changed (49) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/bin/surea11y-core.js +0 -0
  3. package/docs/ARIA_DEPRECATION.md +95 -0
  4. package/docs/RULE_CATALOG.md +6 -6
  5. package/package.json +3 -1
  6. package/src/checks/automatic/aria-allowed-attr.js +648 -83
  7. package/src/checks/automatic/aria-deprecated-role.js +105 -37
  8. package/src/checks/automatic/aria-roles-valid.js +31 -6
  9. package/src/checks/automatic/autocomplete-valid.js +37 -1
  10. package/src/checks/automatic/avoid-inline-spacing.js +179 -19
  11. package/src/checks/automatic/binary-control-name-present.js +11 -3
  12. package/src/checks/automatic/button-name-present.js +50 -17
  13. package/src/checks/automatic/canvas-text-alternative-present.js +15 -8
  14. package/src/checks/automatic/combobox-name-present.js +8 -1
  15. package/src/checks/automatic/dialog-name-present.js +8 -1
  16. package/src/checks/automatic/form-control-programmatic-label-present.js +48 -4
  17. package/src/checks/automatic/form-control-single-label.js +107 -39
  18. package/src/checks/automatic/img-alt-present.js +16 -9
  19. package/src/checks/automatic/input-image-alt-present.js +99 -48
  20. package/src/checks/automatic/label-in-name.js +82 -8
  21. package/src/checks/automatic/language-page-present.js +5 -1
  22. package/src/checks/automatic/link-name-present.js +50 -17
  23. package/src/checks/automatic/listbox-name-present.js +8 -1
  24. package/src/checks/automatic/menuitem-name-present.js +8 -1
  25. package/src/checks/automatic/meta-refresh-no-exceptions.js +29 -0
  26. package/src/checks/automatic/meta-refresh-timing-absent.js +30 -4
  27. package/src/checks/automatic/meta-viewport-zoom-enabled.js +37 -15
  28. package/src/checks/automatic/meter-name-present.js +8 -1
  29. package/src/checks/automatic/nested-interactive-controls-absent.js +161 -31
  30. package/src/checks/automatic/object-text-alternative-present.js +14 -7
  31. package/src/checks/automatic/option-name-present.js +8 -1
  32. package/src/checks/automatic/progressbar-name-present.js +8 -1
  33. package/src/checks/automatic/searchbox-name-present.js +8 -1
  34. package/src/checks/automatic/slider-name-present.js +11 -2
  35. package/src/checks/automatic/spinbutton-name-present.js +8 -1
  36. package/src/checks/automatic/summary-name-present.js +8 -1
  37. package/src/checks/automatic/tab-name-present.js +8 -1
  38. package/src/checks/automatic/table-th-has-data-cells.js +67 -6
  39. package/src/checks/automatic/textbox-name-present.js +8 -1
  40. package/src/checks/automatic/tooltip-name-present.js +8 -1
  41. package/src/checks/automatic/treeitem-name-present.js +8 -1
  42. package/src/checks/automatic/valid-lang.js +16 -3
  43. package/src/checks/{automatic/bypass-blocks-present.js → manual/bypass-blocks-present-manual.js} +97 -35
  44. package/src/checks/manual/input-image-alt-decorative-manual.js +24 -0
  45. package/src/checks/manual/landmark-banner-is-top-level-manual.js +11 -22
  46. package/src/checks/manual/landmark-contentinfo-is-top-level-manual.js +7 -10
  47. package/src/core.js +4853 -964
  48. package/src/sarif.js +2 -2
  49. package/surea11y.browser.js +2256 -433
@@ -505,8 +505,8 @@ function runa11yCoreInPage(pageUrl, contextSelector, engineOptions, runOnly) {
505
505
  },
506
506
  {
507
507
  "ruleId": "aria-deprecated-role",
508
- "title": "role attribute must not use a deprecated or author-prohibited ARIA role",
509
- "description": "Checks that an explicit role=\"\" attribute does not use a role deprecated by the WAI-ARIA specification, or one reserved for user-agent-internal use only (e.g. role=\"generic\").",
508
+ "title": "role attribute should not use a deprecated or author-discouraged ARIA role",
509
+ "description": "Checks that an explicit role=\"\" attribute does not use a role deprecated by the WAI-ARIA specification, or one reserved for user-agent-internal use (e.g. role=\"generic\").",
510
510
  "i18n": {
511
511
  "titleKey": "ariaDeprecatedRole_title",
512
512
  "descriptionKey": "ariaDeprecatedRole_description"
@@ -1278,8 +1278,8 @@ function runa11yCoreInPage(pageUrl, contextSelector, engineOptions, runOnly) {
1278
1278
  },
1279
1279
  {
1280
1280
  "ruleId": "avoid-inline-spacing",
1281
- "title": "Inline style must not force text spacing with !important",
1282
- "description": "Checks that inline style does not set line-height, letter-spacing, or word-spacing with !important, which blocks user text-spacing overrides.",
1281
+ "title": "Inline style must not force text spacing below the WCAG metric",
1282
+ "description": "Checks that where inline style forces line-height, letter-spacing or word-spacing with !important, the value already meets WCAG 1.4.12, so the user has nothing left to override.",
1283
1283
  "i18n": {
1284
1284
  "titleKey": "avoidInlineSpacing_title",
1285
1285
  "descriptionKey": "avoidInlineSpacing_description"
@@ -1460,7 +1460,7 @@ function runa11yCoreInPage(pageUrl, contextSelector, engineOptions, runOnly) {
1460
1460
  "wcag241",
1461
1461
  "navigation",
1462
1462
  "atomic",
1463
- "automatic",
1463
+ "manual",
1464
1464
  "a11ycore"
1465
1465
  ],
1466
1466
  "wcagSc": [
@@ -1475,9 +1475,9 @@ function runa11yCoreInPage(pageUrl, contextSelector, engineOptions, runOnly) {
1475
1475
  "conformanceLevel": "A"
1476
1476
  }
1477
1477
  ],
1478
- "defaultSeverity": "serious",
1478
+ "defaultSeverity": "moderate",
1479
1479
  "defaultConfidence": "medium",
1480
- "type": "automatic",
1480
+ "type": "manual",
1481
1481
  "coverage": {
1482
1482
  "facetsBySc": {
1483
1483
  "2.4.1": [
@@ -7838,6 +7838,7 @@ function runa11yCoreInPage(pageUrl, contextSelector, engineOptions, runOnly) {
7838
7838
  // Declared inside runInPage (rather than at module scope) because the
7839
7839
  // build inlines only this function's own source text — see
7840
7840
  // scripts/build-core.js header ("runInPage MUST be self-contained").
7841
+ // <generated:aria-global-attrs>
7841
7842
  const GLOBAL_ATTRS = [
7842
7843
  'aria-atomic',
7843
7844
  'aria-braillelabel',
@@ -7848,14 +7849,10 @@ function runa11yCoreInPage(pageUrl, contextSelector, engineOptions, runOnly) {
7848
7849
  'aria-describedby',
7849
7850
  'aria-description',
7850
7851
  'aria-details',
7851
- 'aria-disabled',
7852
7852
  'aria-dropeffect',
7853
- 'aria-errormessage',
7854
7853
  'aria-flowto',
7855
7854
  'aria-grabbed',
7856
- 'aria-haspopup',
7857
7855
  'aria-hidden',
7858
- 'aria-invalid',
7859
7856
  'aria-keyshortcuts',
7860
7857
  'aria-label',
7861
7858
  'aria-labelledby',
@@ -7864,194 +7861,736 @@ function runa11yCoreInPage(pageUrl, contextSelector, engineOptions, runOnly) {
7864
7861
  'aria-relevant',
7865
7862
  'aria-roledescription'
7866
7863
  ];
7864
+ // </generated:aria-global-attrs>
7867
7865
 
7868
7866
  // Per-role supported (non-global) states/properties. Deliberately
7869
7867
  // conservative — see src/core/aria-helpers.js file header for the same
7870
7868
  // confidence-scoping rationale; only well-established, unambiguous
7871
7869
  // role/attribute pairings from the WAI-ARIA role definitions are listed.
7870
+ // <generated:aria-implicit-roles>
7871
+ const IMPLICIT_ROLE_BY_ELEMENT = {
7872
+ article: 'article',
7873
+ blockquote: 'blockquote',
7874
+ button: 'button',
7875
+ caption: 'caption',
7876
+ code: 'code',
7877
+ dd: 'definition',
7878
+ del: 'deletion',
7879
+ details: 'group',
7880
+ dfn: 'term',
7881
+ dialog: 'dialog',
7882
+ dt: 'term',
7883
+ em: 'emphasis',
7884
+ fieldset: 'group',
7885
+ figure: 'figure',
7886
+ h1: 'heading',
7887
+ h2: 'heading',
7888
+ h3: 'heading',
7889
+ h4: 'heading',
7890
+ h5: 'heading',
7891
+ h6: 'heading',
7892
+ hr: 'separator',
7893
+ ins: 'insertion',
7894
+ main: 'main',
7895
+ mark: 'mark',
7896
+ menu: 'list',
7897
+ meter: 'meter',
7898
+ nav: 'navigation',
7899
+ ol: 'list',
7900
+ optgroup: 'group',
7901
+ option: 'option',
7902
+ output: 'status',
7903
+ p: 'paragraph',
7904
+ progress: 'progressbar',
7905
+ strong: 'strong',
7906
+ sub: 'subscript',
7907
+ sup: 'superscript',
7908
+ textarea: 'textbox',
7909
+ time: 'time',
7910
+ ul: 'list',
7911
+ 'input[type=text]': 'textbox',
7912
+ 'input[type=tel]': 'textbox',
7913
+ 'input[type=url]': 'textbox',
7914
+ 'input[type=email]': 'textbox',
7915
+ 'input[type=password]': 'textbox',
7916
+ 'input[type=search]': 'searchbox',
7917
+ 'input[type=number]': 'spinbutton',
7918
+ 'input[type=range]': 'slider',
7919
+ 'input[type=checkbox]': 'checkbox',
7920
+ 'input[type=radio]': 'radio',
7921
+ 'input[type=button]': 'button',
7922
+ 'input[type=submit]': 'button',
7923
+ 'input[type=reset]': 'button',
7924
+ 'input[type=image]': 'button'
7925
+ };
7926
+ const NON_GLOBAL_ARIA_ATTR_SELECTOR =
7927
+ '[aria-activedescendant], [aria-autocomplete], [aria-checked], [aria-colcount], [aria-colindex], [aria-colspan], [aria-disabled], [aria-errormessage], [aria-expanded], [aria-haspopup], [aria-invalid], [aria-level], [aria-modal], [aria-multiline], [aria-multiselectable], [aria-orientation], [aria-placeholder], [aria-posinset], [aria-pressed], [aria-readonly], [aria-required], [aria-rowcount], [aria-rowindex], [aria-rowspan], [aria-selected], [aria-setsize], [aria-sort], [aria-valuemax], [aria-valuemin], [aria-valuenow], [aria-valuetext]';
7928
+ // </generated:aria-implicit-roles>
7929
+
7930
+ // <generated:aria-role-attrs>
7872
7931
  const SUPPORTED_ATTRS_BY_ROLE = {
7932
+ alert: [],
7873
7933
  alertdialog: ['aria-modal'],
7874
- checkbox: ['aria-checked', 'aria-readonly', 'aria-required', 'aria-expanded'],
7934
+ application: [
7935
+ 'aria-activedescendant',
7936
+ 'aria-disabled',
7937
+ 'aria-errormessage',
7938
+ 'aria-expanded',
7939
+ 'aria-haspopup',
7940
+ 'aria-invalid'
7941
+ ],
7942
+ article: ['aria-posinset', 'aria-setsize'],
7943
+ banner: [],
7944
+ blockquote: [],
7945
+ button: ['aria-disabled', 'aria-expanded', 'aria-haspopup', 'aria-pressed'],
7946
+ caption: [],
7947
+ cell: ['aria-colindex', 'aria-colspan', 'aria-rowindex', 'aria-rowspan'],
7948
+ checkbox: [
7949
+ 'aria-checked',
7950
+ 'aria-disabled',
7951
+ 'aria-errormessage',
7952
+ 'aria-expanded',
7953
+ 'aria-invalid',
7954
+ 'aria-readonly',
7955
+ 'aria-required'
7956
+ ],
7957
+ code: [],
7875
7958
  columnheader: [
7876
- 'aria-sort',
7877
7959
  'aria-colindex',
7878
7960
  'aria-colspan',
7961
+ 'aria-disabled',
7962
+ 'aria-errormessage',
7963
+ 'aria-expanded',
7964
+ 'aria-haspopup',
7965
+ 'aria-invalid',
7879
7966
  'aria-readonly',
7880
7967
  'aria-required',
7881
7968
  'aria-rowindex',
7882
7969
  'aria-rowspan',
7883
7970
  'aria-selected',
7884
- 'aria-expanded'
7971
+ 'aria-sort'
7885
7972
  ],
7886
7973
  combobox: [
7887
- 'aria-expanded',
7974
+ 'aria-activedescendant',
7888
7975
  'aria-autocomplete',
7976
+ 'aria-disabled',
7977
+ 'aria-errormessage',
7978
+ 'aria-expanded',
7979
+ 'aria-haspopup',
7980
+ 'aria-invalid',
7889
7981
  'aria-readonly',
7890
- 'aria-required',
7891
- 'aria-activedescendant'
7982
+ 'aria-required'
7892
7983
  ],
7984
+ complementary: [],
7985
+ contentinfo: [],
7986
+ definition: [],
7987
+ deletion: [],
7893
7988
  dialog: ['aria-modal'],
7989
+ directory: [],
7990
+ 'doc-abstract': [
7991
+ 'aria-disabled',
7992
+ 'aria-errormessage',
7993
+ 'aria-expanded',
7994
+ 'aria-haspopup',
7995
+ 'aria-invalid'
7996
+ ],
7997
+ 'doc-acknowledgments': [
7998
+ 'aria-disabled',
7999
+ 'aria-errormessage',
8000
+ 'aria-expanded',
8001
+ 'aria-haspopup',
8002
+ 'aria-invalid'
8003
+ ],
8004
+ 'doc-afterword': [
8005
+ 'aria-disabled',
8006
+ 'aria-errormessage',
8007
+ 'aria-expanded',
8008
+ 'aria-haspopup',
8009
+ 'aria-invalid'
8010
+ ],
8011
+ 'doc-appendix': [
8012
+ 'aria-disabled',
8013
+ 'aria-errormessage',
8014
+ 'aria-expanded',
8015
+ 'aria-haspopup',
8016
+ 'aria-invalid'
8017
+ ],
8018
+ 'doc-backlink': [
8019
+ 'aria-disabled',
8020
+ 'aria-errormessage',
8021
+ 'aria-expanded',
8022
+ 'aria-haspopup',
8023
+ 'aria-invalid'
8024
+ ],
8025
+ 'doc-biblioentry': [
8026
+ 'aria-disabled',
8027
+ 'aria-errormessage',
8028
+ 'aria-expanded',
8029
+ 'aria-haspopup',
8030
+ 'aria-invalid',
8031
+ 'aria-level',
8032
+ 'aria-posinset',
8033
+ 'aria-setsize'
8034
+ ],
8035
+ 'doc-bibliography': [
8036
+ 'aria-disabled',
8037
+ 'aria-errormessage',
8038
+ 'aria-expanded',
8039
+ 'aria-haspopup',
8040
+ 'aria-invalid'
8041
+ ],
8042
+ 'doc-biblioref': [
8043
+ 'aria-disabled',
8044
+ 'aria-errormessage',
8045
+ 'aria-expanded',
8046
+ 'aria-haspopup',
8047
+ 'aria-invalid'
8048
+ ],
8049
+ 'doc-chapter': [
8050
+ 'aria-disabled',
8051
+ 'aria-errormessage',
8052
+ 'aria-expanded',
8053
+ 'aria-haspopup',
8054
+ 'aria-invalid'
8055
+ ],
8056
+ 'doc-colophon': [
8057
+ 'aria-disabled',
8058
+ 'aria-errormessage',
8059
+ 'aria-expanded',
8060
+ 'aria-haspopup',
8061
+ 'aria-invalid'
8062
+ ],
8063
+ 'doc-conclusion': [
8064
+ 'aria-disabled',
8065
+ 'aria-errormessage',
8066
+ 'aria-expanded',
8067
+ 'aria-haspopup',
8068
+ 'aria-invalid'
8069
+ ],
8070
+ 'doc-cover': [
8071
+ 'aria-disabled',
8072
+ 'aria-errormessage',
8073
+ 'aria-expanded',
8074
+ 'aria-haspopup',
8075
+ 'aria-invalid'
8076
+ ],
8077
+ 'doc-credit': [
8078
+ 'aria-disabled',
8079
+ 'aria-errormessage',
8080
+ 'aria-expanded',
8081
+ 'aria-haspopup',
8082
+ 'aria-invalid'
8083
+ ],
8084
+ 'doc-credits': [
8085
+ 'aria-disabled',
8086
+ 'aria-errormessage',
8087
+ 'aria-expanded',
8088
+ 'aria-haspopup',
8089
+ 'aria-invalid'
8090
+ ],
8091
+ 'doc-dedication': [
8092
+ 'aria-disabled',
8093
+ 'aria-errormessage',
8094
+ 'aria-expanded',
8095
+ 'aria-haspopup',
8096
+ 'aria-invalid'
8097
+ ],
8098
+ 'doc-endnote': [
8099
+ 'aria-disabled',
8100
+ 'aria-errormessage',
8101
+ 'aria-expanded',
8102
+ 'aria-haspopup',
8103
+ 'aria-invalid',
8104
+ 'aria-level',
8105
+ 'aria-posinset',
8106
+ 'aria-setsize'
8107
+ ],
8108
+ 'doc-endnotes': [
8109
+ 'aria-disabled',
8110
+ 'aria-errormessage',
8111
+ 'aria-expanded',
8112
+ 'aria-haspopup',
8113
+ 'aria-invalid'
8114
+ ],
8115
+ 'doc-epigraph': [
8116
+ 'aria-disabled',
8117
+ 'aria-errormessage',
8118
+ 'aria-expanded',
8119
+ 'aria-haspopup',
8120
+ 'aria-invalid'
8121
+ ],
8122
+ 'doc-epilogue': [
8123
+ 'aria-disabled',
8124
+ 'aria-errormessage',
8125
+ 'aria-expanded',
8126
+ 'aria-haspopup',
8127
+ 'aria-invalid'
8128
+ ],
8129
+ 'doc-errata': [
8130
+ 'aria-disabled',
8131
+ 'aria-errormessage',
8132
+ 'aria-expanded',
8133
+ 'aria-haspopup',
8134
+ 'aria-invalid'
8135
+ ],
8136
+ 'doc-example': [
8137
+ 'aria-disabled',
8138
+ 'aria-errormessage',
8139
+ 'aria-expanded',
8140
+ 'aria-haspopup',
8141
+ 'aria-invalid'
8142
+ ],
8143
+ 'doc-footnote': [
8144
+ 'aria-disabled',
8145
+ 'aria-errormessage',
8146
+ 'aria-expanded',
8147
+ 'aria-haspopup',
8148
+ 'aria-invalid'
8149
+ ],
8150
+ 'doc-foreword': [
8151
+ 'aria-disabled',
8152
+ 'aria-errormessage',
8153
+ 'aria-expanded',
8154
+ 'aria-haspopup',
8155
+ 'aria-invalid'
8156
+ ],
8157
+ 'doc-glossary': [
8158
+ 'aria-disabled',
8159
+ 'aria-errormessage',
8160
+ 'aria-expanded',
8161
+ 'aria-haspopup',
8162
+ 'aria-invalid'
8163
+ ],
8164
+ 'doc-glossref': [
8165
+ 'aria-disabled',
8166
+ 'aria-errormessage',
8167
+ 'aria-expanded',
8168
+ 'aria-haspopup',
8169
+ 'aria-invalid'
8170
+ ],
8171
+ 'doc-index': [
8172
+ 'aria-disabled',
8173
+ 'aria-errormessage',
8174
+ 'aria-expanded',
8175
+ 'aria-haspopup',
8176
+ 'aria-invalid'
8177
+ ],
8178
+ 'doc-introduction': [
8179
+ 'aria-disabled',
8180
+ 'aria-errormessage',
8181
+ 'aria-expanded',
8182
+ 'aria-haspopup',
8183
+ 'aria-invalid'
8184
+ ],
8185
+ 'doc-noteref': [
8186
+ 'aria-disabled',
8187
+ 'aria-errormessage',
8188
+ 'aria-expanded',
8189
+ 'aria-haspopup',
8190
+ 'aria-invalid'
8191
+ ],
8192
+ 'doc-notice': [
8193
+ 'aria-disabled',
8194
+ 'aria-errormessage',
8195
+ 'aria-expanded',
8196
+ 'aria-haspopup',
8197
+ 'aria-invalid'
8198
+ ],
8199
+ 'doc-pagebreak': [
8200
+ 'aria-disabled',
8201
+ 'aria-errormessage',
8202
+ 'aria-expanded',
8203
+ 'aria-haspopup',
8204
+ 'aria-invalid',
8205
+ 'aria-orientation',
8206
+ 'aria-valuemax',
8207
+ 'aria-valuemin',
8208
+ 'aria-valuenow',
8209
+ 'aria-valuetext'
8210
+ ],
8211
+ 'doc-pagefooter': ['aria-disabled', 'aria-errormessage', 'aria-haspopup', 'aria-invalid'],
8212
+ 'doc-pageheader': ['aria-disabled', 'aria-errormessage', 'aria-haspopup', 'aria-invalid'],
8213
+ 'doc-pagelist': [
8214
+ 'aria-disabled',
8215
+ 'aria-errormessage',
8216
+ 'aria-expanded',
8217
+ 'aria-haspopup',
8218
+ 'aria-invalid'
8219
+ ],
8220
+ 'doc-part': [
8221
+ 'aria-disabled',
8222
+ 'aria-errormessage',
8223
+ 'aria-expanded',
8224
+ 'aria-haspopup',
8225
+ 'aria-invalid'
8226
+ ],
8227
+ 'doc-preface': [
8228
+ 'aria-disabled',
8229
+ 'aria-errormessage',
8230
+ 'aria-expanded',
8231
+ 'aria-haspopup',
8232
+ 'aria-invalid'
8233
+ ],
8234
+ 'doc-prologue': [
8235
+ 'aria-disabled',
8236
+ 'aria-errormessage',
8237
+ 'aria-expanded',
8238
+ 'aria-haspopup',
8239
+ 'aria-invalid'
8240
+ ],
8241
+ 'doc-pullquote': [],
8242
+ 'doc-qna': [
8243
+ 'aria-disabled',
8244
+ 'aria-errormessage',
8245
+ 'aria-expanded',
8246
+ 'aria-haspopup',
8247
+ 'aria-invalid'
8248
+ ],
8249
+ 'doc-subtitle': [
8250
+ 'aria-disabled',
8251
+ 'aria-errormessage',
8252
+ 'aria-expanded',
8253
+ 'aria-haspopup',
8254
+ 'aria-invalid'
8255
+ ],
8256
+ 'doc-tip': [
8257
+ 'aria-disabled',
8258
+ 'aria-errormessage',
8259
+ 'aria-expanded',
8260
+ 'aria-haspopup',
8261
+ 'aria-invalid'
8262
+ ],
8263
+ 'doc-toc': [
8264
+ 'aria-disabled',
8265
+ 'aria-errormessage',
8266
+ 'aria-expanded',
8267
+ 'aria-haspopup',
8268
+ 'aria-invalid'
8269
+ ],
8270
+ document: [],
8271
+ emphasis: [],
8272
+ feed: [],
8273
+ figure: [],
8274
+ form: [],
8275
+ generic: [],
8276
+ 'graphics-document': [
8277
+ 'aria-disabled',
8278
+ 'aria-errormessage',
8279
+ 'aria-expanded',
8280
+ 'aria-haspopup',
8281
+ 'aria-invalid'
8282
+ ],
8283
+ 'graphics-object': [
8284
+ 'aria-activedescendant',
8285
+ 'aria-disabled',
8286
+ 'aria-errormessage',
8287
+ 'aria-expanded',
8288
+ 'aria-haspopup',
8289
+ 'aria-invalid'
8290
+ ],
8291
+ 'graphics-symbol': [
8292
+ 'aria-disabled',
8293
+ 'aria-errormessage',
8294
+ 'aria-expanded',
8295
+ 'aria-haspopup',
8296
+ 'aria-invalid'
8297
+ ],
7894
8298
  grid: [
8299
+ 'aria-activedescendant',
8300
+ 'aria-colcount',
8301
+ 'aria-disabled',
7895
8302
  'aria-multiselectable',
7896
8303
  'aria-readonly',
7897
- 'aria-colcount',
7898
- 'aria-rowcount',
7899
- 'aria-activedescendant'
8304
+ 'aria-rowcount'
7900
8305
  ],
7901
8306
  gridcell: [
7902
- 'aria-selected',
7903
- 'aria-readonly',
7904
- 'aria-required',
7905
8307
  'aria-colindex',
7906
8308
  'aria-colspan',
8309
+ 'aria-disabled',
8310
+ 'aria-errormessage',
8311
+ 'aria-expanded',
8312
+ 'aria-haspopup',
8313
+ 'aria-invalid',
8314
+ 'aria-readonly',
8315
+ 'aria-required',
7907
8316
  'aria-rowindex',
7908
8317
  'aria-rowspan',
7909
- 'aria-expanded'
8318
+ 'aria-selected'
7910
8319
  ],
8320
+ group: ['aria-activedescendant', 'aria-disabled'],
7911
8321
  heading: ['aria-level'],
8322
+ img: [],
8323
+ insertion: [],
8324
+ link: ['aria-disabled', 'aria-expanded', 'aria-haspopup'],
8325
+ list: [],
7912
8326
  listbox: [
8327
+ 'aria-activedescendant',
8328
+ 'aria-disabled',
8329
+ 'aria-errormessage',
8330
+ 'aria-expanded',
8331
+ 'aria-invalid',
7913
8332
  'aria-multiselectable',
7914
- 'aria-readonly',
7915
- 'aria-required',
7916
8333
  'aria-orientation',
7917
- 'aria-expanded',
7918
- 'aria-activedescendant'
8334
+ 'aria-readonly',
8335
+ 'aria-required'
7919
8336
  ],
7920
8337
  listitem: ['aria-level', 'aria-posinset', 'aria-setsize'],
7921
- menu: ['aria-activedescendant', 'aria-orientation'],
7922
- menubar: ['aria-activedescendant', 'aria-orientation'],
8338
+ log: [],
8339
+ main: [],
8340
+ mark: [],
8341
+ marquee: [],
8342
+ math: [],
8343
+ menu: ['aria-activedescendant', 'aria-disabled', 'aria-orientation'],
8344
+ menubar: ['aria-activedescendant', 'aria-disabled', 'aria-orientation'],
8345
+ menuitem: ['aria-disabled', 'aria-expanded', 'aria-haspopup', 'aria-posinset', 'aria-setsize'],
7923
8346
  menuitemcheckbox: [
7924
8347
  'aria-checked',
8348
+ 'aria-disabled',
8349
+ 'aria-errormessage',
7925
8350
  'aria-expanded',
8351
+ 'aria-haspopup',
8352
+ 'aria-invalid',
8353
+ 'aria-posinset',
7926
8354
  'aria-readonly',
7927
8355
  'aria-required',
7928
- 'aria-posinset',
7929
8356
  'aria-setsize'
7930
8357
  ],
7931
8358
  menuitemradio: [
7932
8359
  'aria-checked',
8360
+ 'aria-disabled',
8361
+ 'aria-errormessage',
7933
8362
  'aria-expanded',
8363
+ 'aria-haspopup',
8364
+ 'aria-invalid',
8365
+ 'aria-posinset',
7934
8366
  'aria-readonly',
7935
8367
  'aria-required',
7936
- 'aria-posinset',
7937
8368
  'aria-setsize'
7938
8369
  ],
7939
- meter: ['aria-valuenow', 'aria-valuemin', 'aria-valuemax', 'aria-valuetext'],
7940
- option: ['aria-selected', 'aria-checked', 'aria-posinset', 'aria-setsize'],
7941
- progressbar: ['aria-valuenow', 'aria-valuemin', 'aria-valuemax', 'aria-valuetext'],
7942
- radio: ['aria-checked', 'aria-posinset', 'aria-setsize'],
7943
- radiogroup: ['aria-readonly', 'aria-required', 'aria-orientation', 'aria-activedescendant'],
8370
+ meter: ['aria-valuemax', 'aria-valuemin', 'aria-valuenow', 'aria-valuetext'],
8371
+ navigation: [],
8372
+ none: [],
8373
+ note: [],
8374
+ option: ['aria-checked', 'aria-disabled', 'aria-posinset', 'aria-selected', 'aria-setsize'],
8375
+ paragraph: [],
8376
+ presentation: [],
8377
+ progressbar: ['aria-valuemax', 'aria-valuemin', 'aria-valuenow', 'aria-valuetext'],
8378
+ radio: ['aria-checked', 'aria-disabled', 'aria-posinset', 'aria-setsize'],
8379
+ radiogroup: [
8380
+ 'aria-activedescendant',
8381
+ 'aria-disabled',
8382
+ 'aria-errormessage',
8383
+ 'aria-invalid',
8384
+ 'aria-orientation',
8385
+ 'aria-readonly',
8386
+ 'aria-required'
8387
+ ],
8388
+ region: [],
7944
8389
  row: [
7945
- 'aria-selected',
8390
+ 'aria-activedescendant',
8391
+ 'aria-colindex',
8392
+ 'aria-disabled',
8393
+ 'aria-expanded',
7946
8394
  'aria-level',
7947
8395
  'aria-posinset',
7948
- 'aria-setsize',
7949
- 'aria-colindex',
7950
8396
  'aria-rowindex',
7951
- 'aria-expanded',
7952
- 'aria-activedescendant'
8397
+ 'aria-selected',
8398
+ 'aria-setsize'
7953
8399
  ],
8400
+ rowgroup: [],
7954
8401
  rowheader: [
7955
- 'aria-sort',
7956
8402
  'aria-colindex',
7957
8403
  'aria-colspan',
8404
+ 'aria-disabled',
8405
+ 'aria-errormessage',
8406
+ 'aria-expanded',
8407
+ 'aria-haspopup',
8408
+ 'aria-invalid',
7958
8409
  'aria-readonly',
7959
8410
  'aria-required',
7960
8411
  'aria-rowindex',
7961
8412
  'aria-rowspan',
7962
8413
  'aria-selected',
7963
- 'aria-expanded'
8414
+ 'aria-sort'
7964
8415
  ],
7965
8416
  scrollbar: [
7966
- 'aria-valuenow',
7967
- 'aria-valuemin',
7968
- 'aria-valuemax',
7969
- 'aria-valuetext',
8417
+ 'aria-disabled',
7970
8418
  'aria-orientation',
7971
- 'aria-controls'
8419
+ 'aria-valuemax',
8420
+ 'aria-valuemin',
8421
+ 'aria-valuenow',
8422
+ 'aria-valuetext'
7972
8423
  ],
8424
+ search: [],
7973
8425
  searchbox: [
7974
8426
  'aria-activedescendant',
7975
8427
  'aria-autocomplete',
8428
+ 'aria-disabled',
8429
+ 'aria-errormessage',
8430
+ 'aria-haspopup',
8431
+ 'aria-invalid',
7976
8432
  'aria-multiline',
7977
8433
  'aria-placeholder',
7978
8434
  'aria-readonly',
7979
8435
  'aria-required'
7980
8436
  ],
7981
8437
  separator: [
7982
- 'aria-valuenow',
7983
- 'aria-valuemin',
8438
+ 'aria-disabled',
8439
+ 'aria-orientation',
7984
8440
  'aria-valuemax',
7985
- 'aria-valuetext',
7986
- 'aria-orientation'
8441
+ 'aria-valuemin',
8442
+ 'aria-valuenow',
8443
+ 'aria-valuetext'
7987
8444
  ],
7988
8445
  slider: [
7989
- 'aria-valuenow',
7990
- 'aria-valuemin',
7991
- 'aria-valuemax',
7992
- 'aria-valuetext',
8446
+ 'aria-disabled',
8447
+ 'aria-errormessage',
8448
+ 'aria-haspopup',
8449
+ 'aria-invalid',
7993
8450
  'aria-orientation',
7994
- 'aria-readonly'
8451
+ 'aria-readonly',
8452
+ 'aria-valuemax',
8453
+ 'aria-valuemin',
8454
+ 'aria-valuenow',
8455
+ 'aria-valuetext'
7995
8456
  ],
7996
8457
  spinbutton: [
7997
- 'aria-valuenow',
7998
- 'aria-valuemin',
7999
- 'aria-valuemax',
8000
- 'aria-valuetext',
8458
+ 'aria-activedescendant',
8459
+ 'aria-disabled',
8460
+ 'aria-errormessage',
8461
+ 'aria-invalid',
8001
8462
  'aria-readonly',
8002
8463
  'aria-required',
8003
- 'aria-activedescendant'
8464
+ 'aria-valuemax',
8465
+ 'aria-valuemin',
8466
+ 'aria-valuenow',
8467
+ 'aria-valuetext'
8468
+ ],
8469
+ status: [],
8470
+ strong: [],
8471
+ subscript: [],
8472
+ superscript: [],
8473
+ switch: [
8474
+ 'aria-checked',
8475
+ 'aria-disabled',
8476
+ 'aria-errormessage',
8477
+ 'aria-expanded',
8478
+ 'aria-invalid',
8479
+ 'aria-readonly',
8480
+ 'aria-required'
8481
+ ],
8482
+ tab: [
8483
+ 'aria-disabled',
8484
+ 'aria-expanded',
8485
+ 'aria-haspopup',
8486
+ 'aria-posinset',
8487
+ 'aria-selected',
8488
+ 'aria-setsize'
8004
8489
  ],
8005
- switch: ['aria-checked', 'aria-expanded', 'aria-readonly', 'aria-required'],
8006
- tab: ['aria-selected', 'aria-expanded', 'aria-posinset', 'aria-setsize'],
8007
8490
  table: ['aria-colcount', 'aria-rowcount'],
8008
- tablist: ['aria-multiselectable', 'aria-orientation', 'aria-level', 'aria-activedescendant'],
8491
+ tablist: [
8492
+ 'aria-activedescendant',
8493
+ 'aria-disabled',
8494
+ 'aria-level',
8495
+ 'aria-multiselectable',
8496
+ 'aria-orientation'
8497
+ ],
8498
+ tabpanel: [],
8499
+ term: [],
8009
8500
  textbox: [
8010
8501
  'aria-activedescendant',
8011
8502
  'aria-autocomplete',
8503
+ 'aria-disabled',
8504
+ 'aria-errormessage',
8505
+ 'aria-haspopup',
8506
+ 'aria-invalid',
8012
8507
  'aria-multiline',
8013
8508
  'aria-placeholder',
8014
8509
  'aria-readonly',
8015
8510
  'aria-required'
8016
8511
  ],
8017
- toolbar: ['aria-activedescendant', 'aria-orientation'],
8018
- tree: ['aria-multiselectable', 'aria-required', 'aria-orientation', 'aria-activedescendant'],
8512
+ time: [],
8513
+ timer: [],
8514
+ toolbar: ['aria-activedescendant', 'aria-disabled', 'aria-orientation'],
8515
+ tooltip: [],
8516
+ tree: [
8517
+ 'aria-activedescendant',
8518
+ 'aria-disabled',
8519
+ 'aria-errormessage',
8520
+ 'aria-invalid',
8521
+ 'aria-multiselectable',
8522
+ 'aria-orientation',
8523
+ 'aria-required'
8524
+ ],
8019
8525
  treegrid: [
8526
+ 'aria-activedescendant',
8527
+ 'aria-colcount',
8528
+ 'aria-disabled',
8529
+ 'aria-errormessage',
8530
+ 'aria-invalid',
8020
8531
  'aria-multiselectable',
8532
+ 'aria-orientation',
8021
8533
  'aria-readonly',
8022
8534
  'aria-required',
8023
- 'aria-orientation',
8024
- 'aria-colcount',
8025
- 'aria-rowcount',
8026
- 'aria-activedescendant'
8535
+ 'aria-rowcount'
8027
8536
  ],
8028
8537
  treeitem: [
8029
8538
  'aria-checked',
8030
- 'aria-selected',
8539
+ 'aria-disabled',
8031
8540
  'aria-expanded',
8541
+ 'aria-haspopup',
8032
8542
  'aria-level',
8033
8543
  'aria-posinset',
8544
+ 'aria-selected',
8034
8545
  'aria-setsize'
8035
8546
  ]
8036
8547
  };
8548
+ // </generated:aria-role-attrs>
8037
8549
 
8038
8550
  const globalSet = new Set(GLOBAL_ATTRS);
8551
+ // [role] keeps the explicit-role path; the attribute selector brings in
8552
+ // elements judged by their implicit role. Only non-global attributes can be
8553
+ // disallowed, so nothing else needs visiting.
8554
+ const selector = '[role], ' + NON_GLOBAL_ARIA_ATTR_SELECTOR;
8039
8555
  const nodes = helpers.queryAllSmart
8040
- ? helpers.queryAllSmart('[role]')
8041
- : helpers.queryAll('[role]');
8556
+ ? helpers.queryAllSmart(selector)
8557
+ : helpers.queryAll(selector);
8042
8558
 
8043
- const occurrences = [];
8559
+ const failOccurrences = [];
8560
+ const cantTellOccurrences = [];
8044
8561
  let applicableCount = 0;
8045
8562
 
8046
8563
  for (const el of nodes) {
8047
8564
  if (!el || !el.attributes) continue;
8048
8565
 
8049
- const role = ariaHelpers.getExplicitRole(el);
8566
+ // ACT 5c01ea scopes the rule to any element carrying an ARIA attribute, so
8567
+ // an element with no role attribute is judged against its implicit role.
8568
+ // Only elements whose implicit role is context-free are covered; the
8569
+ // generator lists what is excluded and why.
8570
+ const explicitRole = ariaHelpers.getExplicitRole(el);
8571
+ let role = explicitRole;
8572
+ if (!role) {
8573
+ const tag = String(el.tagName || '').toLowerCase();
8574
+ const key =
8575
+ tag === 'input'
8576
+ ? 'input[type=' + String(el.getAttribute('type') || 'text').toLowerCase() + ']'
8577
+ : tag;
8578
+ role = Object.prototype.hasOwnProperty.call(IMPLICIT_ROLE_BY_ELEMENT, key)
8579
+ ? IMPLICIT_ROLE_BY_ELEMENT[key]
8580
+ : '';
8581
+ }
8050
8582
  if (!role || !ariaHelpers.isValidConcreteRole(role)) continue; // aria-roles-valid's concern
8051
8583
 
8052
- // Deliberately scoped: only roles with an explicit supported-attrs
8053
- // entry are evaluated (see file header — kept narrow to avoid
8054
- // over-claiming constraints for roles not yet modeled here).
8584
+ // Presentational role conflict resolution drops role="none"/"presentation"
8585
+ // when the element is focusable or carries global ARIA, so the implicit
8586
+ // role decides which attributes are supported. Judging against the
8587
+ // presentational role would flag valid markup such as
8588
+ // <button role="none" aria-pressed="false">; presentation-role-conflict
8589
+ // owns this case.
8590
+ if (role === 'none' || role === 'presentation') continue;
8591
+
8592
+ // Roles absent from the generated table are unknown to ARIA, so there is
8593
+ // nothing to judge them against.
8055
8594
  const roleSupported = SUPPORTED_ATTRS_BY_ROLE[role];
8056
8595
  if (!roleSupported) continue;
8057
8596
  const roleSupportedSet = new Set(roleSupported);
@@ -8077,11 +8616,36 @@ function runa11yCoreInPage(pageUrl, contextSelector, engineOptions, runOnly) {
8077
8616
  const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
8078
8617
 
8079
8618
  for (const name of disallowed) {
8080
- occurrences.push({
8619
+ // A property ARIA deprecated (rather than prohibited) on this role is
8620
+ // still allowed — surfaced as cantTell for the author to decide, not a
8621
+ // not-allowed fail.
8622
+ const deprecated =
8623
+ typeof ariaHelpers.isDeprecatedAttr === 'function' &&
8624
+ ariaHelpers.isDeprecatedAttr(name, role);
8625
+ if (deprecated) {
8626
+ cantTellOccurrences.push({
8627
+ selector: stableSelector,
8628
+ html,
8629
+ summary: 'This ARIA attribute is deprecated for this element’s role.',
8630
+ hint: 'It is still allowed but discouraged; remove it or use a role that supports it, as a future ARIA version may disallow it.',
8631
+ occurrenceOutcome: 'cantTell',
8632
+ i18n: {
8633
+ summaryKey: 'ariaAllowedAttr_summary_cantTell',
8634
+ hintKey: 'ariaAllowedAttr_hint_cantTell',
8635
+ params: { attr: name, role }
8636
+ },
8637
+ data: {
8638
+ details: { reasonCode: 'ARIA_ATTR_DEPRECATED', attr: name, role }
8639
+ }
8640
+ });
8641
+ continue;
8642
+ }
8643
+ failOccurrences.push({
8081
8644
  selector: stableSelector,
8082
8645
  html,
8083
8646
  summary: 'This ARIA attribute is not permitted for this element’s role.',
8084
8647
  hint: 'Remove this attribute, or use a role that supports it.',
8648
+ occurrenceOutcome: 'fail',
8085
8649
  i18n: {
8086
8650
  summaryKey: 'ariaAllowedAttr_summary_fail',
8087
8651
  hintKey: 'ariaAllowedAttr_hint_fail',
@@ -8097,15 +8661,13 @@ function runa11yCoreInPage(pageUrl, contextSelector, engineOptions, runOnly) {
8097
8661
  if (applicableCount === 0) {
8098
8662
  return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
8099
8663
  }
8100
- if (occurrences.length) {
8101
- return {
8102
- ruleId: rule.ruleId,
8103
- outcome: 'fail',
8104
- severity: rule.defaultSeverity || 'moderate',
8105
- occurrences
8106
- };
8107
- }
8108
- return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
8664
+
8665
+ const resolved = helpers.resolveTieredOutcome(
8666
+ failOccurrences,
8667
+ cantTellOccurrences,
8668
+ rule.defaultSeverity || 'moderate'
8669
+ );
8670
+ return { ruleId: rule.ruleId, ...resolved };
8109
8671
  }), applicability: null },
8110
8672
  "aria-allowed-role": { run: (function runInPage(ctx) {
8111
8673
  const { helpers, rule } = ctx;
@@ -8427,12 +8989,31 @@ function runa11yCoreInPage(pageUrl, contextSelector, engineOptions, runOnly) {
8427
8989
  ? helpers.queryAllSmart('[role]')
8428
8990
  : helpers.queryAll('[role]');
8429
8991
 
8430
- const occurrences = [];
8992
+ const failOccurrences = [];
8993
+ const cantTellOccurrences = [];
8431
8994
  let applicableCount = 0;
8432
8995
 
8996
+ // A role on an element hidden from assistive technology has no effect, so
8997
+ // ACT 674b10 does not apply to it.
8998
+ function isHidden(el) {
8999
+ try {
9000
+ if (typeof helpers.isDomVisibleEligible === 'function') {
9001
+ if (!helpers.isDomVisibleEligible(el, ctx)) return true;
9002
+ }
9003
+ for (let n = el; n && n.getAttribute; n = n.parentElement) {
9004
+ if (String(n.getAttribute('aria-hidden') || '').toLowerCase() === 'true') return true;
9005
+ }
9006
+ } catch {
9007
+ return false;
9008
+ }
9009
+ return false;
9010
+ }
9011
+
8433
9012
  for (const el of nodes) {
8434
9013
  if (!el || !el.getAttribute) continue;
8435
9014
 
9015
+ if (isHidden(el)) continue;
9016
+
8436
9017
  const role = ariaHelpers.getExplicitRole(el);
8437
9018
  if (!role) continue;
8438
9019
 
@@ -8442,7 +9023,14 @@ function runa11yCoreInPage(pageUrl, contextSelector, engineOptions, runOnly) {
8442
9023
 
8443
9024
  applicableCount += 1;
8444
9025
 
8445
- if (!ariaHelpers.isDeprecatedRole(role)) continue;
9026
+ const deprecated = ariaHelpers.isDeprecatedRole(role);
9027
+ const discouraged =
9028
+ typeof ariaHelpers.isAuthorDiscouragedRole === 'function' &&
9029
+ ariaHelpers.isAuthorDiscouragedRole(role);
9030
+ const prohibited =
9031
+ typeof ariaHelpers.isAuthorProhibitedRole === 'function' &&
9032
+ ariaHelpers.isAuthorProhibitedRole(role);
9033
+ if (!deprecated && !discouraged && !prohibited) continue;
8446
9034
 
8447
9035
  const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
8448
9036
  const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
@@ -8450,34 +9038,70 @@ function runa11yCoreInPage(pageUrl, contextSelector, engineOptions, runOnly) {
8450
9038
  ? ariaHelpers.getDeprecatedRoleGuidance(role)
8451
9039
  : 'Replace the deprecated role with its recommended replacement.';
8452
9040
 
8453
- occurrences.push({
8454
- selector: stableSelector,
8455
- html,
8456
- summary: `This element uses role="${role}", which authors must not explicitly declare.`,
8457
- hint: guidance,
8458
- i18n: {
8459
- summaryKey: 'ariaDeprecatedRole_summary_fail',
8460
- hintKey: 'ariaDeprecatedRole_hint_fail',
8461
- params: { role, guidance }
8462
- },
8463
- data: {
8464
- details: { reasonCode: 'ARIA_ROLE_DEPRECATED', role, guidance }
8465
- }
8466
- });
9041
+ if (prohibited) {
9042
+ // Author MUST NOT: the usage is non-conforming, not merely discouraged.
9043
+ failOccurrences.push({
9044
+ selector: stableSelector,
9045
+ html,
9046
+ summary: `This element uses role="${role}", which authors must not explicitly declare.`,
9047
+ hint: guidance,
9048
+ occurrenceOutcome: 'fail',
9049
+ i18n: {
9050
+ summaryKey: 'ariaDeprecatedRole_summary_fail',
9051
+ hintKey: 'ariaDeprecatedRole_hint_fail',
9052
+ params: { role, guidance }
9053
+ },
9054
+ data: {
9055
+ details: { reasonCode: 'ARIA_ROLE_AUTHOR_PROHIBITED', role, guidance }
9056
+ }
9057
+ });
9058
+ } else if (discouraged) {
9059
+ // Reserved for user-agent-internal use, at SHOULD NOT strength.
9060
+ cantTellOccurrences.push({
9061
+ selector: stableSelector,
9062
+ html,
9063
+ summary: `This element uses role="${role}", which is reserved for user agents (still valid, but discouraged).`,
9064
+ hint: guidance,
9065
+ occurrenceOutcome: 'cantTell',
9066
+ i18n: {
9067
+ summaryKey: 'ariaDeprecatedRole_summary_cantTell_discouraged',
9068
+ hintKey: 'ariaDeprecatedRole_hint_cantTell',
9069
+ params: { role, guidance }
9070
+ },
9071
+ data: {
9072
+ details: { reasonCode: 'ARIA_ROLE_AUTHOR_DISCOURAGED', role, guidance }
9073
+ }
9074
+ });
9075
+ } else {
9076
+ // Deprecated but still valid: surfaced for the author to decide.
9077
+ cantTellOccurrences.push({
9078
+ selector: stableSelector,
9079
+ html,
9080
+ summary: `This element uses role="${role}", which is deprecated in WAI-ARIA.`,
9081
+ hint: guidance,
9082
+ occurrenceOutcome: 'cantTell',
9083
+ i18n: {
9084
+ summaryKey: 'ariaDeprecatedRole_summary_cantTell',
9085
+ hintKey: 'ariaDeprecatedRole_hint_cantTell',
9086
+ params: { role, guidance }
9087
+ },
9088
+ data: {
9089
+ details: { reasonCode: 'ARIA_ROLE_DEPRECATED', role, guidance }
9090
+ }
9091
+ });
9092
+ }
8467
9093
  }
8468
9094
 
8469
9095
  if (applicableCount === 0) {
8470
9096
  return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
8471
9097
  }
8472
- if (occurrences.length) {
8473
- return {
8474
- ruleId: rule.ruleId,
8475
- outcome: 'fail',
8476
- severity: rule.defaultSeverity || 'moderate',
8477
- occurrences
8478
- };
8479
- }
8480
- return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
9098
+
9099
+ const resolved = helpers.resolveTieredOutcome(
9100
+ failOccurrences,
9101
+ cantTellOccurrences,
9102
+ rule.defaultSeverity || 'moderate'
9103
+ );
9104
+ return { ruleId: rule.ruleId, ...resolved };
8481
9105
  }), applicability: null },
8482
9106
  "aria-hidden-body": { run: (function runInPage(ctx) {
8483
9107
  const { document, helpers, rule } = ctx;
@@ -10483,19 +11107,44 @@ function runa11yCoreInPage(pageUrl, contextSelector, engineOptions, runOnly) {
10483
11107
  const occurrences = [];
10484
11108
  let applicableCount = 0;
10485
11109
 
11110
+ // Programmatically hidden per the ACT glossary: display:none, visibility not
11111
+ // visible, or aria-hidden on the element or an ancestor.
11112
+ function isHidden(el) {
11113
+ try {
11114
+ if (typeof helpers.isDomVisibleEligible === 'function') {
11115
+ if (!helpers.isDomVisibleEligible(el, ctx)) return true;
11116
+ }
11117
+ for (let n = el; n && n.getAttribute; n = n.parentElement) {
11118
+ if (String(n.getAttribute('aria-hidden') || '').toLowerCase() === 'true') return true;
11119
+ }
11120
+ } catch {
11121
+ return false;
11122
+ }
11123
+ return false;
11124
+ }
11125
+
10486
11126
  for (const el of nodes) {
10487
11127
  if (!el || !el.getAttribute) continue;
10488
11128
 
10489
- const role = ariaHelpers.getExplicitRole(el);
10490
- if (!role) continue; // role="" or whitespace-only: not this rule's concern
11129
+ // ACT 674b10 is not applicable to a programmatically hidden element.
11130
+ if (isHidden(el)) continue;
10491
11131
 
10492
- applicableCount += 1;
11132
+ // role takes a fallback list and the first token the browser recognises
11133
+ // wins, so role="searchfield searchbox" resolves to searchbox. The rule
11134
+ // fails only when no token names a concrete role.
11135
+ const tokens =
11136
+ typeof ariaHelpers.getAllRoleTokens === 'function'
11137
+ ? ariaHelpers.getAllRoleTokens(el)
11138
+ : [ariaHelpers.getExplicitRole(el)].filter(Boolean);
11139
+ if (!tokens.length) continue; // role="" or whitespace-only: not this rule's concern
10493
11140
 
10494
- const isAbstract = ariaHelpers.isAbstractRole(role);
10495
- const isKnown = ariaHelpers.isKnownRole(role);
11141
+ applicableCount += 1;
10496
11142
 
10497
- if (isKnown && !isAbstract) continue;
11143
+ const usable = tokens.find((t) => ariaHelpers.isKnownRole(t) && !ariaHelpers.isAbstractRole(t));
11144
+ if (usable) continue;
10498
11145
 
11146
+ const role = tokens[0];
11147
+ const isKnown = tokens.some((t) => ariaHelpers.isKnownRole(t));
10499
11148
  const reasonCode = !isKnown ? 'ARIA_ROLE_INVALID' : 'ARIA_ROLE_ABSTRACT';
10500
11149
  const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
10501
11150
  const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
@@ -10813,7 +11462,15 @@ function runa11yCoreInPage(pageUrl, contextSelector, engineOptions, runOnly) {
10813
11462
  if (tokens[i] && tokens[i].startsWith('section-') && tokens[i].length > 'section-'.length)
10814
11463
  i += 1;
10815
11464
  if (tokens[i] === 'shipping' || tokens[i] === 'billing') i += 1;
10816
- if (CONTACT_MODALITY.has(tokens[i])) i += 1;
11465
+ // A contact modality token is only allowed when the field that follows is
11466
+ // a contact field, so "work photo" is invalid while "work email" is not.
11467
+ if (CONTACT_MODALITY.has(tokens[i])) {
11468
+ const next = tokens[i + 1];
11469
+ const isContactField =
11470
+ next === 'email' || next === 'impp' || next === 'tel' || (next || '').startsWith('tel-');
11471
+ if (!isContactField) return false;
11472
+ i += 1;
11473
+ }
10817
11474
 
10818
11475
  let end = tokens.length;
10819
11476
  if (tokens[end - 1] === 'webauthn') end -= 1;
@@ -10830,11 +11487,39 @@ function runa11yCoreInPage(pageUrl, contextSelector, engineOptions, runOnly) {
10830
11487
  const occurrences = [];
10831
11488
  let applicableCount = 0;
10832
11489
 
11490
+ // ACT 73f2c2 exempts controls where the attribute cannot describe an input
11491
+ // purpose: the on/off toggle, disabled controls, input types with a fixed
11492
+ // value, and controls that take no input.
11493
+ const FIXED_VALUE_TYPES = new Set([
11494
+ 'button',
11495
+ 'checkbox',
11496
+ 'file',
11497
+ 'image',
11498
+ 'radio',
11499
+ 'reset',
11500
+ 'submit'
11501
+ ]);
11502
+
11503
+ function isExempt(el) {
11504
+ const tag = String(el.tagName || '').toLowerCase();
11505
+ if (tag === 'input') {
11506
+ const type = String(el.getAttribute('type') || 'text').toLowerCase();
11507
+ if (FIXED_VALUE_TYPES.has(type)) return true;
11508
+ }
11509
+ if (el.hasAttribute && el.hasAttribute('disabled')) return true;
11510
+ if (String(el.getAttribute('aria-disabled') || '').toLowerCase() === 'true') return true;
11511
+ return false;
11512
+ }
11513
+
10833
11514
  for (const el of nodes) {
10834
11515
  if (!el || !el.getAttribute) continue;
10835
11516
  const raw = String(el.getAttribute('autocomplete') || '').trim();
10836
11517
  if (!raw) continue;
10837
11518
 
11519
+ const tokens = raw.toLowerCase().split(/\s+/).filter(Boolean);
11520
+ if (tokens.length === 1 && (tokens[0] === 'on' || tokens[0] === 'off')) continue;
11521
+ if (isExempt(el)) continue;
11522
+
10838
11523
  applicableCount += 1;
10839
11524
 
10840
11525
  if (isValidAutocomplete(raw)) continue;
@@ -10876,6 +11561,20 @@ function runa11yCoreInPage(pageUrl, contextSelector, engineOptions, runOnly) {
10876
11561
  const { helpers, rule } = ctx;
10877
11562
 
10878
11563
  const SPACING_PROPS = ['line-height', 'letter-spacing', 'word-spacing'];
11564
+ // WCAG 1.4.12's own metrics, as multiples of the font size.
11565
+ const MIN_RATIO = { 'line-height': 1.5, 'letter-spacing': 0.12, 'word-spacing': 0.16 };
11566
+ // Only these two take the value from the parent, leaving this declaration
11567
+ // specifying no spacing of its own. `initial` and `revert` resolve to a
11568
+ // concrete value (`normal` for all three properties), so they stay in scope.
11569
+ const INHERITED_KEYWORDS = ['inherit', 'unset'];
11570
+ // No user agent's `normal` line height reaches 1.5, so a forced `normal`
11571
+ // always falls short of the metric.
11572
+ const NORMAL_LINE_HEIGHT_RATIO = 1.2;
11573
+ const CAMEL = {
11574
+ 'line-height': 'lineHeight',
11575
+ 'letter-spacing': 'letterSpacing',
11576
+ 'word-spacing': 'wordSpacing'
11577
+ };
10879
11578
 
10880
11579
  const nodes = helpers.queryAllSmart
10881
11580
  ? helpers.queryAllSmart('[style]')
@@ -10884,6 +11583,136 @@ function runa11yCoreInPage(pageUrl, contextSelector, engineOptions, runOnly) {
10884
11583
  const occurrences = [];
10885
11584
  let applicableCount = 0;
10886
11585
 
11586
+ // Within one declaration block, importance wins over order, so the last
11587
+ // important declaration is the one that takes effect. Passed Example 5 of ACT
11588
+ // 78fd32 turns on this and Passed Example 6 on the non-important half.
11589
+ function effectiveDeclaration(raw, prop) {
11590
+ const re = new RegExp('(?:^|;)\\s*' + prop + '\\s*:\\s*([^;]*)', 'gi');
11591
+ let chosen = null;
11592
+ let m;
11593
+ while ((m = re.exec(raw))) {
11594
+ const value = String(m[1] || '').trim();
11595
+ const important = /!\s*important\s*$/i.test(value);
11596
+ const clean = value.replace(/!\s*important\s*$/i, '').trim();
11597
+ if (!clean) continue;
11598
+ if (!chosen || important || !chosen.important) {
11599
+ if (chosen && chosen.important && !important) continue;
11600
+ chosen = { value: clean, important };
11601
+ }
11602
+ }
11603
+ return chosen;
11604
+ }
11605
+
11606
+ function hasVisibleTextChild(el) {
11607
+ let kids;
11608
+ try {
11609
+ kids = el.childNodes ? Array.from(el.childNodes) : [];
11610
+ } catch {
11611
+ return false;
11612
+ }
11613
+ return kids.some((n) => n && n.nodeType === 3 && String(n.nodeValue || '').trim() !== '');
11614
+ }
11615
+
11616
+ function isRendered(el) {
11617
+ if (helpers.isDomVisibleEligible) {
11618
+ try {
11619
+ return !!helpers.isDomVisibleEligible(el, ctx, { targetSet: 'dom' }).eligible;
11620
+ } catch {
11621
+ return true;
11622
+ }
11623
+ }
11624
+ return true;
11625
+ }
11626
+
11627
+ const px = (v) => {
11628
+ const n = parseFloat(v);
11629
+ return Number.isFinite(n) && /px\s*$/.test(String(v)) ? n : null;
11630
+ };
11631
+
11632
+ function computedStyleOf(el) {
11633
+ if (helpers && typeof helpers.computedStyle === 'function') {
11634
+ try {
11635
+ const cs = helpers.computedStyle(el);
11636
+ if (cs) return cs;
11637
+ } catch {
11638
+ // fall through to the realm's own view
11639
+ }
11640
+ }
11641
+ try {
11642
+ const view = el.ownerDocument && el.ownerDocument.defaultView;
11643
+ if (view && typeof view.getComputedStyle === 'function') return view.getComputedStyle(el);
11644
+ } catch {
11645
+ // no computed style available
11646
+ }
11647
+ return null;
11648
+ }
11649
+
11650
+ // ACT scopes these rules to text visible on screen, and text pushed far off
11651
+ // canvas is the one hidden shape the shared eligibility check keeps eligible.
11652
+ function isOffScreen(el) {
11653
+ if (!helpers.getVisibilityHintsInfo) return false;
11654
+ try {
11655
+ const info = helpers.getVisibilityHintsInfo(el, ctx, {});
11656
+ return !!(info && Array.isArray(info.hints) && info.hints.indexOf('offscreen') !== -1);
11657
+ } catch {
11658
+ return false;
11659
+ }
11660
+ }
11661
+
11662
+ // A realm that does not lay the document out reports font-size as the CSS
11663
+ // absolute-size keyword rather than a length. Resolving those keeps px-valued
11664
+ // spacing checkable there, and cannot manufacture a failure: a font size
11665
+ // larger than assumed only makes a px ratio smaller.
11666
+ const ABSOLUTE_FONT_SIZES = {
11667
+ 'xx-small': 9,
11668
+ 'x-small': 10,
11669
+ small: 13,
11670
+ medium: 16,
11671
+ large: 18,
11672
+ 'x-large': 24,
11673
+ 'xx-large': 32
11674
+ };
11675
+
11676
+ function fontSizeOf(cs) {
11677
+ if (!cs) return null;
11678
+ const direct = px(cs.fontSize);
11679
+ if (direct !== null) return direct;
11680
+ const keyword = String(cs.fontSize || '')
11681
+ .trim()
11682
+ .toLowerCase();
11683
+ return ABSOLUTE_FONT_SIZES[keyword] || null;
11684
+ }
11685
+
11686
+ /**
11687
+ * The spacing as a multiple of the font size, or null when it cannot be
11688
+ * resolved. Computed style is preferred because it already applies the
11689
+ * cascade and unit resolution; the declared value is only a fallback for
11690
+ * environments that do not lay the document out.
11691
+ */
11692
+ function spacingRatio(el, prop, declared) {
11693
+ const cs = computedStyleOf(el);
11694
+ const fontSize = fontSizeOf(cs);
11695
+ if (cs && fontSize) {
11696
+ const used = px(cs[CAMEL[prop]]);
11697
+ if (used !== null) return used / fontSize;
11698
+ }
11699
+
11700
+ const v = String(declared || '')
11701
+ .trim()
11702
+ .toLowerCase();
11703
+ // `normal`, and the keywords that resolve to it, add nothing for the two
11704
+ // spacing properties and stay under the metric for line height.
11705
+ if (v === 'normal' || v === 'initial' || v === 'revert' || v === 'revert-layer') {
11706
+ return prop === 'line-height' ? NORMAL_LINE_HEIGHT_RATIO : 0;
11707
+ }
11708
+ if (/^[0-9.]+$/.test(v)) return parseFloat(v);
11709
+ if (/em$/.test(v)) return parseFloat(v);
11710
+ if (/%$/.test(v)) return parseFloat(v) / 100;
11711
+ const asPx = px(v);
11712
+ if (asPx !== null && fontSize) return asPx / fontSize;
11713
+ return null;
11714
+ }
11715
+
10887
11716
  for (const el of nodes) {
10888
11717
  if (!el || !el.getAttribute) continue;
10889
11718
  const raw = String(el.getAttribute('style') || '');
@@ -10893,14 +11722,25 @@ function runa11yCoreInPage(pageUrl, contextSelector, engineOptions, runOnly) {
10893
11722
  const hasAnySpacingProp = SPACING_PROPS.some((p) => lower.includes(p));
10894
11723
  if (!hasAnySpacingProp) continue;
10895
11724
 
10896
- applicableCount += 1;
11725
+ // The rule is about text the user needs to re-space, so an element with no
11726
+ // text of its own, or none that renders, is not a target.
11727
+ if (!hasVisibleTextChild(el) || !isRendered(el) || isOffScreen(el)) continue;
10897
11728
 
10898
11729
  const flagged = [];
11730
+ let inScope = false;
10899
11731
  for (const prop of SPACING_PROPS) {
10900
- const re = new RegExp(prop.replace('-', '\\-') + '\\s*:[^;]*!important', 'i');
10901
- if (re.test(raw)) flagged.push(prop);
10902
- }
10903
-
11732
+ const decl = effectiveDeclaration(raw, prop);
11733
+ if (!decl || !decl.important) continue;
11734
+ if (INHERITED_KEYWORDS.indexOf(decl.value.toLowerCase()) !== -1) continue;
11735
+ inScope = true;
11736
+ const ratio = spacingRatio(el, prop, decl.value);
11737
+ // Unresolvable spacing is left alone: this engine reserves fail for
11738
+ // high-confidence violations.
11739
+ if (ratio === null) continue;
11740
+ if (ratio < MIN_RATIO[prop]) flagged.push(prop);
11741
+ }
11742
+
11743
+ if (inScope) applicableCount += 1;
10904
11744
  if (!flagged.length) continue;
10905
11745
 
10906
11746
  const tag = el.tagName.toLowerCase();
@@ -10910,8 +11750,8 @@ function runa11yCoreInPage(pageUrl, contextSelector, engineOptions, runOnly) {
10910
11750
  occurrences.push({
10911
11751
  selector: stableSelector,
10912
11752
  html,
10913
- summary: `This element's inline style forces ${flagged.join(', ')} with !important, blocking user text-spacing overrides.`,
10914
- hint: 'Remove !important from line-height/letter-spacing/word-spacing in inline styles so users can override text spacing.',
11753
+ summary: `This element's inline style forces ${flagged.join(', ')} with !important below the WCAG text-spacing metric, so the user cannot raise it.`,
11754
+ hint: 'Remove !important from line-height/letter-spacing/word-spacing in inline styles, or set a value that already meets the metric (line-height 1.5, letter-spacing 0.12em, word-spacing 0.16em).',
10915
11755
  i18n: {
10916
11756
  summaryKey: 'avoidInlineSpacing_summary_fail',
10917
11757
  hintKey: 'avoidInlineSpacing_hint_fail',
@@ -11030,9 +11870,16 @@ function runa11yCoreInPage(pageUrl, contextSelector, engineOptions, runOnly) {
11030
11870
  return '';
11031
11871
  }
11032
11872
 
11873
+ // Naming rules apply only to elements included in the accessibility tree
11874
+ // (ACT c487ae and siblings), which excludes focusable aria-hidden content.
11875
+ // aria-hidden-focus (ACT 6cfa84) covers that markup instead.
11033
11876
  function isEligibleAcc(helpers, el, ctx) {
11034
11877
  const fn =
11035
- helpers && typeof helpers.isAccTreeEligible === 'function' ? helpers.isAccTreeEligible : null;
11878
+ helpers && typeof helpers.isIncludedInAccessibilityTree === 'function'
11879
+ ? helpers.isIncludedInAccessibilityTree
11880
+ : helpers && typeof helpers.isAccTreeEligible === 'function'
11881
+ ? helpers.isAccTreeEligible
11882
+ : null;
11036
11883
  if (!fn) return true;
11037
11884
  try {
11038
11885
  const r = fn(el, ctx);
@@ -11046,8 +11893,9 @@ function runa11yCoreInPage(pageUrl, contextSelector, engineOptions, runOnly) {
11046
11893
  const occurrences = [];
11047
11894
  let applicableCount = 0;
11048
11895
 
11049
- const selector =
11050
- 'input[type="checkbox"], input[type="radio"], [role="checkbox"], [role="radio"], [role="switch"]';
11896
+ // Native checkbox/radio without an explicit role belongs to
11897
+ // form-control-programmatic-label-present.
11898
+ const selector = '[role="checkbox"], [role="radio"], [role="switch"]';
11051
11899
  const nodes = helpers.queryAllSmart
11052
11900
  ? helpers.queryAllSmart(selector)
11053
11901
  : helpers.queryAll(selector);
@@ -11240,7 +12088,14 @@ function runa11yCoreInPage(pageUrl, contextSelector, engineOptions, runOnly) {
11240
12088
 
11241
12089
  for (const el of nodes) {
11242
12090
  // isAccTreeEligible returns { eligible, reasons }, not a boolean.
11243
- const eligResult = helpers.isAccTreeEligible ? helpers.isAccTreeEligible(el, ctx) : true;
12091
+ // Naming rules apply only to elements included in the accessibility tree
12092
+ // (ACT c487ae), which excludes focusable aria-hidden content;
12093
+ // aria-hidden-focus (ACT 6cfa84) covers that markup instead.
12094
+ const eligResult = helpers.isIncludedInAccessibilityTree
12095
+ ? helpers.isIncludedInAccessibilityTree(el, ctx)
12096
+ : helpers.isAccTreeEligible
12097
+ ? helpers.isAccTreeEligible(el, ctx)
12098
+ : true;
11244
12099
  const eligible =
11245
12100
  typeof eligResult === 'boolean' ? eligResult : !!(eligResult && eligResult.eligible);
11246
12101
  if (!eligible) continue;
@@ -11266,24 +12121,50 @@ function runa11yCoreInPage(pageUrl, contextSelector, engineOptions, runOnly) {
11266
12121
  inputValueName = getInputButtonValueName(el);
11267
12122
  }
11268
12123
 
11269
- // A native <button> (or [role="button"]) whose role has been overridden to
11270
- // one of these roles is no longer semantically a button — per the WAI-ARIA
11271
- // Accessible Name and Description Computation spec these roles are
11272
- // name-from-author-only, and their rendered content represents a VALUE,
11273
- // not a NAME. E.g. <button role="combobox">List</button> where "List" is
11274
- // the combobox's currently selected value, not a label — crediting it as
11275
- // the accessible name would mask a missing name.
11276
- const VALUE_ROLES = [
11277
- 'textbox',
11278
- 'progressbar',
11279
- 'scrollbar',
11280
- 'slider',
11281
- 'spinbutton',
11282
- 'combobox',
11283
- 'listbox'
12124
+ // ARIA 1.2 "Name From: author, contents". Every other known role is
12125
+ // name-from-author-only: <button role="combobox">List</button> exposes a
12126
+ // value, not a label. An unknown role falls back to the implicit role.
12127
+ // <generated:aria-name-from-content>
12128
+ const NAME_FROM_CONTENT_ROLES = [
12129
+ 'button',
12130
+ 'cell',
12131
+ 'checkbox',
12132
+ 'columnheader',
12133
+ 'doc-backlink',
12134
+ 'doc-biblioref',
12135
+ 'doc-glossref',
12136
+ 'doc-noteref',
12137
+ 'graphics-object',
12138
+ 'gridcell',
12139
+ 'heading',
12140
+ 'link',
12141
+ 'menuitem',
12142
+ 'menuitemcheckbox',
12143
+ 'menuitemradio',
12144
+ 'option',
12145
+ 'radio',
12146
+ 'row',
12147
+ 'rowgroup',
12148
+ 'rowheader',
12149
+ 'switch',
12150
+ 'tab',
12151
+ 'tooltip',
12152
+ 'treeitem'
11284
12153
  ];
12154
+ // </generated:aria-name-from-content>
12155
+ const isKnownRoleToken =
12156
+ helpers && helpers.aria && typeof helpers.aria.isKnownRole === 'function'
12157
+ ? (() => {
12158
+ try {
12159
+ return !!helpers.aria.isKnownRole(roleNorm);
12160
+ } catch {
12161
+ return false;
12162
+ }
12163
+ })()
12164
+ : false;
11285
12165
  const isContentNameCandidate =
11286
- (tag === 'button' || role === 'button') && !VALUE_ROLES.includes(roleNorm);
12166
+ (tag === 'button' || role === 'button') &&
12167
+ (!roleNorm || !isKnownRoleToken || NAME_FROM_CONTENT_ROLES.includes(roleNorm));
11287
12168
  const contentName =
11288
12169
  !trustedProgrammaticName && !inputValueName && isContentNameCandidate
11289
12170
  ? getConservativeSubtreeText(el)
@@ -11382,7 +12263,8 @@ function runa11yCoreInPage(pageUrl, contextSelector, engineOptions, runOnly) {
11382
12263
  // (e.g. a page whose only <h1> sits inside a display:none ancestor,
11383
12264
  // unreachable by sighted and screen reader users alike). A fully
11384
12265
  // non-rendered <main>/heading must not be credited here, since that would
11385
- // wrongly return `pass` for a page with zero actual bypass mechanisms.
12266
+ // wrongly treat a page with zero currently-exposed bypass mechanisms as
12267
+ // having one.
11386
12268
  function hasMainLandmark() {
11387
12269
  for (const el of queryAll('main, [role="main"]')) {
11388
12270
  if (el && isExposedToAt(el)) return true;
@@ -11390,10 +12272,41 @@ function runa11yCoreInPage(pageUrl, contextSelector, engineOptions, runOnly) {
11390
12272
  return false;
11391
12273
  }
11392
12274
 
12275
+ // Resolve a fragment id (or legacy <a name>) inside a specific root node
12276
+ // (a Document or a ShadowRoot). Both expose getElementById; querySelector
12277
+ // is used for the legacy anchor-name fallback.
12278
+ function resolveInRoot(root, fragment) {
12279
+ if (!root) return null;
12280
+ let target;
12281
+ try {
12282
+ target = typeof root.getElementById === 'function' ? root.getElementById(fragment) : null;
12283
+ } catch {
12284
+ target = null;
12285
+ }
12286
+ if (target) return target;
12287
+ try {
12288
+ target =
12289
+ typeof root.querySelector === 'function'
12290
+ ? root.querySelector('a[name="' + fragment.replace(/"/g, '\\"') + '"]')
12291
+ : null;
12292
+ } catch {
12293
+ target = null;
12294
+ }
12295
+ return target;
12296
+ }
12297
+
12298
+ // Shadow-DOM-aware: gather anchors via queryAllSmart (pierces shadow roots
12299
+ // when includeShadowDom is enabled, and drops hard-hidden links), and
12300
+ // resolve each fragment in the link's own root before falling back to the
12301
+ // document. This credits a skip link encapsulated in a web component the
12302
+ // same way as one authored in the light DOM.
11393
12303
  function hasWorkingAnchorLink() {
11394
12304
  let links;
11395
12305
  try {
11396
- links = document.querySelectorAll('a[href]');
12306
+ links =
12307
+ helpers && typeof helpers.queryAllSmart === 'function'
12308
+ ? helpers.queryAllSmart('a[href]')
12309
+ : document.querySelectorAll('a[href]');
11397
12310
  } catch {
11398
12311
  links = [];
11399
12312
  }
@@ -11410,18 +12323,19 @@ function runa11yCoreInPage(pageUrl, contextSelector, engineOptions, runOnly) {
11410
12323
  fragment = fragment.trim();
11411
12324
  if (!fragment) continue;
11412
12325
 
11413
- let target;
12326
+ let root = document;
11414
12327
  try {
11415
- target = document.getElementById(fragment);
12328
+ if (typeof a.getRootNode === 'function') {
12329
+ const r = a.getRootNode();
12330
+ if (r) root = r;
12331
+ }
11416
12332
  } catch {
11417
- target = null;
12333
+ root = document;
11418
12334
  }
11419
- if (!target) {
11420
- try {
11421
- target = document.querySelector('a[name="' + fragment.replace(/"/g, '\\"') + '"]');
11422
- } catch {
11423
- target = null;
11424
- }
12335
+
12336
+ let target = resolveInRoot(root, fragment);
12337
+ if (!target && root !== document) {
12338
+ target = resolveInRoot(document, fragment);
11425
12339
  }
11426
12340
  if (target) return true;
11427
12341
  }
@@ -11439,8 +12353,9 @@ function runa11yCoreInPage(pageUrl, contextSelector, engineOptions, runOnly) {
11439
12353
  const anchorLink = mainLandmark ? false : hasWorkingAnchorLink();
11440
12354
  const heading = mainLandmark || anchorLink ? false : hasHeading();
11441
12355
 
12356
+ // A recognized mechanism is present -> nothing to review on this page.
11442
12357
  if (mainLandmark || anchorLink || heading) {
11443
- return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
12358
+ return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
11444
12359
  }
11445
12360
 
11446
12361
  const stableSelector = helpers.buildSelector ? helpers.buildSelector(body) : 'body';
@@ -11452,11 +12367,12 @@ function runa11yCoreInPage(pageUrl, contextSelector, engineOptions, runOnly) {
11452
12367
  {
11453
12368
  selector: stableSelector,
11454
12369
  html,
11455
- summary: 'This page has no recognized way to bypass repeated blocks of content.',
11456
- hint: 'Add a main landmark (<main> or role="main"), a working "skip to content" link, or heading elements that assistive technology can use to jump past repeated content.',
12370
+ summary:
12371
+ 'No recognized way to bypass repeated blocks of content was detected on this page — verify a bypass mechanism exists.',
12372
+ hint: 'Confirm the page offers a bypass mechanism: a main landmark (<main> or role="main"), a working "skip to content" link, or heading elements that assistive technology can use to jump past repeated content. (A mechanism may be temporarily hidden — e.g. while a modal dialog makes the page inert — or provided on a per-site basis; this needs human confirmation.)',
11457
12373
  i18n: {
11458
- summaryKey: 'bypassBlocksPresent_summary_fail',
11459
- hintKey: 'bypassBlocksPresent_hint_fail',
12374
+ summaryKey: 'bypassBlocksPresent_summary_cantTell',
12375
+ hintKey: 'bypassBlocksPresent_hint_cantTell',
11460
12376
  params: {}
11461
12377
  },
11462
12378
  data: {
@@ -11468,8 +12384,8 @@ function runa11yCoreInPage(pageUrl, contextSelector, engineOptions, runOnly) {
11468
12384
 
11469
12385
  return {
11470
12386
  ruleId: rule.ruleId,
11471
- outcome: 'fail',
11472
- severity: rule.defaultSeverity || 'serious',
12387
+ outcome: 'cantTell',
12388
+ severity: rule.defaultSeverity || 'moderate',
11473
12389
  occurrences
11474
12390
  };
11475
12391
  }), applicability: (function applicability(ctx) {
@@ -11497,8 +12413,15 @@ function runa11yCoreInPage(pageUrl, contextSelector, engineOptions, runOnly) {
11497
12413
  const getEligibilityInfo =
11498
12414
  helpers && typeof helpers.getEligibilityInfo === 'function' ? helpers.getEligibilityInfo : null;
11499
12415
 
11500
- const isAccTreeEligible =
11501
- helpers && typeof helpers.isAccTreeEligible === 'function' ? helpers.isAccTreeEligible : null;
12416
+ // aria-hidden removes the canvas from the accessibility tree, so a text
12417
+ // alternative on it cannot reach anyone; aria-hidden-focus reports a
12418
+ // focusable one. Matches the other non-text-content rules.
12419
+ const isEligibleHelper =
12420
+ helpers && typeof helpers.isIncludedInAccessibilityTree === 'function'
12421
+ ? helpers.isIncludedInAccessibilityTree
12422
+ : helpers && typeof helpers.isAccTreeEligible === 'function'
12423
+ ? helpers.isAccTreeEligible
12424
+ : null;
11502
12425
 
11503
12426
  const getTextAlternativeInfo =
11504
12427
  helpers && typeof helpers.getTextAlternativeInfo === 'function'
@@ -11523,15 +12446,16 @@ function runa11yCoreInPage(pageUrl, contextSelector, engineOptions, runOnly) {
11523
12446
  for (const el of canvases) {
11524
12447
  if (!el) continue;
11525
12448
 
11526
- // Applicability: eligible in the accessibility tree (with focusable/IDREF exceptions handled by helper).
11527
- if (isAccTreeEligible) {
12449
+ // Applicability: included in the accessibility tree.
12450
+ if (isEligibleHelper) {
11528
12451
  const elig = (() => {
11529
12452
  try {
11530
- return isAccTreeEligible(el, ctx);
12453
+ return isEligibleHelper(el, ctx);
11531
12454
  } catch {
11532
12455
  return { eligible: true, reasons: [] };
11533
12456
  }
11534
12457
  })();
12458
+ if (elig === false) continue;
11535
12459
  if (elig && elig.eligible === false) continue;
11536
12460
  }
11537
12461
 
@@ -11846,9 +12770,16 @@ function runa11yCoreInPage(pageUrl, contextSelector, engineOptions, runOnly) {
11846
12770
  return '';
11847
12771
  }
11848
12772
 
12773
+ // Naming rules apply only to elements included in the accessibility tree
12774
+ // (ACT c487ae and siblings), which excludes focusable aria-hidden content.
12775
+ // aria-hidden-focus (ACT 6cfa84) covers that markup instead.
11849
12776
  function isEligibleAcc(helpers, el, ctx) {
11850
12777
  const fn =
11851
- helpers && typeof helpers.isAccTreeEligible === 'function' ? helpers.isAccTreeEligible : null;
12778
+ helpers && typeof helpers.isIncludedInAccessibilityTree === 'function'
12779
+ ? helpers.isIncludedInAccessibilityTree
12780
+ : helpers && typeof helpers.isAccTreeEligible === 'function'
12781
+ ? helpers.isAccTreeEligible
12782
+ : null;
11852
12783
  if (!fn) return true;
11853
12784
  try {
11854
12785
  const r = fn(el, ctx);
@@ -14075,9 +15006,16 @@ function runa11yCoreInPage(pageUrl, contextSelector, engineOptions, runOnly) {
14075
15006
  return '';
14076
15007
  }
14077
15008
 
15009
+ // Naming rules apply only to elements included in the accessibility tree
15010
+ // (ACT c487ae and siblings), which excludes focusable aria-hidden content.
15011
+ // aria-hidden-focus (ACT 6cfa84) covers that markup instead.
14078
15012
  function isEligibleAcc(helpers, el, ctx) {
14079
15013
  const fn =
14080
- helpers && typeof helpers.isAccTreeEligible === 'function' ? helpers.isAccTreeEligible : null;
15014
+ helpers && typeof helpers.isIncludedInAccessibilityTree === 'function'
15015
+ ? helpers.isIncludedInAccessibilityTree
15016
+ : helpers && typeof helpers.isAccTreeEligible === 'function'
15017
+ ? helpers.isAccTreeEligible
15018
+ : null;
14081
15019
  if (!fn) return true;
14082
15020
  try {
14083
15021
  const r = fn(el, ctx);
@@ -15044,8 +15982,15 @@ function runa11yCoreInPage(pageUrl, contextSelector, engineOptions, runOnly) {
15044
15982
  "form-control-programmatic-label-present": { run: (function runInPage(ctx) {
15045
15983
  const { helpers, rule } = ctx;
15046
15984
 
15047
- const isAccTreeEligible =
15048
- helpers && typeof helpers.isAccTreeEligible === 'function' ? helpers.isAccTreeEligible : null;
15985
+ // ACT e086e5 applies only to controls included in the accessibility tree, so
15986
+ // a focusable control inside aria-hidden is out of scope; aria-hidden-focus
15987
+ // reports that markup instead.
15988
+ const isEligibleHelper =
15989
+ helpers && typeof helpers.isIncludedInAccessibilityTree === 'function'
15990
+ ? helpers.isIncludedInAccessibilityTree
15991
+ : helpers && typeof helpers.isAccTreeEligible === 'function'
15992
+ ? helpers.isAccTreeEligible
15993
+ : null;
15049
15994
  const getEligibilityInfo =
15050
15995
  helpers && typeof helpers.getEligibilityInfo === 'function' ? helpers.getEligibilityInfo : null;
15051
15996
 
@@ -15056,6 +16001,41 @@ function runa11yCoreInPage(pageUrl, contextSelector, engineOptions, runOnly) {
15056
16001
 
15057
16002
  const trim = (v) => (v == null ? '' : String(v)).trim();
15058
16003
 
16004
+ // Roles with their own *-name-present rule; a control carrying one
16005
+ // explicitly is skipped here so it is reported once.
16006
+ const ROLE_OWNED_ELSEWHERE = [
16007
+ 'alertdialog',
16008
+ 'button',
16009
+ 'checkbox',
16010
+ 'combobox',
16011
+ 'dialog',
16012
+ 'grid',
16013
+ 'link',
16014
+ 'listbox',
16015
+ 'menu',
16016
+ 'menubar',
16017
+ 'menuitem',
16018
+ 'menuitemcheckbox',
16019
+ 'menuitemradio',
16020
+ 'meter',
16021
+ 'option',
16022
+ 'progressbar',
16023
+ 'radio',
16024
+ 'radiogroup',
16025
+ 'scrollbar',
16026
+ 'searchbox',
16027
+ 'slider',
16028
+ 'spinbutton',
16029
+ 'switch',
16030
+ 'tab',
16031
+ 'tablist',
16032
+ 'textbox',
16033
+ 'toolbar',
16034
+ 'tooltip',
16035
+ 'tree',
16036
+ 'treeitem'
16037
+ ];
16038
+
15059
16039
  const metrics = {
15060
16040
  applicableCount: 0,
15061
16041
  passCount: 0,
@@ -15079,9 +16059,9 @@ function runa11yCoreInPage(pageUrl, contextSelector, engineOptions, runOnly) {
15079
16059
  }
15080
16060
 
15081
16061
  function isEligibleAcc(el) {
15082
- if (!isAccTreeEligible) return true;
16062
+ if (!isEligibleHelper) return true;
15083
16063
  try {
15084
- const r = isAccTreeEligible(el, ctx);
16064
+ const r = isEligibleHelper(el, ctx);
15085
16065
  if (typeof r === 'boolean') return r;
15086
16066
  return !!(r && r.eligible);
15087
16067
  } catch {
@@ -15164,6 +16144,8 @@ function runa11yCoreInPage(pageUrl, contextSelector, engineOptions, runOnly) {
15164
16144
  role = '';
15165
16145
  }
15166
16146
 
16147
+ if (role && ROLE_OWNED_ELSEWHERE.indexOf(role) !== -1) continue;
16148
+
15167
16149
  if (role === 'presentation' || role === 'none') {
15168
16150
  const fi = getFocusableInfo
15169
16151
  ? (() => {
@@ -15459,6 +16441,12 @@ function runa11yCoreInPage(pageUrl, contextSelector, engineOptions, runOnly) {
15459
16441
 
15460
16442
  const isAccTreeEligible =
15461
16443
  helpers && typeof helpers.isAccTreeEligible === 'function' ? helpers.isAccTreeEligible : null;
16444
+ const getAriaNameInfo =
16445
+ helpers && typeof helpers.getAriaNameInfo === 'function' ? helpers.getAriaNameInfo : null;
16446
+ const labelContributesName =
16447
+ helpers && typeof helpers.labelContributesAccessibleName === 'function'
16448
+ ? helpers.labelContributesAccessibleName
16449
+ : null;
15462
16450
 
15463
16451
  const selector =
15464
16452
  'input:not([type="hidden"]):not([type="submit"]):not([type="reset"]):not([type="button"]):not([type="image"]),select,textarea';
@@ -15479,7 +16467,8 @@ function runa11yCoreInPage(pageUrl, contextSelector, engineOptions, runOnly) {
15479
16467
  labelsByFor.get(forValue).push(lab);
15480
16468
  }
15481
16469
 
15482
- const occurrences = [];
16470
+ const failOccurrences = [];
16471
+ const cantTellOccurrences = [];
15483
16472
  let applicableCount = 0;
15484
16473
 
15485
16474
  for (const el of nodes) {
@@ -15508,42 +16497,94 @@ function runa11yCoreInPage(pageUrl, contextSelector, engineOptions, runOnly) {
15508
16497
 
15509
16498
  if (eligibleLabels.size <= 1) continue;
15510
16499
 
16500
+ // An override (aria-labelledby / aria-label) supersedes every native
16501
+ // <label>, so the labels contribute nothing to the accessible name and
16502
+ // cannot compete.
16503
+ const override = getAriaNameInfo ? getAriaNameInfo(el, ctx) : null;
16504
+ if (override && override.present && override.value) continue;
16505
+
16506
+ // Without an override the labels feed the name; only labels with their
16507
+ // own text compete for it.
16508
+ const contributing = labelContributesName
16509
+ ? [...eligibleLabels].filter((lab) => labelContributesName(lab))
16510
+ : [...eligibleLabels];
16511
+
15511
16512
  const tag = el.tagName.toLowerCase();
15512
16513
  const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
15513
16514
  const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
15514
16515
 
15515
- occurrences.push({
15516
- selector: stableSelector,
15517
- html,
15518
- summary: 'This form control is associated with more than one <label>.',
15519
- hint: 'Keep only one <label> per form control (either wrapping it or referencing it via for/id).',
15520
- i18n: {
15521
- summaryKey: 'formControlSingleLabel_summary_fail',
15522
- hintKey: 'formControlSingleLabel_hint_fail',
15523
- params: { element: tag, labelCount: String(eligibleLabels.size) }
15524
- },
15525
- data: {
15526
- details: {
15527
- reasonCode: 'FORM_FIELD_MULTIPLE_LABELS',
15528
- element: tag,
15529
- labelCount: eligibleLabels.size
16516
+ if (contributing.length >= 2) {
16517
+ failOccurrences.push({
16518
+ selector: stableSelector,
16519
+ html,
16520
+ summary: 'This form control is associated with more than one <label>.',
16521
+ hint: 'Keep only one <label> per form control (either wrapping it or referencing it via for/id).',
16522
+ occurrenceOutcome: 'fail',
16523
+ i18n: {
16524
+ summaryKey: 'formControlSingleLabel_summary_fail',
16525
+ hintKey: 'formControlSingleLabel_hint_fail',
16526
+ params: { element: tag, labelCount: String(contributing.length) }
16527
+ },
16528
+ data: {
16529
+ details: {
16530
+ reasonCode: 'FORM_FIELD_MULTIPLE_LABELS',
16531
+ element: tag,
16532
+ labelCount: contributing.length
16533
+ }
15530
16534
  }
15531
- }
15532
- });
16535
+ });
16536
+ } else if (contributing.length === 1) {
16537
+ cantTellOccurrences.push({
16538
+ selector: stableSelector,
16539
+ html,
16540
+ summary:
16541
+ 'This form control has one labelling <label> plus an extra empty <label> association.',
16542
+ hint: 'Remove the redundant empty <label> so exactly one <label> is associated with the control.',
16543
+ occurrenceOutcome: 'cantTell',
16544
+ i18n: {
16545
+ summaryKey: 'formControlSingleLabel_summary_cantTell',
16546
+ hintKey: 'formControlSingleLabel_hint_cantTell',
16547
+ params: { element: tag, labelCount: String(eligibleLabels.size) }
16548
+ },
16549
+ data: {
16550
+ details: {
16551
+ reasonCode: 'FORM_FIELD_EXTRA_EMPTY_LABEL',
16552
+ element: tag,
16553
+ labelCount: eligibleLabels.size,
16554
+ contributingLabelCount: contributing.length
16555
+ }
16556
+ }
16557
+ });
16558
+ }
16559
+ // contributing.length === 0: no label carries text, so nothing competes
16560
+ // for the name. A control left unnamed is form-control-programmatic-
16561
+ // label-present's concern.
15533
16562
  }
15534
16563
 
15535
16564
  if (applicableCount === 0) {
15536
16565
  return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
15537
16566
  }
15538
- if (occurrences.length) {
15539
- return {
15540
- ruleId: rule.ruleId,
15541
- outcome: 'fail',
15542
- severity: rule.defaultSeverity || 'moderate',
15543
- occurrences
15544
- };
15545
- }
15546
- return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
16567
+
16568
+ const resolved = helpers.resolveTieredOutcome
16569
+ ? helpers.resolveTieredOutcome(
16570
+ failOccurrences,
16571
+ cantTellOccurrences,
16572
+ rule.defaultSeverity || 'moderate'
16573
+ )
16574
+ : failOccurrences.length
16575
+ ? {
16576
+ outcome: 'fail',
16577
+ severity: rule.defaultSeverity || 'moderate',
16578
+ occurrences: failOccurrences
16579
+ }
16580
+ : cantTellOccurrences.length
16581
+ ? {
16582
+ outcome: 'cantTell',
16583
+ severity: rule.defaultSeverity || 'moderate',
16584
+ occurrences: cantTellOccurrences
16585
+ }
16586
+ : { outcome: 'pass', severity: 'minor', occurrences: [] };
16587
+ return { ruleId: rule.ruleId, ...resolved };
15547
16588
  }), applicability: null },
15548
16589
  "heading-order": { run: (function runInPage(ctx) {
15549
16590
  const { helpers, rule } = ctx;
@@ -15731,7 +16772,11 @@ function runa11yCoreInPage(pageUrl, contextSelector, engineOptions, runOnly) {
15731
16772
  }
15732
16773
 
15733
16774
  // Minimal BCP47 primary subtag check
15734
- if (!/^[a-zA-Z]{2,3}(-[a-zA-Z0-9]{2,8})*$/.test(lang)) {
16775
+ const isValidTag =
16776
+ helpers && typeof helpers.isValidLanguageTag === 'function'
16777
+ ? helpers.isValidLanguageTag
16778
+ : (v) => /^[a-zA-Z]{2,3}(-[a-zA-Z0-9]{2,8})*$/.test(String(v || ''));
16779
+ if (!isValidTag(lang)) {
15735
16780
  return {
15736
16781
  ruleId: rule.ruleId,
15737
16782
  outcome: 'fail',
@@ -16603,8 +17648,15 @@ function runa11yCoreInPage(pageUrl, contextSelector, engineOptions, runOnly) {
16603
17648
  const getEligibilityInfo =
16604
17649
  helpers && typeof helpers.getEligibilityInfo === 'function' ? helpers.getEligibilityInfo : null;
16605
17650
 
16606
- const isAccTreeEligible =
16607
- helpers && typeof helpers.isAccTreeEligible === 'function' ? helpers.isAccTreeEligible : null;
17651
+ // ACT 23a2a8 exempts programmatically hidden images, and that glossary term
17652
+ // has no focusability carve-out, so an aria-hidden image stays out of scope
17653
+ // even when tabbable; aria-hidden-focus reports that markup instead.
17654
+ const isEligibleHelper =
17655
+ helpers && typeof helpers.isIncludedInAccessibilityTree === 'function'
17656
+ ? helpers.isIncludedInAccessibilityTree
17657
+ : helpers && typeof helpers.isAccTreeEligible === 'function'
17658
+ ? helpers.isAccTreeEligible
17659
+ : null;
16608
17660
  const getFocusableInfo =
16609
17661
  helpers && typeof helpers.getFocusableInfo === 'function' ? helpers.getFocusableInfo : null;
16610
17662
  const getAriaNameInfo =
@@ -16656,11 +17708,11 @@ function runa11yCoreInPage(pageUrl, contextSelector, engineOptions, runOnly) {
16656
17708
  const el = imgs[i];
16657
17709
  if (!el || !el.getAttribute) continue;
16658
17710
 
16659
- // Eligibility: only imgs exposed to assistive tech (with focusable/IDREF exceptions handled by helper)
16660
- if (isAccTreeEligible) {
17711
+ // Eligibility: only imgs exposed to assistive tech.
17712
+ if (isEligibleHelper) {
16661
17713
  let elig;
16662
17714
  try {
16663
- elig = isAccTreeEligible(el, ctx);
17715
+ elig = isEligibleHelper(el, ctx);
16664
17716
  } catch {
16665
17717
  elig = null;
16666
17718
  }
@@ -16973,6 +18025,9 @@ function runa11yCoreInPage(pageUrl, contextSelector, engineOptions, runOnly) {
16973
18025
  const isAccTreeEligible =
16974
18026
  helpers && typeof helpers.isAccTreeEligible === 'function' ? helpers.isAccTreeEligible : null;
16975
18027
 
18028
+ const getAriaNameInfo =
18029
+ helpers && typeof helpers.getAriaNameInfo === 'function' ? helpers.getAriaNameInfo : null;
18030
+
16976
18031
  const getFocusableInfo =
16977
18032
  helpers && typeof helpers.getFocusableInfo === 'function' ? helpers.getFocusableInfo : null;
16978
18033
 
@@ -17009,6 +18064,26 @@ function runa11yCoreInPage(pageUrl, contextSelector, engineOptions, runOnly) {
17009
18064
  return !focusable;
17010
18065
  }
17011
18066
 
18067
+ // alt="" plus a name from aria-label/aria-labelledby/title is the judgement
18068
+ // call this rule reviews. alt="" with no other source leaves the control
18069
+ // unnamed, which input-image-alt-present fails outright.
18070
+ function hasNameFromOtherSource(el) {
18071
+ if (getAriaNameInfo) {
18072
+ try {
18073
+ const aria = getAriaNameInfo(el, ctx);
18074
+ if (aria && aria.present && String(aria.value || '').trim()) return true;
18075
+ } catch {
18076
+ // fall through to title
18077
+ }
18078
+ }
18079
+ try {
18080
+ const title = el.getAttribute('title');
18081
+ return title != null && String(title).trim() !== '';
18082
+ } catch {
18083
+ return false;
18084
+ }
18085
+ }
18086
+
17012
18087
  const els = (() => {
17013
18088
  try {
17014
18089
  return Array.from(
@@ -17045,6 +18120,7 @@ function runa11yCoreInPage(pageUrl, contextSelector, engineOptions, runOnly) {
17045
18120
 
17046
18121
  // Rule-specific applicability (only elements that already have a text alternative mechanism)
17047
18122
  if (!(el.getAttribute('alt') != null && String(el.getAttribute('alt')).trim() === '')) continue;
18123
+ if (!hasNameFromOtherSource(el)) continue;
17048
18124
 
17049
18125
  applicableCount += 1;
17050
18126
 
@@ -17099,8 +18175,14 @@ function runa11yCoreInPage(pageUrl, contextSelector, engineOptions, runOnly) {
17099
18175
  const getEligibilityInfo =
17100
18176
  helpers && typeof helpers.getEligibilityInfo === 'function' ? helpers.getEligibilityInfo : null;
17101
18177
 
17102
- const isAccTreeEligible =
17103
- helpers && typeof helpers.isAccTreeEligible === 'function' ? helpers.isAccTreeEligible : null;
18178
+ // ACT 59796f applies to image buttons included in the accessibility tree,
18179
+ // which excludes focusable content inside aria-hidden.
18180
+ const isEligibleHelper =
18181
+ helpers && typeof helpers.isIncludedInAccessibilityTree === 'function'
18182
+ ? helpers.isIncludedInAccessibilityTree
18183
+ : helpers && typeof helpers.isAccTreeEligible === 'function'
18184
+ ? helpers.isAccTreeEligible
18185
+ : null;
17104
18186
 
17105
18187
  const getAriaNameInfo =
17106
18188
  helpers && typeof helpers.getAriaNameInfo === 'function' ? helpers.getAriaNameInfo : null;
@@ -17126,63 +18208,109 @@ function runa11yCoreInPage(pageUrl, contextSelector, engineOptions, runOnly) {
17126
18208
  for (const el of inputs) {
17127
18209
  if (!el || !el.getAttribute) continue;
17128
18210
 
17129
- // Applicability: eligible in the acc tree (with helper exceptions).
17130
- if (isAccTreeEligible) {
18211
+ if (isEligibleHelper) {
17131
18212
  const elig = (() => {
17132
18213
  try {
17133
- return isAccTreeEligible(el, ctx);
18214
+ return isEligibleHelper(el, ctx);
17134
18215
  } catch {
17135
- return { eligible: true, reasons: [] };
18216
+ return true;
17136
18217
  }
17137
18218
  })();
17138
- if (elig && elig.eligible === false) continue;
18219
+ const eligible = typeof elig === 'boolean' ? elig : !(elig && elig.eligible === false);
18220
+ if (!eligible) continue;
17139
18221
  }
17140
18222
 
17141
18223
  applicableCount += 1;
17142
18224
 
17143
- const hasAlt = el.getAttribute('alt') !== null;
17144
- if (hasAlt) continue;
17145
-
17146
- // aria-label / aria-labelledby is also a valid, standards-recognized
17147
- // text-alternative mechanism for <input type="image"> (HTML-AAM
17148
- // accessible name computation includes ARIA naming before falling
17149
- // back to alt).
17150
- if (getAriaNameInfo) {
17151
- let ariaName;
17152
- try {
17153
- ariaName = getAriaNameInfo(el, ctx);
17154
- } catch {
17155
- ariaName = null;
18225
+ // The browser's own fallback name for an image button carries no
18226
+ // information, so an author-supplied name equal to it is treated as no
18227
+ // name at all (ACT 59796f). Only the English defaults are recognised:
18228
+ // "Submit Query" from HTML-AAM, "Submit" from Chrome.
18229
+ const effectiveName = (() => {
18230
+ let v = '';
18231
+ if (getAriaNameInfo) {
18232
+ try {
18233
+ const aria = getAriaNameInfo(el, ctx);
18234
+ if (aria && aria.present && aria.value) v = String(aria.value);
18235
+ } catch {
18236
+ v = '';
18237
+ }
17156
18238
  }
17157
- if (ariaName && ariaName.present) continue;
17158
- }
17159
-
17160
- // A non-empty title attribute is HTML-AAM's own next fallback naming
17161
- // source once alt is entirely absent. Same gap img-alt-present handles
17162
- // for <img title="..."> with no alt.
17163
- const titleRaw = (() => {
17164
- try {
17165
- return el.getAttribute('title');
17166
- } catch {
17167
- return null;
18239
+ if (!v) {
18240
+ const alt = el.getAttribute('alt');
18241
+ if (alt != null && String(alt).trim()) v = String(alt);
18242
+ }
18243
+ if (!v) {
18244
+ const t = el.getAttribute('title');
18245
+ if (t != null && String(t).trim()) v = String(t);
17168
18246
  }
18247
+ return v.trim().toLowerCase();
17169
18248
  })();
17170
- if (titleRaw !== null && String(titleRaw).trim()) continue;
18249
+
18250
+ if (effectiveName === 'submit query' || effectiveName === 'submit') {
18251
+ const eligInfoDefault = getEligibilityInfo
18252
+ ? getEligibilityInfo(el, ctx, { targetSet: 'acc' })
18253
+ : null;
18254
+ const defaultNameOccurrence = {
18255
+ summary:
18256
+ 'Accessible name is the browser default for an image button, which conveys nothing.',
18257
+ hint: 'Replace it with text describing what the button does, for example "Search".',
18258
+ i18n: {
18259
+ summaryKey: 'inputImage_altPresent_summary_defaultName',
18260
+ hintKey: 'inputImage_altPresent_hint_defaultName',
18261
+ params: { element: 'input[type=image]' }
18262
+ },
18263
+ data: {
18264
+ visibilityFilter: eligInfoDefault || { targetSet: 'acc', accEligible: null, reasons: [] },
18265
+ details: { reasonCode: 'default_name' }
18266
+ }
18267
+ };
18268
+ occurrences.push(
18269
+ helpers && typeof helpers.reportOccurrence === 'function'
18270
+ ? helpers.reportOccurrence(el, defaultNameOccurrence)
18271
+ : { selector: '', html: '', ...defaultNameOccurrence }
18272
+ );
18273
+ continue;
18274
+ }
18275
+
18276
+ // effectiveName already covers the naming sources HTML-AAM allows here,
18277
+ // in order: aria-label/aria-labelledby, then alt, then title.
18278
+ if (effectiveName) continue;
18279
+
18280
+ // An image button is a control, so an empty name fails whether alt is
18281
+ // absent or present-but-empty. alt="" marks a decorative image, and an
18282
+ // image button is never decorative.
18283
+ const emptyAlt = el.getAttribute('alt') !== null;
17171
18284
 
17172
18285
  const eligInfo = getEligibilityInfo ? getEligibilityInfo(el, ctx, { targetSet: 'acc' }) : null;
17173
18286
 
17174
- const baseOccurrence = {
17175
- summary: 'Missing alt attribute on <input type="image">.',
17176
- hint: 'Add an alt attribute (use alt="" only when a separate accessible name is provided).',
17177
- i18n: {
17178
- summaryKey: 'inputImage_altPresent_summary_fail',
17179
- hintKey: 'inputImage_altPresent_hint_fail',
17180
- params: { element: 'input[type=image]' }
17181
- },
17182
- data: {
17183
- visibilityFilter: eligInfo || { targetSet: 'acc', accEligible: null, reasons: [] }
17184
- }
17185
- };
18287
+ const baseOccurrence = emptyAlt
18288
+ ? {
18289
+ summary: 'Empty alt="" on <input type="image"> leaves the control unnamed.',
18290
+ hint: 'Describe the action in alt, or name the control with aria-label or aria-labelledby.',
18291
+ i18n: {
18292
+ summaryKey: 'inputImage_altPresent_summary_emptyAlt',
18293
+ hintKey: 'inputImage_altPresent_hint_emptyAlt',
18294
+ params: { element: 'input[type=image]' }
18295
+ },
18296
+ data: {
18297
+ visibilityFilter: eligInfo || { targetSet: 'acc', accEligible: null, reasons: [] },
18298
+ details: { reasonCode: 'empty_alt' }
18299
+ }
18300
+ }
18301
+ : {
18302
+ summary: 'Missing alt attribute on <input type="image">.',
18303
+ hint: 'Add an alt attribute (use alt="" only when a separate accessible name is provided).',
18304
+ i18n: {
18305
+ summaryKey: 'inputImage_altPresent_summary_fail',
18306
+ hintKey: 'inputImage_altPresent_hint_fail',
18307
+ params: { element: 'input[type=image]' }
18308
+ },
18309
+ data: {
18310
+ visibilityFilter: eligInfo || { targetSet: 'acc', accEligible: null, reasons: [] },
18311
+ details: { reasonCode: 'missing_alt' }
18312
+ }
18313
+ };
17186
18314
 
17187
18315
  if (helpers && typeof helpers.reportOccurrence === 'function') {
17188
18316
  occurrences.push(helpers.reportOccurrence(el, baseOccurrence));
@@ -17357,6 +18485,56 @@ function runa11yCoreInPage(pageUrl, contextSelector, engineOptions, runOnly) {
17357
18485
  return v.replace(/\s+/g, ' ').trim().toLowerCase();
17358
18486
  }
17359
18487
 
18488
+ // WCAG 2.5.3's label-in-name comparison is over words, not characters: drop
18489
+ // parenthesised text, case-fold and NFKD-normalise, then reduce every
18490
+ // non-letter/digit to a space. `hyphensJoin` deletes hyphens instead of
18491
+ // splitting on them, which distinguishes a real mismatch from one that is
18492
+ // only a hyphenation difference.
18493
+ function tokenize(s, hyphensJoin) {
18494
+ let v = (s == null ? '' : String(s)).replace(/\([^)]*\)/g, ' ').toLowerCase();
18495
+ try {
18496
+ v = v.normalize('NFKD');
18497
+ } catch {
18498
+ // Realm without String#normalize: the word comparison below still holds.
18499
+ }
18500
+ if (hyphensJoin) v = v.replace(/[-‐-―−]/g, '');
18501
+ return v
18502
+ .replace(/[^\p{L}\p{N}]+/gu, ' ')
18503
+ .split(' ')
18504
+ .filter(Boolean);
18505
+ }
18506
+
18507
+ // The label's words must appear adjacent and in order inside the name, so a
18508
+ // scattered subsequence does not count. Words in `prefixable` may match a
18509
+ // longer name word they start.
18510
+ function containsWordRun(needle, hay, prefixable) {
18511
+ if (!needle.length) return true;
18512
+ for (let i = 0; i + needle.length <= hay.length; i++) {
18513
+ let ok = true;
18514
+ for (let j = 0; j < needle.length; j++) {
18515
+ const want = needle[j];
18516
+ const got = hay[i + j];
18517
+ if (got === want) continue;
18518
+ if (prefixable && prefixable.has(want) && got.indexOf(want) === 0) continue;
18519
+ ok = false;
18520
+ break;
18521
+ }
18522
+ if (ok) return true;
18523
+ }
18524
+ return false;
18525
+ }
18526
+
18527
+ // A trailing period marks a word the author may have shortened ("Ave." for
18528
+ // "Avenue"). Tokenizing removes the period, so collect these beforehand.
18529
+ function abbreviatedWords(s) {
18530
+ const out = new Set();
18531
+ for (const w of (s == null ? '' : String(s)).split(/\s+/)) {
18532
+ const m = /^([\p{L}\p{N}]+)\.$/u.exec(w);
18533
+ if (m) out.add(m[1].toLowerCase());
18534
+ }
18535
+ return out;
18536
+ }
18537
+
17360
18538
  function getElementDescriptor(el) {
17361
18539
  const tag = el && el.tagName ? String(el.tagName).toLowerCase() : 'element';
17362
18540
  let role;
@@ -17602,7 +18780,23 @@ function runa11yCoreInPage(pageUrl, contextSelector, engineOptions, runOnly) {
17602
18780
  const accName = acc && acc.value != null ? String(acc.value) : '';
17603
18781
  const accNorm = norm(accName);
17604
18782
 
17605
- const contains = !!(accNorm && visibleNorm && accNorm.indexOf(visibleNorm) !== -1);
18783
+ const labelTokens = tokenize(visibleLabel, false);
18784
+ const nameTokens = tokenize(accName, false);
18785
+ const contains = containsWordRun(labelTokens, nameTokens, null);
18786
+
18787
+ // An abbreviation, or a word hyphenated differently in the two places, is
18788
+ // not something markup settles: the author may have meant either. Report
18789
+ // without asserting a defect instead of failing or staying silent.
18790
+ let uncertainty = '';
18791
+ if (!contains) {
18792
+ if (containsWordRun(tokenize(visibleLabel, true), tokenize(accName, true), null)) {
18793
+ uncertainty = 'HYPHENATION_DIFFERS';
18794
+ } else {
18795
+ const abbreviated = abbreviatedWords(visibleLabel);
18796
+ if (abbreviated.size && containsWordRun(labelTokens, nameTokens, abbreviated))
18797
+ uncertainty = 'POSSIBLE_ABBREVIATION';
18798
+ }
18799
+ }
17606
18800
 
17607
18801
  if (!contains) {
17608
18802
  const selectorOut = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
@@ -17613,11 +18807,16 @@ function runa11yCoreInPage(pageUrl, contextSelector, engineOptions, runOnly) {
17613
18807
  occurrences.push({
17614
18808
  selector: selectorOut,
17615
18809
  html,
17616
- summary: 'Accessible name does not contain the visible label text.',
17617
- hint: 'Ensure the accessible name includes the visible text label (e.g., update aria-label/aria-labelledby to include the visible wording).',
18810
+ ...(uncertainty ? { outcome: 'cantTell' } : null),
18811
+ summary: uncertainty
18812
+ ? 'Accessible name may not contain the visible label text.'
18813
+ : 'Accessible name does not contain the visible label text.',
18814
+ hint: uncertainty
18815
+ ? 'Check by hand: the two differ only by an abbreviation or by hyphenation, which markup cannot settle.'
18816
+ : 'Ensure the accessible name includes the visible text label (e.g., update aria-label/aria-labelledby to include the visible wording).',
17618
18817
  i18n: {
17619
- summaryKey: 'labelInName_summary_fail',
17620
- hintKey: 'labelInName_hint_fail',
18818
+ summaryKey: uncertainty ? 'labelInName_summary_cantTell' : 'labelInName_summary_fail',
18819
+ hintKey: uncertainty ? 'labelInName_hint_cantTell' : 'labelInName_hint_fail',
17621
18820
  params: {
17622
18821
  element: getElementDescriptor(el),
17623
18822
  visibleLabel: clipForSummary(visibleLabel),
@@ -17627,10 +18826,11 @@ function runa11yCoreInPage(pageUrl, contextSelector, engineOptions, runOnly) {
17627
18826
  },
17628
18827
  data: {
17629
18828
  details: {
17630
- reasonCode: 'VISIBLE_LABEL_NOT_IN_ACCESSIBLE_NAME',
18829
+ reasonCode: uncertainty || 'VISIBLE_LABEL_NOT_IN_ACCESSIBLE_NAME',
17631
18830
  visibleLabel,
17632
18831
  accessibleName: accName,
17633
18832
  normalized: { visibleLabel: visibleNorm, accessibleName: accNorm },
18833
+ tokenized: { visibleLabel: labelTokens, accessibleName: nameTokens },
17634
18834
  labelSource: labelInfo && labelInfo.source ? labelInfo.source : 'none',
17635
18835
  nameMechanism: acc && acc.mechanism ? acc.mechanism : 'none'
17636
18836
  }
@@ -17641,13 +18841,15 @@ function runa11yCoreInPage(pageUrl, contextSelector, engineOptions, runOnly) {
17641
18841
 
17642
18842
  if (applicableCount === 0)
17643
18843
  return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
17644
- if (occurrences.length)
18844
+ if (occurrences.length) {
18845
+ const anyFail = occurrences.some((o) => o.outcome !== 'cantTell');
17645
18846
  return {
17646
18847
  ruleId: rule.ruleId,
17647
- outcome: 'fail',
18848
+ outcome: anyFail ? 'fail' : 'cantTell',
17648
18849
  severity: rule.defaultSeverity || 'minor',
17649
18850
  occurrences
17650
18851
  };
18852
+ }
17651
18853
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
17652
18854
  }), applicability: null },
17653
18855
  "label-title-only": { run: (function runInPage(ctx) {
@@ -17810,16 +19012,11 @@ function runa11yCoreInPage(pageUrl, contextSelector, engineOptions, runOnly) {
17810
19012
  return getImplicitLandmarkRole(el);
17811
19013
  }
17812
19014
 
17813
- // Candidate selection is deliberately NOT the same as getLandmarkRole()
17814
- // === 'banner' — see the fix note above. A <header> is a
17815
- // candidate purely by tag + absence of any role attribute, independent
17816
- // of whether sectioning-ancestor nesting would currently suppress its
17817
- // implicit role; an explicit role="banner" is always a candidate too.
19015
+ // A candidate must actually have the banner role. Per HTML-AAM a <header>
19016
+ // descended from article/aside/main/nav/section is not a banner at all, so
19017
+ // flagging it as a nested banner reports a landmark that does not exist.
17818
19018
  function isBannerCandidate(el) {
17819
- if (!el || !el.getAttribute) return false;
17820
- const explicit = getExplicitRoleToken(el);
17821
- if (explicit) return explicit === 'banner';
17822
- return !!(el.tagName && el.tagName.toLowerCase() === 'header');
19019
+ return getLandmarkRole(el) === 'banner';
17823
19020
  }
17824
19021
 
17825
19022
  function hasLandmarkAncestor(el) {
@@ -17998,11 +19195,11 @@ function runa11yCoreInPage(pageUrl, contextSelector, engineOptions, runOnly) {
17998
19195
  // a candidate purely by tag + absence of any role attribute, independent
17999
19196
  // of whether sectioning-ancestor nesting would currently suppress its
18000
19197
  // implicit role; an explicit role="contentinfo" is always a candidate too.
19198
+ // A candidate must actually have the contentinfo role — a <footer> inside
19199
+ // article/aside/main/nav/section is not one, so flagging it as nested
19200
+ // would report a landmark that does not exist.
18001
19201
  function isContentinfoCandidate(el) {
18002
- if (!el || !el.getAttribute) return false;
18003
- const explicit = getExplicitRoleToken(el);
18004
- if (explicit) return explicit === 'contentinfo';
18005
- return !!(el.tagName && el.tagName.toLowerCase() === 'footer');
19202
+ return getLandmarkRole(el) === 'contentinfo';
18006
19203
  }
18007
19204
 
18008
19205
  function hasLandmarkAncestor(el) {
@@ -19151,7 +20348,14 @@ function runa11yCoreInPage(pageUrl, contextSelector, engineOptions, runOnly) {
19151
20348
 
19152
20349
  for (const el of nodes) {
19153
20350
  // isAccTreeEligible returns { eligible, reasons }, not a boolean.
19154
- const eligResult = helpers.isAccTreeEligible ? helpers.isAccTreeEligible(el, ctx) : true;
20351
+ // Naming rules apply only to elements included in the accessibility tree
20352
+ // (ACT c487ae), which excludes focusable aria-hidden content;
20353
+ // aria-hidden-focus (ACT 6cfa84) covers that markup instead.
20354
+ const eligResult = helpers.isIncludedInAccessibilityTree
20355
+ ? helpers.isIncludedInAccessibilityTree(el, ctx)
20356
+ : helpers.isAccTreeEligible
20357
+ ? helpers.isAccTreeEligible(el, ctx)
20358
+ : true;
19155
20359
  const eligible =
19156
20360
  typeof eligResult === 'boolean' ? eligResult : !!(eligResult && eligResult.eligible);
19157
20361
  if (!eligible) continue;
@@ -19161,33 +20365,59 @@ function runa11yCoreInPage(pageUrl, contextSelector, engineOptions, runOnly) {
19161
20365
  const nameInfo = helpers.getAccessibleNameInfo ? helpers.getAccessibleNameInfo(el, ctx) : null;
19162
20366
  const programmaticName = nameInfo && typeof nameInfo.value === 'string' ? nameInfo.value : '';
19163
20367
 
19164
- // A native <a href>/<area href> (or [role="link"]) whose role has been
19165
- // overridden to one of these roles is no longer semantically a link —
19166
- // per the WAI-ARIA Accessible Name and Description Computation spec
19167
- // these roles are name-from-author-only, and their rendered content
19168
- // represents a VALUE, not a NAME (see button-name-present.js's identical
19169
- // fix for a real-world example — <button role="combobox">List</button> —
19170
- // of the same class of bug on a different host tag).
19171
20368
  const role = el.getAttribute ? el.getAttribute('role') : null;
19172
20369
  const roleNorm = String(role || '')
19173
20370
  .replace(/\s+/g, ' ')
19174
20371
  .trim()
19175
20372
  .toLowerCase();
19176
- const VALUE_ROLES = [
19177
- 'textbox',
19178
- 'progressbar',
19179
- 'scrollbar',
19180
- 'slider',
19181
- 'spinbutton',
19182
- 'combobox',
19183
- 'listbox'
20373
+ // ARIA 1.2 "Name From: author, contents". Every other known role is
20374
+ // name-from-author-only. An unknown role falls back to the implicit role.
20375
+ // <generated:aria-name-from-content>
20376
+ const NAME_FROM_CONTENT_ROLES = [
20377
+ 'button',
20378
+ 'cell',
20379
+ 'checkbox',
20380
+ 'columnheader',
20381
+ 'doc-backlink',
20382
+ 'doc-biblioref',
20383
+ 'doc-glossref',
20384
+ 'doc-noteref',
20385
+ 'graphics-object',
20386
+ 'gridcell',
20387
+ 'heading',
20388
+ 'link',
20389
+ 'menuitem',
20390
+ 'menuitemcheckbox',
20391
+ 'menuitemradio',
20392
+ 'option',
20393
+ 'radio',
20394
+ 'row',
20395
+ 'rowgroup',
20396
+ 'rowheader',
20397
+ 'switch',
20398
+ 'tab',
20399
+ 'tooltip',
20400
+ 'treeitem'
19184
20401
  ];
19185
- const isContentNameCandidate = !VALUE_ROLES.includes(roleNorm);
20402
+ // </generated:aria-name-from-content>
20403
+ const isKnownRoleToken =
20404
+ helpers && helpers.aria && typeof helpers.aria.isKnownRole === 'function'
20405
+ ? (() => {
20406
+ try {
20407
+ return !!helpers.aria.isKnownRole(roleNorm);
20408
+ } catch {
20409
+ return false;
20410
+ }
20411
+ })()
20412
+ : false;
20413
+ const isContentNameCandidate =
20414
+ !roleNorm || !isKnownRoleToken || NAME_FROM_CONTENT_ROLES.includes(roleNorm);
19186
20415
 
19187
20416
  const contentName =
19188
20417
  programmaticName.trim().length === 0 && isContentNameCandidate
19189
20418
  ? getConservativeSubtreeText(el)
19190
20419
  : '';
20420
+
19191
20421
  const finalName = (programmaticName.trim().length ? programmaticName : contentName).trim();
19192
20422
 
19193
20423
  if (finalName.length === 0) {
@@ -19516,9 +20746,16 @@ function runa11yCoreInPage(pageUrl, contextSelector, engineOptions, runOnly) {
19516
20746
  return '';
19517
20747
  }
19518
20748
 
20749
+ // Naming rules apply only to elements included in the accessibility tree
20750
+ // (ACT c487ae and siblings), which excludes focusable aria-hidden content.
20751
+ // aria-hidden-focus (ACT 6cfa84) covers that markup instead.
19519
20752
  function isEligibleAcc(helpers, el, ctx) {
19520
20753
  const fn =
19521
- helpers && typeof helpers.isAccTreeEligible === 'function' ? helpers.isAccTreeEligible : null;
20754
+ helpers && typeof helpers.isIncludedInAccessibilityTree === 'function'
20755
+ ? helpers.isIncludedInAccessibilityTree
20756
+ : helpers && typeof helpers.isAccTreeEligible === 'function'
20757
+ ? helpers.isAccTreeEligible
20758
+ : null;
19522
20759
  if (!fn) return true;
19523
20760
  try {
19524
20761
  const r = fn(el, ctx);
@@ -20178,9 +21415,16 @@ function runa11yCoreInPage(pageUrl, contextSelector, engineOptions, runOnly) {
20178
21415
  return '';
20179
21416
  }
20180
21417
 
21418
+ // Naming rules apply only to elements included in the accessibility tree
21419
+ // (ACT c487ae and siblings), which excludes focusable aria-hidden content.
21420
+ // aria-hidden-focus (ACT 6cfa84) covers that markup instead.
20181
21421
  function isEligibleAcc(helpers, el, ctx) {
20182
21422
  const fn =
20183
- helpers && typeof helpers.isAccTreeEligible === 'function' ? helpers.isAccTreeEligible : null;
21423
+ helpers && typeof helpers.isIncludedInAccessibilityTree === 'function'
21424
+ ? helpers.isIncludedInAccessibilityTree
21425
+ : helpers && typeof helpers.isAccTreeEligible === 'function'
21426
+ ? helpers.isAccTreeEligible
21427
+ : null;
20184
21428
  if (!fn) return true;
20185
21429
  try {
20186
21430
  const r = fn(el, ctx);
@@ -20278,6 +21522,34 @@ function runa11yCoreInPage(pageUrl, contextSelector, engineOptions, runOnly) {
20278
21522
  : [];
20279
21523
 
20280
21524
  const occurrences = [];
21525
+ // HTML's shared declarative refresh steps. Returns the delay in seconds, or
21526
+ // null when the value is not a valid refresh directive, in which case the
21527
+ // browser never refreshes and there is nothing to report. Rejects a leading
21528
+ // sign ("+72001"), a non-numeric time ("foo"), and a separator other than
21529
+ // "," or ";" ("0:1").
21530
+ function parseRefreshDelay(input) {
21531
+ const s = String(input == null ? '' : input);
21532
+ const isSpace = (c) => c === ' ' || c === '\t' || c === '\n' || c === '\f' || c === '\r';
21533
+ let i = 0;
21534
+ while (i < s.length && isSpace(s[i])) i++;
21535
+ let digits = '';
21536
+ while (i < s.length && s[i] >= '0' && s[i] <= '9') digits += s[i++];
21537
+ if (!digits) return null;
21538
+ if (s[i] === '.') {
21539
+ i++;
21540
+ while (i < s.length && s[i] >= '0' && s[i] <= '9') i++;
21541
+ }
21542
+ if (i >= s.length) return parseInt(digits, 10);
21543
+ let sawSpace = false;
21544
+ while (i < s.length && isSpace(s[i])) {
21545
+ sawSpace = true;
21546
+ i++;
21547
+ }
21548
+ if (i >= s.length) return parseInt(digits, 10);
21549
+ if (s[i] === ';' || s[i] === ',' || sawSpace) return parseInt(digits, 10);
21550
+ return null;
21551
+ }
21552
+
20281
21553
  let applicableCount = 0;
20282
21554
 
20283
21555
  for (const el of nodes) {
@@ -20285,6 +21557,7 @@ function runa11yCoreInPage(pageUrl, contextSelector, engineOptions, runOnly) {
20285
21557
  if (el.closest && el.closest('noscript')) continue; // never applies with scripting enabled
20286
21558
  const raw = String(el.getAttribute('content') || '').trim();
20287
21559
  if (!raw) continue;
21560
+ if (parseRefreshDelay(raw) === null) continue;
20288
21561
 
20289
21562
  applicableCount += 1;
20290
21563
 
@@ -20336,16 +21609,42 @@ function runa11yCoreInPage(pageUrl, contextSelector, engineOptions, runOnly) {
20336
21609
  const occurrences = [];
20337
21610
  let applicableCount = 0;
20338
21611
 
21612
+ // HTML's shared declarative refresh steps. Returns the delay in seconds, or
21613
+ // null when the value is not a valid refresh directive, in which case the
21614
+ // browser never refreshes and there is nothing to report. Rejects a leading
21615
+ // sign ("+72001"), a non-numeric time ("foo"), and a separator other than
21616
+ // "," or ";" ("0:1").
21617
+ function parseRefreshDelay(input) {
21618
+ const s = String(input == null ? '' : input);
21619
+ const isSpace = (c) => c === ' ' || c === '\t' || c === '\n' || c === '\f' || c === '\r';
21620
+ let i = 0;
21621
+ while (i < s.length && isSpace(s[i])) i++;
21622
+ let digits = '';
21623
+ while (i < s.length && s[i] >= '0' && s[i] <= '9') digits += s[i++];
21624
+ if (!digits) return null;
21625
+ if (s[i] === '.') {
21626
+ i++;
21627
+ while (i < s.length && s[i] >= '0' && s[i] <= '9') i++;
21628
+ }
21629
+ if (i >= s.length) return parseInt(digits, 10);
21630
+ let sawSpace = false;
21631
+ while (i < s.length && isSpace(s[i])) {
21632
+ sawSpace = true;
21633
+ i++;
21634
+ }
21635
+ if (i >= s.length) return parseInt(digits, 10);
21636
+ if (s[i] === ';' || s[i] === ',' || sawSpace) return parseInt(digits, 10);
21637
+ return null;
21638
+ }
21639
+
20339
21640
  for (const el of nodes) {
20340
21641
  if (!el || !el.getAttribute) continue;
20341
21642
  if (el.closest && el.closest('noscript')) continue; // never applies with scripting enabled — see meta-refresh-no-exceptions.js's header comment
20342
21643
  const raw = String(el.getAttribute('content') || '').trim();
20343
21644
  if (!raw) continue;
20344
21645
 
20345
- const match = raw.match(/^([0-9]*\.?[0-9]+)/);
20346
- if (!match) continue;
20347
- const delay = parseFloat(match[1]);
20348
- if (Number.isNaN(delay)) continue;
21646
+ const delay = parseRefreshDelay(raw);
21647
+ if (delay === null) continue;
20349
21648
 
20350
21649
  applicableCount += 1;
20351
21650
 
@@ -20500,20 +21799,40 @@ function runa11yCoreInPage(pageUrl, contextSelector, engineOptions, runOnly) {
20500
21799
  const raw = String(el.getAttribute('content') || '').trim();
20501
21800
  if (!raw) continue;
20502
21801
 
20503
- applicableCount += 1;
20504
-
20505
21802
  const parsed = parseContent(raw);
21803
+
21804
+ // ACT b4f0c3 applies only when content carries maximum-scale or
21805
+ // user-scalable; content that sets neither cannot restrict zoom.
21806
+ if (parsed['user-scalable'] === undefined && parsed['maximum-scale'] === undefined) continue;
21807
+
21808
+ applicableCount += 1;
20506
21809
  const reasons = [];
20507
21810
 
21811
+ // CSS Device Adaptation translates an unparseable value to 0, so
21812
+ // user-scalable=invalid and maximum-scale=yes disable zoom just as
21813
+ // user-scalable=no does. A negative maximum-scale is out of range and
21814
+ // dropped instead, which is why it does not restrict anything.
20508
21815
  const userScalable = parsed['user-scalable'];
20509
- if (userScalable === 'no' || userScalable === '0') {
20510
- reasons.push('user-scalable=' + userScalable);
21816
+ if (
21817
+ userScalable !== undefined &&
21818
+ userScalable !== 'yes' &&
21819
+ userScalable !== 'device-width' &&
21820
+ userScalable !== 'device-height'
21821
+ ) {
21822
+ const scale = parseFloat(userScalable);
21823
+ if (Number.isNaN(scale) || (scale > -1 && scale < 1)) {
21824
+ reasons.push('user-scalable=' + userScalable);
21825
+ }
20511
21826
  }
20512
21827
 
20513
21828
  const maxScaleRaw = parsed['maximum-scale'];
20514
- if (maxScaleRaw !== undefined) {
21829
+ if (
21830
+ maxScaleRaw !== undefined &&
21831
+ maxScaleRaw !== 'device-width' &&
21832
+ maxScaleRaw !== 'device-height'
21833
+ ) {
20515
21834
  const maxScale = parseFloat(maxScaleRaw);
20516
- if (!Number.isNaN(maxScale) && maxScale < 2) {
21835
+ if (Number.isNaN(maxScale) || (maxScale >= 0 && maxScale < 2)) {
20517
21836
  reasons.push('maximum-scale=' + maxScaleRaw);
20518
21837
  }
20519
21838
  }
@@ -20591,9 +21910,16 @@ function runa11yCoreInPage(pageUrl, contextSelector, engineOptions, runOnly) {
20591
21910
  return '';
20592
21911
  }
20593
21912
 
21913
+ // Naming rules apply only to elements included in the accessibility tree
21914
+ // (ACT c487ae and siblings), which excludes focusable aria-hidden content.
21915
+ // aria-hidden-focus (ACT 6cfa84) covers that markup instead.
20594
21916
  function isEligibleAcc(helpers, el, ctx) {
20595
21917
  const fn =
20596
- helpers && typeof helpers.isAccTreeEligible === 'function' ? helpers.isAccTreeEligible : null;
21918
+ helpers && typeof helpers.isIncludedInAccessibilityTree === 'function'
21919
+ ? helpers.isIncludedInAccessibilityTree
21920
+ : helpers && typeof helpers.isAccTreeEligible === 'function'
21921
+ ? helpers.isAccTreeEligible
21922
+ : null;
20597
21923
  if (!fn) return true;
20598
21924
  try {
20599
21925
  const r = fn(el, ctx);
@@ -20799,6 +22125,8 @@ function runa11yCoreInPage(pageUrl, contextSelector, engineOptions, runOnly) {
20799
22125
 
20800
22126
  const isAccTreeEligible =
20801
22127
  helpers && typeof helpers.isAccTreeEligible === 'function' ? helpers.isAccTreeEligible : null;
22128
+ const getFocusableInfo =
22129
+ helpers && typeof helpers.getFocusableInfo === 'function' ? helpers.getFocusableInfo : null;
20802
22130
 
20803
22131
  function isEligible(node) {
20804
22132
  if (!isAccTreeEligible) return true;
@@ -20810,6 +22138,122 @@ function runa11yCoreInPage(pageUrl, contextSelector, engineOptions, runOnly) {
20810
22138
  }
20811
22139
  }
20812
22140
 
22141
+ function matchesInteractive(node) {
22142
+ return !!(
22143
+ node &&
22144
+ node.nodeType === 1 &&
22145
+ typeof node.matches === 'function' &&
22146
+ node.matches(INTERACTIVE_SELECTOR)
22147
+ );
22148
+ }
22149
+
22150
+ // A composite widget owns children of a fixed role and drives their focus
22151
+ // itself (via roving tabindex or aria-activedescendant). The roving-tabindex
22152
+ // model gives the active child tabindex="0" and the rest tabindex="-1", so
22153
+ // the owned children ARE platform-focusable even though the container, not
22154
+ // the child, is the operable unit. Such a child is part of the composite,
22155
+ // not a control nested inside another control, so it must not be reported
22156
+ // regardless of its tabindex. The map is keyed by the child role and lists
22157
+ // the container roles that legitimately own it.
22158
+ const COMPOSITE_CHILD_CONTAINERS = {
22159
+ option: ['listbox', 'combobox'],
22160
+ tab: ['tablist'],
22161
+ treeitem: ['tree'],
22162
+ menuitem: ['menu', 'menubar'],
22163
+ menuitemcheckbox: ['menu', 'menubar'],
22164
+ menuitemradio: ['menu', 'menubar'],
22165
+ radio: ['radiogroup']
22166
+ };
22167
+
22168
+ function getExplicitRole(node) {
22169
+ if (!node || node.nodeType !== 1 || typeof node.getAttribute !== 'function') return '';
22170
+ const raw = node.getAttribute('role');
22171
+ if (!raw) return '';
22172
+ // role accepts a space-separated fallback list; the first token wins.
22173
+ const first = raw.trim().split(/\s+/)[0];
22174
+ return first ? first.toLowerCase() : '';
22175
+ }
22176
+
22177
+ function parentElementOf(node) {
22178
+ if (!node) return null;
22179
+ if (node.parentElement) return node.parentElement;
22180
+ const p = node.parentNode;
22181
+ return p && p.nodeType === 1 ? p : null;
22182
+ }
22183
+
22184
+ // True when `node` is an owned child of a composite widget: its role is a
22185
+ // composite-child role and a matching container role is present on one of
22186
+ // its ancestors. The roving tab stop (tabindex="0") such a container places
22187
+ // on its active child does not make that child an independently nested
22188
+ // control.
22189
+ function isManagedCompositeChild(node) {
22190
+ const role = getExplicitRole(node);
22191
+ const containers = COMPOSITE_CHILD_CONTAINERS[role];
22192
+ if (!containers) return false;
22193
+ let p = parentElementOf(node);
22194
+ while (p && p.nodeType === 1) {
22195
+ if (containers.indexOf(getExplicitRole(p)) !== -1) return true;
22196
+ p = parentElementOf(p);
22197
+ }
22198
+ return false;
22199
+ }
22200
+
22201
+ // Operable = a native-interactive / ARIA-widget element that is exposed to
22202
+ // the accessibility tree and platform-focusable. A widget-role element with
22203
+ // no independent focus (e.g. a role="option" with no tabindex, driven by
22204
+ // its container via roving tabindex or aria-activedescendant) is not
22205
+ // operable and does not nest an interactive control. When no focus model is
22206
+ // available, role membership alone qualifies. (Composite-owned children are
22207
+ // handled earlier as attribution boundaries in collectNestedOperable, so
22208
+ // they never reach here.)
22209
+ function isOperableInteractive(node) {
22210
+ if (!matchesInteractive(node)) return false;
22211
+ if (!isEligible(node)) return false;
22212
+ if (!getFocusableInfo) return true;
22213
+ try {
22214
+ const info = getFocusableInfo(node, ctx);
22215
+ return !!(info && info.focusable);
22216
+ } catch {
22217
+ return true;
22218
+ }
22219
+ }
22220
+
22221
+ // Shallowest operable interactive descendants of `root`. Traversal stops at
22222
+ // each counted node instead of descending into it, so a control nested
22223
+ // inside another operable control is attributed to its nearest operable
22224
+ // ancestor rather than to every enclosing container. Eligibility is applied
22225
+ // per node during the walk, so hidden or aria-hidden subtrees drop out.
22226
+ function collectNestedOperable(root) {
22227
+ const out = [];
22228
+ const top = root && root.children;
22229
+ if (!top || !top.length) return out;
22230
+ const stack = [];
22231
+ for (let i = top.length - 1; i >= 0; i--) stack.push(top[i]);
22232
+ while (stack.length) {
22233
+ const node = stack.pop();
22234
+ if (node && node.nodeType === 1) {
22235
+ // A composite-owned child (option in a listbox/combobox, tab in a
22236
+ // tablist, ...) is not a nested interactive control: its container
22237
+ // owns it and drives its focus (roving tabindex or
22238
+ // aria-activedescendant). Treat it as an attribution boundary — do not
22239
+ // count it, and do not descend past it. Any control genuinely nested
22240
+ // inside it is attributed to the child itself (examined as its own
22241
+ // container in the main loop), keeping the report at the nearest
22242
+ // interactive ancestor rather than bubbling up to the composite.
22243
+ if (isManagedCompositeChild(node)) continue;
22244
+ if (isOperableInteractive(node)) {
22245
+ out.push(node);
22246
+ continue; // do not descend into a counted control
22247
+ }
22248
+ }
22249
+ const kids = node && node.children;
22250
+ if (kids && kids.length) {
22251
+ for (let i = kids.length - 1; i >= 0; i--) stack.push(kids[i]);
22252
+ }
22253
+ }
22254
+ return out;
22255
+ }
22256
+
20813
22257
  const nodes = helpers.queryAllSmart
20814
22258
  ? helpers.queryAllSmart(INTERACTIVE_SELECTOR)
20815
22259
  : helpers.queryAll(INTERACTIVE_SELECTOR);
@@ -20818,30 +22262,16 @@ function runa11yCoreInPage(pageUrl, contextSelector, engineOptions, runOnly) {
20818
22262
  let applicableCount = 0;
20819
22263
 
20820
22264
  for (const el of nodes) {
20821
- if (!el || !el.querySelectorAll) continue;
22265
+ if (!el || el.nodeType !== 1) continue;
20822
22266
  if (!isEligible(el)) continue;
20823
22267
 
20824
22268
  applicableCount += 1;
20825
22269
 
20826
- // The nested search uses the raw native querySelectorAll (not
20827
- // helpers.queryAllSmart), so it isn't subject to that helper's
20828
- // default hidden-content policy at all -- not even hard CSS-based
20829
- // hiding, let alone aria-hidden. A nested descendant that is never
20830
- // actually rendered or exposed to AT (display:none, aria-hidden,
20831
- // etc.) creates no real ambiguity for ANY user, since it isn't there
20832
- // to be confused with the outer control.
20833
- let nested;
20834
- try {
20835
- nested = Array.from(el.querySelectorAll(INTERACTIVE_SELECTOR)).filter(isEligible);
20836
- } catch {
20837
- nested = [];
20838
- }
22270
+ const nested = collectNestedOperable(el);
20839
22271
  if (!nested.length) continue;
20840
22272
 
20841
22273
  const nestedTags = nested.map((n) => (n && n.tagName ? n.tagName.toLowerCase() : 'unknown'));
20842
- const dedupedNestedTags = [
20843
- ...new Set(nested.map((n) => (n && n.tagName ? n.tagName.toLowerCase() : 'unknown')))
20844
- ];
22274
+ const dedupedNestedTags = [...new Set(nestedTags)];
20845
22275
 
20846
22276
  const tag = el.tagName.toLowerCase();
20847
22277
  const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
@@ -20964,8 +22394,15 @@ function runa11yCoreInPage(pageUrl, contextSelector, engineOptions, runOnly) {
20964
22394
  const getEligibilityInfo =
20965
22395
  helpers && typeof helpers.getEligibilityInfo === 'function' ? helpers.getEligibilityInfo : null;
20966
22396
 
20967
- const isAccTreeEligible =
20968
- helpers && typeof helpers.isAccTreeEligible === 'function' ? helpers.isAccTreeEligible : null;
22397
+ // ACT 8fc3b6 applies only to objects included in the accessibility tree, so a
22398
+ // focusable object inside aria-hidden is out of scope; aria-hidden-focus
22399
+ // reports that markup instead.
22400
+ const isEligibleHelper =
22401
+ helpers && typeof helpers.isIncludedInAccessibilityTree === 'function'
22402
+ ? helpers.isIncludedInAccessibilityTree
22403
+ : helpers && typeof helpers.isAccTreeEligible === 'function'
22404
+ ? helpers.isAccTreeEligible
22405
+ : null;
20969
22406
 
20970
22407
  const getFocusableInfo =
20971
22408
  helpers && typeof helpers.getFocusableInfo === 'function' ? helpers.getFocusableInfo : null;
@@ -21041,14 +22478,15 @@ function runa11yCoreInPage(pageUrl, contextSelector, engineOptions, runOnly) {
21041
22478
  for (const el of objects) {
21042
22479
  if (!el || !el.getAttribute) continue;
21043
22480
 
21044
- if (isAccTreeEligible) {
22481
+ if (isEligibleHelper) {
21045
22482
  const elig = (() => {
21046
22483
  try {
21047
- return isAccTreeEligible(el, ctx);
22484
+ return isEligibleHelper(el, ctx);
21048
22485
  } catch {
21049
22486
  return { eligible: true, reasons: [] };
21050
22487
  }
21051
22488
  })();
22489
+ if (elig === false) continue;
21052
22490
  if (elig && elig.eligible === false) continue;
21053
22491
  }
21054
22492
 
@@ -21356,9 +22794,16 @@ function runa11yCoreInPage(pageUrl, contextSelector, engineOptions, runOnly) {
21356
22794
  return '';
21357
22795
  }
21358
22796
 
22797
+ // Naming rules apply only to elements included in the accessibility tree
22798
+ // (ACT c487ae and siblings), which excludes focusable aria-hidden content.
22799
+ // aria-hidden-focus (ACT 6cfa84) covers that markup instead.
21359
22800
  function isEligibleAcc(helpers, el, ctx) {
21360
22801
  const fn =
21361
- helpers && typeof helpers.isAccTreeEligible === 'function' ? helpers.isAccTreeEligible : null;
22802
+ helpers && typeof helpers.isIncludedInAccessibilityTree === 'function'
22803
+ ? helpers.isIncludedInAccessibilityTree
22804
+ : helpers && typeof helpers.isAccTreeEligible === 'function'
22805
+ ? helpers.isAccTreeEligible
22806
+ : null;
21362
22807
  if (!fn) return true;
21363
22808
  try {
21364
22809
  const r = fn(el, ctx);
@@ -22147,9 +23592,16 @@ function runa11yCoreInPage(pageUrl, contextSelector, engineOptions, runOnly) {
22147
23592
  return '';
22148
23593
  }
22149
23594
 
23595
+ // Naming rules apply only to elements included in the accessibility tree
23596
+ // (ACT c487ae and siblings), which excludes focusable aria-hidden content.
23597
+ // aria-hidden-focus (ACT 6cfa84) covers that markup instead.
22150
23598
  function isEligibleAcc(helpers, el, ctx) {
22151
23599
  const fn =
22152
- helpers && typeof helpers.isAccTreeEligible === 'function' ? helpers.isAccTreeEligible : null;
23600
+ helpers && typeof helpers.isIncludedInAccessibilityTree === 'function'
23601
+ ? helpers.isIncludedInAccessibilityTree
23602
+ : helpers && typeof helpers.isAccTreeEligible === 'function'
23603
+ ? helpers.isAccTreeEligible
23604
+ : null;
22153
23605
  if (!fn) return true;
22154
23606
  try {
22155
23607
  const r = fn(el, ctx);
@@ -22980,9 +24432,16 @@ function runa11yCoreInPage(pageUrl, contextSelector, engineOptions, runOnly) {
22980
24432
  return '';
22981
24433
  }
22982
24434
 
24435
+ // Naming rules apply only to elements included in the accessibility tree
24436
+ // (ACT c487ae and siblings), which excludes focusable aria-hidden content.
24437
+ // aria-hidden-focus (ACT 6cfa84) covers that markup instead.
22983
24438
  function isEligibleAcc(helpers, el, ctx) {
22984
24439
  const fn =
22985
- helpers && typeof helpers.isAccTreeEligible === 'function' ? helpers.isAccTreeEligible : null;
24440
+ helpers && typeof helpers.isIncludedInAccessibilityTree === 'function'
24441
+ ? helpers.isIncludedInAccessibilityTree
24442
+ : helpers && typeof helpers.isAccTreeEligible === 'function'
24443
+ ? helpers.isAccTreeEligible
24444
+ : null;
22986
24445
  if (!fn) return true;
22987
24446
  try {
22988
24447
  const r = fn(el, ctx);
@@ -23463,9 +24922,16 @@ function runa11yCoreInPage(pageUrl, contextSelector, engineOptions, runOnly) {
23463
24922
  return '';
23464
24923
  }
23465
24924
 
24925
+ // Naming rules apply only to elements included in the accessibility tree
24926
+ // (ACT c487ae and siblings), which excludes focusable aria-hidden content.
24927
+ // aria-hidden-focus (ACT 6cfa84) covers that markup instead.
23466
24928
  function isEligibleAcc(helpers, el, ctx) {
23467
24929
  const fn =
23468
- helpers && typeof helpers.isAccTreeEligible === 'function' ? helpers.isAccTreeEligible : null;
24930
+ helpers && typeof helpers.isIncludedInAccessibilityTree === 'function'
24931
+ ? helpers.isIncludedInAccessibilityTree
24932
+ : helpers && typeof helpers.isAccTreeEligible === 'function'
24933
+ ? helpers.isAccTreeEligible
24934
+ : null;
23469
24935
  if (!fn) return true;
23470
24936
  try {
23471
24937
  const r = fn(el, ctx);
@@ -23479,7 +24945,9 @@ function runa11yCoreInPage(pageUrl, contextSelector, engineOptions, runOnly) {
23479
24945
  const occurrences = [];
23480
24946
  let applicableCount = 0;
23481
24947
 
23482
- const selector = 'input[type="range"], [role="slider"]';
24948
+ // Native input[type=range] belongs to
24949
+ // form-control-programmatic-label-present.
24950
+ const selector = '[role="slider"]';
23483
24951
  const nodes = helpers.queryAllSmart
23484
24952
  ? helpers.queryAllSmart(selector)
23485
24953
  : helpers.queryAll(selector);
@@ -23683,9 +25151,16 @@ function runa11yCoreInPage(pageUrl, contextSelector, engineOptions, runOnly) {
23683
25151
  return '';
23684
25152
  }
23685
25153
 
25154
+ // Naming rules apply only to elements included in the accessibility tree
25155
+ // (ACT c487ae and siblings), which excludes focusable aria-hidden content.
25156
+ // aria-hidden-focus (ACT 6cfa84) covers that markup instead.
23686
25157
  function isEligibleAcc(helpers, el, ctx) {
23687
25158
  const fn =
23688
- helpers && typeof helpers.isAccTreeEligible === 'function' ? helpers.isAccTreeEligible : null;
25159
+ helpers && typeof helpers.isIncludedInAccessibilityTree === 'function'
25160
+ ? helpers.isIncludedInAccessibilityTree
25161
+ : helpers && typeof helpers.isAccTreeEligible === 'function'
25162
+ ? helpers.isAccTreeEligible
25163
+ : null;
23689
25164
  if (!fn) return true;
23690
25165
  try {
23691
25166
  const r = fn(el, ctx);
@@ -23880,9 +25355,16 @@ function runa11yCoreInPage(pageUrl, contextSelector, engineOptions, runOnly) {
23880
25355
  return '';
23881
25356
  }
23882
25357
 
25358
+ // Naming rules apply only to elements included in the accessibility tree
25359
+ // (ACT c487ae and siblings), which excludes focusable aria-hidden content.
25360
+ // aria-hidden-focus (ACT 6cfa84) covers that markup instead.
23883
25361
  function isEligibleAcc(helpers, el, ctx) {
23884
25362
  const fn =
23885
- helpers && typeof helpers.isAccTreeEligible === 'function' ? helpers.isAccTreeEligible : null;
25363
+ helpers && typeof helpers.isIncludedInAccessibilityTree === 'function'
25364
+ ? helpers.isIncludedInAccessibilityTree
25365
+ : helpers && typeof helpers.isAccTreeEligible === 'function'
25366
+ ? helpers.isAccTreeEligible
25367
+ : null;
23886
25368
  if (!fn) return true;
23887
25369
  try {
23888
25370
  const r = fn(el, ctx);
@@ -24712,9 +26194,16 @@ function runa11yCoreInPage(pageUrl, contextSelector, engineOptions, runOnly) {
24712
26194
  return '';
24713
26195
  }
24714
26196
 
26197
+ // Naming rules apply only to elements included in the accessibility tree
26198
+ // (ACT c487ae and siblings), which excludes focusable aria-hidden content.
26199
+ // aria-hidden-focus (ACT 6cfa84) covers that markup instead.
24715
26200
  function isEligibleAcc(helpers, el, ctx) {
24716
26201
  const fn =
24717
- helpers && typeof helpers.isAccTreeEligible === 'function' ? helpers.isAccTreeEligible : null;
26202
+ helpers && typeof helpers.isIncludedInAccessibilityTree === 'function'
26203
+ ? helpers.isIncludedInAccessibilityTree
26204
+ : helpers && typeof helpers.isAccTreeEligible === 'function'
26205
+ ? helpers.isAccTreeEligible
26206
+ : null;
24718
26207
  if (!fn) return true;
24719
26208
  try {
24720
26209
  const r = fn(el, ctx);
@@ -25106,16 +26595,73 @@ function runa11yCoreInPage(pageUrl, contextSelector, engineOptions, runOnly) {
25106
26595
  const occurrences = [];
25107
26596
  let applicableCount = 0;
25108
26597
 
26598
+ function explicitRole(el) {
26599
+ try {
26600
+ return String((el && el.getAttribute && el.getAttribute('role')) || '')
26601
+ .trim()
26602
+ .toLowerCase()
26603
+ .split(/\s+/)[0];
26604
+ } catch {
26605
+ return '';
26606
+ }
26607
+ }
26608
+
26609
+ function isIncludedInTree(el) {
26610
+ const fn =
26611
+ helpers && typeof helpers.isIncludedInAccessibilityTree === 'function'
26612
+ ? helpers.isIncludedInAccessibilityTree
26613
+ : null;
26614
+ if (!fn) return true;
26615
+ try {
26616
+ const r = fn(el, ctx);
26617
+ return typeof r === 'boolean' ? r : !!(r && r.eligible);
26618
+ } catch {
26619
+ return true;
26620
+ }
26621
+ }
26622
+
26623
+ // role="presentation"/"none" removes the table role, and with it every
26624
+ // header cell the role would have implied. A table is not focusable, so
26625
+ // presentational role conflict resolution does not restore it.
26626
+ function hasTableSemantics(table) {
26627
+ const role = explicitRole(table);
26628
+ if (role === 'presentation' || role === 'none') return false;
26629
+ if (role && role !== 'table' && role !== 'grid' && role !== 'treegrid') return false;
26630
+ return isIncludedInTree(table);
26631
+ }
26632
+
26633
+ // A <th> only carries a rowheader/columnheader role while nothing overrides
26634
+ // it, and a hidden header describes nothing to a screen reader either way.
26635
+ function isHeaderCellInScope(th) {
26636
+ if (!th) return false;
26637
+ const role = explicitRole(th);
26638
+ if (role && role !== 'rowheader' && role !== 'columnheader') return false;
26639
+ if (helpers.isDomVisibleEligible) {
26640
+ try {
26641
+ if (!helpers.isDomVisibleEligible(th, ctx, { targetSet: 'dom' }).eligible) return false;
26642
+ } catch {
26643
+ // fall through to the tree check
26644
+ }
26645
+ }
26646
+ return isIncludedInTree(th);
26647
+ }
26648
+
25109
26649
  for (const table of tables) {
25110
26650
  if (!table || !table.querySelectorAll) continue;
25111
26651
 
26652
+ // A table stripped of its semantics has no header cells to describe
26653
+ // anything, so nothing in it is in scope.
26654
+ if (!hasTableSemantics(table)) continue;
26655
+
25112
26656
  let ths;
25113
26657
  try {
25114
26658
  ths = table.querySelectorAll('th');
25115
26659
  } catch {
25116
26660
  ths = [];
25117
26661
  }
25118
- if (!ths.length) continue;
26662
+
26663
+ const headers = Array.from(ths).filter(isHeaderCellInScope);
26664
+ if (!headers.length) continue;
25119
26665
 
25120
26666
  applicableCount += 1;
25121
26667
 
@@ -25127,7 +26673,7 @@ function runa11yCoreInPage(pageUrl, contextSelector, engineOptions, runOnly) {
25127
26673
  }
25128
26674
  if (hasDataCell) continue;
25129
26675
 
25130
- for (const th of ths) {
26676
+ for (const th of headers) {
25131
26677
  if (!th) continue;
25132
26678
  const stableSelector = helpers.buildSelector ? helpers.buildSelector(th) : 'html';
25133
26679
  const html = helpers.getOuterHtmlSnippet
@@ -25933,9 +27479,16 @@ function runa11yCoreInPage(pageUrl, contextSelector, engineOptions, runOnly) {
25933
27479
  return '';
25934
27480
  }
25935
27481
 
27482
+ // Naming rules apply only to elements included in the accessibility tree
27483
+ // (ACT c487ae and siblings), which excludes focusable aria-hidden content.
27484
+ // aria-hidden-focus (ACT 6cfa84) covers that markup instead.
25936
27485
  function isEligibleAcc(helpers, el, ctx) {
25937
27486
  const fn =
25938
- helpers && typeof helpers.isAccTreeEligible === 'function' ? helpers.isAccTreeEligible : null;
27487
+ helpers && typeof helpers.isIncludedInAccessibilityTree === 'function'
27488
+ ? helpers.isIncludedInAccessibilityTree
27489
+ : helpers && typeof helpers.isAccTreeEligible === 'function'
27490
+ ? helpers.isAccTreeEligible
27491
+ : null;
25939
27492
  if (!fn) return true;
25940
27493
  try {
25941
27494
  const r = fn(el, ctx);
@@ -26130,9 +27683,16 @@ function runa11yCoreInPage(pageUrl, contextSelector, engineOptions, runOnly) {
26130
27683
  return '';
26131
27684
  }
26132
27685
 
27686
+ // Naming rules apply only to elements included in the accessibility tree
27687
+ // (ACT c487ae and siblings), which excludes focusable aria-hidden content.
27688
+ // aria-hidden-focus (ACT 6cfa84) covers that markup instead.
26133
27689
  function isEligibleAcc(helpers, el, ctx) {
26134
27690
  const fn =
26135
- helpers && typeof helpers.isAccTreeEligible === 'function' ? helpers.isAccTreeEligible : null;
27691
+ helpers && typeof helpers.isIncludedInAccessibilityTree === 'function'
27692
+ ? helpers.isIncludedInAccessibilityTree
27693
+ : helpers && typeof helpers.isAccTreeEligible === 'function'
27694
+ ? helpers.isAccTreeEligible
27695
+ : null;
26136
27696
  if (!fn) return true;
26137
27697
  try {
26138
27698
  const r = fn(el, ctx);
@@ -26274,9 +27834,16 @@ function runa11yCoreInPage(pageUrl, contextSelector, engineOptions, runOnly) {
26274
27834
  return '';
26275
27835
  }
26276
27836
 
27837
+ // Naming rules apply only to elements included in the accessibility tree
27838
+ // (ACT c487ae and siblings), which excludes focusable aria-hidden content.
27839
+ // aria-hidden-focus (ACT 6cfa84) covers that markup instead.
26277
27840
  function isEligibleAcc(helpers, el, ctx) {
26278
27841
  const fn =
26279
- helpers && typeof helpers.isAccTreeEligible === 'function' ? helpers.isAccTreeEligible : null;
27842
+ helpers && typeof helpers.isIncludedInAccessibilityTree === 'function'
27843
+ ? helpers.isIncludedInAccessibilityTree
27844
+ : helpers && typeof helpers.isAccTreeEligible === 'function'
27845
+ ? helpers.isAccTreeEligible
27846
+ : null;
26280
27847
  if (!fn) return true;
26281
27848
  try {
26282
27849
  const r = fn(el, ctx);
@@ -26365,7 +27932,13 @@ function runa11yCoreInPage(pageUrl, contextSelector, engineOptions, runOnly) {
26365
27932
  "valid-lang": { run: (function runInPage(ctx) {
26366
27933
  const { helpers, rule } = ctx;
26367
27934
 
27935
+ // Shape alone accepts unregistered tags such as "eng" and "em-US", so the
27936
+ // primary subtag is checked against the IANA registry via the shared helper.
26368
27937
  const BCP47_RE = /^[a-zA-Z]{2,3}(-[a-zA-Z0-9]{2,8})*$/;
27938
+ const isValidTag =
27939
+ typeof helpers.isValidLanguageTag === 'function'
27940
+ ? helpers.isValidLanguageTag
27941
+ : (v) => BCP47_RE.test(String(v || ''));
26369
27942
 
26370
27943
  const nodes = helpers.queryAllSmart
26371
27944
  ? helpers.queryAllSmart('[lang]')
@@ -26378,12 +27951,19 @@ function runa11yCoreInPage(pageUrl, contextSelector, engineOptions, runOnly) {
26378
27951
  if (!el || !el.getAttribute) continue;
26379
27952
  if (el.tagName && el.tagName.toLowerCase() === 'html') continue;
26380
27953
 
26381
- const raw = String(el.getAttribute('lang') || '').trim();
26382
- if (!raw) continue;
27954
+ const rawAttr = el.getAttribute('lang');
27955
+ if (rawAttr === null || rawAttr === '') continue; // ACT de46e4: empty is out of scope
27956
+
27957
+ // The rule applies only where text actually inherits the language, so an
27958
+ // element with no text, or whose text reaches no one, has nothing to
27959
+ // declare a language for.
27960
+ if (!String(el.textContent || '').trim()) continue;
26383
27961
 
26384
27962
  applicableCount += 1;
26385
27963
 
26386
- if (BCP47_RE.test(raw)) continue;
27964
+ // A whitespace-only value is in scope and has no primary language tag.
27965
+ const raw = String(rawAttr).trim();
27966
+ if (isValidTag(raw)) continue;
26387
27967
 
26388
27968
  const tag = el.tagName.toLowerCase();
26389
27969
  const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
@@ -26685,6 +28265,10 @@ const I18N = {
26685
28265
  "inputImage_altPresent_description": "Prüft, ob <input type=\"image\">-Elemente ein alt-Attribut bereitstellen, um einen Mechanismus für eine Textalternative zu unterstützen.",
26686
28266
  "inputImage_altPresent_summary_fail": "Fehlendes alt-Attribut auf <input type=\"image\">.",
26687
28267
  "inputImage_altPresent_hint_fail": "Fügen Sie ein alt-Attribut hinzu (verwenden Sie alt=\"\" nur, wenn ein separater zugänglicher Name bereitgestellt wird).",
28268
+ "inputImage_altPresent_summary_defaultName": "Der zugaengliche Name ist der Browser-Standard fuer eine Bildschaltflaeche und sagt nichts aus.",
28269
+ "inputImage_altPresent_hint_defaultName": "Ersetzen Sie ihn durch Text, der die Aktion beschreibt, zum Beispiel \"Suchen\".",
28270
+ "inputImage_altPresent_summary_emptyAlt": "Ein leeres alt=\"\" auf <input type=\"image\"> laesst das Steuerelement ohne Namen.",
28271
+ "inputImage_altPresent_hint_emptyAlt": "Beschreiben Sie die Aktion in alt, oder benennen Sie das Steuerelement mit aria-label oder aria-labelledby.",
26688
28272
  "ariaHidden_programmaticFocus_review_title": "Programmatischen Fokus bei aria-hidden überprüfen",
26689
28273
  "ariaHidden_programmaticFocus_review_description": "Markiert Elemente, die aria-hidden sind, aber aufgrund von programmatischem Fokus (z. B. tabindex < 0) als geeignet gelten. Überprüfen Sie die beabsichtigte Fokusverwaltung und die Sichtbarkeit für assistive Technologien.",
26690
28274
  "ariaHidden_programmaticFocus_review_summary": "Überprüfung: Ein aria-hidden-Element ist programmatisch fokussierbar.",
@@ -26966,16 +28550,21 @@ const I18N = {
26966
28550
  "labelInName_description": "Prüft, ob bei einem Formularelement mit sichtbarer Textbeschriftung der zugängliche Name diesen sichtbaren Beschriftungstext enthält (WCAG 2.5.3).",
26967
28551
  "labelInName_summary_fail": "{{element}}: Die sichtbare Beschriftung „{{visibleLabel}}“ (aus {{labelSource}}) ist nicht im zugänglichen Namen enthalten (aus {{nameMechanism}}).",
26968
28552
  "labelInName_hint_fail": "Aktualisieren Sie aria-label/aria-labelledby (oder den sichtbaren Beschriftungstext), damit der zugängliche Name den Wortlaut der sichtbaren Beschriftung enthält.",
28553
+ "labelInName_summary_cantTell": "{{element}}: Die sichtbare Beschriftung „{{visibleLabel}}“ (aus {{labelSource}}) unterscheidet sich vom zugänglichen Namen (aus {{nameMechanism}}) nur durch eine Abkürzung oder eine Bindestrichschreibung.",
28554
+ "labelInName_hint_cantTell": "Prüfen Sie manuell, ob beide Formulierungen übereinstimmen: Aus dem Markup lässt sich eine beabsichtigte Abkürzung nicht von einer Abweichung unterscheiden.",
26969
28555
  "ariaRolesValid_title": "Das role-Attribut muss eine gültige, nicht abstrakte ARIA-Rolle sein",
26970
28556
  "ariaRolesValid_description": "Prüft, ob ein explizites role=\"\"-Attribut zu einer echten, nicht abstrakten WAI-ARIA-Rolle aufgelöst wird.",
26971
28557
  "ariaRolesValid_summary_invalid": "role=\"{{role}}\" ist keine erkannte ARIA-Rolle.",
26972
28558
  "ariaRolesValid_hint_invalid": "Verwenden Sie einen gültigen ARIA-Rollenwert, oder entfernen Sie das role-Attribut, wenn keine Rolle zutrifft.",
26973
28559
  "ariaRolesValid_summary_abstract": "role=\"{{role}}\" ist eine abstrakte ARIA-Rolle, die nicht direkt verwendet werden darf.",
26974
28560
  "ariaRolesValid_hint_abstract": "Ersetzen Sie diese abstrakte Rolle durch eine konkrete Rolle, die zu dem Widget/der Struktur passt.",
26975
- "ariaDeprecatedRole_title": "Das role-Attribut darf keine veraltete oder für Autoren untersagte ARIA-Rolle verwenden",
26976
- "ariaDeprecatedRole_description": "Prüft, ob ein explizites role=\"\"-Attribut keine durch die WAI-ARIA-Spezifikation als veraltet markierte Rolle verwendet, und keine Rolle, die ausschließlich für die interne Verwendung durch den User-Agent reserviert ist (z. B. role=\"generic\").",
28561
+ "ariaDeprecatedRole_title": "Das role-Attribut sollte keine veraltete oder für Autoren nicht empfohlene ARIA-Rolle verwenden",
28562
+ "ariaDeprecatedRole_description": "Prüft, ob ein explizites role=\"\"-Attribut keine durch die WAI-ARIA-Spezifikation als veraltet markierte Rolle verwendet, und keine Rolle, die für die interne Verwendung durch den User-Agent reserviert ist (z. B. role=\"generic\").",
26977
28563
  "ariaDeprecatedRole_summary_fail": "Dieses Element verwendet role=\"{{role}}\", was Autoren nicht explizit deklarieren dürfen.",
26978
28564
  "ariaDeprecatedRole_hint_fail": "{{guidance}}",
28565
+ "ariaDeprecatedRole_summary_cantTell": "Dieses Element verwendet role=\"{{role}}\", die in WAI-ARIA veraltet ist (weiterhin gültig, aber nicht empfohlen).",
28566
+ "ariaDeprecatedRole_summary_cantTell_discouraged": "Dieses Element verwendet role=\"{{role}}\", die User-Agents vorbehalten ist (weiterhin gültig, aber nicht empfohlen).",
28567
+ "ariaDeprecatedRole_hint_cantTell": "{{guidance}}",
26979
28568
  "ariaValidAttr_title": "aria-*-Attribute müssen echte, definierte ARIA-Attribute sein",
26980
28569
  "ariaValidAttr_description": "Prüft, ob jeder im DOM vorhandene aria-*-Attributname ein echtes, durch die WAI-ARIA-Spezifikation definiertes Attribut ist.",
26981
28570
  "ariaValidAttr_summary_fail": "{{attr}} ist kein erkanntes ARIA-Attribut.",
@@ -26988,6 +28577,8 @@ const I18N = {
26988
28577
  "ariaAllowedAttr_description": "Prüft, ob jedes erkannte aria-*-Attribut auf einem Element mit expliziter Rolle entweder global unterstützt wird oder von dieser Rolle unterstützt wird.",
26989
28578
  "ariaAllowedAttr_summary_fail": "{{attr}} ist bei role=\"{{role}}\" nicht zulässig.",
26990
28579
  "ariaAllowedAttr_hint_fail": "Entfernen Sie dieses Attribut, oder verwenden Sie eine Rolle, die es unterstützt.",
28580
+ "ariaAllowedAttr_summary_cantTell": "{{attr}} ist bei role=\"{{role}}\" veraltet (weiterhin zulässig, aber nicht empfohlen).",
28581
+ "ariaAllowedAttr_hint_cantTell": "Dieses Attribut wurde in ARIA 1.2 aus dem globalen Satz entfernt; entfernen Sie es oder verwenden Sie eine Rolle, die es unterstützt, da eine künftige ARIA-Version es möglicherweise nicht mehr zulässt.",
26991
28582
  "ariaProhibitedAttr_title": "ARIA-Benennungsattribute dürfen nicht bei Rollen verwendet werden, die sie untersagen",
26992
28583
  "ariaProhibitedAttr_description": "Prüft, ob aria-label/aria-labelledby nicht bei WAI-ARIA-Rollen vorhanden sind, deren Spezifikation die ARIA-Benennung ausdrücklich untersagt (z. B. generic, emphasis, strong, paragraph).",
26993
28584
  "ariaProhibitedAttr_summary_fail": "{{attr}} ist bei role=\"{{role}}\" untersagt.",
@@ -27102,14 +28693,16 @@ const I18N = {
27102
28693
  "formControlSingleLabel_description": "Prüft, ob ein Formularelement mit höchstens einem <label> verknüpft ist (durch Umschließen oder durch label[for]).",
27103
28694
  "formControlSingleLabel_summary_fail": "Dieses <{{element}}> ist mit {{labelCount}} Beschriftungen verknüpft.",
27104
28695
  "formControlSingleLabel_hint_fail": "Behalten Sie nur ein <label> pro Formularelement (entweder umschließend oder über for/id referenziert).",
28696
+ "formControlSingleLabel_summary_cantTell": "Dieses <{{element}}> hat ein beschriftendes <label> und zusätzlich eine leere <label>-Verknüpfung; prüfen Sie, wie es angesagt wird.",
28697
+ "formControlSingleLabel_hint_cantTell": "Entfernen Sie das überflüssige leere <label>, sodass genau ein <label> mit dem Element verknüpft ist.",
27105
28698
  "nestedInteractiveControlsAbsent_title": "Interaktive Formularelemente dürfen nicht verschachtelt sein",
27106
28699
  "nestedInteractiveControlsAbsent_description": "Prüft, ob ein interaktives Element (Link, Schaltfläche, Formularelement oder ARIA-Widget-Rolle) kein weiteres interaktives Element enthält.",
27107
28700
  "nestedInteractiveControlsAbsent_summary_fail": "Dieses <{{element}}> enthält ein oder mehrere verschachtelte interaktive Elemente: {{nestedElements}}.",
27108
28701
  "nestedInteractiveControlsAbsent_hint_fail": "Verschieben Sie die verschachtelten interaktiven Elemente außerhalb dieses Elements; verschachtelte interaktive Elemente sind über assistive Technologien nicht zuverlässig bedienbar.",
27109
28702
  "bypassBlocksPresent_title": "Die Seite muss eine Möglichkeit bieten, wiederkehrende Blöcke zu überspringen",
27110
28703
  "bypassBlocksPresent_description": "Prüft, ob die Seite mindestens einen anerkannten Mechanismus nach WCAG 2.4.1 zum Überspringen wiederkehrender Blöcke hat: eine main-Landmarke, einen funktionierenden Anker-Link auf derselben Seite oder eine Überschrift.",
27111
- "bypassBlocksPresent_summary_fail": "Diese Seite hat keine anerkannte Möglichkeit, wiederkehrende Inhaltsblöcke zu überspringen.",
27112
- "bypassBlocksPresent_hint_fail": "Fügen Sie eine main-Landmarke (<main> oder role=\"main\"), einen funktionierenden „Zum Inhalt springen“-Link oder Überschriften-Elemente hinzu, die assistive Technologien nutzen können, um wiederkehrende Inhalte zu überspringen.",
28704
+ "bypassBlocksPresent_summary_cantTell": "Auf dieser Seite wurde keine anerkannte Möglichkeit erkannt, wiederkehrende Inhaltsblöcke zu überspringen – prüfen Sie, ob ein Überspring-Mechanismus vorhanden ist.",
28705
+ "bypassBlocksPresent_hint_cantTell": "Bestätigen Sie, dass die Seite einen Überspring-Mechanismus bietet: eine main-Landmarke (<main> oder role=\"main\"), einen funktionierenden „Zum Inhalt springen“-Link oder Überschriften-Elemente, die assistive Technologien nutzen können, um wiederkehrende Inhalte zu überspringen. (Ein Mechanismus kann vorübergehend verborgen sein – z. B. während ein modaler Dialog die Seite inert macht – oder seitenweit bereitgestellt werden; dies erfordert eine menschliche Bestätigung.)",
27113
28706
  "landmarkBannerIsTopLevel_title": "Die banner-Landmarke muss auf oberster Ebene liegen",
27114
28707
  "landmarkBannerIsTopLevel_description": "Prüft, ob die banner-Landmarke (role=\"banner\" oder ein nicht verschachtelter <header>) nicht innerhalb einer anderen Landmarke verschachtelt ist.",
27115
28708
  "landmarkBannerIsTopLevel_summary_cantTell": "Diese banner-Landmarke ist innerhalb einer anderen Landmarke verschachtelt.",
@@ -27211,8 +28804,8 @@ const I18N = {
27211
28804
  "htmlXmlLangMismatch_description": "Prüft, ob die Attribute lang und xml:lang des <html>-Elements dieselbe Hauptsprache deklarieren, wenn beide vorhanden sind.",
27212
28805
  "htmlXmlLangMismatch_summary_fail": "Die Attribute lang („{{lang}}“) und xml:lang („{{xmlLang}}“) deklarieren unterschiedliche Sprachen.",
27213
28806
  "htmlXmlLangMismatch_hint_fail": "Lassen Sie lang und xml:lang dieselbe Hauptsprache deklarieren, oder entfernen Sie das veraltete Attribut xml:lang.",
27214
- "avoidInlineSpacing_title": "Inline-Stile dürfen den Textabstand nicht mit !important erzwingen",
27215
- "avoidInlineSpacing_description": "Prüft, ob Inline-Stile line-height, letter-spacing oder word-spacing nicht mit !important setzen, was das Überschreiben des Textabstands durch den Nutzer blockiert.",
28807
+ "avoidInlineSpacing_title": "Inline-Stile dürfen den Textabstand nicht unter den WCAG-Wert erzwingen",
28808
+ "avoidInlineSpacing_description": "Prüft, dass ein per !important erzwungener Wert für line-height, letter-spacing oder word-spacing WCAG 1.4.12 bereits erfüllt, sodass dem Nutzer nichts zu überschreiben bleibt.",
27216
28809
  "avoidInlineSpacing_summary_fail": "Der Inline-Stil dieses Elements erzwingt {{properties}} mit !important und blockiert damit Überschreibungen des Textabstands durch den Nutzer.",
27217
28810
  "avoidInlineSpacing_hint_fail": "Entfernen Sie !important von line-height/letter-spacing/word-spacing in Inline-Stilen, damit Nutzer den Textabstand überschreiben können.",
27218
28811
  "metaRefreshNoExceptions_title": "Die Seite darf überhaupt keinen Meta-Refresh verwenden (AAA)",
@@ -27301,6 +28894,10 @@ const I18N = {
27301
28894
  "inputImage_altPresent_description": "Checks that <input type=\"image\"> elements provide an alt attribute to support a text alternative mechanism.",
27302
28895
  "inputImage_altPresent_summary_fail": "Missing alt attribute on <input type=\"image\">.",
27303
28896
  "inputImage_altPresent_hint_fail": "Add an alt attribute (use alt=\"\" only when a separate accessible name is provided).",
28897
+ "inputImage_altPresent_summary_defaultName": "Accessible name is the browser default for an image button, which conveys nothing.",
28898
+ "inputImage_altPresent_hint_defaultName": "Replace it with text describing what the button does, for example \"Search\".",
28899
+ "inputImage_altPresent_summary_emptyAlt": "Empty alt=\"\" on <input type=\"image\"> leaves the control unnamed.",
28900
+ "inputImage_altPresent_hint_emptyAlt": "Describe the action in alt, or name the control with aria-label or aria-labelledby.",
27304
28901
  "ariaHidden_programmaticFocus_review_title": "Review aria-hidden programmatic focus",
27305
28902
  "ariaHidden_programmaticFocus_review_description": "Flags elements that are aria-hidden but considered eligible due to programmatic focus (e.g., tabindex < 0). Verify intended focus management and assistive technology exposure.",
27306
28903
  "ariaHidden_programmaticFocus_review_summary": "Review: aria-hidden element is programmatically focusable.",
@@ -27582,16 +29179,21 @@ const I18N = {
27582
29179
  "labelInName_description": "Checks that when a control has a visible text label, the accessible name contains that visible label text (WCAG 2.5.3).",
27583
29180
  "labelInName_summary_fail": "{{element}}: visible label \"{{visibleLabel}}\" (from {{labelSource}}) is not included in the accessible name (from {{nameMechanism}}).",
27584
29181
  "labelInName_hint_fail": "Update aria-label/aria-labelledby (or the visible label text) so the accessible name includes the visible label wording.",
29182
+ "labelInName_summary_cantTell": "{{element}}: visible label \"{{visibleLabel}}\" (from {{labelSource}}) differs from the accessible name (from {{nameMechanism}}) only by an abbreviation or by hyphenation.",
29183
+ "labelInName_hint_cantTell": "Check by hand whether the two wordings match: markup cannot tell an intended abbreviation from a mismatch.",
27585
29184
  "ariaRolesValid_title": "role attribute must be a valid, non-abstract ARIA role",
27586
29185
  "ariaRolesValid_description": "Checks that an explicit role=\"\" attribute resolves to a real, non-abstract WAI-ARIA role.",
27587
29186
  "ariaRolesValid_summary_invalid": "role=\"{{role}}\" is not a recognized ARIA role.",
27588
29187
  "ariaRolesValid_hint_invalid": "Use a valid ARIA role token, or remove the role attribute if none applies.",
27589
29188
  "ariaRolesValid_summary_abstract": "role=\"{{role}}\" is an abstract ARIA role, which must not be used directly.",
27590
29189
  "ariaRolesValid_hint_abstract": "Replace this abstract role with a concrete role appropriate for the widget/structure.",
27591
- "ariaDeprecatedRole_title": "role attribute must not use a deprecated or author-prohibited ARIA role",
27592
- "ariaDeprecatedRole_description": "Checks that an explicit role=\"\" attribute does not use a role deprecated by the WAI-ARIA specification, or one reserved for user-agent-internal use only (e.g. role=\"generic\").",
29190
+ "ariaDeprecatedRole_title": "role attribute should not use a deprecated or author-discouraged ARIA role",
29191
+ "ariaDeprecatedRole_description": "Checks that an explicit role=\"\" attribute does not use a role deprecated by the WAI-ARIA specification, or one reserved for user-agent-internal use (e.g. role=\"generic\").",
27593
29192
  "ariaDeprecatedRole_summary_fail": "This element uses role=\"{{role}}\", which authors must not explicitly declare.",
27594
29193
  "ariaDeprecatedRole_hint_fail": "{{guidance}}",
29194
+ "ariaDeprecatedRole_summary_cantTell": "This element uses role=\"{{role}}\", which is deprecated in WAI-ARIA (still valid, but discouraged).",
29195
+ "ariaDeprecatedRole_summary_cantTell_discouraged": "This element uses role=\"{{role}}\", which is reserved for user agents (still valid, but discouraged).",
29196
+ "ariaDeprecatedRole_hint_cantTell": "{{guidance}}",
27595
29197
  "ariaValidAttr_title": "aria-* attributes must be real, defined ARIA attributes",
27596
29198
  "ariaValidAttr_description": "Checks that every aria-* attribute name present in the DOM is a real attribute defined by the WAI-ARIA specification.",
27597
29199
  "ariaValidAttr_summary_fail": "{{attr}} is not a recognized ARIA attribute.",
@@ -27604,6 +29206,8 @@ const I18N = {
27604
29206
  "ariaAllowedAttr_description": "Checks that every recognized aria-* attribute present on an element with an explicit role is either globally supported or supported by that role.",
27605
29207
  "ariaAllowedAttr_summary_fail": "{{attr}} is not permitted on role=\"{{role}}\".",
27606
29208
  "ariaAllowedAttr_hint_fail": "Remove this attribute, or use a role that supports it.",
29209
+ "ariaAllowedAttr_summary_cantTell": "{{attr}} is deprecated on role=\"{{role}}\" (still allowed, but discouraged).",
29210
+ "ariaAllowedAttr_hint_cantTell": "This attribute was removed from the ARIA global set in 1.2; remove it or use a role that supports it, as a future ARIA version may disallow it.",
27607
29211
  "ariaProhibitedAttr_title": "ARIA naming attributes must not be used on roles that prohibit them",
27608
29212
  "ariaProhibitedAttr_description": "Checks that aria-label/aria-labelledby are not present on WAI-ARIA roles whose specification explicitly prohibits ARIA naming (e.g. generic, emphasis, strong, paragraph).",
27609
29213
  "ariaProhibitedAttr_summary_fail": "{{attr}} is prohibited on role=\"{{role}}\".",
@@ -27718,14 +29322,16 @@ const I18N = {
27718
29322
  "formControlSingleLabel_description": "Checks that a form control is associated with at most one <label> (by wrapping or by label[for]).",
27719
29323
  "formControlSingleLabel_summary_fail": "This <{{element}}> is associated with {{labelCount}} labels.",
27720
29324
  "formControlSingleLabel_hint_fail": "Keep only one <label> per form control (either wrapping it or referencing it via for/id).",
29325
+ "formControlSingleLabel_summary_cantTell": "This <{{element}}> has one labelling <label> plus an extra empty <label> association; verify how it is announced.",
29326
+ "formControlSingleLabel_hint_cantTell": "Remove the redundant empty <label> so exactly one <label> is associated with the control.",
27721
29327
  "nestedInteractiveControlsAbsent_title": "Interactive controls must not be nested",
27722
29328
  "nestedInteractiveControlsAbsent_description": "Checks that an interactive control (link, button, form control, or ARIA widget role) does not contain another interactive control.",
27723
29329
  "nestedInteractiveControlsAbsent_summary_fail": "This <{{element}}> contains one or more nested interactive controls: {{nestedElements}}.",
27724
29330
  "nestedInteractiveControlsAbsent_hint_fail": "Move the nested interactive control(s) outside this element; nested interactive controls are not reliably operable via assistive technology.",
27725
29331
  "bypassBlocksPresent_title": "Page must provide a way to bypass repeated blocks",
27726
29332
  "bypassBlocksPresent_description": "Checks that the page has at least one recognized WCAG 2.4.1 bypass-blocks mechanism: a main landmark, a working same-page anchor link, or a heading.",
27727
- "bypassBlocksPresent_summary_fail": "This page has no recognized way to bypass repeated blocks of content.",
27728
- "bypassBlocksPresent_hint_fail": "Add a main landmark (<main> or role=\"main\"), a working \"skip to content\" link, or heading elements that assistive technology can use to jump past repeated content.",
29333
+ "bypassBlocksPresent_summary_cantTell": "No recognized way to bypass repeated blocks of content was detected on this page — verify a bypass mechanism exists.",
29334
+ "bypassBlocksPresent_hint_cantTell": "Confirm the page offers a bypass mechanism: a main landmark (<main> or role=\"main\"), a working \"skip to content\" link, or heading elements that assistive technology can use to jump past repeated content. (A mechanism may be temporarily hidden — e.g. while a modal dialog makes the page inert — or provided on a per-site basis; this needs human confirmation.)",
27729
29335
  "landmarkBannerIsTopLevel_title": "Banner landmark must be top-level",
27730
29336
  "landmarkBannerIsTopLevel_description": "Checks that the banner landmark (role=\"banner\" or a non-nested <header>) is not nested inside another landmark region.",
27731
29337
  "landmarkBannerIsTopLevel_summary_cantTell": "This banner landmark is nested inside another landmark region.",
@@ -27827,8 +29433,8 @@ const I18N = {
27827
29433
  "htmlXmlLangMismatch_description": "Checks that the <html> element's lang and xml:lang attributes declare the same primary language, when both are present.",
27828
29434
  "htmlXmlLangMismatch_summary_fail": "The lang (\"{{lang}}\") and xml:lang (\"{{xmlLang}}\") attributes declare different languages.",
27829
29435
  "htmlXmlLangMismatch_hint_fail": "Make lang and xml:lang declare the same primary language, or remove the deprecated xml:lang attribute.",
27830
- "avoidInlineSpacing_title": "Inline style must not force text spacing with !important",
27831
- "avoidInlineSpacing_description": "Checks that inline style does not set line-height, letter-spacing, or word-spacing with !important, which blocks user text-spacing overrides.",
29436
+ "avoidInlineSpacing_title": "Inline style must not force text spacing below the WCAG metric",
29437
+ "avoidInlineSpacing_description": "Checks that where inline style forces line-height, letter-spacing or word-spacing with !important, the value already meets WCAG 1.4.12, so the user has nothing left to override.",
27832
29438
  "avoidInlineSpacing_summary_fail": "This element's inline style forces {{properties}} with !important, blocking user text-spacing overrides.",
27833
29439
  "avoidInlineSpacing_hint_fail": "Remove !important from line-height/letter-spacing/word-spacing in inline styles so users can override text spacing.",
27834
29440
  "metaRefreshNoExceptions_title": "Page must not use a meta refresh at all (AAA)",
@@ -27917,6 +29523,10 @@ const I18N = {
27917
29523
  "inputImage_altPresent_description": "Comprueba que los elementos <input type=\"image\"> incluyan un atributo alt para ofrecer un mecanismo de alternativa textual.",
27918
29524
  "inputImage_altPresent_summary_fail": "Falta el atributo alt en <input type=\"image\">.",
27919
29525
  "inputImage_altPresent_hint_fail": "Agregar un atributo alt (usar alt=\"\" solo cuando se proporcione un nombre accesible por separado).",
29526
+ "inputImage_altPresent_summary_defaultName": "El nombre accesible es el predeterminado del navegador para un boton de imagen y no aporta informacion.",
29527
+ "inputImage_altPresent_hint_defaultName": "Sustituyelo por un texto que describa la accion del boton, por ejemplo \"Buscar\".",
29528
+ "inputImage_altPresent_summary_emptyAlt": "Un alt=\"\" vacio en <input type=\"image\"> deja el control sin nombre.",
29529
+ "inputImage_altPresent_hint_emptyAlt": "Describe la accion en alt, o nombra el control con aria-label o aria-labelledby.",
27920
29530
  "ariaHidden_programmaticFocus_review_title": "Revisar el foco programático en aria-hidden",
27921
29531
  "ariaHidden_programmaticFocus_review_description": "Señala elementos aria-hidden considerados elegibles debido a un foco programático (por ejemplo, tabindex < 0). Verificar que la gestión del foco es intencionada y la exposición a las tecnologías de asistencia.",
27922
29532
  "ariaHidden_programmaticFocus_review_summary": "Revisión: un elemento aria-hidden es enfocable de forma programática.",
@@ -28198,16 +29808,21 @@ const I18N = {
28198
29808
  "labelInName_description": "Comprueba que, cuando un control tiene una etiqueta de texto visible, el nombre accesible contenga ese texto de etiqueta visible (WCAG 2.5.3).",
28199
29809
  "labelInName_summary_fail": "{{element}}: la etiqueta visible \"{{visibleLabel}}\" (de {{labelSource}}) no está incluida en el nombre accesible (de {{nameMechanism}}).",
28200
29810
  "labelInName_hint_fail": "Actualizar aria-label/aria-labelledby (o el texto de la etiqueta visible) para que el nombre accesible incluya el texto de la etiqueta visible.",
29811
+ "labelInName_summary_cantTell": "{{element}}: la etiqueta visible \"{{visibleLabel}}\" (de {{labelSource}}) se diferencia del nombre accesible (de {{nameMechanism}}) solo por una abreviatura o por la separación con guion.",
29812
+ "labelInName_hint_cantTell": "Comprobar manualmente si ambas redacciones coinciden: el marcado no permite distinguir una abreviatura intencionada de una discrepancia.",
28201
29813
  "ariaRolesValid_title": "El atributo role debe ser un rol ARIA válido y no abstracto",
28202
29814
  "ariaRolesValid_description": "Comprueba que un atributo role=\"\" explícito se resuelva en un rol WAI-ARIA real y no abstracto.",
28203
29815
  "ariaRolesValid_summary_invalid": "role=\"{{role}}\" no es un rol ARIA reconocido.",
28204
29816
  "ariaRolesValid_hint_invalid": "Usar un token de rol ARIA válido, o eliminar el atributo role si no aplica ninguno.",
28205
29817
  "ariaRolesValid_summary_abstract": "role=\"{{role}}\" es un rol ARIA abstracto, que no debe usarse directamente.",
28206
29818
  "ariaRolesValid_hint_abstract": "Reemplazar este rol abstracto por un rol concreto adecuado para el widget o la estructura.",
28207
- "ariaDeprecatedRole_title": "El atributo role no debe usar un rol ARIA obsoleto o prohibido para autores",
28208
- "ariaDeprecatedRole_description": "Comprueba que un atributo role=\"\" explícito no use un rol obsoleto según la especificación WAI-ARIA, ni uno reservado únicamente para uso interno del agente de usuario (por ejemplo, role=\"generic\").",
29819
+ "ariaDeprecatedRole_title": "El atributo role no debería usar un rol ARIA obsoleto o desaconsejado para autores",
29820
+ "ariaDeprecatedRole_description": "Comprueba que un atributo role=\"\" explícito no use un rol obsoleto según la especificación WAI-ARIA, ni uno reservado para uso interno del agente de usuario (por ejemplo, role=\"generic\").",
28209
29821
  "ariaDeprecatedRole_summary_fail": "Este elemento usa role=\"{{role}}\", que los autores no deben declarar explícitamente.",
28210
29822
  "ariaDeprecatedRole_hint_fail": "{{guidance}}",
29823
+ "ariaDeprecatedRole_summary_cantTell": "Este elemento usa role=\"{{role}}\", que está obsoleto en WAI-ARIA (todavía válido, pero desaconsejado).",
29824
+ "ariaDeprecatedRole_summary_cantTell_discouraged": "Este elemento usa role=\"{{role}}\", que está reservado para los agentes de usuario (todavía válido, pero desaconsejado).",
29825
+ "ariaDeprecatedRole_hint_cantTell": "{{guidance}}",
28211
29826
  "ariaValidAttr_title": "Los atributos aria-* deben ser atributos ARIA reales y definidos",
28212
29827
  "ariaValidAttr_description": "Comprueba que cada nombre de atributo aria-* presente en el DOM sea un atributo real definido por la especificación WAI-ARIA.",
28213
29828
  "ariaValidAttr_summary_fail": "{{attr}} no es un atributo ARIA reconocido.",
@@ -28220,6 +29835,8 @@ const I18N = {
28220
29835
  "ariaAllowedAttr_description": "Comprueba que cada atributo aria-* reconocido presente en un elemento con un rol explícito esté admitido globalmente o admitido por ese rol.",
28221
29836
  "ariaAllowedAttr_summary_fail": "{{attr}} no está permitido en role=\"{{role}}\".",
28222
29837
  "ariaAllowedAttr_hint_fail": "Eliminar este atributo, o usar un rol que lo admita.",
29838
+ "ariaAllowedAttr_summary_cantTell": "{{attr}} está obsoleto en role=\"{{role}}\" (todavía permitido, pero desaconsejado).",
29839
+ "ariaAllowedAttr_hint_cantTell": "Este atributo se eliminó del conjunto global de ARIA en 1.2; elimínelo o use un rol que lo admita, ya que una versión futura de ARIA podría no permitirlo.",
28223
29840
  "ariaProhibitedAttr_title": "Los atributos de nombrado ARIA no deben usarse en roles que los prohíban",
28224
29841
  "ariaProhibitedAttr_description": "Comprueba que aria-label/aria-labelledby no estén presentes en roles WAI-ARIA cuya especificación prohíbe explícitamente el nombrado ARIA (por ejemplo, generic, emphasis, strong, paragraph).",
28225
29842
  "ariaProhibitedAttr_summary_fail": "{{attr}} está prohibido en role=\"{{role}}\".",
@@ -28334,14 +29951,16 @@ const I18N = {
28334
29951
  "formControlSingleLabel_description": "Comprueba que un control de formulario esté asociado a como máximo una <label> (por envoltura o mediante label[for]).",
28335
29952
  "formControlSingleLabel_summary_fail": "Este <{{element}}> está asociado a {{labelCount}} etiquetas.",
28336
29953
  "formControlSingleLabel_hint_fail": "Mantener solo una <label> por control de formulario (ya sea envolviéndolo o referenciándolo mediante for/id).",
29954
+ "formControlSingleLabel_summary_cantTell": "Este <{{element}}> tiene una <label> que lo etiqueta más una asociación de <label> vacía adicional; verifique cómo se anuncia.",
29955
+ "formControlSingleLabel_hint_cantTell": "Elimine la <label> vacía redundante para que solo una <label> quede asociada al control.",
28337
29956
  "nestedInteractiveControlsAbsent_title": "Los controles interactivos no deben estar anidados",
28338
29957
  "nestedInteractiveControlsAbsent_description": "Comprueba que un control interactivo (enlace, botón, control de formulario o rol de widget ARIA) no contenga otro control interactivo.",
28339
29958
  "nestedInteractiveControlsAbsent_summary_fail": "Este <{{element}}> contiene uno o más controles interactivos anidados: {{nestedElements}}.",
28340
29959
  "nestedInteractiveControlsAbsent_hint_fail": "Mover el/los control(es) interactivo(s) anidado(s) fuera de este elemento; los controles interactivos anidados no son operables de forma fiable mediante tecnología de asistencia.",
28341
29960
  "bypassBlocksPresent_title": "La página debe proporcionar una forma de omitir bloques repetidos",
28342
29961
  "bypassBlocksPresent_description": "Comprueba que la página tenga al menos un mecanismo reconocido de omisión de bloques del criterio de éxito 2.4.1 de WCAG: un landmark main, un enlace de anclaje funcional dentro de la misma página, o un encabezado.",
28343
- "bypassBlocksPresent_summary_fail": "Esta página no tiene ninguna forma reconocida de omitir bloques de contenido repetidos.",
28344
- "bypassBlocksPresent_hint_fail": "Agregar un landmark main (<main> o role=\"main\"), un enlace funcional de \"saltar al contenido\", o elementos de encabezado que las tecnologías de asistencia puedan usar para saltar el contenido repetido.",
29962
+ "bypassBlocksPresent_summary_cantTell": "No se detectó ninguna forma reconocida de omitir bloques de contenido repetidos en esta página; verifique que exista un mecanismo de omisión.",
29963
+ "bypassBlocksPresent_hint_cantTell": "Confirme que la página ofrece un mecanismo de omisión: un landmark main (<main> o role=\"main\"), un enlace funcional de \"saltar al contenido\", o elementos de encabezado que las tecnologías de asistencia puedan usar para saltar el contenido repetido. (Un mecanismo puede estar oculto temporalmente —por ejemplo, mientras un diálogo modal deja la página inerte— o proporcionarse a nivel de sitio; esto requiere confirmación humana.)",
28345
29964
  "landmarkBannerIsTopLevel_title": "El landmark banner debe ser de nivel superior",
28346
29965
  "landmarkBannerIsTopLevel_description": "Comprueba que el landmark banner (role=\"banner\" o un <header> no anidado) no esté anidado dentro de otra región landmark.",
28347
29966
  "landmarkBannerIsTopLevel_summary_cantTell": "Este landmark banner está anidado dentro de otra región landmark.",
@@ -28443,8 +30062,8 @@ const I18N = {
28443
30062
  "htmlXmlLangMismatch_description": "Comprueba que los atributos lang y xml:lang del elemento <html> declaren el mismo idioma principal, cuando ambos están presentes.",
28444
30063
  "htmlXmlLangMismatch_summary_fail": "Los atributos lang (\"{{lang}}\") y xml:lang (\"{{xmlLang}}\") declaran idiomas diferentes.",
28445
30064
  "htmlXmlLangMismatch_hint_fail": "Hacer que lang y xml:lang declaren el mismo idioma principal, o eliminar el atributo obsoleto xml:lang.",
28446
- "avoidInlineSpacing_title": "El estilo en línea no debe forzar el espaciado de texto con !important",
28447
- "avoidInlineSpacing_description": "Comprueba que el estilo en línea no establezca line-height, letter-spacing o word-spacing con !important, lo que bloquea las anulaciones de espaciado de texto del usuario.",
30065
+ "avoidInlineSpacing_title": "El estilo en línea no debe forzar el espaciado de texto por debajo del umbral WCAG",
30066
+ "avoidInlineSpacing_description": "Comprueba que cuando el estilo en línea fuerza line-height, letter-spacing o word-spacing con !important, el valor ya cumple WCAG 1.4.12, por lo que al usuario no le queda nada que anular.",
28448
30067
  "avoidInlineSpacing_summary_fail": "El estilo en línea de este elemento fuerza {{properties}} con !important, bloqueando las anulaciones de espaciado de texto del usuario.",
28449
30068
  "avoidInlineSpacing_hint_fail": "Eliminar !important de line-height/letter-spacing/word-spacing en los estilos en línea para que los usuarios puedan anular el espaciado de texto.",
28450
30069
  "metaRefreshNoExceptions_title": "La página no debe usar un meta refresh en absoluto (AAA)",
@@ -28533,6 +30152,10 @@ const I18N = {
28533
30152
  "inputImage_altPresent_description": "Vérifie que les éléments <input type=\"image\"> fournissent un attribut alt afin de proposer un mécanisme d’alternative textuelle.",
28534
30153
  "inputImage_altPresent_summary_fail": "Attribut alt manquant sur <input type=\"image\">.",
28535
30154
  "inputImage_altPresent_hint_fail": "Ajoutez un attribut alt (utilisez alt=\"\" uniquement lorsqu’un nom accessible séparé est fourni).",
30155
+ "inputImage_altPresent_summary_defaultName": "Le nom accessible est celui par defaut du navigateur pour un bouton image et n'apporte aucune information.",
30156
+ "inputImage_altPresent_hint_defaultName": "Remplacez-le par un texte decrivant l action du bouton, par exemple \"Rechercher\".",
30157
+ "inputImage_altPresent_summary_emptyAlt": "Un alt=\"\" vide sur <input type=\"image\"> laisse le controle sans nom.",
30158
+ "inputImage_altPresent_hint_emptyAlt": "Decrivez l action dans alt, ou nommez le controle avec aria-label ou aria-labelledby.",
28536
30159
  "ariaHidden_programmaticFocus_review_title": "Vérifier le focus programmatique avec aria-hidden",
28537
30160
  "ariaHidden_programmaticFocus_review_description": "Signale les éléments aria-hidden considérés comme éligibles uniquement via un focus programmatique (ex. tabindex < 0). Vérifiez l’intention de gestion du focus et l’exposition aux technologies d’assistance.",
28538
30161
  "ariaHidden_programmaticFocus_review_summary": "Vérification : un élément aria-hidden est focusable de façon programmatique.",
@@ -28814,16 +30437,21 @@ const I18N = {
28814
30437
  "labelInName_description": "Vérifie que lorsqu’un composant possède un libellé textuel visible, le nom accessible contient ce libellé visible (WCAG 2.5.3).",
28815
30438
  "labelInName_summary_fail": "{{element}} : le libellé visible « {{visibleLabel}} » (source : {{labelSource}}) n’est pas inclus dans le nom accessible (source : {{nameMechanism}}).",
28816
30439
  "labelInName_hint_fail": "Modifiez aria-label ou aria-labelledby (ou le texte du libellé visible) afin que le nom accessible inclue le libellé visible.",
30440
+ "labelInName_summary_cantTell": "{{element}} : le libellé visible « {{visibleLabel}} » (source : {{labelSource}}) ne diffère du nom accessible (source : {{nameMechanism}}) que par une abréviation ou une césure.",
30441
+ "labelInName_hint_cantTell": "Vérifiez manuellement que les deux formulations correspondent : le balisage ne permet pas de distinguer une abréviation volontaire d’une incohérence.",
28817
30442
  "ariaRolesValid_title": "L’attribut role doit être un rôle ARIA valide et non abstrait",
28818
30443
  "ariaRolesValid_description": "Vérifie qu’un attribut role=\"\" explicite correspond à un rôle WAI-ARIA réel et non abstrait.",
28819
30444
  "ariaRolesValid_summary_invalid": "role=\"{{role}}\" n’est pas un rôle ARIA reconnu.",
28820
30445
  "ariaRolesValid_hint_invalid": "Utilisez un rôle ARIA valide, ou retirez l’attribut role si aucun ne s’applique.",
28821
30446
  "ariaRolesValid_summary_abstract": "role=\"{{role}}\" est un rôle ARIA abstrait, qui ne doit pas être utilisé directement.",
28822
30447
  "ariaRolesValid_hint_abstract": "Remplacez ce rôle abstrait par un rôle concret adapté au composant/à la structure.",
28823
- "ariaDeprecatedRole_title": "L’attribut role ne doit pas utiliser un rôle ARIA obsolète ou interdit aux auteurs",
30448
+ "ariaDeprecatedRole_title": "L’attribut role ne devrait pas utiliser un rôle ARIA obsolète ou déconseillé aux auteurs",
28824
30449
  "ariaDeprecatedRole_description": "Vérifie qu’un attribut role=\"\" explicite n’utilise pas un rôle rendu obsolète par la spécification WAI-ARIA, ni un rôle réservé à un usage interne à l’agent utilisateur (ex. role=\"generic\").",
28825
30450
  "ariaDeprecatedRole_summary_fail": "Cet élément utilise role=\"{{role}}\", que les auteurs ne doivent pas déclarer explicitement.",
28826
30451
  "ariaDeprecatedRole_hint_fail": "{{guidance}}",
30452
+ "ariaDeprecatedRole_summary_cantTell": "Cet élément utilise role=\"{{role}}\", qui est obsolète dans WAI-ARIA (toujours valide, mais déconseillé).",
30453
+ "ariaDeprecatedRole_summary_cantTell_discouraged": "Cet élément utilise role=\"{{role}}\", qui est réservé aux agents utilisateurs (toujours valide, mais déconseillé).",
30454
+ "ariaDeprecatedRole_hint_cantTell": "{{guidance}}",
28827
30455
  "ariaValidAttr_title": "Les attributs aria-* doivent être des attributs ARIA réels et définis",
28828
30456
  "ariaValidAttr_description": "Vérifie que chaque nom d’attribut aria-* présent dans le DOM est un attribut réel défini par la spécification WAI-ARIA.",
28829
30457
  "ariaValidAttr_summary_fail": "{{attr}} n’est pas un attribut ARIA reconnu.",
@@ -28836,6 +30464,8 @@ const I18N = {
28836
30464
  "ariaAllowedAttr_description": "Vérifie que chaque attribut aria-* reconnu présent sur un élément ayant un rôle explicite est soit globalement pris en charge, soit pris en charge par ce rôle.",
28837
30465
  "ariaAllowedAttr_summary_fail": "{{attr}} n’est pas autorisé sur role=\"{{role}}\".",
28838
30466
  "ariaAllowedAttr_hint_fail": "Retirez cet attribut, ou utilisez un rôle qui le prend en charge.",
30467
+ "ariaAllowedAttr_summary_cantTell": "{{attr}} est obsolète sur role=\"{{role}}\" (toujours autorisé, mais déconseillé).",
30468
+ "ariaAllowedAttr_hint_cantTell": "Cet attribut a été retiré de l’ensemble global d’ARIA en 1.2 ; retirez-le ou utilisez un rôle qui le prend en charge, car une future version d’ARIA pourrait l’interdire.",
28839
30469
  "ariaProhibitedAttr_title": "Les attributs de nommage ARIA ne doivent pas être utilisés sur des rôles qui les interdisent",
28840
30470
  "ariaProhibitedAttr_description": "Vérifie que aria-label/aria-labelledby ne sont pas présents sur des rôles WAI-ARIA dont la spécification interdit explicitement le nommage ARIA (ex. generic, emphasis, strong, paragraph).",
28841
30471
  "ariaProhibitedAttr_summary_fail": "{{attr}} est interdit sur role=\"{{role}}\".",
@@ -28950,14 +30580,16 @@ const I18N = {
28950
30580
  "formControlSingleLabel_description": "Vérifie qu’un contrôle de formulaire est associé à au plus un <label> (par imbrication ou par label[for]).",
28951
30581
  "formControlSingleLabel_summary_fail": "Ce <{{element}}> est associé à {{labelCount}} étiquettes.",
28952
30582
  "formControlSingleLabel_hint_fail": "Conservez une seule <label> par contrôle de formulaire (soit en l’enveloppant, soit en la référençant via for/id).",
30583
+ "formControlSingleLabel_summary_cantTell": "Ce <{{element}}> a une <label> qui l’étiquette plus une association de <label> vide supplémentaire ; vérifiez comment il est annoncé.",
30584
+ "formControlSingleLabel_hint_cantTell": "Supprimez la <label> vide redondante afin qu’une seule <label> soit associée au contrôle.",
28953
30585
  "nestedInteractiveControlsAbsent_title": "Les contrôles interactifs ne doivent pas être imbriqués",
28954
30586
  "nestedInteractiveControlsAbsent_description": "Vérifie qu’un contrôle interactif (lien, bouton, contrôle de formulaire, ou rôle de widget ARIA) ne contient pas un autre contrôle interactif.",
28955
30587
  "nestedInteractiveControlsAbsent_summary_fail": "Ce <{{element}}> contient un ou plusieurs contrôles interactifs imbriqués : {{nestedElements}}.",
28956
30588
  "nestedInteractiveControlsAbsent_hint_fail": "Déplacez le(s) contrôle(s) interactif(s) imbriqué(s) hors de cet élément ; les contrôles interactifs imbriqués ne sont pas utilisables de façon fiable via les technologies d’assistance.",
28957
30589
  "bypassBlocksPresent_title": "La page doit proposer un moyen de contourner les blocs répétés",
28958
30590
  "bypassBlocksPresent_description": "Vérifie que la page dispose d’au moins un mécanisme reconnu de contournement des blocs répétés (WCAG 2.4.1) : un point de repère main, un lien d’ancrage fonctionnel vers la même page, ou un titre.",
28959
- "bypassBlocksPresent_summary_fail": "Cette page n’a aucun moyen reconnu de contourner les blocs de contenu répétés.",
28960
- "bypassBlocksPresent_hint_fail": "Ajoutez un point de repère main (<main> ou role=\"main\"), un lien « aller au contenu » fonctionnel, ou des éléments de titre que les technologies d’assistance peuvent utiliser pour passer le contenu répété.",
30591
+ "bypassBlocksPresent_summary_cantTell": "Aucun moyen reconnu de contourner les blocs de contenu répétés n’a été détecté sur cette page — vérifiez qu’un mécanisme de contournement existe.",
30592
+ "bypassBlocksPresent_hint_cantTell": "Confirmez que la page propose un mécanisme de contournement : un point de repère main (<main> ou role=\"main\"), un lien « aller au contenu » fonctionnel, ou des éléments de titre que les technologies d’assistance peuvent utiliser pour passer le contenu répété. (Un mécanisme peut être temporairement masqué — par exemple pendant qu’une boîte de dialogue modale rend la page inerte — ou fourni à l’échelle du site ; cela nécessite une confirmation humaine.)",
28961
30593
  "landmarkBannerIsTopLevel_title": "Le point de repère banner doit être de premier niveau",
28962
30594
  "landmarkBannerIsTopLevel_description": "Vérifie que le point de repère banner (role=\"banner\" ou un <header> non imbriqué) n’est pas imbriqué dans un autre point de repère.",
28963
30595
  "landmarkBannerIsTopLevel_summary_cantTell": "Ce point de repère banner est imbriqué dans un autre point de repère.",
@@ -29059,8 +30691,8 @@ const I18N = {
29059
30691
  "htmlXmlLangMismatch_description": "Vérifie que les attributs lang et xml:lang de l’élément <html> déclarent la même langue principale, lorsque les deux sont présents.",
29060
30692
  "htmlXmlLangMismatch_summary_fail": "Les attributs lang (« {{lang}} ») et xml:lang (« {{xmlLang}} ») déclarent des langues différentes.",
29061
30693
  "htmlXmlLangMismatch_hint_fail": "Faites en sorte que lang et xml:lang déclarent la même langue principale, ou retirez l’attribut obsolète xml:lang.",
29062
- "avoidInlineSpacing_title": "Un style en ligne ne doit pas forcer l’espacement du texte avec !important",
29063
- "avoidInlineSpacing_description": "Vérifie qu’un style en ligne ne définit pas line-height, letter-spacing, ou word-spacing avec !important, ce qui bloque les surcharges d’espacement de texte par l’utilisateur.",
30694
+ "avoidInlineSpacing_title": "Un style en ligne ne doit pas forcer l’espacement du texte en dessous du seuil WCAG",
30695
+ "avoidInlineSpacing_description": "Vérifie que lorsqu’un style en ligne force line-height, letter-spacing ou word-spacing avec !important, la valeur respecte déjà WCAG 1.4.12, ne laissant rien à surcharger à l’utilisateur.",
29064
30696
  "avoidInlineSpacing_summary_fail": "Le style en ligne de cet élément force {{properties}} avec !important, bloquant les surcharges d’espacement de texte par l’utilisateur.",
29065
30697
  "avoidInlineSpacing_hint_fail": "Retirez !important de line-height/letter-spacing/word-spacing dans les styles en ligne afin que les utilisateurs puissent surcharger l’espacement du texte.",
29066
30698
  "metaRefreshNoExceptions_title": "La page ne doit utiliser aucun rafraîchissement meta (AAA)",
@@ -30073,6 +31705,35 @@ const createContrastHelpers = (function createContrastHelpers(opts, shared) {
30073
31705
 
30074
31706
  const eligCache = new WeakMap();
30075
31707
  const inactiveCache = new WeakMap();
31708
+ const clipHiddenCache = new WeakMap();
31709
+
31710
+ // clip:rect(0,0,0,0) / clip-path:inset(50%+) (the sr-only technique) has
31711
+ // no visually-presented color, so contrast rules exempt it even though
31712
+ // isDomVisibleEligible itself keeps it eligible for other callers
31713
+ // (aria-hidden-focus etc. need to find clipped-but-focusable elements).
31714
+ const isClipHidden = (el) => {
31715
+ if (!helpers || typeof helpers.getVisibilityHintsInfo !== 'function') return false;
31716
+ if (clipHiddenCache.has(el)) return clipHiddenCache.get(el);
31717
+
31718
+ let hidden = false;
31719
+ try {
31720
+ let cur = el;
31721
+ let guard = 0;
31722
+ while (cur && cur.nodeType === 1 && guard++ < 100) {
31723
+ const info = helpers.getVisibilityHintsInfo(cur, ctx, {});
31724
+ if (info && Array.isArray(info.hints) && info.hints.indexOf('clipped') !== -1) {
31725
+ hidden = true;
31726
+ break;
31727
+ }
31728
+ cur = composedParent ? composedParent(cur) : cur.parentElement;
31729
+ }
31730
+ } catch {
31731
+ hidden = false;
31732
+ }
31733
+
31734
+ clipHiddenCache.set(el, hidden);
31735
+ return hidden;
31736
+ };
30076
31737
 
30077
31738
  const isVisibleEligible = (el) => {
30078
31739
  if (!helpers || typeof helpers.isDomVisibleEligible !== 'function') return true;
@@ -30082,6 +31743,7 @@ const createContrastHelpers = (function createContrastHelpers(opts, shared) {
30082
31743
  try {
30083
31744
  const r = helpers.isDomVisibleEligible(el, ctx, { visibilityMode });
30084
31745
  ok = __asEligibilityBool(r);
31746
+ if (ok && isClipHidden(el)) ok = false;
30085
31747
  } catch {
30086
31748
  ok = false;
30087
31749
  }
@@ -31196,6 +32858,7 @@ const createAriaHelpers = (function createAriaHelpers(opts, shared) {
31196
32858
  // -------------------------------------------------------------------
31197
32859
  // A) Abstract roles — MUST NOT be used directly in a role="" attribute.
31198
32860
  // -------------------------------------------------------------------
32861
+ // <generated:aria-abstract-roles>
31199
32862
  const ABSTRACT_ROLES = new Set([
31200
32863
  'command',
31201
32864
  'composite',
@@ -31210,23 +32873,44 @@ const createAriaHelpers = (function createAriaHelpers(opts, shared) {
31210
32873
  'widget',
31211
32874
  'window'
31212
32875
  ]);
32876
+ // </generated:aria-abstract-roles>
31213
32877
 
31214
32878
  // -------------------------------------------------------------------
31215
- // B) Valid, concrete (non-abstract) roles that authors must never
31216
- // explicitly declare — either because WAI-ARIA has deprecated them
31217
- // (a direct replacement exists) or because they are reserved for
31218
- // user-agent-internal use only (not a spec deprecation, but the
31219
- // same "valid token, prohibited for authors" shape). Flagged by
31220
- // aria-deprecated-role, not aria-roles-valid (which only checks
31221
- // existence/abstractness) — see DEPRECATED_ROLE_GUIDANCE below for
31222
- // per-role, reason-accurate messaging.
32879
+ // B) Valid, concrete (non-abstract) roles authors should not explicitly
32880
+ // declare — either because WAI-ARIA has deprecated them (a direct
32881
+ // replacement exists) or because they are reserved for
32882
+ // user-agent-internal use. Flagged by aria-deprecated-role, not
32883
+ // aria-roles-valid (which only checks existence/abstractness) — see
32884
+ // DEPRECATED_ROLE_GUIDANCE below for per-role, reason-accurate
32885
+ // messaging.
31223
32886
  // -------------------------------------------------------------------
32887
+ // Deprecated but still VALID roles (SHOULD NOT, still conforming). Reported
32888
+ // as cantTell so the author decides whether it matters to them.
31224
32889
  const DEPRECATED_ROLES = new Set([
31225
- 'directory', // superseded by role="list"
31226
- // WAI-ARIA 1.2: "intended for use as the implicit role of generic
31227
- // elements in host languages for use by user agents only; not for
31228
- // use by developers." MDN: "It should not be used by web authors."
31229
- 'generic'
32890
+ 'directory' // superseded by role="list"
32891
+ ]);
32892
+
32893
+ // Valid roles reserved for user-agent-internal use, which ARIA states at
32894
+ // SHOULD NOT strength — conforming, so reported as cantTell.
32895
+ const AUTHOR_DISCOURAGED_ROLES = new Set([
32896
+ 'generic' // "primarily for implementors of user agents"
32897
+ ]);
32898
+
32899
+ // Roles carrying an author MUST NOT, reported as fail. Empty under ARIA 1.2
32900
+ // and 1.3, whose only author MUST NOT covers abstract roles — the concern of
32901
+ // aria-roles-valid.
32902
+ const AUTHOR_PROHIBITED_ROLES = new Set([]);
32903
+
32904
+ // Deprecated but still ALLOWED states/properties (SHOULD NOT, still
32905
+ // conforming): the four ARIA 1.2 keeps in the global set as deprecated and
32906
+ // marks "deprecated on this role" wherever a role does not support them.
32907
+ // Reported as cantTell, not a not-allowed fail. Flat rather than per-role
32908
+ // because the deprecation is uniform and no role prohibits any of the four.
32909
+ const DEPRECATED_ATTRS = new Set([
32910
+ 'aria-disabled',
32911
+ 'aria-errormessage',
32912
+ 'aria-haspopup',
32913
+ 'aria-invalid'
31230
32914
  ]);
31231
32915
 
31232
32916
  const DEPRECATED_ROLE_GUIDANCE = {
@@ -31252,103 +32936,140 @@ const createAriaHelpers = (function createAriaHelpers(opts, shared) {
31252
32936
  // Digital Publishing WAI-ARIA (doc-abstract etc.) is a separate
31253
32937
  // module, deliberately left out of scope for now.
31254
32938
  // -------------------------------------------------------------------
32939
+ // <generated:aria-concrete-roles>
31255
32940
  const CONCRETE_ROLES = new Set([
31256
- // WAI-ARIA Graphics Module 1.0 (see comment above)
31257
- 'graphics-document',
31258
- 'graphics-object',
31259
- 'graphics-symbol',
31260
- // Live region / window roles
31261
32941
  'alert',
31262
32942
  'alertdialog',
31263
- 'dialog',
31264
- 'log',
31265
- 'marquee',
31266
- 'status',
31267
- 'timer',
31268
- // Landmark roles
31269
- 'banner',
31270
- 'complementary',
31271
- 'contentinfo',
31272
- 'form',
31273
- 'main',
31274
- 'navigation',
31275
- 'region',
31276
- 'search',
31277
- // Widget roles (leaf)
31278
- 'button',
31279
- 'checkbox',
31280
- 'gridcell',
31281
- 'link',
31282
- 'menuitem',
31283
- 'menuitemcheckbox',
31284
- 'menuitemradio',
31285
- 'option',
31286
- 'progressbar',
31287
- 'radio',
31288
- 'scrollbar',
31289
- 'searchbox',
31290
- 'separator',
31291
- 'slider',
31292
- 'spinbutton',
31293
- 'switch',
31294
- 'tab',
31295
- 'tabpanel',
31296
- 'textbox',
31297
- 'treeitem',
31298
- 'tooltip',
31299
- // Composite widget roles
31300
- 'combobox',
31301
- 'grid',
31302
- 'listbox',
31303
- 'menu',
31304
- 'menubar',
31305
- 'radiogroup',
31306
- 'tablist',
31307
- 'tree',
31308
- 'treegrid',
31309
- // Document structure roles
31310
32943
  'application',
31311
32944
  'article',
32945
+ 'banner',
31312
32946
  'blockquote',
32947
+ 'button',
31313
32948
  'caption',
31314
32949
  'cell',
32950
+ 'checkbox',
31315
32951
  'code',
31316
32952
  'columnheader',
32953
+ 'combobox',
31317
32954
  'comment',
32955
+ 'complementary',
32956
+ 'contentinfo',
31318
32957
  'definition',
31319
32958
  'deletion',
32959
+ 'dialog',
31320
32960
  'directory',
32961
+ 'doc-abstract',
32962
+ 'doc-acknowledgments',
32963
+ 'doc-afterword',
32964
+ 'doc-appendix',
32965
+ 'doc-backlink',
32966
+ 'doc-biblioentry',
32967
+ 'doc-bibliography',
32968
+ 'doc-biblioref',
32969
+ 'doc-chapter',
32970
+ 'doc-colophon',
32971
+ 'doc-conclusion',
32972
+ 'doc-cover',
32973
+ 'doc-credit',
32974
+ 'doc-credits',
32975
+ 'doc-dedication',
32976
+ 'doc-endnote',
32977
+ 'doc-endnotes',
32978
+ 'doc-epigraph',
32979
+ 'doc-epilogue',
32980
+ 'doc-errata',
32981
+ 'doc-example',
32982
+ 'doc-footnote',
32983
+ 'doc-foreword',
32984
+ 'doc-glossary',
32985
+ 'doc-glossref',
32986
+ 'doc-index',
32987
+ 'doc-introduction',
32988
+ 'doc-noteref',
32989
+ 'doc-notice',
32990
+ 'doc-pagebreak',
32991
+ 'doc-pagefooter',
32992
+ 'doc-pageheader',
32993
+ 'doc-pagelist',
32994
+ 'doc-part',
32995
+ 'doc-preface',
32996
+ 'doc-prologue',
32997
+ 'doc-pullquote',
32998
+ 'doc-qna',
32999
+ 'doc-subtitle',
33000
+ 'doc-tip',
33001
+ 'doc-toc',
31321
33002
  'document',
31322
33003
  'emphasis',
31323
33004
  'feed',
31324
33005
  'figure',
33006
+ 'form',
31325
33007
  'generic',
33008
+ 'graphics-document',
33009
+ 'graphics-object',
33010
+ 'graphics-symbol',
33011
+ 'grid',
33012
+ 'gridcell',
31326
33013
  'group',
31327
33014
  'heading',
31328
33015
  'img',
31329
33016
  'insertion',
33017
+ 'link',
31330
33018
  'list',
33019
+ 'listbox',
31331
33020
  'listitem',
33021
+ 'log',
33022
+ 'main',
31332
33023
  'mark',
33024
+ 'marquee',
31333
33025
  'math',
33026
+ 'menu',
33027
+ 'menubar',
33028
+ 'menuitem',
33029
+ 'menuitemcheckbox',
33030
+ 'menuitemradio',
31334
33031
  'meter',
33032
+ 'navigation',
31335
33033
  'none',
31336
33034
  'note',
33035
+ 'option',
31337
33036
  'paragraph',
31338
33037
  'presentation',
33038
+ 'progressbar',
33039
+ 'radio',
33040
+ 'radiogroup',
33041
+ 'region',
31339
33042
  'row',
31340
33043
  'rowgroup',
31341
33044
  'rowheader',
33045
+ 'scrollbar',
33046
+ 'search',
33047
+ 'searchbox',
33048
+ 'separator',
33049
+ 'slider',
33050
+ 'spinbutton',
33051
+ 'status',
31342
33052
  'strong',
31343
33053
  'subscript',
31344
33054
  'suggestion',
31345
33055
  'superscript',
33056
+ 'switch',
33057
+ 'tab',
31346
33058
  'table',
33059
+ 'tablist',
33060
+ 'tabpanel',
31347
33061
  'term',
31348
33062
  'text',
33063
+ 'textbox',
31349
33064
  'time',
31350
- 'toolbar'
33065
+ 'timer',
33066
+ 'toolbar',
33067
+ 'tooltip',
33068
+ 'tree',
33069
+ 'treegrid',
33070
+ 'treeitem'
31351
33071
  ]);
33072
+ // </generated:aria-concrete-roles>
31352
33073
 
31353
33074
  // -------------------------------------------------------------------
31354
33075
  // D) ARIA attribute value types.
@@ -31864,6 +33585,18 @@ const createAriaHelpers = (function createAriaHelpers(opts, shared) {
31864
33585
  return DEPRECATED_ROLES.has(lower(role));
31865
33586
  }
31866
33587
 
33588
+ function isAuthorDiscouragedRole(role) {
33589
+ return AUTHOR_DISCOURAGED_ROLES.has(lower(role));
33590
+ }
33591
+
33592
+ function isAuthorProhibitedRole(role) {
33593
+ return AUTHOR_PROHIBITED_ROLES.has(lower(role));
33594
+ }
33595
+
33596
+ function isDeprecatedAttr(attr /* , role */) {
33597
+ return DEPRECATED_ATTRS.has(lower(attr));
33598
+ }
33599
+
31867
33600
  function isKnownRole(role) {
31868
33601
  const r = lower(role);
31869
33602
  return ABSTRACT_ROLES.has(r) || CONCRETE_ROLES.has(r);
@@ -32139,6 +33872,9 @@ const createAriaHelpers = (function createAriaHelpers(opts, shared) {
32139
33872
  getAllRoleTokens,
32140
33873
  isAbstractRole,
32141
33874
  isDeprecatedRole,
33875
+ isAuthorDiscouragedRole,
33876
+ isAuthorProhibitedRole,
33877
+ isDeprecatedAttr,
32142
33878
  getDeprecatedRoleGuidance,
32143
33879
  isKnownRole,
32144
33880
  isValidConcreteRole,
@@ -32226,6 +33962,28 @@ const resolveContextRoots = (function resolveContextRoots(document, contextSelec
32226
33962
  return { ctxSelector, roots };
32227
33963
  });
32228
33964
  const createDomHelpers = (function createDomHelpers(opts) {
33965
+ // <generated:language-subtags>
33966
+ const LANGUAGE_SUBTAGS =
33967
+ 'aa aaa aab aac aad aae aaf aag aah aai aak aal aam aan aao aap aaq aas aat aau aav aaw aax aaz ab aba abb abc abd abe abf abg abh abi abj abl abm abn abo abp abq abr abs abt abu abv abw abx aby abz aca acb acd ace acf ach aci ack acl acm acn acp acq acr acs act acu acv acw acx acy acz ada adb add ade adf adg adh adi adj adl adn ado adp adq adr ads adt adu adw adx ady adz ae aea aeb aec aed aee aek ael aem aen aeq aer aes aeu aew aey aez af afa afb afd afe afg afh afi afk afn afo afp afs aft afu afz aga agb agc agd age agf agg agh agi agj agk agl agm agn ago agp agq agr ags agt agu agv agw agx agy agz aha ahb ahg ahh ahi ahk ahl ahm ahn aho ahp ahr ahs aht aia aib aic aid aie aif aig aih aii aij aik ail aim ain aio aip aiq air ais ait aiw aix aiy aja ajg aji ajn ajp ajs ajt aju ajw ajz ak akb akc akd ake akf akg akh aki akj akk akl akm ako akp akq akr aks akt aku akv akw akx aky akz ala alc ald ale alf alg alh ali alj alk all alm aln alo alp alq alr als alt alu alv alw alx aly alz am ama amb amc ame amf amg ami amj amk aml amm amn amo amp amq amr ams amt amu amv amw amx amy amz an ana anb anc and ane anf ang anh ani anj ank anl anm ann ano anp anq anr ans ant anu anv anw anx any anz aoa aob aoc aod aoe aof aog aoh aoi aoj aok aol aom aon aor aos aot aou aox aoz apa apb apc apd ape apf apg aph api apj apk apl apm apn apo app apq apr aps apt apu apv apw apx apy apz aqa aqc aqd aqg aqk aql aqm aqn aqp aqr aqt aqz ar arb arc ard are arh ari arj ark arl arn aro arp arq arr ars art aru arv arw arx ary arz as asa asb asc asd ase asf asg ash asi asj ask asl asn aso asp asq asr ass ast asu asv asw asx asy asz ata atb atc atd ate atg ath ati atj atk atl atm atn ato atp atq atr ats att atu atv atw atx aty atz aua aub auc aud aue auf aug auh aui auj auk aul aum aun auo aup auq aur aus aut auu auw aux auy auz av avb avd avi avk avl avm avn avo avs avt avu avv awa awb awc awd awe awg awh awi awk awm awn awo awr aws awt awu awv aww awx awy axb axe axg axk axl axm axx ay aya ayb ayc ayd aye ayg ayh ayi ayk ayl ayn ayo ayp ayq ayr ays ayt ayu ayx ayy ayz az aza azb azc azd azg azj azm azn azo azt azz ba baa bab bac bad bae baf bag bah bai baj bal ban bao bap bar bas bat bau bav baw bax bay baz bba bbb bbc bbd bbe bbf bbg bbh bbi bbj bbk bbl bbm bbn bbo bbp bbq bbr bbs bbt bbu bbv bbw bbx bby bbz bca bcb bcc bcd bce bcf bcg bch bci bcj bck bcl bcm bcn bco bcp bcq bcr bcs bct bcu bcv bcw bcy bcz bda bdb bdc bdd bde bdf bdg bdh bdi bdj bdk bdl bdm bdn bdo bdp bdq bdr bds bdt bdu bdv bdw bdx bdy bdz be bea beb bec bed bee bef beg beh bei bej bek bem beo bep beq ber bes bet beu bev bew bex bey bez bfa bfb bfc bfd bfe bff bfg bfh bfi bfj bfk bfl bfm bfn bfo bfp bfq bfr bfs bft bfu bfw bfx bfy bfz bg bga bgb bgc bgd bge bgf bgg bgi bgj bgk bgl bgm bgn bgo bgp bgq bgr bgs bgt bgu bgv bgw bgx bgy bgz bh bha bhb bhc bhd bhe bhf bhg bhh bhi bhj bhk bhl bhm bhn bho bhp bhq bhr bhs bht bhu bhv bhw bhx bhy bhz bi bia bib bic bid bie bif big bij bik bil bim bin bio bip biq bir bit biu biv biw bix biy biz bja bjb bjc bjd bje bjf bjg bjh bji bjj bjk bjl bjm bjn bjo bjp bjq bjr bjs bjt bju bjv bjw bjx bjy bjz bka bkb bkc bkd bkf bkg bkh bki bkj bkk bkl bkm bkn bko bkp bkq bkr bks bkt bku bkv bkw bkx bky bkz bla blb blc bld ble blf blg blh bli blj blk bll blm bln blo blp blq blr bls blt blv blw blx bly blz bm bma bmb bmc bmd bme bmf bmg bmh bmi bmj bmk bml bmm bmn bmo bmp bmq bmr bms bmt bmu bmv bmw bmx bmy bmz bn bna bnb bnc bnd bne bnf bng bni bnj bnk bnl bnm bnn bno bnp bnq bnr bns bnt bnu bnv bnw bnx bny bnz bo boa bob boe bof bog boh boi boj bok bol bom bon boo bop boq bor bot bou bov bow box boy boz bpa bpb bpc bpd bpe bpg bph bpi bpj bpk bpl bpm bpn bpo bpp bpq bpr bps bpt bpu bpv bpw bpx bpy bpz bqa bqb bqc bqd bqf bqg bqh bqi bqj bqk bql bqm bqn bqo bqp bqq bqr bqs bqt bqu bqv bqw bqx bqy bqz br bra brb brc brd brf brg brh bri brj brk brl brm brn bro brp brq brr brs brt bru brv brw brx bry brz bs bsa bsb bsc bse bsf bsg bsh bsi bsj bsk bsl bsm bsn bso bsp bsq bsr bss bst bsu bsv bsw bsx bsy bta btb btc btd bte btf btg bth bti btj btk btl btm btn bto btp btq btr bts btt btu btv btw btx bty btz bua bub buc bud bue buf bug buh bui buj buk bum bun buo bup buq bus but buu buv buw bux buy buz bva bvb bvc bvd bve bvf bvg bvh bvi bvj bvk bvl bvm bvn bvo bvp bvq bvr bvt bvu bvv bvw bvx bvy bvz bwa bwb bwc bwd bwe bwf bwg bwh bwi bwj bwk bwl bwm bwn bwo bwp bwq bwr bws bwt bwu bww bwx bwy bwz bxa bxb bxc bxd bxe bxf bxg bxh bxi bxj bxk bxl bxm bxn bxo bxp bxq bxr bxs bxu bxv bxw bxx bxz bya byb byc byd bye byf byg byh byi byj byk byl bym byn byo byp byq byr bys byt byv byw byx byy byz bza bzb bzc bzd bze bzf bzg bzh bzi bzj bzk bzl bzm bzn bzo bzp bzq bzr bzs bzt bzu bzv bzw bzx bzy bzz ca caa cab cac cad cae caf cag cah cai caj cak cal cam can cao cap caq car cas cau cav caw cax cay caz cba cbb cbc cbd cbe cbg cbh cbi cbj cbk cbl cbn cbo cbq cbr cbs cbt cbu cbv cbw cby cca ccc ccd cce ccg cch ccj ccl ccm ccn cco ccp ccq ccr ccs cda cdc cdd cde cdf cdg cdh cdi cdj cdm cdn cdo cdr cds cdy cdz ce cea ceb ceg cek cel cen cet cey cfa cfd cfg cfm cga cgc cgg cgk ch chb chc chd chf chg chh chj chk chl chm chn cho chp chq chr cht chw chx chy chz cia cib cic cid cie cih cik cim cin cip cir ciw ciy cja cje cjh cji cjk cjm cjn cjo cjp cjr cjs cjv cjy cka ckb ckh ckl ckm ckn cko ckq ckr cks ckt cku ckv ckx cky ckz cla clc cld cle clh cli clj clk cll clm clo cls clt clu clw cly cma cmc cme cmg cmi cmk cml cmm cmn cmo cmr cms cmt cna cnb cnc cng cnh cni cnk cnl cno cnp cnq cnr cns cnt cnu cnw cnx co coa cob coc cod coe cof cog coh coj cok col com con coo cop coq cot cou cov cow cox coy coz cpa cpb cpc cpe cpf cpg cpi cpn cpo cpp cps cpu cpx cpy cqd cqu cr cra crb crc crd crf crg crh cri crj crk crl crm crn cro crp crq crr crs crt crv crw crx cry crz cs csa csb csc csd cse csf csg csh csi csj csk csl csm csn cso csp csq csr css cst csu csv csw csx csy csz cta ctc ctd cte ctg cth ctl ctm ctn cto ctp cts ctt ctu cty ctz cu cua cub cuc cug cuh cui cuj cuk cul cum cuo cup cuq cur cus cut cuu cuv cuw cux cuy cv cvg cvn cwa cwb cwd cwe cwg cwt cxh cy cya cyb cyo czh czk czn czo czt da daa dac dad dae daf dag dah dai daj dak dal dam dao dap daq dar das dau dav daw dax day daz dba dbb dbd dbe dbf dbg dbi dbj dbl dbm dbn dbo dbp dbq dbr dbt dbu dbv dbw dby dcc dcr dda ddd dde ddg ddi ddj ddn ddo ddr dds ddw de dec ded dee def deg deh dei dek del dem den dep deq der des dev dez dga dgb dgc dgd dge dgg dgh dgi dgk dgl dgn dgo dgr dgs dgt dgu dgw dgx dgz dha dhd dhg dhi dhl dhm dhn dho dhr dhs dhu dhv dhw dhx dia dib dic did dif dig dih dii dij dik dil dim din dio dip diq dir dis dit diu diw dix diy diz dja djb djc djd dje djf dji djj djk djl djm djn djo djr dju djw dka dkg dkk dkl dkr dks dkx dlg dlk dlm dln dma dmb dmc dmd dme dmf dmg dmk dml dmm dmn dmo dmr dms dmu dmv dmw dmx dmy dna dnd dne dng dni dnj dnk dnn dno dnr dnt dnu dnv dnw dny doa dob doc doe dof doh doi dok dol don doo dop doq dor dos dot dov dow dox doy doz dpp dra drb drc drd dre drg drh dri drl drn dro drq drr drs drt dru drw dry dsb dse dsh dsi dsk dsl dsn dso dsq dsz dta dtb dtd dth dti dtk dtm dtn dto dtp dtr dts dtt dtu dty dua dub duc dud due duf dug duh dui duj duk dul dum dun duo dup duq dur dus duu duv duw dux duy duz dv dva dwa dwk dwl dwr dws dwu dww dwy dwz dya dyb dyd dyg dyi dym dyn dyo dyr dyu dyy dz dza dzd dze dzg dzl dzn eaa ebc ebg ebk ebo ebr ebu ecr ecs ecy ee eee efa efe efi ega egl egm ego egx egy ehs ehu eip eit eiv eja eka ekc eke ekg eki ekk ekl ekm eko ekp ekr eky el ele elh eli elk elm elo elp elu elx ema emb eme emg emi emk emm emn emo emp emq ems emu emw emx emy emz en ena enb enc end enf enh enl enm enn eno enq enr enu env enw enx eo eot epi era erg erh eri erk ero err ers ert erw es ese esg esh esi esk esl esm esn eso esq ess esu esx esy et etb etc eth etn eto etr ets ett etu etx etz eu eud euq eve evh evn ewo ext eya eyo eza eze fa faa fab fad faf fag fah fai faj fak fal fam fan fap far fat fau fax fay faz fbl fcs fer ff ffi ffm fgr fi fia fie fif fil fip fir fit fiu fiw fj fkk fkv fla flh fli fll fln flr fly fmp fmu fnb fng fni fo fod foi fom fon for fos fox fpe fqs fr frc frd frk frm fro frp frq frr frs frt fse fsl fss fub fuc fud fue fuf fuh fui fuj fum fun fuq fur fut fuu fuv fuy fvr fwa fwe fy ga gaa gab gac gad gae gaf gag gah gai gaj gak gal gam gan gao gap gaq gar gas gat gau gav gaw gax gay gaz gba gbb gbc gbd gbe gbf gbg gbh gbi gbj gbk gbl gbm gbn gbo gbp gbq gbr gbs gbu gbv gbw gbx gby gbz gcc gcd gce gcf gcl gcn gcr gct gd gda gdb gdc gdd gde gdf gdg gdh gdi gdj gdk gdl gdm gdn gdo gdq gdr gds gdt gdu gdx gea geb gec ged gef geg geh gei gej gek gel gem geq ges gev gew gex gey gez gfk gft gfx gga ggb ggd gge ggg ggk ggl ggn ggo ggr ggt ggu ggw gha ghc ghe ghh ghk ghl ghn gho ghr ghs ght gia gib gic gid gie gig gih gii gil gim gin gio gip giq gir gis git giu giw gix giy giz gji gjk gjm gjn gjr gju gka gkd gke gkn gko gkp gku gl glb glc gld glh gli glj glk gll glo glr glu glw gly gma gmb gmd gme gmg gmh gml gmm gmn gmq gmr gmu gmv gmw gmx gmy gmz gn gna gnb gnc gnd gne gng gnh gni gnj gnk gnl gnm gnn gno gnq gnr gnt gnu gnw gnz goa gob goc god goe gof gog goh goi goj gok gol gom gon goo gop goq gor gos got gou gov gow gox goy goz gpa gpe gpn gqa gqi gqn gqr gqu gra grb grc grd grg grh gri grj grk grm gro grq grr grs grt gru grv grw grx gry grz gse gsg gsl gsm gsn gso gsp gss gsw gta gti gtu gu gua gub guc gud gue guf gug guh gui guk gul gum gun guo gup guq gur gus gut guu guv guw gux guz gv gva gvc gve gvf gvj gvl gvm gvn gvo gvp gvr gvs gvy gwa gwb gwc gwd gwe gwf gwg gwi gwj gwm gwn gwr gwt gwu gww gwx gxx gya gyb gyd gye gyf gyg gyi gyl gym gyn gyo gyr gyy gyz gza gzi gzn ha haa hab hac had hae haf hag hah hai haj hak hal ham han hao hap haq har has hav haw hax hay haz hba hbb hbn hbo hbu hca hch hdn hds hdy he hea hed heg heh hei hem hgm hgw hhi hhr hhy hi hia hib hid hif hig hih hii hij hik hil him hio hir hit hiw hix hji hka hke hkh hkk hkn hks hla hlb hld hle hlt hlu hma hmb hmc hmd hme hmf hmg hmh hmi hmj hmk hml hmm hmn hmp hmq hmr hms hmt hmu hmv hmw hmx hmy hmz hna hnd hne hng hnh hni hnj hnm hnn hno hns hnu ho hoa hob hoc hod hoe hoh hoi hoj hok hol hom hoo hop hor hos hot hov how hoy hoz hpo hps hr hra hrc hre hrk hrm hro hrp hrr hrt hru hrw hrx hrz hsb hsh hsl hsn hss ht hti hto hts htu htx hu hub huc hud hue huf hug huh hui huj huk hul hum huo hup huq hur hus hut huu huv huw hux huy huz hvc hve hvk hvn hvv hwa hwc hwo hy hya hyw hyx hz ia iai ian iap iar iba ibb ibd ibe ibg ibh ibi ibl ibm ibn ibr ibu iby ica ich icl icr id ida idb idc idd ide idi idr ids idt idu ie ifa ifb ife iff ifk ifm ifu ify ig igb ige igg igl igm ign igo igs igw ihb ihi ihp ihw ii iin iir ijc ije ijj ijn ijo ijs ik ike ikh iki ikk ikl iko ikp ikr iks ikt ikv ikw ikx ikz ila ilb ilg ili ilk ill ilm ilo ilp ils ilu ilv ilw ima ime imi iml imn imo imr ims imt imy in inb inc ine ing inh inj inl inm inn ino inp ins int inz io ior iou iow ipi ipo iqu iqw ira ire irh iri irk irn iro irr iru irx iry is isa isc isd ise isg ish isi isk ism isn iso isr ist isu isv it itb itc itd ite iti itk itl itm ito itr its itt itv itw itx ity itz iu ium ivb ivv iw iwk iwm iwo iws ixc ixl iya iyo iyx izh izi izm izr izz ja jaa jab jac jad jae jaf jah jaj jak jal jam jan jao jaq jar jas jat jau jax jay jaz jbe jbi jbj jbk jbm jbn jbo jbr jbt jbu jbw jcs jct jda jdg jdt jeb jee jeg jeh jei jek jel jen jer jet jeu jgb jge jgk jgo jhi jhs ji jia jib jic jid jie jig jih jii jil jim jio jiq jit jiu jiv jiy jje jjr jka jkm jko jkp jkr jks jku jle jls jma jmb jmc jmd jmi jml jmn jmr jms jmw jmx jna jnd jng jni jnj jnl jns job jod jog jor jos jow jpa jpr jpx jqr jra jrb jrr jrt jru jsl jua jub juc jud juh jui juk jul jum jun juo jup jur jus jut juu juw juy jv jvd jvn jw jwi jya jye jyy ka kaa kab kac kad kae kaf kag kah kai kaj kak kam kao kap kaq kar kav kaw kax kay kba kbb kbc kbd kbe kbf kbg kbh kbi kbj kbk kbl kbm kbn kbo kbp kbq kbr kbs kbt kbu kbv kbw kbx kby kbz kca kcb kcc kcd kce kcf kcg kch kci kcj kck kcl kcm kcn kco kcp kcq kcr kcs kct kcu kcv kcw kcx kcy kcz kda kdc kdd kde kdf kdg kdh kdi kdj kdk kdl kdm kdn kdo kdp kdq kdr kdt kdu kdv kdw kdx kdy kdz kea keb kec ked kee kef keg keh kei kej kek kel kem ken keo kep keq ker kes ket keu kev kew kex key kez kfa kfb kfc kfd kfe kff kfg kfh kfi kfj kfk kfl kfm kfn kfo kfp kfq kfr kfs kft kfu kfv kfw kfx kfy kfz kg kga kgb kgc kgd kge kgf kgg kgh kgi kgj kgk kgl kgm kgn kgo kgp kgq kgr kgs kgt kgu kgv kgw kgx kgy kha khb khc khd khe khf khg khh khi khj khk khl khn kho khp khq khr khs kht khu khv khw khx khy khz ki kia kib kic kid kie kif kig kih kii kij kil kim kio kip kiq kis kit kiu kiv kiw kix kiy kiz kj kja kjb kjc kjd kje kjf kjg kjh kji kjj kjk kjl kjm kjn kjo kjp kjq kjr kjs kjt kju kjv kjx kjy kjz kk kka kkb kkc kkd kke kkf kkg kkh kki kkj kkk kkl kkm kkn kko kkp kkq kkr kks kkt kku kkv kkw kkx kky kkz kl kla klb klc kld kle klf klg klh kli klj klk kll klm kln klo klp klq klr kls klt klu klv klw klx kly klz km kma kmb kmc kmd kme kmf kmg kmh kmi kmj kmk kml kmm kmn kmo kmp kmq kmr kms kmt kmu kmv kmw kmx kmy kmz kn kna knb knc knd kne knf kng kni knj knk knl knm knn kno knp knq knr kns knt knu knv knw knx kny knz ko koa koc kod koe kof kog koh koi koj kok kol koo kop koq kos kot kou kov kow kox koy koz kpa kpb kpc kpd kpe kpf kpg kph kpi kpj kpk kpl kpm kpn kpo kpp kpq kpr kps kpt kpu kpv kpw kpx kpy kpz kqa kqb kqc kqd kqe kqf kqg kqh kqi kqj kqk kql kqm kqn kqo kqp kqq kqr kqs kqt kqu kqv kqw kqx kqy kqz kr kra krb krc krd kre krf krh kri krj krk krl krm krn kro krp krr krs krt kru krv krw krx kry krz ks ksa ksb ksc ksd kse ksf ksg ksh ksi ksj ksk ksl ksm ksn kso ksp ksq ksr kss kst ksu ksv ksw ksx ksy ksz kta ktb ktc ktd kte ktf ktg kth kti ktj ktk ktl ktm ktn kto ktp ktq ktr kts ktt ktu ktv ktw ktx kty ktz ku kub kuc kud kue kuf kug kuh kui kuj kuk kul kum kun kuo kup kuq kus kut kuu kuv kuw kux kuy kuz kv kva kvb kvc kvd kve kvf kvg kvh kvi kvj kvk kvl kvm kvn kvo kvp kvq kvr kvs kvt kvu kvv kvw kvx kvy kvz kw kwa kwb kwc kwd kwe kwf kwg kwh kwi kwj kwk kwl kwm kwn kwo kwp kwq kwr kws kwt kwu kwv kww kwx kwy kwz kxa kxb kxc kxd kxe kxf kxh kxi kxj kxk kxl kxm kxn kxo kxp kxq kxr kxs kxt kxu kxv kxw kxx kxy kxz ky kya kyb kyc kyd kye kyf kyg kyh kyi kyj kyk kyl kym kyn kyo kyp kyq kyr kys kyt kyu kyv kyw kyx kyy kyz kza kzb kzc kzd kze kzf kzg kzh kzi kzj kzk kzl kzm kzn kzo kzp kzq kzr kzs kzt kzu kzv kzw kzx kzy kzz la laa lab lac lad lae laf lag lah lai laj lak lal lam lan lap laq lar las lau law lax lay laz lb lba lbb lbc lbe lbf lbg lbi lbj lbk lbl lbm lbn lbo lbq lbr lbs lbt lbu lbv lbw lbx lby lbz lcc lcd lce lcf lch lcl lcm lcp lcq lcs lda ldb ldd ldg ldh ldi ldj ldk ldl ldm ldn ldo ldp ldq lea leb lec led lee lef leg leh lei lej lek lel lem len leo lep leq ler les let leu lev lew lex ley lez lfa lfn lg lga lgb lgg lgh lgi lgk lgl lgm lgn lgo lgq lgr lgs lgt lgu lgz lha lhh lhi lhl lhm lhn lhp lhs lht lhu li lia lib lic lid lie lif lig lih lii lij lik lil lio lip liq lir lis liu liv liw lix liy liz lja lje lji ljl ljp ljw ljx lka lkb lkc lkd lke lkh lki lkj lkl lkm lkn lko lkr lks lkt lku lky lla llb llc lld lle llf llg llh lli llj llk lll llm lln llo llp llq lls llu llx lma lmb lmc lmd lme lmf lmg lmh lmi lmj lmk lml lmm lmn lmo lmp lmq lmr lmu lmv lmw lmx lmy lmz ln lna lnb lnd lng lnh lni lnj lnl lnm lnn lno lns lnu lnw lnz lo loa lob loc loe lof log loh loi loj lok lol lom lon loo lop loq lor los lot lou lov low lox loy loz lpa lpe lpn lpo lpx lqr lra lrc lre lrg lri lrk lrl lrm lrn lro lrr lrt lrv lrz lsa lsb lsc lsd lse lsg lsh lsi lsl lsm lsn lso lsp lsr lss lst lsv lsw lsy lt ltc ltg lth lti ltn lto lts ltu lu lua luc lud lue luf luh lui luj luk lul lum lun luo lup luq lur lus lut luu luv luw luy luz lv lva lvi lvk lvl lvs lvu lwa lwe lwg lwh lwl lwm lwo lws lwt lwu lww lxm lya lyg lyn lzh lzl lzn lzz maa mab mad mae maf mag mai maj mak mam man map maq mas mat mau mav maw max maz mba mbb mbc mbd mbe mbf mbh mbi mbj mbk mbl mbm mbn mbo mbp mbq mbr mbs mbt mbu mbv mbw mbx mby mbz mca mcb mcc mcd mce mcf mcg mch mci mcj mck mcl mcm mcn mco mcp mcq mcr mcs mct mcu mcv mcw mcx mcy mcz mda mdb mdc mdd mde mdf mdg mdh mdi mdj mdk mdl mdm mdn mdp mdq mdr mds mdt mdu mdv mdw mdx mdy mdz mea meb mec med mee mef meg meh mei mej mek mel mem men meo mep meq mer mes met meu mev mew mey mez mfa mfb mfc mfd mfe mff mfg mfh mfi mfj mfk mfl mfm mfn mfo mfp mfq mfr mfs mft mfu mfv mfw mfx mfy mfz mg mga mgb mgc mgd mge mgf mgg mgh mgi mgj mgk mgl mgm mgn mgo mgp mgq mgr mgs mgt mgu mgv mgw mgx mgy mgz mh mha mhb mhc mhd mhe mhf mhg mhh mhi mhj mhk mhl mhm mhn mho mhp mhq mhr mhs mht mhu mhw mhx mhy mhz mi mia mib mic mid mie mif mig mih mii mij mik mil mim min mio mip miq mir mis mit miu miw mix miy miz mja mjb mjc mjd mje mjg mjh mji mjj mjk mjl mjm mjn mjo mjp mjq mjr mjs mjt mju mjv mjw mjx mjy mjz mk mka mkb mkc mke mkf mkg mkh mki mkj mkk mkl mkm mkn mko mkp mkq mkr mks mkt mku mkv mkw mkx mky mkz ml mla mlb mlc mld mle mlf mlh mli mlj mlk mll mlm mln mlo mlp mlq mlr mls mlu mlv mlw mlx mlz mma mmb mmc mmd mme mmf mmg mmh mmi mmj mmk mml mmm mmn mmo mmp mmq mmr mmt mmu mmv mmw mmx mmy mmz mn mna mnb mnc mnd mne mnf mng mnh mni mnj mnk mnl mnm mnn mno mnp mnq mnr mns mnt mnu mnv mnw mnx mny mnz mo moa moc mod moe mof mog moh moi moj mok mom moo mop moq mor mos mot mou mov mow mox moy moz mpa mpb mpc mpd mpe mpg mph mpi mpj mpk mpl mpm mpn mpo mpp mpq mpr mps mpt mpu mpv mpw mpx mpy mpz mqa mqb mqc mqe mqf mqg mqh mqi mqj mqk mql mqm mqn mqo mqp mqq mqr mqs mqt mqu mqv mqw mqx mqy mqz mr mra mrb mrc mrd mre mrf mrg mrh mrj mrk mrl mrm mrn mro mrp mrq mrr mrs mrt mru mrv mrw mrx mry mrz ms msb msc msd mse msf msg msh msi msj msk msl msm msn mso msp msq msr mss mst msu msv msw msx msy msz mt mta mtb mtc mtd mte mtf mtg mth mti mtj mtk mtl mtm mtn mto mtp mtq mtr mts mtt mtu mtv mtw mtx mty mua mub muc mud mue mug muh mui muj muk mul mum mun muo mup muq mur mus mut muu muv mux muy muz mva mvb mvd mve mvf mvg mvh mvi mvk mvl mvm mvn mvo mvp mvq mvr mvs mvt mvu mvv mvw mvx mvy mvz mwa mwb mwc mwd mwe mwf mwg mwh mwi mwj mwk mwl mwm mwn mwo mwp mwq mwr mws mwt mwu mwv mww mwx mwy mwz mxa mxb mxc mxd mxe mxf mxg mxh mxi mxj mxk mxl mxm mxn mxo mxp mxq mxr mxs mxt mxu mxv mxw mxx mxy mxz my myb myc myd mye myf myg myh myi myj myk myl mym myn myo myp myq myr mys myt myu myv myw myx myy myz mza mzb mzc mzd mze mzg mzh mzi mzj mzk mzl mzm mzn mzo mzp mzq mzr mzs mzt mzu mzv mzw mzx mzy mzz na naa nab nac nad nae naf nag nah nai naj nak nal nam nan nao nap naq nar nas nat naw nax nay naz nb nba nbb nbc nbd nbe nbf nbg nbh nbi nbj nbk nbm nbn nbo nbp nbq nbr nbs nbt nbu nbv nbw nbx nby nca ncb ncc ncd nce ncf ncg nch nci ncj nck ncl ncm ncn nco ncp ncq ncr ncs nct ncu ncx ncz nd nda ndb ndc ndd ndf ndg ndh ndi ndj ndk ndl ndm ndn ndp ndq ndr nds ndt ndu ndv ndw ndx ndy ndz ne nea neb nec ned nee nef neg neh nei nej nek nem nen neo neq ner nes net neu nev new nex ney nez nfa nfd nfl nfr nfu ng nga ngb ngc ngd nge ngf ngg ngh ngi ngj ngk ngl ngm ngn ngo ngp ngq ngr ngs ngt ngu ngv ngw ngx ngy ngz nha nhb nhc nhd nhe nhf nhg nhh nhi nhk nhm nhn nho nhp nhq nhr nht nhu nhv nhw nhx nhy nhz nia nib nic nid nie nif nig nih nii nij nik nil nim nin nio niq nir nis nit niu niv niw nix niy niz nja njb njd njh nji njj njl njm njn njo njr njs njt nju njx njy njz nka nkb nkc nkd nke nkf nkg nkh nki nkj nkk nkm nkn nko nkp nkq nkr nks nkt nku nkv nkw nkx nkz nl nla nlc nle nlg nli nlj nlk nll nlm nln nlo nlq nlr nlu nlv nlw nlx nly nlz nma nmb nmc nmd nme nmf nmg nmh nmi nmj nmk nml nmm nmn nmo nmp nmq nmr nms nmt nmu nmv nmw nmx nmy nmz nn nna nnb nnc nnd nne nnf nng nnh nni nnj nnk nnl nnm nnn nnp nnq nnr nns nnt nnu nnv nnw nnx nny nnz no noa noc nod noe nof nog noh noi noj nok nol nom non noo nop noq nos not nou nov now noy noz npa npb npg nph npi npl npn npo nps npu npx npy nqg nqk nql nqm nqn nqo nqq nqt nqy nr nra nrb nrc nre nrf nrg nri nrk nrl nrm nrn nrp nrr nrt nru nrx nrz nsa nsb nsc nsd nse nsf nsg nsh nsi nsk nsl nsm nsn nso nsp nsq nsr nss nst nsu nsv nsw nsx nsy nsz ntd nte ntg nti ntj ntk ntm nto ntp ntr nts ntu ntw ntx nty ntz nua nub nuc nud nue nuf nug nuh nui nuj nuk nul num nun nuo nup nuq nur nus nut nuu nuv nuw nux nuy nuz nv nvh nvm nvo nwa nwb nwc nwe nwg nwi nwm nwo nwr nww nwx nwy nxa nxd nxe nxg nxi nxk nxl nxm nxn nxo nxq nxr nxu nxx ny nyb nyc nyd nye nyf nyg nyh nyi nyj nyk nyl nym nyn nyo nyp nyq nyr nys nyt nyu nyv nyw nyx nyy nza nzb nzd nzi nzk nzm nzr nzs nzu nzy nzz oaa oac oak oar oav obi obk obl obm obo obr obt obu oc oca och ocm oco ocu oda odk odt odu ofo ofs ofu ogb ogc oge ogg ogo ogu oht ohu oia oie oin oj ojb ojc ojg ojp ojs ojv ojw oka okb okc okd oke okg okh oki okj okk okl okm okn oko okr oks oku okv okx okz ola old ole olk olm olo olr olt olu om oma omb omc ome omg omi omk oml omn omo omp omq omr omt omu omv omw omx omy ona onb one ong oni onj onk onn ono onp onr ons ont onu onw onx ood oog oon oor oos opa opk opm opo opt opy or ora orc ore org orh orn oro orr ors ort oru orv orw orx ory orz os osa osc osi osn oso osp ost osu osx ota otb otd ote oti otk otl otm otn oto otq otr ots ott otu otw otx oty otz oua oub oue oui oum oun ovd owi owl oyb oyd oym oyy ozm pa paa pab pac pad pae paf pag pah pai pak pal pam pao pap paq par pas pat pau pav paw pax pay paz pbb pbc pbe pbf pbg pbh pbi pbl pbm pbn pbo pbp pbr pbs pbt pbu pbv pby pbz pca pcb pcc pcd pce pcf pcg pch pci pcj pck pcl pcm pcn pcp pcr pcw pda pdc pdi pdn pdo pdt pdu pea peb ped pee pef peg peh pei pej pek pel pem peo pep peq pes pev pex pey pez pfa pfe pfl pga pgd pgg pgi pgk pgl pgn pgs pgu pgy pgz pha phd phg phh phi phj phk phl phm phn pho phq phr pht phu phv phw pi pia pib pic pid pie pif pig pih pii pij pil pim pin pio pip pir pis pit piu piv piw pix piy piz pjt pka pkb pkc pkg pkh pkn pko pkp pkr pks pkt pku pl pla plb plc pld ple plf plg plh plj plk pll pln plo plp plq plr pls plt plu plv plw ply plz pma pmb pmc pmd pme pmf pmh pmi pmj pmk pml pmm pmn pmo pmq pmr pms pmt pmu pmw pmx pmy pmz pna pnb pnc pnd pne png pnh pni pnj pnk pnl pnm pnn pno pnp pnq pnr pns pnt pnu pnv pnw pnx pny pnz poc pod poe pof pog poh poi pok pom pon poo pop poq pos pot pov pow pox poy poz ppa ppe ppi ppk ppl ppm ppn ppo ppp ppq ppr pps ppt ppu pqa pqe pqm pqw pra prb prc prd pre prf prg prh pri prk prl prm prn pro prp prq prr prs prt pru prw prx pry prz ps psa psc psd pse psg psh psi psl psm psn pso psp psq psr pss pst psu psw psy pt pta pth pti ptn pto ptp ptq ptr ptt ptu ptv ptw pty pua pub puc pud pue puf pug pui puj puk pum puo pup puq pur put puu puw pux puy puz pwa pwb pwg pwi pwm pwn pwo pwr pww pxm pye pym pyn pys pyu pyx pyy pze pzh pzn qaa..qtz qu qua qub quc qud quf qug quh qui quk qul qum qun qup quq qur qus quv quw qux quy quz qva qvc qve qvh qvi qvj qvl qvm qvn qvo qvp qvs qvw qvy qvz qwa qwc qwe qwh qwm qws qwt qxa qxc qxh qxl qxn qxo qxp qxq qxr qxs qxt qxu qxw qya qyp raa rab rac rad raf rag rah rai raj rak ral ram ran rao rap raq rar ras rat rau rav raw rax ray raz rbb rbk rbl rbp rcf rdb rea reb ree reg rei rej rel rem ren rer res ret rey rga rge rgk rgn rgr rgs rgu rhg rhp ria rib rie rif ril rim rin rir rit riu rjg rji rjs rka rkb rkh rki rkm rkt rkw rm rma rmb rmc rmd rme rmf rmg rmh rmi rmk rml rmm rmn rmo rmp rmq rmr rms rmt rmu rmv rmw rmx rmy rmz rn rna rnb rnd rng rnl rnn rnp rnr rnw ro roa rob roc rod roe rof rog rol rom roo rop ror rou row rpn rpt rri rrm rro rrt rsb rsi rsk rsl rsm rsn rsw rtc rth rtm rts rtw ru rub ruc rue ruf rug ruh rui ruk ruo rup ruq rut ruu ruy ruz rw rwa rwk rwl rwm rwo rwr rxd rxw ryn rys ryu rzh sa saa sab sac sad sae saf sah sai saj sak sal sam sao sap saq sar sas sat sau sav saw sax say saz sba sbb sbc sbd sbe sbf sbg sbh sbi sbj sbk sbl sbm sbn sbo sbp sbq sbr sbs sbt sbu sbv sbw sbx sby sbz sc sca scb sce scf scg sch sci sck scl scn sco scp scq scs sct scu scv scw scx sd sda sdb sdc sde sdf sdg sdh sdj sdk sdl sdm sdn sdo sdp sdq sdr sds sdt sdu sdv sdx sdz se sea seb sec sed see sef seg seh sei sej sek sel sem sen seo sep seq ser ses set seu sev sew sey sez sfb sfe sfm sfs sfw sg sga sgb sgc sgd sge sgg sgh sgi sgj sgk sgl sgm sgn sgo sgp sgr sgs sgt sgu sgw sgx sgy sgz sh sha shb shc shd she shg shh shi shj shk shl shm shn sho shp shq shr shs sht shu shv shw shx shy shz si sia sib sid sie sif sig sih sii sij sik sil sim sio sip siq sir sis sit siu siv siw six siy siz sja sjb sjc sjd sje sjg sjk sjl sjm sjn sjo sjp sjr sjs sjt sju sjw sk ska skb skc skd ske skf skg skh ski skj skk skm skn sko skp skq skr sks skt sku skv skw skx sky skz sl sla slc sld sle slf slg slh sli slj sll slm sln slp slq slr sls slt slu slw slx sly slz sm sma smb smc smd smf smg smh smi smj smk sml smm smn smp smq smr sms smt smu smv smw smx smy smz sn snb snc sne snf sng snh sni snj snk snl snm snn sno snp snq snr sns snu snv snw snx sny snz so soa sob soc sod soe sog soh soi soj sok sol son soo sop soq sor sos sou sov sow sox soy soz spb spc spd spe spg spi spk spl spm spn spo spp spq spr sps spt spu spv spx spy sq sqa sqh sqj sqk sqm sqn sqo sqq sqr sqs sqt squ sqx sr sra srb src sre srf srg srh sri srk srl srm srn sro srq srr srs srt sru srv srw srx sry srz ss ssa ssb ssc ssd sse ssf ssg ssh ssi ssj ssk ssl ssm ssn sso ssp ssq ssr sss sst ssu ssv ssx ssy ssz st sta stb std ste stf stg sth sti stj stk stl stm stn sto stp stq str sts stt stu stv stw sty su sua sub suc sue sug sui suj suk sul sum suo suq sur sus sut suv suw sux suy suz sv sva svb svc sve svk svm svr svs svx sw swb swc swf swg swh swi swj swk swl swm swn swo swp swq swr sws swt swu swv sww swx swy sxb sxc sxe sxg sxk sxl sxm sxn sxo sxr sxs sxu sxw sya syb syc syd syi syk syl sym syn syo syr sys syw syx syy sza szb szc szd sze szg szl szn szp szs szv szw szy ta taa tab tac tad tae taf tag tai taj tak tal tan tao tap taq tar tas tau tav taw tax tay taz tba tbb tbc tbd tbe tbf tbg tbh tbi tbj tbk tbl tbm tbn tbo tbp tbq tbr tbs tbt tbu tbv tbw tbx tby tbz tca tcb tcc tcd tce tcf tcg tch tci tck tcl tcm tcn tco tcp tcq tcs tct tcu tcw tcx tcy tcz tda tdb tdc tdd tde tdf tdg tdh tdi tdj tdk tdl tdm tdn tdo tdq tdr tds tdt tdu tdv tdx tdy te tea teb tec ted tee tef teg teh tei tek tem ten teo tep teq ter tes tet teu tev tew tex tey tez tfi tfn tfo tfr tft tg tga tgb tgc tgd tge tgf tgg tgh tgi tgj tgn tgo tgp tgq tgr tgs tgt tgu tgv tgw tgx tgy tgz th thc thd the thf thh thi thk thl thm thn thp thq thr ths tht thu thv thw thx thy thz ti tia tic tid tie tif tig tih tii tij tik til tim tin tio tip tiq tis tit tiu tiv tiw tix tiy tiz tja tjg tji tjj tjl tjm tjn tjo tjp tjs tju tjw tk tka tkb tkd tke tkf tkg tkk tkl tkm tkn tkp tkq tkr tks tkt tku tkv tkw tkx tkz tl tla tlb tlc tld tlf tlg tlh tli tlj tlk tll tlm tln tlo tlp tlq tlr tls tlt tlu tlv tlw tlx tly tma tmb tmc tmd tme tmf tmg tmh tmi tmj tmk tml tmm tmn tmo tmp tmq tmr tms tmt tmu tmv tmw tmy tmz tn tna tnb tnc tnd tne tnf tng tnh tni tnk tnl tnm tnn tno tnp tnq tnr tns tnt tnu tnv tnw tnx tny tnz to tob toc tod toe tof tog toh toi toj tok tol tom too top toq tor tos tou tov tow tox toy toz tpa tpc tpe tpf tpg tpi tpj tpk tpl tpm tpn tpo tpp tpq tpr tpt tpu tpv tpw tpx tpy tpz tqb tql tqm tqn tqo tqp tqq tqr tqt tqu tqw tr tra trb trc trd tre trf trg trh tri trj trk trl trm trn tro trp trq trr trs trt tru trv trw trx try trz ts tsa tsb tsc tsd tse tsf tsg tsh tsi tsj tsk tsl tsm tsp tsq tsr tss tst tsu tsv tsw tsx tsy tsz tt tta ttb ttc ttd tte ttf ttg tth tti ttj ttk ttl ttm ttn tto ttp ttq ttr tts ttt ttu ttv ttw tty ttz tua tub tuc tud tue tuf tug tuh tui tuj tul tum tun tuo tup tuq tus tut tuu tuv tuw tux tuy tuz tva tvd tve tvi tvk tvl tvm tvn tvo tvs tvt tvu tvw tvx tvy tw twa twb twc twd twe twf twg twh twl twm twn two twp twq twr twt twu tww twx twy txa txb txc txe txg txh txi txj txm txn txo txq txr txs txt txu txx txy ty tya tye tyh tyi tyj tyl tyn typ tyr tys tyt tyu tyv tyx tyy tyz tza tzh tzj tzl tzm tzn tzo tzx uam uan uar uba ubi ubl ubr ubu uby uda ude udg udi udj udl udm udu ues ufi ug uga ugb uge ugh ugn ugo ugy uha uhn uis uiv uji uk uka ukg ukh uki ukk ukl ukp ukq uks uku ukv ukw uky ula ulb ulc ule ulf uli ulk ull ulm uln ulu ulw uly uma umb umc umd umg umi umm umn umo ump umr ums umu una und une ung uni unk unm unn unp unr unu unx unz uok uon upi upv ur ura urb urc ure urf urg urh uri urj urk url urm urn uro urp urr urt uru urv urw urx ury urz usa ush usi usk usp uss usu uta ute uth utp utr utu uum uun uur uuu uve uvh uvl uwa uya uz uzn uzs vaa vae vaf vag vah vai vaj val vam van vao vap var vas vau vav vay vbb vbk ve vec ved vel vem veo vep ver vgr vgt vi vic vid vif vig vil vin vis vit viv vjk vka vki vkj vkk vkl vkm vkn vko vkp vkt vku vkz vlp vls vma vmb vmc vmd vme vmf vmg vmh vmi vmj vmk vml vmm vmp vmq vmr vms vmu vmv vmw vmx vmy vmz vnk vnm vnp vo vor vot vra vro vrs vrt vsi vsl vsn vsv vto vum vun vut vwa wa waa wab wac wad wae waf wag wah wai waj wak wal wam wan wao wap waq war was wat wau wav waw wax way waz wba wbb wbe wbf wbh wbi wbj wbk wbl wbm wbp wbq wbr wbs wbt wbv wbw wca wci wdd wdg wdj wdk wdt wdu wdy wea wec wed weg weh wei wem wen weo wep wer wes wet weu wew wfg wga wgb wgg wgi wgo wgu wgw wgy wha whg whk whu wib wic wie wif wig wih wii wij wik wil wim win wir wit wiu wiv wiw wiy wja wji wka wkb wkd wkl wkr wku wkw wky wla wlc wle wlg wlh wli wlk wll wlm wlo wlr wls wlu wlv wlw wlx wly wma wmb wmc wmd wme wmg wmh wmi wmm wmn wmo wms wmt wmw wmx wnb wnc wnd wne wng wni wnk wnm wnn wno wnp wnu wnw wny wo woa wob woc wod woe wof wog woi wok wom won woo wor wos wow woy wpc wra wrb wrd wrg wrh wri wrk wrl wrm wrn wro wrp wrr wrs wru wrv wrw wrx wry wrz wsa wsg wsi wsk wsr wss wsu wsv wtb wtf wth wti wtk wtm wtw wua wub wud wuh wul wum wun wur wut wuu wuv wux wuy wwa wwb wwo wwr www wxa wxw wya wyb wyi wym wyn wyr wyy xaa xab xac xad xae xag xai xaj xak xal xam xan xao xap xaq xar xas xat xau xav xaw xay xba xbb xbc xbd xbe xbg xbi xbj xbm xbn xbo xbp xbr xbw xbx xby xcb xcc xce xcg xch xcl xcm xcn xco xcr xct xcu xcv xcw xcy xda xdc xdk xdm xdo xdq xdy xeb xed xeg xel xem xep xer xes xet xeu xfa xga xgb xgd xgf xgg xgi xgl xgm xgn xgr xgu xgw xh xha xhc xhd xhe xhm xhr xht xhu xhv xia xib xii xil xin xip xir xis xiv xiy xjb xjt xka xkb xkc xkd xke xkf xkg xkh xki xkj xkk xkl xkn xko xkp xkq xkr xks xkt xku xkv xkw xkx xky xkz xla xlb xlc xld xle xlg xli xln xlo xlp xls xlu xly xma xmb xmc xmd xme xmf xmg xmh xmj xmk xml xmm xmn xmo xmp xmq xmr xms xmt xmu xmv xmw xmx xmy xmz xna xnb xnd xng xnh xni xnj xnk xnm xnn xno xnq xnr xns xnt xnu xny xnz xoc xod xog xoi xok xom xon xoo xop xor xow xpa xpb xpc xpd xpe xpf xpg xph xpi xpj xpk xpl xpm xpn xpo xpp xpq xpr xps xpt xpu xpv xpw xpx xpy xpz xqa xqt xra xrb xrd xre xrg xri xrm xrn xrq xrr xrt xru xrw xsa xsb xsc xsd xse xsh xsi xsj xsl xsm xsn xso xsp xsq xsr xss xsu xsv xsy xta xtb xtc xtd xte xtg xth xti xtj xtl xtm xtn xto xtp xtq xtr xts xtt xtu xtv xtw xty xtz xua xub xud xug xuj xul xum xun xuo xup xur xut xuu xve xvi xvn xvo xvs xwa xwc xwd xwe xwg xwj xwk xwl xwo xwr xwt xww xxb xxk xxm xxr xxt xya xyb xyj xyk xyl xyt xyy xzh xzm xzp yaa yab yac yad yae yaf yag yah yai yaj yak yal yam yan yao yap yaq yar yas yat yau yav yaw yax yay yaz yba ybb ybd ybe ybh ybi ybj ybk ybl ybm ybn ybo ybx yby ych ycl ycn ycp ycr yda ydd yde ydg ydk yds yea yec yee yei yej yel yen yer yes yet yeu yev yey yga ygi ygl ygm ygp ygr ygs ygu ygw yha yhd yhl yhs yi yia yif yig yih yii yij yik yil yim yin yip yiq yir yis yit yiu yiv yix yiy yiz yka ykg ykh yki ykk ykl ykm ykn yko ykr ykt yku yky yla ylb yle ylg yli yll ylm yln ylo ylr ylu yly yma ymb ymc ymd yme ymg ymh ymi ymk yml ymm ymn ymo ymp ymq ymr yms ymt ymx ymz yna ynb ynd yne yng ynh ynk ynl ynn yno ynq yns ynu yo yob yog yoi yok yol yom yon yos yot yox yoy ypa ypb ypg yph ypk ypm ypn ypo ypp ypz yra yrb yre yri yrk yrl yrm yrn yro yrs yrw yry ysc ysd ysg ysl ysm ysn yso ysp ysr yss ysy yta ytl ytp ytw yty yua yub yuc yud yue yuf yug yui yuj yuk yul yum yun yup yuq yur yut yuu yuw yux yuy yuz yva yvt ywa ywg ywl ywn ywq ywr ywt ywu yww yxa yxg yxl yxm yxu yxy yyr yyu yyz yzg yzk za zaa zab zac zad zae zaf zag zah zai zaj zak zal zam zao zap zaq zar zas zat zau zav zaw zax zay zaz zba zbc zbe zbl zbt zbu zbw zca zcd zch zdj zea zeg zeh zem zen zga zgb zgh zgm zgn zgr zh zhb zhd zhi zhn zhw zhx zia zib zik zil zim zin zir ziw ziz zka zkb zkd zkg zkh zkk zkn zko zkp zkr zkt zku zkv zkz zla zle zlj zlm zln zlq zls zlu zlw zma zmb zmc zmd zme zmf zmg zmh zmi zmj zmk zml zmm zmn zmo zmp zmq zmr zms zmt zmu zmv zmw zmx zmy zmz zna znd zne zng znk zns zoc zoh zom zoo zoq zor zos zpa zpb zpc zpd zpe zpf zpg zph zpi zpj zpk zpl zpm zpn zpo zpp zpq zpr zps zpt zpu zpv zpw zpx zpy zpz zqe zra zrg zrn zro zrp zrs zsa zsk zsl zsm zsr zsu zte ztg ztl ztm ztn ztp ztq zts ztt ztu ztx zty zu zua zuh zum zun zuy zwa zxx zyb zyg zyj zyn zyp zza zzj';
33968
+ // </generated:language-subtags>
33969
+
33970
+ // BCP 47 well-formedness plus a registry check on the primary subtag. Shape
33971
+ // alone accepts "eng" and "em-US", which look like language tags but are not
33972
+ // registered: the IANA registry lists a three-letter subtag only when no
33973
+ // two-letter one exists, so "en" is registered and "eng" is not.
33974
+ let __languageSubtagSet = null;
33975
+ function isRegisteredLanguageSubtag(subtag) {
33976
+ if (!__languageSubtagSet) __languageSubtagSet = new Set(LANGUAGE_SUBTAGS.split(' '));
33977
+ return __languageSubtagSet.has(String(subtag || '').toLowerCase());
33978
+ }
33979
+
33980
+ function isValidLanguageTag(value) {
33981
+ const raw = String(value == null ? '' : value).trim();
33982
+ if (!raw) return false;
33983
+ if (!/^[a-zA-Z]{2,3}(-[a-zA-Z0-9]{1,8})*$/.test(raw)) return false;
33984
+ return isRegisteredLanguageSubtag(raw.split('-')[0]);
33985
+ }
33986
+
32229
33987
  const document = opts && opts.document ? opts.document : null;
32230
33988
  const window = opts && opts.window ? opts.window : null;
32231
33989
  // Some engine paths may not pass opts.window; recover it from document when possible.
@@ -34934,6 +36692,23 @@ const createDomHelpers = (function createDomHelpers(opts) {
34934
36692
  return { present: !!value, value, mechanism: 'label', flags: value ? [] : ['empty'] };
34935
36693
  }
34936
36694
 
36695
+ // ACT scopes every accessible-name rule to elements "included in the
36696
+ // accessibility tree" (c487ae link-name, 97a4e1 button-name and siblings),
36697
+ // and its glossary puts focusable aria-hidden content outside that set:
36698
+ // "Because they are hidden, these elements are considered not included in
36699
+ // the accessibility tree", even where a browser leaves them in it. The
36700
+ // defect they do represent, aria-hidden over content in the focus order,
36701
+ // belongs to aria-hidden-focus (ACT 6cfa84, WCAG 4.1.2).
36702
+ //
36703
+ // isAccTreeEligible keeps them eligible on purpose so aria-hidden-focus can
36704
+ // reach them, so naming rules need this narrower question instead.
36705
+ function isIncludedInAccessibilityTree(el) {
36706
+ const r = isAccTreeEligible(el);
36707
+ if (!r || !r.eligible) return false;
36708
+ const reasons = Array.isArray(r.reasons) ? r.reasons : [];
36709
+ return !reasons.some((x) => typeof x === 'string' && x.indexOf('ariaHiddenOverridden') === 0);
36710
+ }
36711
+
34937
36712
  function getAccessibleNameInfo(el, _ctx, opts) {
34938
36713
  const flags = [];
34939
36714
  if (!isElement(el))
@@ -35481,13 +37256,51 @@ const createDomHelpers = (function createDomHelpers(opts) {
35481
37256
  }
35482
37257
 
35483
37258
  const ownName = getAccessibleNameInfo(node, _ctx, opts);
35484
- if (ownName && ownName.present && ownName.value) {
37259
+ // A descendant's own name outranks its content for real naming
37260
+ // mechanisms (aria-label/aria-labelledby, a native <label>), but not for
37261
+ // a bare title: accname orders name-from-content ahead of the title
37262
+ // fallback, so a descendant's title may only speak for it when its
37263
+ // content is empty. Same precedence the image-like branch above applies
37264
+ // to alt; without it, a tooltip silently replaces the visible text of
37265
+ // anything named from its content.
37266
+ const titleOnlyName = !!(
37267
+ ownName &&
37268
+ ownName.present &&
37269
+ ownName.value &&
37270
+ ownName.mechanism === 'title'
37271
+ );
37272
+
37273
+ if (ownName && ownName.present && ownName.value && !titleOnlyName) {
35485
37274
  parts.push(ownName.value);
35486
37275
  const tag = `descendant-name-used:${ownName.mechanism || 'unknown'}`;
35487
37276
  if (flags.indexOf(tag) === -1) flags.push(tag);
35488
37277
  return; // this descendant speaks for itself; don't also use its content
35489
37278
  }
35490
37279
 
37280
+ if (titleOnlyName) {
37281
+ // Content first; keep the title only if the content contributed
37282
+ // nothing. Compared on the trimmed join rather than parts.length,
37283
+ // since a whitespace-only text node pushes a part but no name text.
37284
+ const before = parts.length;
37285
+ walkChildren(node, parts);
37286
+ if (!trim(parts.slice(before).join(' '))) {
37287
+ parts.length = before;
37288
+ parts.push(ownName.value);
37289
+ if (flags.indexOf('descendant-name-used:title-fallback') === -1)
37290
+ flags.push('descendant-name-used:title-fallback');
37291
+ } else if (flags.indexOf('descendant-title-superseded-by-content') === -1) {
37292
+ flags.push('descendant-title-superseded-by-content');
37293
+ }
37294
+ return;
37295
+ }
37296
+
37297
+ walkChildren(node, parts);
37298
+ }
37299
+
37300
+ // Extracted from collect() so the title-only branch above can walk a
37301
+ // descendant's children and then decide whether the title was needed,
37302
+ // without duplicating the <slot> handling.
37303
+ function walkChildren(node, parts) {
35491
37304
  // A <slot>'s own childNodes are its FALLBACK content only —
35492
37305
  // rendered solely when nothing is assigned to it. When real content
35493
37306
  // IS distributed into it, that's what's exposed to the accessibility
@@ -36534,6 +38347,9 @@ const createDomHelpers = (function createDomHelpers(opts) {
36534
38347
  }
36535
38348
 
36536
38349
  return {
38350
+ isValidLanguageTag,
38351
+ isRegisteredLanguageSubtag,
38352
+
36537
38353
  // Existing query/snippet utilities
36538
38354
  queryAll,
36539
38355
  queryAllDeep,
@@ -36547,6 +38363,7 @@ const createDomHelpers = (function createDomHelpers(opts) {
36547
38363
  hasAccessibleName,
36548
38364
  isExcluded,
36549
38365
  isAccTreeEligible,
38366
+ isIncludedInAccessibilityTree,
36550
38367
  isDomVisibleEligible,
36551
38368
  isWholeDocumentScope,
36552
38369
 
@@ -36603,6 +38420,12 @@ const createDomHelpers = (function createDomHelpers(opts) {
36603
38420
  getLabelMethod,
36604
38421
  getLabelStrength,
36605
38422
 
38423
+ // Whether a <label> carries text that names its associated control
38424
+ // (own aria-name, else rendered content, else title). Shared so
38425
+ // form-control-single-label and form-control-programmatic-label-present
38426
+ // agree on what a label is worth.
38427
+ labelContributesAccessibleName,
38428
+
36606
38429
  // Flat-tree ancestor walk (assignedSlot-aware, then shadow host) —
36607
38430
  // see this function's own definition above for why assignedSlot
36608
38431
  // must win over parentNode.