abledom 0.6.1 → 0.6.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,9 +1,99 @@
1
1
  # AbleDOM
2
2
 
3
- Continuous detection of typical web application accessibility problems.
3
+ A continuous accessibility (a11y) monitor for modern web applications.
4
+
5
+ AbleDOM is a lightweight JavaScript/TypeScript library that observes your DOM in real-time and detects common accessibility issues as they appear.
4
6
 
5
7
  _Here be dragons_.
6
8
 
9
+ ## Installation
10
+
11
+ ```bash
12
+ npm install abledom
13
+ # or
14
+ yarn add abledom
15
+ # or
16
+ pnpm add abledom
17
+ ```
18
+
19
+ ## Quick start
20
+
21
+ ```typescript
22
+ import { AbleDOM } from "abledom";
23
+
24
+ const _ableDOM = new AbleDOM(window, { log: window.console?.error });
25
+
26
+ // ...Create and add rules and exceptions
27
+ ```
28
+
29
+ ### Using rules
30
+
31
+ ```typescript
32
+ import { AbleDOM, ContrastRule } from "abledom";
33
+
34
+ const contrastRule = new ContrastRule();
35
+ this._ableDOM.addRule(contrastRule);
36
+ ```
37
+
38
+ ### Adding valid exceptions
39
+
40
+ ```typescript
41
+ import { AbleDOM, ContrastRule } from "abledom";
42
+
43
+ const contrastExceptions: ((element: HTMLElement) => boolean)[] = [
44
+ (element: HTMLElement) => {
45
+ return element.style?.display === "none";
46
+ },
47
+ (element: HTMLElement) => {
48
+ return element.datalist?.ignore === "true";
49
+ },
50
+ ];
51
+
52
+ const contrastRule = new ContrastRule();
53
+
54
+ contrastExceptions.forEach((exception) => contrastRule.addException(exception));
55
+
56
+ this._ableDOM.addRule(contrastRule);
57
+ ```
58
+
59
+ ## Rules
60
+
61
+ ### AtomicRule
62
+
63
+ Detects focusable elements nested inside other atomic focusable elements (like buttons, links, or inputs). Prevents confusing interactive hierarchies that can break keyboard navigation and assistive technology functionality.
64
+
65
+ ### BadFocusRule
66
+
67
+ Monitors focus changes to detect when focus is stolen by invisible elements. Helps identify scenarios where focus moves to elements that users cannot see, creating a poor accessibility experience.
68
+
69
+ ### ContrastRule
70
+
71
+ Validates color contrast ratios between text and background colors according to WCAG standards. Ensures text meets minimum contrast requirements (4.5:1 for normal text, 3:1 for large text) for readability.
72
+
73
+ ### ExistingIdRule
74
+
75
+ Verifies that elements referenced by `aria-labelledby`, `aria-describedby`, or `<label for>` attributes actually exist in the DOM. Prevents broken accessibility relationships.
76
+
77
+ ### FocusableElementLabelRule
78
+
79
+ Ensures all focusable interactive elements have accessible labels. Checks for labels through various methods including `<label>` elements, ARIA attributes, alt text, and visible text content.
80
+
81
+ ### FocusLostRule
82
+
83
+ Detects when keyboard focus is lost without being moved to another valid element. Monitors focus/blur events to catch scenarios where users might lose their place while navigating with keyboard.
84
+
85
+ ### NestedInteractiveElementRule
86
+
87
+ Identifies interactive elements nested within other interactive elements (e.g., a button inside a link). This pattern can confuse users and assistive technologies about which element to interact with.
88
+
89
+ ### RequiredParentRule
90
+
91
+ Validates that elements requiring specific parent elements are properly nested. Enforces correct HTML structure for elements like `<li>` (must be in `<ul>` or `<ol>`), table elements, and ARIA roles that have parent requirements.
92
+
93
+ ### TabIndexRule
94
+
95
+ Warns about problematic uses of the `tabindex` attribute, including positive values that break natural tab order and `tabindex` on non-interactive elements. Promotes accessible keyboard navigation patterns.
96
+
7
97
  ## Contributing
8
98
 
9
99
  This project welcomes contributions and suggestions. Most contributions require you to agree to a
package/dist/esm/index.js CHANGED
@@ -57,10 +57,10 @@ var ValidationRule = class {
57
57
  }
58
58
  };
59
59
 
