@a11y-oracle/focus-analyzer 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,204 @@
1
+ # @a11y-oracle/focus-analyzer
2
+
3
+ Focus state analysis for accessibility testing. Provides visual focus indicator inspection, WCAG contrast ratio calculation, DOM tab order extraction, and keyboard trap detection.
4
+
5
+ This is a low-level building block used internally by `@a11y-oracle/core-engine`. Most users should use the Playwright or Cypress plugin instead.
6
+
7
+ ## Installation
8
+
9
+ ```bash
10
+ npm install @a11y-oracle/focus-analyzer
11
+ ```
12
+
13
+ ## Usage
14
+
15
+ ```typescript
16
+ import { FocusAnalyzer } from '@a11y-oracle/focus-analyzer';
17
+
18
+ const analyzer = new FocusAnalyzer(cdpSession);
19
+
20
+ // Analyze the focus indicator on the currently focused element
21
+ const indicator = await analyzer.getFocusIndicator();
22
+ console.log(indicator.isVisible); // true
23
+ console.log(indicator.contrastRatio); // 12.63
24
+ console.log(indicator.meetsWCAG_AA); // true (visible + contrast >= 3.0)
25
+ console.log(indicator.outlineColor); // "rgb(0, 95, 204)"
26
+ console.log(indicator.outlineWidth); // "3px"
27
+
28
+ // Extract all tabbable elements in DOM tab order
29
+ const entries = await analyzer.getTabOrder();
30
+ entries.forEach(e => {
31
+ console.log(`${e.index}: ${e.tag}#${e.id} (tabIndex=${e.tabIndex})`);
32
+ });
33
+
34
+ // Detect keyboard traps (WCAG 2.1.2)
35
+ const result = await analyzer.detectKeyboardTrap('#modal-container', 20);
36
+ if (result.isTrapped) {
37
+ console.log('Focus is trapped!');
38
+ } else {
39
+ console.log(`Focus escaped to: ${result.escapeElement?.tag}`);
40
+ }
41
+ ```
42
+
43
+ ## API Reference
44
+
45
+ ### `FocusAnalyzer`
46
+
47
+ #### `constructor(cdp: CDPSessionLike)`
48
+
49
+ Create a new focus analyzer. Internally creates a `KeyboardEngine` for trap detection.
50
+
51
+ - `cdp` — Any CDP-compatible session (uses the `CDPSessionLike` interface from `@a11y-oracle/cdp-types`).
52
+
53
+ #### `getFocusIndicator(): Promise<FocusIndicator>`
54
+
55
+ Analyze the visual focus indicator of the currently focused element.
56
+
57
+ Extracts computed CSS properties (`outline`, `box-shadow`, `border-color`, `background-color`) via `Runtime.evaluate`, then calculates the contrast ratio of the focus indicator against the background.
58
+
59
+ Returns a default "not visible" indicator if no element has focus.
60
+
61
+ **Visibility detection:**
62
+ - An element has a visible **outline** if `outline-width` is not `0px` and `outline-color` is not `transparent`
63
+ - An element has a visible **box-shadow** if `box-shadow` is not `none`
64
+
65
+ **Contrast calculation:**
66
+ - For outlines: contrast ratio between `outline-color` and `background-color`
67
+ - For box-shadows: extracts the first color from the `box-shadow` value and computes contrast against `background-color`
68
+ - Returns `contrastRatio: null` if colors cannot be reliably parsed (e.g., gradients, complex color functions)
69
+
70
+ **WCAG 2.4.12 AA compliance:** `meetsWCAG_AA` is `true` when the indicator is visible AND the contrast ratio >= 3.0.
71
+
72
+ #### `getTabOrder(): Promise<TabOrderEntry[]>`
73
+
74
+ Extract all tabbable elements from the DOM in tab order.
75
+
76
+ Queries for focusable elements (`a[href]`, `button:not([disabled])`, `input:not([disabled])`, `select:not([disabled])`, `textarea:not([disabled])`, `[tabindex]`), then filters out:
77
+ - Elements with `tabIndex < 0`
78
+ - Hidden elements (`display: none`, `visibility: hidden`)
79
+ - Elements with no layout (`offsetParent === null`)
80
+ - Elements inside `[inert]` containers
81
+
82
+ Results are sorted by `tabIndex` value: positive `tabIndex` values first (ascending), then `tabIndex=0` elements in DOM order.
83
+
84
+ #### `detectKeyboardTrap(selector: string, maxTabs?: number): Promise<TraversalResult>`
85
+
86
+ Detect whether focus is trapped inside a container (WCAG 2.1.2).
87
+
88
+ 1. Focuses the first tabbable element inside the container matching `selector`
89
+ 2. Presses Tab repeatedly (up to `maxTabs`, default 50)
90
+ 3. After each Tab, checks whether focus has left the container
91
+ 4. Returns immediately if focus escapes, or declares a trap if `maxTabs` is exhausted
92
+
93
+ Returns `{ isTrapped: false, tabCount: 0 }` if the container doesn't exist or has no focusable elements.
94
+
95
+ ### Pure Utility Functions
96
+
97
+ The package also exports pure functions for color parsing and WCAG contrast calculation:
98
+
99
+ #### `parseColor(css: string): RGBColor | null`
100
+
101
+ Parse a CSS color string to an `RGBColor` object. Supports:
102
+ - `rgb(r, g, b)` and `rgba(r, g, b, a)`
103
+ - Hex colors: `#RGB`, `#RRGGBB`, `#RRGGBBAA`
104
+ - `transparent` (returns `{ r: 0, g: 0, b: 0, a: 0 }`)
105
+
106
+ Returns `null` for unsupported formats (named colors, `hsl()`, `color()`, etc.).
107
+
108
+ #### `srgbToLinear(channel: number): number`
109
+
110
+ Convert an sRGB channel value (0-255) to linear light.
111
+
112
+ #### `relativeLuminance(color: RGBColor): number`
113
+
114
+ Calculate WCAG relative luminance: `L = 0.2126*R + 0.7152*G + 0.0722*B`
115
+
116
+ #### `contrastRatio(color1: RGBColor, color2: RGBColor): number`
117
+
118
+ Calculate the WCAG contrast ratio between two colors. Returns a value between 1 (identical) and 21 (black on white).
119
+
120
+ #### `meetsAA(ratio: number): boolean`
121
+
122
+ Check if a contrast ratio meets WCAG AA for focus indicators (>= 3.0).
123
+
124
+ ### Types
125
+
126
+ #### `RGBColor`
127
+
128
+ ```typescript
129
+ interface RGBColor {
130
+ r: number; // 0-255
131
+ g: number; // 0-255
132
+ b: number; // 0-255
133
+ a: number; // 0-1
134
+ }
135
+ ```
136
+
137
+ #### `FocusIndicator`
138
+
139
+ Full focus indicator analysis:
140
+
141
+ ```typescript
142
+ interface FocusIndicator {
143
+ isVisible: boolean;
144
+ outline: string; // Raw outline shorthand
145
+ outlineColor: string; // e.g. "rgb(0, 95, 204)"
146
+ outlineWidth: string; // e.g. "3px"
147
+ outlineOffset: string; // e.g. "0px"
148
+ boxShadow: string; // e.g. "0px 0px 0px 3px rgb(52, 152, 219)"
149
+ borderColor: string;
150
+ backgroundColor: string;
151
+ contrastRatio: number | null;
152
+ meetsWCAG_AA: boolean;
153
+ }
154
+ ```
155
+
156
+ #### `TabOrderEntry`
157
+
158
+ ```typescript
159
+ interface TabOrderEntry {
160
+ index: number; // Position in tab order (0-based)
161
+ tag: string; // "BUTTON"
162
+ id: string;
163
+ textContent: string;
164
+ tabIndex: number;
165
+ role: string;
166
+ rect: { x: number; y: number; width: number; height: number };
167
+ }
168
+ ```
169
+
170
+ #### `TabOrderReport`
171
+
172
+ ```typescript
173
+ interface TabOrderReport {
174
+ entries: TabOrderEntry[];
175
+ totalCount: number;
176
+ }
177
+ ```
178
+
179
+ #### `TraversalResult`
180
+
181
+ ```typescript
182
+ interface TraversalResult {
183
+ isTrapped: boolean; // true if focus never escaped
184
+ tabCount: number; // Total Tab presses attempted
185
+ visitedElements: TabOrderEntry[]; // Elements that received focus
186
+ escapeElement: TabOrderEntry | null; // First element outside container
187
+ }
188
+ ```
189
+
190
+ ## Exports
191
+
192
+ ```typescript
193
+ export { FocusAnalyzer } from '@a11y-oracle/focus-analyzer';
194
+ export { parseColor } from '@a11y-oracle/focus-analyzer';
195
+ export { srgbToLinear, relativeLuminance, contrastRatio, meetsAA } from '@a11y-oracle/focus-analyzer';
196
+ export type { RGBColor, FocusIndicator, TabOrderEntry, TraversalResult, TabOrderReport } from '@a11y-oracle/focus-analyzer';
197
+ ```
198
+
199
+ ## Limitations
200
+
201
+ - **Complex focus indicators** — Gradients, CSS `color-mix()`, and other advanced color functions return `contrastRatio: null` because they cannot be reliably parsed into a single color value.
202
+ - **Box-shadow parsing** — Only the first color found in a `box-shadow` value is used for contrast calculation. Multi-layer box-shadows may not be fully analyzed.
203
+ - **Keyboard trap detection** — Tests with Tab key only. Intentional focus traps (e.g., modal dialogs) should be tested separately with Escape key navigation.
204
+ - **`tabIndex` ordering** — The tab order extraction follows the standard algorithm (positive `tabIndex` first, then `tabIndex=0` in DOM order), but doesn't account for Shadow DOM boundaries.
@@ -0,0 +1,14 @@
1
+ /**
2
+ * @module @a11y-oracle/focus-analyzer
3
+ *
4
+ * Focus state analysis for accessibility testing. Provides visual
5
+ * focus indicator inspection, WCAG contrast ratio calculation,
6
+ * DOM tab order extraction, and keyboard trap detection.
7
+ *
8
+ * @packageDocumentation
9
+ */
10
+ export { FocusAnalyzer } from './lib/focus-analyzer.js';
11
+ export { parseColor } from './lib/color-parser.js';
12
+ export { srgbToLinear, relativeLuminance, contrastRatio, meetsAA } from './lib/contrast.js';
13
+ export type { RGBColor, FocusIndicator, TabOrderEntry, TraversalResult, TabOrderReport, } from './lib/types.js';
14
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,aAAa,EAAE,MAAM,yBAAyB,CAAC;AACxD,OAAO,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;AACnD,OAAO,EAAE,YAAY,EAAE,iBAAiB,EAAE,aAAa,EAAE,OAAO,EAAE,MAAM,mBAAmB,CAAC;AAC5F,YAAY,EACV,QAAQ,EACR,cAAc,EACd,aAAa,EACb,eAAe,EACf,cAAc,GACf,MAAM,gBAAgB,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,12 @@
1
+ /**
2
+ * @module @a11y-oracle/focus-analyzer
3
+ *
4
+ * Focus state analysis for accessibility testing. Provides visual
5
+ * focus indicator inspection, WCAG contrast ratio calculation,
6
+ * DOM tab order extraction, and keyboard trap detection.
7
+ *
8
+ * @packageDocumentation
9
+ */
10
+ export { FocusAnalyzer } from './lib/focus-analyzer.js';
11
+ export { parseColor } from './lib/color-parser.js';
12
+ export { srgbToLinear, relativeLuminance, contrastRatio, meetsAA } from './lib/contrast.js';
@@ -0,0 +1,29 @@
1
+ /**
2
+ * @module color-parser
3
+ *
4
+ * Parses CSS color values into normalized RGBA tuples.
5
+ * Handles `rgb()`, `rgba()`, `#hex` (3, 4, 6, 8 digit),
6
+ * and `transparent`.
7
+ */
8
+ import type { RGBColor } from './types.js';
9
+ /**
10
+ * Parse a CSS color string into an {@link RGBColor} tuple.
11
+ *
12
+ * Supports:
13
+ * - `rgb(r, g, b)` and `rgba(r, g, b, a)`
14
+ * - `#RGB`, `#RGBA`, `#RRGGBB`, `#RRGGBBAA`
15
+ * - `transparent` (returns black with alpha 0)
16
+ *
17
+ * @param css - A CSS color value string.
18
+ * @returns The parsed color, or `null` if parsing fails.
19
+ *
20
+ * @example
21
+ * ```typescript
22
+ * parseColor('rgb(255, 0, 0)'); // { r: 255, g: 0, b: 0, a: 1 }
23
+ * parseColor('#3498db'); // { r: 52, g: 152, b: 219, a: 1 }
24
+ * parseColor('rgba(0, 0, 0, 0.5)'); // { r: 0, g: 0, b: 0, a: 0.5 }
25
+ * parseColor('transparent'); // { r: 0, g: 0, b: 0, a: 0 }
26
+ * ```
27
+ */
28
+ export declare function parseColor(css: string): RGBColor | null;
29
+ //# sourceMappingURL=color-parser.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"color-parser.d.ts","sourceRoot":"","sources":["../../src/lib/color-parser.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAE3C;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,QAAQ,GAAG,IAAI,CA0FvD"}
@@ -0,0 +1,110 @@
1
+ /**
2
+ * @module color-parser
3
+ *
4
+ * Parses CSS color values into normalized RGBA tuples.
5
+ * Handles `rgb()`, `rgba()`, `#hex` (3, 4, 6, 8 digit),
6
+ * and `transparent`.
7
+ */
8
+ /**
9
+ * Parse a CSS color string into an {@link RGBColor} tuple.
10
+ *
11
+ * Supports:
12
+ * - `rgb(r, g, b)` and `rgba(r, g, b, a)`
13
+ * - `#RGB`, `#RGBA`, `#RRGGBB`, `#RRGGBBAA`
14
+ * - `transparent` (returns black with alpha 0)
15
+ *
16
+ * @param css - A CSS color value string.
17
+ * @returns The parsed color, or `null` if parsing fails.
18
+ *
19
+ * @example
20
+ * ```typescript
21
+ * parseColor('rgb(255, 0, 0)'); // { r: 255, g: 0, b: 0, a: 1 }
22
+ * parseColor('#3498db'); // { r: 52, g: 152, b: 219, a: 1 }
23
+ * parseColor('rgba(0, 0, 0, 0.5)'); // { r: 0, g: 0, b: 0, a: 0.5 }
24
+ * parseColor('transparent'); // { r: 0, g: 0, b: 0, a: 0 }
25
+ * ```
26
+ */
27
+ export function parseColor(css) {
28
+ if (!css || typeof css !== 'string')
29
+ return null;
30
+ const trimmed = css.trim().toLowerCase();
31
+ // transparent
32
+ if (trimmed === 'transparent') {
33
+ return { r: 0, g: 0, b: 0, a: 0 };
34
+ }
35
+ // rgb() and rgba()
36
+ const rgbMatch = trimmed.match(/^rgba?\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*(?:,\s*([\d.]+)\s*)?\)$/);
37
+ if (rgbMatch) {
38
+ return {
39
+ r: clamp(parseInt(rgbMatch[1], 10), 0, 255),
40
+ g: clamp(parseInt(rgbMatch[2], 10), 0, 255),
41
+ b: clamp(parseInt(rgbMatch[3], 10), 0, 255),
42
+ a: rgbMatch[4] !== undefined ? clamp(parseFloat(rgbMatch[4]), 0, 1) : 1,
43
+ };
44
+ }
45
+ // Modern space-separated rgb() / rgba() syntax: rgb(255 0 0 / 0.5)
46
+ const rgbSpaceMatch = trimmed.match(/^rgba?\(\s*(\d{1,3})\s+(\d{1,3})\s+(\d{1,3})\s*(?:\/\s*([\d.]+%?)\s*)?\)$/);
47
+ if (rgbSpaceMatch) {
48
+ let alpha = 1;
49
+ if (rgbSpaceMatch[4] !== undefined) {
50
+ const alphaStr = rgbSpaceMatch[4];
51
+ alpha = alphaStr.endsWith('%')
52
+ ? clamp(parseFloat(alphaStr) / 100, 0, 1)
53
+ : clamp(parseFloat(alphaStr), 0, 1);
54
+ }
55
+ return {
56
+ r: clamp(parseInt(rgbSpaceMatch[1], 10), 0, 255),
57
+ g: clamp(parseInt(rgbSpaceMatch[2], 10), 0, 255),
58
+ b: clamp(parseInt(rgbSpaceMatch[3], 10), 0, 255),
59
+ a: alpha,
60
+ };
61
+ }
62
+ // Hex colors
63
+ const hexMatch = trimmed.match(/^#([0-9a-f]+)$/);
64
+ if (hexMatch) {
65
+ const hex = hexMatch[1];
66
+ if (hex.length === 3) {
67
+ // #RGB → #RRGGBB
68
+ return {
69
+ r: parseInt(hex[0] + hex[0], 16),
70
+ g: parseInt(hex[1] + hex[1], 16),
71
+ b: parseInt(hex[2] + hex[2], 16),
72
+ a: 1,
73
+ };
74
+ }
75
+ if (hex.length === 4) {
76
+ // #RGBA → #RRGGBBAA
77
+ return {
78
+ r: parseInt(hex[0] + hex[0], 16),
79
+ g: parseInt(hex[1] + hex[1], 16),
80
+ b: parseInt(hex[2] + hex[2], 16),
81
+ a: parseInt(hex[3] + hex[3], 16) / 255,
82
+ };
83
+ }
84
+ if (hex.length === 6) {
85
+ // #RRGGBB
86
+ return {
87
+ r: parseInt(hex.substring(0, 2), 16),
88
+ g: parseInt(hex.substring(2, 4), 16),
89
+ b: parseInt(hex.substring(4, 6), 16),
90
+ a: 1,
91
+ };
92
+ }
93
+ if (hex.length === 8) {
94
+ // #RRGGBBAA
95
+ return {
96
+ r: parseInt(hex.substring(0, 2), 16),
97
+ g: parseInt(hex.substring(2, 4), 16),
98
+ b: parseInt(hex.substring(4, 6), 16),
99
+ a: parseInt(hex.substring(6, 8), 16) / 255,
100
+ };
101
+ }
102
+ }
103
+ return null;
104
+ }
105
+ /**
106
+ * Clamp a value between min and max.
107
+ */
108
+ function clamp(value, min, max) {
109
+ return Math.min(Math.max(value, min), max);
110
+ }
@@ -0,0 +1,54 @@
1
+ /**
2
+ * @module contrast
3
+ *
4
+ * WCAG 2.x relative luminance and contrast ratio calculations.
5
+ *
6
+ * Implements the formulas from:
7
+ * - {@link https://www.w3.org/TR/WCAG21/#dfn-relative-luminance | WCAG 2.1 Relative Luminance}
8
+ * - {@link https://www.w3.org/TR/WCAG21/#dfn-contrast-ratio | WCAG 2.1 Contrast Ratio}
9
+ */
10
+ import type { RGBColor } from './types.js';
11
+ /**
12
+ * Convert an sRGB channel value (0–255) to linear RGB.
13
+ *
14
+ * Applies the sRGB inverse companding function:
15
+ * - If sRGB <= 0.04045: linear = sRGB / 12.92
16
+ * - Otherwise: linear = ((sRGB + 0.055) / 1.055) ^ 2.4
17
+ *
18
+ * @param channel - sRGB channel value in range [0, 255].
19
+ * @returns Linear RGB value in range [0, 1].
20
+ */
21
+ export declare function srgbToLinear(channel: number): number;
22
+ /**
23
+ * Calculate the WCAG relative luminance of a color.
24
+ *
25
+ * Formula: L = 0.2126 * R + 0.7152 * G + 0.0722 * B
26
+ * where R, G, B are linearized sRGB channel values.
27
+ *
28
+ * @param color - The RGB color.
29
+ * @returns Relative luminance in range [0, 1].
30
+ * 0 = darkest black, 1 = lightest white.
31
+ */
32
+ export declare function relativeLuminance(color: RGBColor): number;
33
+ /**
34
+ * Calculate the WCAG contrast ratio between two colors.
35
+ *
36
+ * Formula: (L1 + 0.05) / (L2 + 0.05)
37
+ * where L1 is the lighter luminance and L2 is the darker.
38
+ *
39
+ * @param foreground - The foreground (indicator) color.
40
+ * @param background - The background color.
41
+ * @returns Contrast ratio in range [1, 21].
42
+ * 1:1 = no contrast, 21:1 = maximum contrast.
43
+ */
44
+ export declare function contrastRatio(foreground: RGBColor, background: RGBColor): number;
45
+ /**
46
+ * Check if a contrast ratio meets WCAG 2.4.12 AA for focus indicators.
47
+ *
48
+ * WCAG 2.4.12 requires a minimum 3:1 contrast ratio for focus indicators.
49
+ *
50
+ * @param ratio - The contrast ratio.
51
+ * @returns `true` if the ratio meets or exceeds 3.0.
52
+ */
53
+ export declare function meetsAA(ratio: number): boolean;
54
+ //# sourceMappingURL=contrast.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"contrast.d.ts","sourceRoot":"","sources":["../../src/lib/contrast.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAE3C;;;;;;;;;GASG;AACH,wBAAgB,YAAY,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAGpD;AAED;;;;;;;;;GASG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,QAAQ,GAAG,MAAM,CAKzD;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,aAAa,CAAC,UAAU,EAAE,QAAQ,EAAE,UAAU,EAAE,QAAQ,GAAG,MAAM,CAMhF;AAED;;;;;;;GAOG;AACH,wBAAgB,OAAO,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAE9C"}
@@ -0,0 +1,68 @@
1
+ /**
2
+ * @module contrast
3
+ *
4
+ * WCAG 2.x relative luminance and contrast ratio calculations.
5
+ *
6
+ * Implements the formulas from:
7
+ * - {@link https://www.w3.org/TR/WCAG21/#dfn-relative-luminance | WCAG 2.1 Relative Luminance}
8
+ * - {@link https://www.w3.org/TR/WCAG21/#dfn-contrast-ratio | WCAG 2.1 Contrast Ratio}
9
+ */
10
+ /**
11
+ * Convert an sRGB channel value (0–255) to linear RGB.
12
+ *
13
+ * Applies the sRGB inverse companding function:
14
+ * - If sRGB <= 0.04045: linear = sRGB / 12.92
15
+ * - Otherwise: linear = ((sRGB + 0.055) / 1.055) ^ 2.4
16
+ *
17
+ * @param channel - sRGB channel value in range [0, 255].
18
+ * @returns Linear RGB value in range [0, 1].
19
+ */
20
+ export function srgbToLinear(channel) {
21
+ const s = channel / 255;
22
+ return s <= 0.04045 ? s / 12.92 : Math.pow((s + 0.055) / 1.055, 2.4);
23
+ }
24
+ /**
25
+ * Calculate the WCAG relative luminance of a color.
26
+ *
27
+ * Formula: L = 0.2126 * R + 0.7152 * G + 0.0722 * B
28
+ * where R, G, B are linearized sRGB channel values.
29
+ *
30
+ * @param color - The RGB color.
31
+ * @returns Relative luminance in range [0, 1].
32
+ * 0 = darkest black, 1 = lightest white.
33
+ */
34
+ export function relativeLuminance(color) {
35
+ const r = srgbToLinear(color.r);
36
+ const g = srgbToLinear(color.g);
37
+ const b = srgbToLinear(color.b);
38
+ return 0.2126 * r + 0.7152 * g + 0.0722 * b;
39
+ }
40
+ /**
41
+ * Calculate the WCAG contrast ratio between two colors.
42
+ *
43
+ * Formula: (L1 + 0.05) / (L2 + 0.05)
44
+ * where L1 is the lighter luminance and L2 is the darker.
45
+ *
46
+ * @param foreground - The foreground (indicator) color.
47
+ * @param background - The background color.
48
+ * @returns Contrast ratio in range [1, 21].
49
+ * 1:1 = no contrast, 21:1 = maximum contrast.
50
+ */
51
+ export function contrastRatio(foreground, background) {
52
+ const l1 = relativeLuminance(foreground);
53
+ const l2 = relativeLuminance(background);
54
+ const lighter = Math.max(l1, l2);
55
+ const darker = Math.min(l1, l2);
56
+ return (lighter + 0.05) / (darker + 0.05);
57
+ }
58
+ /**
59
+ * Check if a contrast ratio meets WCAG 2.4.12 AA for focus indicators.
60
+ *
61
+ * WCAG 2.4.12 requires a minimum 3:1 contrast ratio for focus indicators.
62
+ *
63
+ * @param ratio - The contrast ratio.
64
+ * @returns `true` if the ratio meets or exceeds 3.0.
65
+ */
66
+ export function meetsAA(ratio) {
67
+ return ratio >= 3.0;
68
+ }
@@ -0,0 +1,77 @@
1
+ /**
2
+ * @module focus-analyzer
3
+ *
4
+ * Analyzes visual focus indicators, extracts DOM tab order, and
5
+ * detects keyboard traps via CDP `Runtime.evaluate`.
6
+ *
7
+ * @example
8
+ * ```typescript
9
+ * import { FocusAnalyzer } from '@a11y-oracle/focus-analyzer';
10
+ *
11
+ * const analyzer = new FocusAnalyzer(cdpSession);
12
+ * const indicator = await analyzer.getFocusIndicator();
13
+ * console.log(indicator.isVisible, indicator.contrastRatio);
14
+ * ```
15
+ */
16
+ import type { CDPSessionLike } from '@a11y-oracle/cdp-types';
17
+ import type { FocusIndicator, TabOrderEntry, TraversalResult } from './types.js';
18
+ /**
19
+ * Analyzes focus indicators, tab order, and keyboard traps.
20
+ *
21
+ * Uses CDP `Runtime.evaluate` to inspect DOM state and
22
+ * {@link KeyboardEngine} for dispatching Tab keys during
23
+ * trap detection.
24
+ */
25
+ export declare class FocusAnalyzer {
26
+ private cdp;
27
+ private keyboard;
28
+ /**
29
+ * @param cdp - CDP session for sending protocol commands.
30
+ */
31
+ constructor(cdp: CDPSessionLike);
32
+ /**
33
+ * Analyze the visual focus indicator of the currently focused element.
34
+ *
35
+ * Extracts computed CSS properties (`outline`, `box-shadow`, `border`,
36
+ * `background-color`) and calculates the contrast ratio of the focus
37
+ * indicator against the background.
38
+ *
39
+ * @returns Focus indicator analysis, or a default "not visible"
40
+ * indicator if no element has focus.
41
+ */
42
+ getFocusIndicator(): Promise<FocusIndicator>;
43
+ /**
44
+ * Extract all tabbable elements from the DOM in tab order.
45
+ *
46
+ * Queries for focusable elements (`a[href]`, `button`, `input`,
47
+ * `select`, `textarea`, `[tabindex]`), filters out hidden and
48
+ * disabled elements, and sorts by `tabIndex` value.
49
+ *
50
+ * @returns Tab order entries sorted by actual tab key order.
51
+ */
52
+ getTabOrder(): Promise<TabOrderEntry[]>;
53
+ /**
54
+ * Detect whether focus is trapped inside a container.
55
+ *
56
+ * Focuses the first tabbable element inside the container, then
57
+ * presses Tab repeatedly. If focus never leaves the container
58
+ * after `maxTabs` presses, the container is a keyboard trap.
59
+ *
60
+ * @param selector - CSS selector for the container to test.
61
+ * @param maxTabs - Maximum Tab presses before declaring a trap. Default 50.
62
+ * @returns Traversal result indicating trap status.
63
+ */
64
+ detectKeyboardTrap(selector: string, maxTabs?: number): Promise<TraversalResult>;
65
+ /**
66
+ * Extract the dominant color from a CSS box-shadow value.
67
+ *
68
+ * Takes the first color found in a `box-shadow` string.
69
+ * Example: `"0px 0px 0px 3px rgb(52, 152, 219)"` → rgb(52, 152, 219)
70
+ */
71
+ private extractBoxShadowColor;
72
+ /**
73
+ * Create a default "not visible" focus indicator.
74
+ */
75
+ private createEmptyIndicator;
76
+ }
77
+ //# sourceMappingURL=focus-analyzer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"focus-analyzer.d.ts","sourceRoot":"","sources":["../../src/lib/focus-analyzer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AAE7D,OAAO,KAAK,EACV,cAAc,EACd,aAAa,EACb,eAAe,EAChB,MAAM,YAAY,CAAC;AA4EpB;;;;;;GAMG;AACH,qBAAa,aAAa;IAMZ,OAAO,CAAC,GAAG;IALvB,OAAO,CAAC,QAAQ,CAAiB;IAEjC;;OAEG;gBACiB,GAAG,EAAE,cAAc;IAIvC;;;;;;;;;OASG;IACG,iBAAiB,IAAI,OAAO,CAAC,cAAc,CAAC;IAiDlD;;;;;;;;OAQG;IACG,WAAW,IAAI,OAAO,CAAC,aAAa,EAAE,CAAC;IAS7C;;;;;;;;;;OAUG;IACG,kBAAkB,CACtB,QAAQ,EAAE,MAAM,EAChB,OAAO,GAAE,MAAW,GACnB,OAAO,CAAC,eAAe,CAAC;IAgG3B;;;;;OAKG;IACH,OAAO,CAAC,qBAAqB;IAgB7B;;OAEG;IACH,OAAO,CAAC,oBAAoB;CAc7B"}
@@ -0,0 +1,299 @@
1
+ /**
2
+ * @module focus-analyzer
3
+ *
4
+ * Analyzes visual focus indicators, extracts DOM tab order, and
5
+ * detects keyboard traps via CDP `Runtime.evaluate`.
6
+ *
7
+ * @example
8
+ * ```typescript
9
+ * import { FocusAnalyzer } from '@a11y-oracle/focus-analyzer';
10
+ *
11
+ * const analyzer = new FocusAnalyzer(cdpSession);
12
+ * const indicator = await analyzer.getFocusIndicator();
13
+ * console.log(indicator.isVisible, indicator.contrastRatio);
14
+ * ```
15
+ */
16
+ import { KeyboardEngine } from '@a11y-oracle/keyboard-engine';
17
+ import { parseColor } from './color-parser.js';
18
+ import { contrastRatio, meetsAA } from './contrast.js';
19
+ /**
20
+ * JavaScript expression to extract computed focus styles from
21
+ * `document.activeElement`.
22
+ */
23
+ const GET_FOCUS_STYLES_JS = `
24
+ (() => {
25
+ const el = document.activeElement;
26
+ if (!el || el === document.body || el === document.documentElement) {
27
+ return null;
28
+ }
29
+ const cs = window.getComputedStyle(el);
30
+ return {
31
+ outline: cs.outline || '',
32
+ outlineColor: cs.outlineColor || '',
33
+ outlineWidth: cs.outlineWidth || '',
34
+ outlineOffset: cs.outlineOffset || '',
35
+ boxShadow: cs.boxShadow || '',
36
+ borderColor: cs.borderColor || '',
37
+ backgroundColor: cs.backgroundColor || '',
38
+ };
39
+ })()
40
+ `;
41
+ /**
42
+ * JavaScript expression to extract all tabbable elements in DOM order.
43
+ */
44
+ const GET_TAB_ORDER_JS = `
45
+ (() => {
46
+ const selector = [
47
+ 'a[href]',
48
+ 'button:not([disabled])',
49
+ 'input:not([disabled]):not([type="hidden"])',
50
+ 'select:not([disabled])',
51
+ 'textarea:not([disabled])',
52
+ '[tabindex]',
53
+ ].join(', ');
54
+ const elements = Array.from(document.querySelectorAll(selector));
55
+ return elements
56
+ .filter(el => {
57
+ if (el.tabIndex < 0) return false;
58
+ if (el.offsetParent === null && el.tagName !== 'BODY') return false;
59
+ const cs = window.getComputedStyle(el);
60
+ if (cs.visibility === 'hidden' || cs.display === 'none') return false;
61
+ if (el.closest('[inert]')) return false;
62
+ return true;
63
+ })
64
+ .sort((a, b) => {
65
+ if (a.tabIndex === 0 && b.tabIndex === 0) return 0;
66
+ if (a.tabIndex === 0) return 1;
67
+ if (b.tabIndex === 0) return -1;
68
+ return a.tabIndex - b.tabIndex;
69
+ })
70
+ .map((el, i) => {
71
+ const rect = el.getBoundingClientRect();
72
+ return {
73
+ index: i,
74
+ tag: el.tagName,
75
+ id: el.id || '',
76
+ textContent: (el.textContent || '').trim().substring(0, 200),
77
+ tabIndex: el.tabIndex,
78
+ role: el.getAttribute('role') || '',
79
+ rect: {
80
+ x: Math.round(rect.x),
81
+ y: Math.round(rect.y),
82
+ width: Math.round(rect.width),
83
+ height: Math.round(rect.height),
84
+ },
85
+ };
86
+ });
87
+ })()
88
+ `;
89
+ /**
90
+ * Analyzes focus indicators, tab order, and keyboard traps.
91
+ *
92
+ * Uses CDP `Runtime.evaluate` to inspect DOM state and
93
+ * {@link KeyboardEngine} for dispatching Tab keys during
94
+ * trap detection.
95
+ */
96
+ export class FocusAnalyzer {
97
+ cdp;
98
+ keyboard;
99
+ /**
100
+ * @param cdp - CDP session for sending protocol commands.
101
+ */
102
+ constructor(cdp) {
103
+ this.cdp = cdp;
104
+ this.keyboard = new KeyboardEngine(cdp);
105
+ }
106
+ /**
107
+ * Analyze the visual focus indicator of the currently focused element.
108
+ *
109
+ * Extracts computed CSS properties (`outline`, `box-shadow`, `border`,
110
+ * `background-color`) and calculates the contrast ratio of the focus
111
+ * indicator against the background.
112
+ *
113
+ * @returns Focus indicator analysis, or a default "not visible"
114
+ * indicator if no element has focus.
115
+ */
116
+ async getFocusIndicator() {
117
+ const result = (await this.cdp.send('Runtime.evaluate', {
118
+ expression: GET_FOCUS_STYLES_JS,
119
+ returnByValue: true,
120
+ }));
121
+ const styles = result.result.value;
122
+ if (!styles) {
123
+ return this.createEmptyIndicator();
124
+ }
125
+ const outlineWidth = styles.outlineWidth || '0px';
126
+ const hasOutline = outlineWidth !== '0px' &&
127
+ outlineWidth !== '0' &&
128
+ styles.outlineColor !== 'transparent';
129
+ const hasBoxShadow = styles.boxShadow !== 'none' && styles.boxShadow !== '';
130
+ const isVisible = hasOutline || hasBoxShadow;
131
+ // Calculate contrast ratio between focus indicator and background
132
+ let ratio = null;
133
+ if (isVisible) {
134
+ const indicatorColor = hasOutline
135
+ ? parseColor(styles.outlineColor)
136
+ : this.extractBoxShadowColor(styles.boxShadow);
137
+ const bgColor = parseColor(styles.backgroundColor);
138
+ if (indicatorColor && bgColor) {
139
+ ratio = contrastRatio(indicatorColor, bgColor);
140
+ }
141
+ }
142
+ return {
143
+ isVisible,
144
+ outline: styles.outline || '',
145
+ outlineColor: styles.outlineColor || '',
146
+ outlineWidth: styles.outlineWidth || '',
147
+ outlineOffset: styles.outlineOffset || '',
148
+ boxShadow: styles.boxShadow || '',
149
+ borderColor: styles.borderColor || '',
150
+ backgroundColor: styles.backgroundColor || '',
151
+ contrastRatio: ratio,
152
+ meetsWCAG_AA: isVisible && ratio !== null && meetsAA(ratio),
153
+ };
154
+ }
155
+ /**
156
+ * Extract all tabbable elements from the DOM in tab order.
157
+ *
158
+ * Queries for focusable elements (`a[href]`, `button`, `input`,
159
+ * `select`, `textarea`, `[tabindex]`), filters out hidden and
160
+ * disabled elements, and sorts by `tabIndex` value.
161
+ *
162
+ * @returns Tab order entries sorted by actual tab key order.
163
+ */
164
+ async getTabOrder() {
165
+ const result = (await this.cdp.send('Runtime.evaluate', {
166
+ expression: GET_TAB_ORDER_JS,
167
+ returnByValue: true,
168
+ }));
169
+ return result.result.value ?? [];
170
+ }
171
+ /**
172
+ * Detect whether focus is trapped inside a container.
173
+ *
174
+ * Focuses the first tabbable element inside the container, then
175
+ * presses Tab repeatedly. If focus never leaves the container
176
+ * after `maxTabs` presses, the container is a keyboard trap.
177
+ *
178
+ * @param selector - CSS selector for the container to test.
179
+ * @param maxTabs - Maximum Tab presses before declaring a trap. Default 50.
180
+ * @returns Traversal result indicating trap status.
181
+ */
182
+ async detectKeyboardTrap(selector, maxTabs = 50) {
183
+ // Focus the first tabbable element in the container
184
+ const focusResult = (await this.cdp.send('Runtime.evaluate', {
185
+ expression: `
186
+ (() => {
187
+ const container = document.querySelector(${JSON.stringify(selector)});
188
+ if (!container) return { error: 'Container not found' };
189
+ const focusable = container.querySelector(
190
+ 'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])'
191
+ );
192
+ if (!focusable) return { error: 'No focusable elements in container' };
193
+ focusable.focus();
194
+ return { success: true };
195
+ })()
196
+ `,
197
+ returnByValue: true,
198
+ }));
199
+ if (focusResult.result.value?.error) {
200
+ return {
201
+ isTrapped: false,
202
+ tabCount: 0,
203
+ visitedElements: [],
204
+ escapeElement: null,
205
+ };
206
+ }
207
+ const visitedElements = [];
208
+ let escapeElement = null;
209
+ for (let i = 0; i < maxTabs; i++) {
210
+ await this.keyboard.press('Tab');
211
+ // Short delay for focus to settle
212
+ await new Promise((resolve) => setTimeout(resolve, 20));
213
+ // Check where focus is now
214
+ const checkResult = (await this.cdp.send('Runtime.evaluate', {
215
+ expression: `
216
+ (() => {
217
+ const container = document.querySelector(${JSON.stringify(selector)});
218
+ const el = document.activeElement;
219
+ if (!el || el === document.body) return { outside: true, element: null };
220
+ const rect = el.getBoundingClientRect();
221
+ const entry = {
222
+ index: ${i},
223
+ tag: el.tagName,
224
+ id: el.id || '',
225
+ textContent: (el.textContent || '').trim().substring(0, 200),
226
+ tabIndex: el.tabIndex,
227
+ role: el.getAttribute('role') || '',
228
+ rect: {
229
+ x: Math.round(rect.x),
230
+ y: Math.round(rect.y),
231
+ width: Math.round(rect.width),
232
+ height: Math.round(rect.height),
233
+ },
234
+ };
235
+ const isInside = container && container.contains(el);
236
+ return { outside: !isInside, element: entry };
237
+ })()
238
+ `,
239
+ returnByValue: true,
240
+ }));
241
+ const check = checkResult.result.value;
242
+ if (check.element) {
243
+ if (check.outside) {
244
+ escapeElement = check.element;
245
+ return {
246
+ isTrapped: false,
247
+ tabCount: i + 1,
248
+ visitedElements,
249
+ escapeElement,
250
+ };
251
+ }
252
+ visitedElements.push(check.element);
253
+ }
254
+ }
255
+ // If we exhausted all tabs and focus never left, it's a trap
256
+ return {
257
+ isTrapped: true,
258
+ tabCount: maxTabs,
259
+ visitedElements,
260
+ escapeElement: null,
261
+ };
262
+ }
263
+ /**
264
+ * Extract the dominant color from a CSS box-shadow value.
265
+ *
266
+ * Takes the first color found in a `box-shadow` string.
267
+ * Example: `"0px 0px 0px 3px rgb(52, 152, 219)"` → rgb(52, 152, 219)
268
+ */
269
+ extractBoxShadowColor(boxShadow) {
270
+ // Try to find an rgb/rgba color in the box-shadow
271
+ const rgbMatch = boxShadow.match(/rgba?\([^)]+\)/);
272
+ if (rgbMatch) {
273
+ return parseColor(rgbMatch[0]);
274
+ }
275
+ // Try to find a hex color
276
+ const hexMatch = boxShadow.match(/#[0-9a-fA-F]{3,8}/);
277
+ if (hexMatch) {
278
+ return parseColor(hexMatch[0]);
279
+ }
280
+ return null;
281
+ }
282
+ /**
283
+ * Create a default "not visible" focus indicator.
284
+ */
285
+ createEmptyIndicator() {
286
+ return {
287
+ isVisible: false,
288
+ outline: '',
289
+ outlineColor: '',
290
+ outlineWidth: '',
291
+ outlineOffset: '',
292
+ boxShadow: '',
293
+ borderColor: '',
294
+ backgroundColor: '',
295
+ contrastRatio: null,
296
+ meetsWCAG_AA: false,
297
+ };
298
+ }
299
+ }
@@ -0,0 +1,97 @@
1
+ /**
2
+ * @module types
3
+ *
4
+ * Type definitions for focus analysis, tab order traversal,
5
+ * and keyboard trap detection.
6
+ */
7
+ /**
8
+ * An RGBA color tuple with channels in the range [0, 255]
9
+ * and alpha in the range [0, 1].
10
+ */
11
+ export interface RGBColor {
12
+ r: number;
13
+ g: number;
14
+ b: number;
15
+ a: number;
16
+ }
17
+ /**
18
+ * Analysis of the visual focus indicator on the currently focused element.
19
+ *
20
+ * Includes raw CSS values, parsed contrast ratio, and WCAG 2.4.12 AA pass/fail.
21
+ */
22
+ export interface FocusIndicator {
23
+ /** Whether any visual focus indicator is detected. */
24
+ isVisible: boolean;
25
+ /** Raw `outline` shorthand value. */
26
+ outline: string;
27
+ /** Computed `outline-color`. */
28
+ outlineColor: string;
29
+ /** Computed `outline-width`. */
30
+ outlineWidth: string;
31
+ /** Computed `outline-offset`. */
32
+ outlineOffset: string;
33
+ /** Computed `box-shadow`. */
34
+ boxShadow: string;
35
+ /** Computed `border-color`. */
36
+ borderColor: string;
37
+ /** Computed `background-color`. */
38
+ backgroundColor: string;
39
+ /**
40
+ * Contrast ratio of the focus indicator against the background.
41
+ * `null` if the colors could not be reliably parsed.
42
+ */
43
+ contrastRatio: number | null;
44
+ /**
45
+ * Whether the focus indicator meets WCAG 2.4.12 AA
46
+ * (contrast ratio >= 3.0 and indicator is visible).
47
+ */
48
+ meetsWCAG_AA: boolean;
49
+ }
50
+ /**
51
+ * A single entry in the tab order — an element that can receive
52
+ * focus via the Tab key.
53
+ */
54
+ export interface TabOrderEntry {
55
+ /** Position in the actual tab order (0-based). */
56
+ index: number;
57
+ /** Tag name (e.g. `'BUTTON'`). */
58
+ tag: string;
59
+ /** Element `id` attribute, or empty string. */
60
+ id: string;
61
+ /** Trimmed text content of the element. */
62
+ textContent: string;
63
+ /** The element's `tabIndex` property. */
64
+ tabIndex: number;
65
+ /** The element's `role` attribute, or empty string. */
66
+ role: string;
67
+ /** Bounding rectangle. */
68
+ rect: {
69
+ x: number;
70
+ y: number;
71
+ width: number;
72
+ height: number;
73
+ };
74
+ }
75
+ /**
76
+ * Result of a keyboard trap detection traversal.
77
+ */
78
+ export interface TraversalResult {
79
+ /** Whether focus was trapped (could not escape the container). */
80
+ isTrapped: boolean;
81
+ /** Total number of Tab presses attempted. */
82
+ tabCount: number;
83
+ /** Elements that received focus during the traversal. */
84
+ visitedElements: TabOrderEntry[];
85
+ /** The first element outside the container that received focus, or `null` if trapped. */
86
+ escapeElement: TabOrderEntry | null;
87
+ }
88
+ /**
89
+ * Full report of a tab order traversal across the page.
90
+ */
91
+ export interface TabOrderReport {
92
+ /** Tabbable elements in actual tab-key order. */
93
+ entries: TabOrderEntry[];
94
+ /** Total number of tabbable elements found. */
95
+ totalCount: number;
96
+ }
97
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/lib/types.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH;;;GAGG;AACH,MAAM,WAAW,QAAQ;IACvB,CAAC,EAAE,MAAM,CAAC;IACV,CAAC,EAAE,MAAM,CAAC;IACV,CAAC,EAAE,MAAM,CAAC;IACV,CAAC,EAAE,MAAM,CAAC;CACX;AAED;;;;GAIG;AACH,MAAM,WAAW,cAAc;IAC7B,sDAAsD;IACtD,SAAS,EAAE,OAAO,CAAC;IACnB,qCAAqC;IACrC,OAAO,EAAE,MAAM,CAAC;IAChB,gCAAgC;IAChC,YAAY,EAAE,MAAM,CAAC;IACrB,gCAAgC;IAChC,YAAY,EAAE,MAAM,CAAC;IACrB,iCAAiC;IACjC,aAAa,EAAE,MAAM,CAAC;IACtB,6BAA6B;IAC7B,SAAS,EAAE,MAAM,CAAC;IAClB,+BAA+B;IAC/B,WAAW,EAAE,MAAM,CAAC;IACpB,mCAAmC;IACnC,eAAe,EAAE,MAAM,CAAC;IACxB;;;OAGG;IACH,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B;;;OAGG;IACH,YAAY,EAAE,OAAO,CAAC;CACvB;AAED;;;GAGG;AACH,MAAM,WAAW,aAAa;IAC5B,kDAAkD;IAClD,KAAK,EAAE,MAAM,CAAC;IACd,kCAAkC;IAClC,GAAG,EAAE,MAAM,CAAC;IACZ,+CAA+C;IAC/C,EAAE,EAAE,MAAM,CAAC;IACX,2CAA2C;IAC3C,WAAW,EAAE,MAAM,CAAC;IACpB,yCAAyC;IACzC,QAAQ,EAAE,MAAM,CAAC;IACjB,uDAAuD;IACvD,IAAI,EAAE,MAAM,CAAC;IACb,0BAA0B;IAC1B,IAAI,EAAE;QAAE,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;CAC/D;AAED;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,kEAAkE;IAClE,SAAS,EAAE,OAAO,CAAC;IACnB,6CAA6C;IAC7C,QAAQ,EAAE,MAAM,CAAC;IACjB,yDAAyD;IACzD,eAAe,EAAE,aAAa,EAAE,CAAC;IACjC,yFAAyF;IACzF,aAAa,EAAE,aAAa,GAAG,IAAI,CAAC;CACrC;AAED;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B,iDAAiD;IACjD,OAAO,EAAE,aAAa,EAAE,CAAC;IACzB,+CAA+C;IAC/C,UAAU,EAAE,MAAM,CAAC;CACpB"}
@@ -0,0 +1,7 @@
1
+ /**
2
+ * @module types
3
+ *
4
+ * Type definitions for focus analysis, tab order traversal,
5
+ * and keyboard trap detection.
6
+ */
7
+ export {};
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "@a11y-oracle/focus-analyzer",
3
+ "version": "1.0.0",
4
+ "description": "Focus indicator CSS analysis (WCAG 2.4.12), tab order extraction, and keyboard trap detection",
5
+ "license": "MIT",
6
+ "author": "a11y-oracle",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/a11y-oracle/a11y-oracle.git",
10
+ "directory": "libs/focus-analyzer"
11
+ },
12
+ "bugs": {
13
+ "url": "https://github.com/a11y-oracle/a11y-oracle/issues"
14
+ },
15
+ "homepage": "https://github.com/a11y-oracle/a11y-oracle/tree/main/libs/focus-analyzer",
16
+ "keywords": [
17
+ "accessibility",
18
+ "a11y",
19
+ "focus",
20
+ "wcag",
21
+ "keyboard-trap",
22
+ "contrast",
23
+ "testing"
24
+ ],
25
+ "publishConfig": {
26
+ "access": "public"
27
+ },
28
+ "type": "module",
29
+ "main": "./dist/index.js",
30
+ "module": "./dist/index.js",
31
+ "types": "./dist/index.d.ts",
32
+ "exports": {
33
+ "./package.json": "./package.json",
34
+ ".": {
35
+ "types": "./dist/index.d.ts",
36
+ "import": "./dist/index.js",
37
+ "default": "./dist/index.js"
38
+ }
39
+ },
40
+ "files": [
41
+ "dist",
42
+ "!**/*.tsbuildinfo"
43
+ ],
44
+ "dependencies": {
45
+ "@a11y-oracle/cdp-types": "1.0.0",
46
+ "@a11y-oracle/keyboard-engine": "1.0.0",
47
+ "tslib": "^2.3.0"
48
+ }
49
+ }