@wix/zero-config-implementation 1.65.0 → 1.66.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/dist/{index-BLr9vmnH.js → index-BEdi2EUy.js} +9055 -8961
- package/dist/{index-DbvAWhJF.js → index-CtX9f415.js} +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -1
- package/package.json +2 -2
- package/src/converters/states-builder.ts +58 -0
- package/src/converters/to-editor-component.ts +14 -0
- package/src/information-extractors/react/extractors/index.ts +3 -0
- package/src/information-extractors/react/extractors/prop-tracker.ts +8 -0
- package/src/information-extractors/react/extractors/state-markers.test.ts +143 -0
- package/src/information-extractors/react/extractors/state-markers.ts +142 -0
- package/src/information-extractors/react/index.ts +2 -0
package/dist/index.d.ts
CHANGED
|
@@ -2369,6 +2369,8 @@ export declare interface PropTrackerData {
|
|
|
2369
2369
|
role?: string;
|
|
2370
2370
|
boundProps: string[];
|
|
2371
2371
|
concatenatedAttrs: Map<string, string>;
|
|
2372
|
+
/** on*-handler prop names whose value is a function on this element (e.g. ['onClick', 'onChange']). */
|
|
2373
|
+
eventHandlers: string[];
|
|
2372
2374
|
}
|
|
2373
2375
|
|
|
2374
2376
|
export declare interface PropTrackerExtractorState {
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { B as t, D as e, E as o, I as n, N as c, P as l, R as i, a as p, V as E, b as m, c as x, d as f, e as d, f as u, h as C, i as I, j as k, k as y, l as A, m as D, n as P, o as R, p as h, q as B, r as M, s as T, t as b, w } from "./index-
|
|
1
|
+
import { B as t, D as e, E as o, I as n, N as c, P as l, R as i, a as p, V as E, b as m, c as x, d as f, e as d, f as u, h as C, i as I, j as k, k as y, l as A, m as D, n as P, o as R, p as h, q as B, r as M, s as T, t as b, w } from "./index-BEdi2EUy.js";
|
|
2
2
|
import "react";
|
|
3
3
|
export {
|
|
4
4
|
t as BaseError,
|
package/package.json
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"registry": "https://registry.npmjs.org/",
|
|
5
5
|
"access": "public"
|
|
6
6
|
},
|
|
7
|
-
"version": "1.
|
|
7
|
+
"version": "1.66.0",
|
|
8
8
|
"description": "Core library for extracting component manifests from JS and CSS files",
|
|
9
9
|
"type": "module",
|
|
10
10
|
"main": "dist/index.js",
|
|
@@ -81,5 +81,5 @@
|
|
|
81
81
|
]
|
|
82
82
|
}
|
|
83
83
|
},
|
|
84
|
-
"falconPackageHash": "
|
|
84
|
+
"falconPackageHash": "2a3fd8ab08ce40ac87e3bf2f07ad6928e67ddb1262856b12f593624b"
|
|
85
85
|
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Builds the `states` block for the root EditorElement and inline elements.
|
|
3
|
+
*
|
|
4
|
+
* Native states only (hover/focus/disabled/invalid). Signals come from the
|
|
5
|
+
* React extractor (tag, role, event handlers). The CSS extractor is not
|
|
6
|
+
* consulted.
|
|
7
|
+
*
|
|
8
|
+
* className follows BEM by appending `--<state>` to the element's own
|
|
9
|
+
* semantic class. Component authors are expected to write inner elements
|
|
10
|
+
* with full BEM class names (e.g. `toggle__label`), so the resulting state
|
|
11
|
+
* className is `<semanticClass>--<state>`:
|
|
12
|
+
* - root `toggle` → `toggle--hover`
|
|
13
|
+
* - inner `toggle__label` → `toggle__label--hover`
|
|
14
|
+
*
|
|
15
|
+
* `props` is never emitted on native-state entries — that field is reserved
|
|
16
|
+
* for functional (non-native) states.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import type { States } from '@wix/react-component-schema'
|
|
20
|
+
import { NATIVE_STATE_TYPE } from '@wix/react-component-schema'
|
|
21
|
+
import {
|
|
22
|
+
type ExtractedElement,
|
|
23
|
+
type SupportedNativeStates,
|
|
24
|
+
inferSupportedNativeStates,
|
|
25
|
+
} from '../information-extractors/react'
|
|
26
|
+
|
|
27
|
+
const STATE_DISPLAY_NAMES = {
|
|
28
|
+
hover: 'Hover',
|
|
29
|
+
focus: 'Focus',
|
|
30
|
+
disabled: 'Disabled',
|
|
31
|
+
invalid: 'Invalid',
|
|
32
|
+
} as const
|
|
33
|
+
|
|
34
|
+
type NativeStateName = keyof SupportedNativeStates
|
|
35
|
+
|
|
36
|
+
const PSEUDO_CLASS_BY_STATE: Record<NativeStateName, NATIVE_STATE_TYPE> = {
|
|
37
|
+
hover: NATIVE_STATE_TYPE.hover,
|
|
38
|
+
focus: NATIVE_STATE_TYPE.focus,
|
|
39
|
+
disabled: NATIVE_STATE_TYPE.disabled,
|
|
40
|
+
invalid: NATIVE_STATE_TYPE.invalid,
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function buildStatesBlock(element: ExtractedElement, semanticClass: string): States | undefined {
|
|
44
|
+
const supported = inferSupportedNativeStates(element)
|
|
45
|
+
const states: States = {}
|
|
46
|
+
|
|
47
|
+
for (const stateName of Object.keys(supported) as NativeStateName[]) {
|
|
48
|
+
if (!supported[stateName]) continue
|
|
49
|
+
|
|
50
|
+
states[stateName] = {
|
|
51
|
+
displayName: STATE_DISPLAY_NAMES[stateName],
|
|
52
|
+
className: `${semanticClass}--${stateName}`,
|
|
53
|
+
pseudoClass: PSEUDO_CLASS_BY_STATE[stateName],
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return Object.keys(states).length > 0 ? states : undefined
|
|
58
|
+
}
|
|
@@ -22,6 +22,7 @@ import { getDefaultDisplayForTag, resolveDisplayValue } from '../information-ext
|
|
|
22
22
|
import { findPreferredSemanticClass } from '../utils/css-class'
|
|
23
23
|
import { resolveLonghand } from './css-longhand-resolver'
|
|
24
24
|
import { buildDataItem } from './data-item-builder'
|
|
25
|
+
import { buildStatesBlock } from './states-builder'
|
|
25
26
|
import { formatDisplayName } from './utils'
|
|
26
27
|
|
|
27
28
|
// Props to exclude from data items (same as in element-data.ts)
|
|
@@ -42,6 +43,9 @@ function buildEditorElement(
|
|
|
42
43
|
const childElements = rootElement?.children ?? []
|
|
43
44
|
const rootCustomProps = rootElement ? (nearestCommonAncestorCustomProps.get(rootElement.traceId) ?? {}) : {}
|
|
44
45
|
|
|
46
|
+
const rootSemanticClass = getSemanticBlockName(rootElement)
|
|
47
|
+
const rootStates = rootElement && rootSemanticClass ? buildStatesBlock(rootElement, rootSemanticClass) : undefined
|
|
48
|
+
|
|
45
49
|
return {
|
|
46
50
|
selector: buildSelector(rootElement),
|
|
47
51
|
displayName: formatDisplayName(component.componentName),
|
|
@@ -54,9 +58,15 @@ function buildEditorElement(
|
|
|
54
58
|
),
|
|
55
59
|
cssProperties: buildCssProperties(rootElement),
|
|
56
60
|
cssCustomProperties: rootCustomProps,
|
|
61
|
+
...(rootStates && { states: rootStates }),
|
|
57
62
|
}
|
|
58
63
|
}
|
|
59
64
|
|
|
65
|
+
function getSemanticBlockName(element: ExtractedElement | undefined): string | undefined {
|
|
66
|
+
if (!element?.attributes.class) return undefined
|
|
67
|
+
return findPreferredSemanticClass(element.attributes.class.split(' '))
|
|
68
|
+
}
|
|
69
|
+
|
|
60
70
|
function buildSelector(rootElement?: ExtractedElement): string {
|
|
61
71
|
if (rootElement?.attributes.class) {
|
|
62
72
|
const semanticClass = findPreferredSemanticClass(rootElement.attributes.class.split(' '))
|
|
@@ -101,6 +111,9 @@ function buildElements(
|
|
|
101
111
|
const cssProps = buildCssProperties(element)
|
|
102
112
|
const cssCustomProps = nearestCommonAncestorCustomProps.get(element.traceId) ?? {}
|
|
103
113
|
|
|
114
|
+
const semanticClass = getSemanticBlockName(element)
|
|
115
|
+
const states = semanticClass ? buildStatesBlock(element, semanticClass) : undefined
|
|
116
|
+
|
|
104
117
|
result[element.name] = {
|
|
105
118
|
elementType: ELEMENTS.ELEMENT_TYPE.inlineElement,
|
|
106
119
|
inlineElement: {
|
|
@@ -113,6 +126,7 @@ function buildElements(
|
|
|
113
126
|
...(Object.keys(cssProps).length > 0 && { cssProperties: cssProps }),
|
|
114
127
|
// CSS custom properties placed on the Nearest Common Ancestor of all elements that use each variable
|
|
115
128
|
...(Object.keys(cssCustomProps).length > 0 && { cssCustomProperties: cssCustomProps }),
|
|
129
|
+
...(states && { states }),
|
|
116
130
|
// Recursively build nested elements
|
|
117
131
|
elements:
|
|
118
132
|
element.children.length > 0
|
|
@@ -20,6 +20,8 @@ export interface PropTrackerData {
|
|
|
20
20
|
role?: string
|
|
21
21
|
boundProps: string[]
|
|
22
22
|
concatenatedAttrs: Map<string, string> // attrName → propName (for concatenated values)
|
|
23
|
+
/** on*-handler prop names whose value is a function on this element (e.g. ['onClick', 'onChange']). */
|
|
24
|
+
eventHandlers: string[]
|
|
23
25
|
}
|
|
24
26
|
|
|
25
27
|
export interface PropTrackerExtractorState {
|
|
@@ -110,10 +112,15 @@ export function createPropTrackerExtractor(): {
|
|
|
110
112
|
|
|
111
113
|
const boundProps = new Set<string>()
|
|
112
114
|
const concatenatedAttrs = new Map<string, string>()
|
|
115
|
+
const eventHandlers: string[] = []
|
|
113
116
|
|
|
114
117
|
Object.entries(props).forEach(([key, value]) => {
|
|
115
118
|
if (key.startsWith('__') || key === TRACE_ATTR) return
|
|
116
119
|
|
|
120
|
+
if (typeof value === 'function' && key.length > 2 && key.startsWith('on') && key[2] === key[2].toUpperCase()) {
|
|
121
|
+
eventHandlers.push(key)
|
|
122
|
+
}
|
|
123
|
+
|
|
117
124
|
const spies = extractSpies(value)
|
|
118
125
|
|
|
119
126
|
spies.forEach((spy) => {
|
|
@@ -141,6 +148,7 @@ export function createPropTrackerExtractor(): {
|
|
|
141
148
|
role: htmlProps.role,
|
|
142
149
|
boundProps: [...boundProps],
|
|
143
150
|
concatenatedAttrs,
|
|
151
|
+
eventHandlers,
|
|
144
152
|
}
|
|
145
153
|
store.set(traceId, 'prop-tracker', data)
|
|
146
154
|
},
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import type { ExtractedElement } from './core/tree-builder'
|
|
3
|
+
import type { PropTrackerData } from './prop-tracker'
|
|
4
|
+
import { inferSupportedNativeStates } from './state-markers'
|
|
5
|
+
|
|
6
|
+
function makeElement(input: {
|
|
7
|
+
tag: string
|
|
8
|
+
attributes?: Record<string, string>
|
|
9
|
+
role?: string
|
|
10
|
+
eventHandlers?: string[]
|
|
11
|
+
}): ExtractedElement {
|
|
12
|
+
const propTrackerData: PropTrackerData = {
|
|
13
|
+
tag: input.tag,
|
|
14
|
+
role: input.role,
|
|
15
|
+
boundProps: [],
|
|
16
|
+
concatenatedAttrs: new Map(),
|
|
17
|
+
eventHandlers: input.eventHandlers ?? [],
|
|
18
|
+
}
|
|
19
|
+
return {
|
|
20
|
+
traceId: 'trace-0',
|
|
21
|
+
name: 'root',
|
|
22
|
+
tag: input.tag,
|
|
23
|
+
attributes: input.attributes ?? {},
|
|
24
|
+
extractorData: new Map([['prop-tracker', propTrackerData]]),
|
|
25
|
+
children: [],
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
describe('inferSupportedNativeStates', () => {
|
|
30
|
+
describe('hover + focus (interactive)', () => {
|
|
31
|
+
it('returns true for native form tags', () => {
|
|
32
|
+
for (const tag of ['button', 'a', 'input', 'select', 'textarea', 'summary']) {
|
|
33
|
+
const states = inferSupportedNativeStates(makeElement({ tag }))
|
|
34
|
+
expect(states.hover, `hover for <${tag}>`).toBe(true)
|
|
35
|
+
expect(states.focus, `focus for <${tag}>`).toBe(true)
|
|
36
|
+
}
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it('returns true when role is interactive', () => {
|
|
40
|
+
const interactiveRoles = ['switch', 'checkbox', 'tab', 'menuitem', 'combobox', 'slider']
|
|
41
|
+
for (const role of interactiveRoles) {
|
|
42
|
+
const states = inferSupportedNativeStates(makeElement({ tag: 'div', role }))
|
|
43
|
+
expect(states.hover, `hover for role=${role}`).toBe(true)
|
|
44
|
+
expect(states.focus, `focus for role=${role}`).toBe(true)
|
|
45
|
+
}
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
it('returns true when any pointer/keyboard/form event handler is present', () => {
|
|
49
|
+
const handlers = ['onClick', 'onPointerDown', 'onFocus', 'onKeyDown', 'onChange']
|
|
50
|
+
for (const handler of handlers) {
|
|
51
|
+
const states = inferSupportedNativeStates(makeElement({ tag: 'div', eventHandlers: [handler] }))
|
|
52
|
+
expect(states.hover, `hover for ${handler}`).toBe(true)
|
|
53
|
+
expect(states.focus, `focus for ${handler}`).toBe(true)
|
|
54
|
+
}
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
it('returns false for a plain div with no signals', () => {
|
|
58
|
+
const states = inferSupportedNativeStates(makeElement({ tag: 'div' }))
|
|
59
|
+
expect(states.hover).toBe(false)
|
|
60
|
+
expect(states.focus).toBe(false)
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
it('reads role from the rendered attributes when prop-tracker data is absent', () => {
|
|
64
|
+
const element: ExtractedElement = {
|
|
65
|
+
traceId: 'trace-0',
|
|
66
|
+
name: 'root',
|
|
67
|
+
tag: 'div',
|
|
68
|
+
attributes: { role: 'button' },
|
|
69
|
+
extractorData: new Map(),
|
|
70
|
+
children: [],
|
|
71
|
+
}
|
|
72
|
+
const states = inferSupportedNativeStates(element)
|
|
73
|
+
expect(states.hover).toBe(true)
|
|
74
|
+
expect(states.focus).toBe(true)
|
|
75
|
+
})
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
describe('disabled', () => {
|
|
79
|
+
it('returns true for form-disableable native tags', () => {
|
|
80
|
+
for (const tag of ['button', 'input', 'select', 'textarea', 'fieldset']) {
|
|
81
|
+
const states = inferSupportedNativeStates(makeElement({ tag }))
|
|
82
|
+
expect(states.disabled, `disabled for <${tag}>`).toBe(true)
|
|
83
|
+
}
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
it('returns true for form-control roles', () => {
|
|
87
|
+
for (const role of ['button', 'switch', 'checkbox', 'radio', 'menuitem', 'tab', 'option', 'link']) {
|
|
88
|
+
const states = inferSupportedNativeStates(makeElement({ tag: 'div', role }))
|
|
89
|
+
expect(states.disabled, `disabled for role=${role}`).toBe(true)
|
|
90
|
+
}
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
it('returns false for plain divs and non-form roles', () => {
|
|
94
|
+
expect(inferSupportedNativeStates(makeElement({ tag: 'div' })).disabled).toBe(false)
|
|
95
|
+
expect(inferSupportedNativeStates(makeElement({ tag: 'div', role: 'navigation' })).disabled).toBe(false)
|
|
96
|
+
})
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
describe('invalid', () => {
|
|
100
|
+
it('returns true for form-validatable native tags', () => {
|
|
101
|
+
for (const tag of ['input', 'select', 'textarea', 'form']) {
|
|
102
|
+
const states = inferSupportedNativeStates(makeElement({ tag }))
|
|
103
|
+
expect(states.invalid, `invalid for <${tag}>`).toBe(true)
|
|
104
|
+
}
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
it('returns false for a plain button (not form-validatable)', () => {
|
|
108
|
+
expect(inferSupportedNativeStates(makeElement({ tag: 'button' })).invalid).toBe(false)
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
it('ignores the aria-invalid attribute on non-form tags', () => {
|
|
112
|
+
const states = inferSupportedNativeStates(makeElement({ tag: 'div', attributes: { 'aria-invalid': 'true' } }))
|
|
113
|
+
expect(states.invalid).toBe(false)
|
|
114
|
+
})
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
describe('role="presentation" / "none" opt-out', () => {
|
|
118
|
+
it('suppresses every native state when role is "presentation"', () => {
|
|
119
|
+
const states = inferSupportedNativeStates(makeElement({ tag: 'button', role: 'presentation' }))
|
|
120
|
+
expect(states).toEqual({ hover: false, focus: false, disabled: false, invalid: false })
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
it('suppresses every native state when role is "none"', () => {
|
|
124
|
+
const states = inferSupportedNativeStates(makeElement({ tag: 'input', role: 'none' }))
|
|
125
|
+
expect(states).toEqual({ hover: false, focus: false, disabled: false, invalid: false })
|
|
126
|
+
})
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
describe('tabindex="-1" suppresses focus', () => {
|
|
130
|
+
it('keeps hover and disabled but suppresses focus on an interactive tag with tabindex="-1"', () => {
|
|
131
|
+
const states = inferSupportedNativeStates(makeElement({ tag: 'button', attributes: { tabindex: '-1' } }))
|
|
132
|
+
expect(states.hover).toBe(true)
|
|
133
|
+
expect(states.focus).toBe(false)
|
|
134
|
+
expect(states.disabled).toBe(true)
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
it('does not affect non-interactive elements (no focus to suppress)', () => {
|
|
138
|
+
const states = inferSupportedNativeStates(makeElement({ tag: 'div', attributes: { tabindex: '-1' } }))
|
|
139
|
+
expect(states.focus).toBe(false)
|
|
140
|
+
expect(states.hover).toBe(false)
|
|
141
|
+
})
|
|
142
|
+
})
|
|
143
|
+
})
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Design-state inference from React-extractor signals.
|
|
3
|
+
*
|
|
4
|
+
* The new design (Notion: "New Design") supports only the four native states
|
|
5
|
+
* (hover/focus/disabled/invalid) and derives them purely from the React side:
|
|
6
|
+
* html tag, role attribute, and event handlers. The CSS extractor is not
|
|
7
|
+
* involved.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { ExtractedElement } from './core/tree-builder'
|
|
11
|
+
import type { PropTrackerData } from './prop-tracker'
|
|
12
|
+
|
|
13
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
14
|
+
// Types
|
|
15
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
16
|
+
|
|
17
|
+
export interface SupportedNativeStates {
|
|
18
|
+
hover: boolean
|
|
19
|
+
focus: boolean
|
|
20
|
+
disabled: boolean
|
|
21
|
+
invalid: boolean
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
25
|
+
// Signal sets
|
|
26
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
27
|
+
|
|
28
|
+
const INTERACTIVE_TAGS = new Set(['button', 'a', 'input', 'select', 'textarea', 'summary'])
|
|
29
|
+
|
|
30
|
+
const INTERACTIVE_ROLES = new Set([
|
|
31
|
+
'button',
|
|
32
|
+
'link',
|
|
33
|
+
'switch',
|
|
34
|
+
'checkbox',
|
|
35
|
+
'radio',
|
|
36
|
+
'tab',
|
|
37
|
+
'option',
|
|
38
|
+
'menuitem',
|
|
39
|
+
'menuitemcheckbox',
|
|
40
|
+
'menuitemradio',
|
|
41
|
+
'treeitem',
|
|
42
|
+
'combobox',
|
|
43
|
+
'slider',
|
|
44
|
+
'spinbutton',
|
|
45
|
+
'searchbox',
|
|
46
|
+
'textbox',
|
|
47
|
+
'listbox',
|
|
48
|
+
])
|
|
49
|
+
|
|
50
|
+
const INTERACTIVE_EVENT_HANDLERS = new Set([
|
|
51
|
+
'onClick',
|
|
52
|
+
'onMouseDown',
|
|
53
|
+
'onMouseUp',
|
|
54
|
+
'onMouseEnter',
|
|
55
|
+
'onMouseLeave',
|
|
56
|
+
'onPointerDown',
|
|
57
|
+
'onPointerUp',
|
|
58
|
+
'onFocus',
|
|
59
|
+
'onBlur',
|
|
60
|
+
'onKeyDown',
|
|
61
|
+
'onKeyUp',
|
|
62
|
+
'onKeyPress',
|
|
63
|
+
'onChange',
|
|
64
|
+
'onInput',
|
|
65
|
+
'onSubmit',
|
|
66
|
+
])
|
|
67
|
+
|
|
68
|
+
const DISABLEABLE_TAGS = new Set(['button', 'input', 'select', 'textarea', 'fieldset'])
|
|
69
|
+
|
|
70
|
+
const DISABLEABLE_ROLES = new Set(['button', 'switch', 'checkbox', 'radio', 'menuitem', 'tab', 'option', 'link'])
|
|
71
|
+
|
|
72
|
+
const VALIDATABLE_TAGS = new Set(['input', 'select', 'textarea', 'form'])
|
|
73
|
+
|
|
74
|
+
const PRESENTATION_ROLES = new Set(['presentation', 'none'])
|
|
75
|
+
|
|
76
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
77
|
+
// Inference
|
|
78
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
79
|
+
|
|
80
|
+
function getRole(element: ExtractedElement): string | undefined {
|
|
81
|
+
const propTrackerData = element.extractorData.get('prop-tracker') as PropTrackerData | undefined
|
|
82
|
+
return propTrackerData?.role ?? element.attributes.role
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function getEventHandlers(element: ExtractedElement): string[] {
|
|
86
|
+
const propTrackerData = element.extractorData.get('prop-tracker') as PropTrackerData | undefined
|
|
87
|
+
return propTrackerData?.eventHandlers ?? []
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* `role="presentation"` and `role="none"` strip the element's implicit ARIA
|
|
92
|
+
* semantics — a `<button role="presentation">` is no longer a button. Treat
|
|
93
|
+
* either as an explicit opt-out from every native state.
|
|
94
|
+
*/
|
|
95
|
+
function hasPresentationRole(role: string | undefined): boolean {
|
|
96
|
+
return role !== undefined && PRESENTATION_ROLES.has(role)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function hasInteractiveSignal(tag: string, role: string | undefined, eventHandlers: string[]): boolean {
|
|
100
|
+
if (INTERACTIVE_TAGS.has(tag)) return true
|
|
101
|
+
if (role !== undefined && INTERACTIVE_ROLES.has(role)) return true
|
|
102
|
+
return eventHandlers.some((handler) => INTERACTIVE_EVENT_HANDLERS.has(handler))
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function hasDisableableSignal(tag: string, role: string | undefined): boolean {
|
|
106
|
+
if (DISABLEABLE_TAGS.has(tag)) return true
|
|
107
|
+
if (role !== undefined && DISABLEABLE_ROLES.has(role)) return true
|
|
108
|
+
return false
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function hasValidatableSignal(tag: string): boolean {
|
|
112
|
+
return VALIDATABLE_TAGS.has(tag)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Decides which of the four native states the given element supports, based on
|
|
117
|
+
* its tag, role, and event handlers.
|
|
118
|
+
*
|
|
119
|
+
* Two attribute-level overrides apply:
|
|
120
|
+
* - `role="presentation"` / `role="none"` opts the element out of every state.
|
|
121
|
+
* - `tabindex="-1"` suppresses `focus` only (the element is programmatically
|
|
122
|
+
* focusable but not via keyboard tab).
|
|
123
|
+
*/
|
|
124
|
+
export function inferSupportedNativeStates(element: ExtractedElement): SupportedNativeStates {
|
|
125
|
+
const tag = element.tag.toLowerCase()
|
|
126
|
+
const role = getRole(element)
|
|
127
|
+
|
|
128
|
+
if (hasPresentationRole(role)) {
|
|
129
|
+
return { hover: false, focus: false, disabled: false, invalid: false }
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const eventHandlers = getEventHandlers(element)
|
|
133
|
+
const interactive = hasInteractiveSignal(tag, role, eventHandlers)
|
|
134
|
+
const isUntabbable = element.attributes.tabindex === '-1'
|
|
135
|
+
|
|
136
|
+
return {
|
|
137
|
+
hover: interactive,
|
|
138
|
+
focus: interactive && !isUntabbable,
|
|
139
|
+
disabled: hasDisableableSignal(tag, role),
|
|
140
|
+
invalid: hasValidatableSignal(tag),
|
|
141
|
+
}
|
|
142
|
+
}
|
|
@@ -19,6 +19,7 @@ export {
|
|
|
19
19
|
enrichGapProperties,
|
|
20
20
|
resolveDisplayValue,
|
|
21
21
|
getDefaultDisplayForTag,
|
|
22
|
+
inferSupportedNativeStates,
|
|
22
23
|
} from './extractors'
|
|
23
24
|
|
|
24
25
|
export type {
|
|
@@ -34,6 +35,7 @@ export type {
|
|
|
34
35
|
PropTrackerData,
|
|
35
36
|
PropTrackerExtractorState,
|
|
36
37
|
CssPropertiesData,
|
|
38
|
+
SupportedNativeStates,
|
|
37
39
|
} from './extractors'
|
|
38
40
|
|
|
39
41
|
// ─────────────────────────────────────────────────────────────────────────────
|