60
- // inline-file:/Users/marata/Documents/Work/abledom/src/ui/ui.css
60
+ // inline-file:/Users/marata/tmp/abledom/src/ui/ui.css
61
61
  var ui_default = "#abledom-report {\n bottom: 20px;\n display: flex;\n flex-direction: column;\n left: 10px;\n max-height: 80%;\n max-width: 60%;\n padding: 4px 8px;\n position: fixed;\n z-index: 100500;\n}\n\n#abledom-report :focus-visible {\n outline: 3px solid red;\n mix-blend-mode: difference;\n}\n\n#abledom-report.abledom-align-left {\n left: 10px;\n right: auto;\n}\n\n#abledom-report.abledom-align-right {\n left: auto;\n right: 10px;\n}\n\n#abledom-report.abledom-align-bottom {\n bottom: 20px;\n top: auto;\n}\n\n#abledom-report.abledom-align-top {\n /* flex-direction: column-reverse; */\n bottom: auto;\n top: 10px;\n}\n\n.abledom-menu-container {\n backdrop-filter: blur(3px);\n border-radius: 8px;\n box-shadow: 0px 0px 4px rgba(127, 127, 127, 0.5);\n display: inline-block;\n margin: 2px auto 2px 0;\n}\n\n#abledom-report.abledom-align-right .abledom-menu-container {\n margin: 2px 0 2px auto;\n}\n\n.abledom-menu {\n background-color: rgba(140, 10, 121, 0.7);\n border-radius: 8px;\n color: white;\n display: inline flex;\n font-family: Arial, Helvetica, sans-serif;\n font-size: 16px;\n line-height: 26px;\n padding: 4px;\n}\n\n.abledom-menu .issues-count {\n margin: 0 8px;\n display: inline-block;\n}\n\n.abledom-menu .button {\n all: unset;\n color: #000;\n cursor: pointer;\n background: linear-gradient(\n 180deg,\n rgba(255, 255, 255, 1) 0%,\n rgba(200, 200, 200, 1) 100%\n );\n border-radius: 6px;\n border: 1px solid rgba(255, 255, 255, 0.4);\n box-sizing: border-box;\n line-height: 0px;\n margin-right: 4px;\n max-height: 26px;\n padding: 2px;\n text-decoration: none;\n}\n\n.abledom-menu .align-button {\n border-right-color: rgba(0, 0, 0, 0.4);\n border-radius: 0;\n margin: 0;\n}\n\n.abledom-menu .align-button:active,\n#abledom-report .pressed {\n background: linear-gradient(\n 180deg,\n rgba(130, 130, 130, 1) 0%,\n rgba(180, 180, 180, 1) 100%\n );\n}\n\n.abledom-menu .align-button-first {\n border-top-left-radius: 6px;\n border-bottom-left-radius: 6px;\n margin-left: 8px;\n}\n.abledom-menu .align-button-last {\n border-top-right-radius: 6px;\n border-bottom-right-radius: 6px;\n border-right-color: rgba(255, 255, 255, 0.4);\n}\n\n.abledom-issues-container {\n overflow: auto;\n max-height: calc(100vh - 100px);\n}\n\n#abledom-report.abledom-align-right .abledom-issues-container {\n text-align: right;\n}\n\n.abledom-issue-container {\n backdrop-filter: blur(3px);\n border-radius: 8px;\n box-shadow: 0px 0px 4px rgba(127, 127, 127, 0.5);\n display: inline-flex;\n margin: 2px 0;\n}\n\n.abledom-issue {\n background-color: rgba(164, 2, 2, 0.7);\n border-radius: 8px;\n color: white;\n display: inline flex;\n font-family: Arial, Helvetica, sans-serif;\n font-size: 16px;\n line-height: 26px;\n padding: 4px;\n}\n.abledom-issue_warning {\n background-color: rgba(163, 82, 1, 0.7);\n}\n.abledom-issue_info {\n background-color: rgba(0, 0, 255, 0.7);\n}\n\n.abledom-issue .button {\n all: unset;\n color: #000;\n cursor: pointer;\n background: linear-gradient(\n 180deg,\n rgba(255, 255, 255, 1) 0%,\n rgba(200, 200, 200, 1) 100%\n );\n border-radius: 6px;\n border: 1px solid rgba(255, 255, 255, 0.4);\n box-sizing: border-box;\n line-height: 0px;\n margin-right: 4px;\n max-height: 26px;\n padding: 2px;\n text-decoration: none;\n}\n\n.abledom-issue .button:hover {\n opacity: 0.7;\n}\n\n.abledom-issue .button.close {\n background: none;\n border-color: transparent;\n color: #fff;\n margin: 0;\n}\n";
62
62
 
63
- // inline-file:/Users/marata/Documents/Work/abledom/src/ui/highlighter.css
63
+ // inline-file:/Users/marata/tmp/abledom/src/ui/highlighter.css
64
64
  var highlighter_default = ".abledom-highlight {\n background-color: yellow;\n box-sizing: border-box;\n display: none;\n opacity: 0.6;\n position: fixed;\n z-index: 100499;\n}\n\n.abledom-highlight-border1 {\n border-top: 2px solid red;\n border-bottom: 2px solid red;\n box-sizing: border-box;\n position: absolute;\n top: -2px;\n width: calc(100% + 20px);\n height: calc(100% + 4px);\n margin: 0 -10px;\n}\n\n.abledom-highlight-border2 {\n border-left: 2px solid red;\n border-right: 2px solid red;\n box-sizing: border-box;\n position: absolute;\n width: calc(100% + 4px);\n left: -2px;\n height: calc(100% + 20px);\n margin: -10px 0;\n}\n";
65
65
 
66
66
  // src/ui/domBuilder.ts
@@ -118,7 +118,7 @@ var DOMBuilder = class {
118
118
  }
119
119
  };
120
120
 
121
- // inline-file:/Users/marata/Documents/Work/abledom/src/ui/close.svg
121
+ // inline-file:/Users/marata/tmp/abledom/src/ui/close.svg
122
122
  var close_default = (function buildSVG(parent) {
123
123
  const builder = new DOMBuilder(parent);
124
124
  builder.openTag("svg", { "width": "20", "height": "20", "viewBox": "0 0 24 24", "fill": "none", "stroke": "currentColor", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round", "xmlns": "http://www.w3.org/2000/svg" }, void 0, "http://www.w3.org/2000/svg");
@@ -132,7 +132,7 @@ var close_default = (function buildSVG(parent) {
132
132
  return parent.firstElementChild;
133
133
  });
134
134
 
135
- // inline-file:/Users/marata/Documents/Work/abledom/src/ui/help.svg
135
+ // inline-file:/Users/marata/tmp/abledom/src/ui/help.svg
136
136
  var help_default = (function buildSVG2(parent) {
137
137
  const builder = new DOMBuilder(parent);
138
138
  builder.openTag("svg", { "width": "20", "height": "20", "viewBox": "0 0 24 24", "fill": "none", "stroke": "currentColor", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round", "xmlns": "http://www.w3.org/2000/svg" }, void 0, "http://www.w3.org/2000/svg");
@@ -146,7 +146,7 @@ var help_default = (function buildSVG2(parent) {
146
146
  return parent.firstElementChild;
147
147
  });
148
148
 
149
- // inline-file:/Users/marata/Documents/Work/abledom/src/ui/log.svg
149
+ // inline-file:/Users/marata/tmp/abledom/src/ui/log.svg
150
150
  var log_default = (function buildSVG3(parent) {
151
151
  const builder = new DOMBuilder(parent);
152
152
  builder.openTag("svg", { "width": "20", "height": "20", "viewBox": "0 0 24 24", "fill": "none", "stroke": "currentColor", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round", "xmlns": "http://www.w3.org/2000/svg" }, void 0, "http://www.w3.org/2000/svg");
@@ -158,7 +158,7 @@ var log_default = (function buildSVG3(parent) {
158
158
  return parent.firstElementChild;
159
159
  });
160
160
 
161
- // inline-file:/Users/marata/Documents/Work/abledom/src/ui/reveal.svg
161
+ // inline-file:/Users/marata/tmp/abledom/src/ui/reveal.svg
162
162
  var reveal_default = (function buildSVG4(parent) {
163
163
  const builder = new DOMBuilder(parent);
164
164
  builder.openTag("svg", { "width": "20", "height": "20", "viewBox": "0 0 24 24", "fill": "none", "stroke": "currentColor", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round", "xmlns": "http://www.w3.org/2000/svg" }, void 0, "http://www.w3.org/2000/svg");
@@ -176,7 +176,7 @@ var reveal_default = (function buildSVG4(parent) {
176
176
  return parent.firstElementChild;
177
177
  });
178
178
 
179
- // inline-file:/Users/marata/Documents/Work/abledom/src/ui/bug.svg
179
+ // inline-file:/Users/marata/tmp/abledom/src/ui/bug.svg
180
180
  var bug_default = (function buildSVG5(parent) {
181
181
  const builder = new DOMBuilder(parent);
182
182
  builder.openTag("svg", { "width": "20", "height": "20", "viewBox": "0 0 20 20", "fill": "none", "stroke": "currentColor" }, void 0, "http://www.w3.org/2000/svg");
@@ -204,7 +204,7 @@ var bug_default = (function buildSVG5(parent) {
204
204
  return parent.firstElementChild;
205
205
  });
206
206
 
207
- // inline-file:/Users/marata/Documents/Work/abledom/src/ui/hideall.svg
207
+ // inline-file:/Users/marata/tmp/abledom/src/ui/hideall.svg
208
208
  var hideall_default = (function buildSVG6(parent) {
209
209
  const builder = new DOMBuilder(parent);
210
210
  builder.openTag("svg", { "width": "20", "height": "20", "viewBox": "0 0 20 20", "fill": "none", "stroke": "currentColor" }, void 0, "http://www.w3.org/2000/svg");
@@ -218,7 +218,7 @@ var hideall_default = (function buildSVG6(parent) {
218
218
  return parent.firstElementChild;
219
219
  });
220
220
 
221
- // inline-file:/Users/marata/Documents/Work/abledom/src/ui/muteall.svg
221
+ // inline-file:/Users/marata/tmp/abledom/src/ui/muteall.svg
222
222
  var muteall_default = (function buildSVG7(parent) {
223
223
  const builder = new DOMBuilder(parent);
224
224
  builder.openTag("svg", { "width": "20", "height": "20", "viewBox": "0 0 20 20", "fill": "none", "stroke": "currentColor" }, void 0, "http://www.w3.org/2000/svg");
@@ -234,7 +234,7 @@ var muteall_default = (function buildSVG7(parent) {
234
234
  return parent.firstElementChild;
235
235
  });
236
236
 
237
- // inline-file:/Users/marata/Documents/Work/abledom/src/ui/showall.svg
237
+ // inline-file:/Users/marata/tmp/abledom/src/ui/showall.svg
238
238
  var showall_default = (function buildSVG8(parent) {
239
239
  const builder = new DOMBuilder(parent);
240
240
  builder.openTag("svg", { "width": "20", "height": "20", "viewBox": "0 0 20 20", "fill": "none", "stroke": "currentColor" }, void 0, "http://www.w3.org/2000/svg");
@@ -246,7 +246,7 @@ var showall_default = (function buildSVG8(parent) {
246
246
  return parent.firstElementChild;
247
247
  });
248
248
 
249
- // inline-file:/Users/marata/Documents/Work/abledom/src/ui/aligntopleft.svg
249
+ // inline-file:/Users/marata/tmp/abledom/src/ui/aligntopleft.svg
250
250
  var aligntopleft_default = (function buildSVG9(parent) {
251
251
  const builder = new DOMBuilder(parent);
252
252
  builder.openTag("svg", { "width": "20", "height": "20", "viewBox": "0 0 20 20", "fill": "none", "stroke": "currentColor" }, void 0, "http://www.w3.org/2000/svg");
@@ -256,7 +256,7 @@ var aligntopleft_default = (function buildSVG9(parent) {
256
256
  return parent.firstElementChild;
257
257
  });
258
258
 
259
- // inline-file:/Users/marata/Documents/Work/abledom/src/ui/aligntopright.svg
259
+ // inline-file:/Users/marata/tmp/abledom/src/ui/aligntopright.svg
260
260
  var aligntopright_default = (function buildSVG10(parent) {
261
261
  const builder = new DOMBuilder(parent);
262
262
  builder.openTag("svg", { "width": "20", "height": "20", "viewBox": "0 0 20 20", "fill": "none", "stroke": "currentColor" }, void 0, "http://www.w3.org/2000/svg");
@@ -266,7 +266,7 @@ var aligntopright_default = (function buildSVG10(parent) {
266
266
  return parent.firstElementChild;
267
267
  });
268
268
 
269
- // inline-file:/Users/marata/Documents/Work/abledom/src/ui/alignbottomright.svg
269
+ // inline-file:/Users/marata/tmp/abledom/src/ui/alignbottomright.svg
270
270
  var alignbottomright_default = (function buildSVG11(parent) {
271
271
  const builder = new DOMBuilder(parent);
272
272
  builder.openTag("svg", { "width": "20", "height": "20", "viewBox": "0 0 20 20", "fill": "none", "stroke": "currentColor" }, void 0, "http://www.w3.org/2000/svg");
@@ -276,7 +276,7 @@ var alignbottomright_default = (function buildSVG11(parent) {
276
276
  return parent.firstElementChild;
277
277
  });
278
278
 
279
- // inline-file:/Users/marata/Documents/Work/abledom/src/ui/alignbottomleft.svg
279
+ // inline-file:/Users/marata/tmp/abledom/src/ui/alignbottomleft.svg
280
280
  var alignbottomleft_default = (function buildSVG12(parent) {
281
281
  const builder = new DOMBuilder(parent);
282
282
  builder.openTag("svg", { "width": "20", "height": "20", "viewBox": "0 0 20 20", "fill": "none", "stroke": "currentColor" }, void 0, "http://www.w3.org/2000/svg");
@@ -1058,6 +1058,7 @@ var AbleDOM = class {
1058
1058
  __publicField(this, "_idleResolve");
1059
1059
  __publicField(this, "_currentAnchoredIssues", /* @__PURE__ */ new Map());
1060
1060
  __publicField(this, "_currentNotAnchoredIssues", []);
1061
+ __publicField(this, "_readIssues", /* @__PURE__ */ new Set());
1061
1062
  __publicField(this, "_getHighlighter", () => {
1062
1063
  if (!this._elementHighlighter && !this._isDisposed) {
1063
1064
  this._elementHighlighter = new ElementHighlighter(this._win);
@@ -1099,6 +1100,9 @@ var AbleDOM = class {
1099
1100
  ((_c = (_b = this._win) == null ? void 0 : _b.console) == null ? void 0 : _c.error)) == null ? void 0 : _d.apply(null, args);
1100
1101
  });
1101
1102
  this._win = win;
1103
+ if (props.exposeInstanceForTesting) {
1104
+ this._win.ableDOMInstanceForTesting = this;
1105
+ }
1102
1106
  this._props = props;
1103
1107
  const _elementsToValidate = /* @__PURE__ */ new Set();
1104
1108
  const _elementsToRemove = /* @__PURE__ */ new Set();
@@ -1414,29 +1418,60 @@ var AbleDOM = class {
1414
1418
  rules.forEach((rule) => this._removeIssue(element, rule));
1415
1419
  });
1416
1420
  }
1417
- _getCurrentIssues() {
1418
- const issues = this._currentNotAnchoredIssues.slice(0);
1421
+ _getCurrentIssues(markAsRead) {
1422
+ const issues = [];
1423
+ this._currentNotAnchoredIssues.forEach((issue) => {
1424
+ if (!this._readIssues.has(issue)) {
1425
+ issues.push(issue);
1426
+ }
1427
+ if (markAsRead) {
1428
+ this._readIssues.add(issue);
1429
+ }
1430
+ });
1419
1431
  this._currentAnchoredIssues.forEach((issueByRule) => {
1420
1432
  issueByRule.forEach((issue) => {
1421
- issues.push(issue);
1433
+ if (!this._readIssues.has(issue)) {
1434
+ issues.push(issue);
1435
+ }
1436
+ if (markAsRead) {
1437
+ this._readIssues.add(issue);
1438
+ }
1422
1439
  });
1423
1440
  });
1424
1441
  return issues;
1425
1442
  }
1426
- idle() {
1443
+ idle(markAsRead, timeout) {
1427
1444
  if (!this._clearValidationTimeout) {
1428
- return Promise.resolve(this._getCurrentIssues());
1429
- }
1445
+ return Promise.resolve(this._getCurrentIssues(!!markAsRead));
1446
+ }
1447
+ let timeoutClear;
1448
+ let timeoutResolve;
1449
+ let timeoutPromise = timeout ? new Promise((resolve) => {
1450
+ timeoutResolve = () => {
1451
+ timeoutClear == null ? void 0 : timeoutClear();
1452
+ timeoutResolve = void 0;
1453
+ resolve(null);
1454
+ };
1455
+ let timeoutTimer = this._win.setTimeout(() => {
1456
+ timeoutClear = void 0;
1457
+ timeoutResolve == null ? void 0 : timeoutResolve();
1458
+ }, timeout);
1459
+ timeoutClear = () => {
1460
+ this._win.clearTimeout(timeoutTimer);
1461
+ timeoutClear = void 0;
1462
+ };
1463
+ }) : void 0;
1430
1464
  if (!this._idlePromise) {
1431
1465
  this._idlePromise = new Promise((resolve) => {
1432
1466
  this._idleResolve = () => {
1433
1467
  delete this._idlePromise;
1434
1468
  delete this._idleResolve;
1435
- resolve(this._getCurrentIssues());
1469
+ resolve(this._getCurrentIssues(!!markAsRead));
1470
+ timeoutResolve == null ? void 0 : timeoutResolve();
1436
1471
  };
1437
1472
  });
1438
1473
  }
1439
- return this._idlePromise;
1474
+ return timeoutPromise ? Promise.race([this._idlePromise, timeoutPromise]) : this._idlePromise;
1440
1475
  }
1441
1476
  clearCurrentIssues(anchored = true, notAnchored = true) {
1442
1477
  if (anchored) {