@wix/zero-config-implementation 1.88.0 → 1.90.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.d.ts +5 -5
- package/dist/index.js +5559 -5500
- package/package.json +2 -2
- package/src/converters/custom-states-builder.test.ts +3 -2
- package/src/converters/custom-states-builder.ts +6 -39
- package/src/converters/state-class-trigger.ts +41 -0
- package/src/converters/states-builder.test.ts +96 -0
- package/src/converters/states-builder.ts +13 -5
- package/src/converters/to-editor-component.ts +29 -16
- package/src/index.ts +11 -3
- package/src/information-extractors/css/parse.ts +2 -2
- package/src/information-extractors/css/types.ts +4 -4
- package/src/information-extractors/react/extractors/state-markers.test.ts +75 -7
- package/src/information-extractors/react/extractors/state-markers.ts +38 -6
- package/src/validators/editor-element-validator.test.ts +142 -0
- package/src/validators/editor-element-validator.ts +64 -0
|
@@ -2,9 +2,8 @@
|
|
|
2
2
|
* Design-state inference from React-extractor signals.
|
|
3
3
|
*
|
|
4
4
|
* The new design (Notion: "New Design") supports only the four native states
|
|
5
|
-
* (hover/focus/disabled/invalid)
|
|
6
|
-
*
|
|
7
|
-
* involved.
|
|
5
|
+
* (hover/focus/disabled/invalid). React signals provide the defaults; a caller
|
|
6
|
+
* may additionally report a focus state the component author wrote in CSS.
|
|
8
7
|
*/
|
|
9
8
|
|
|
10
9
|
import type { ExtractedElement } from './core/tree-builder'
|
|
@@ -65,6 +64,25 @@ const INTERACTIVE_EVENT_HANDLERS = new Set([
|
|
|
65
64
|
'onSubmit',
|
|
66
65
|
])
|
|
67
66
|
|
|
67
|
+
// Elements that get a focus design state without the author asking for one: the
|
|
68
|
+
// ones whose focused appearance is part of using them. Mirrored in the wix-app
|
|
69
|
+
// skill's `editor-react-component/DESIGN-STATES.md` eligibility table.
|
|
70
|
+
const DEFAULT_FOCUS_TAGS = new Set(['select', 'textarea'])
|
|
71
|
+
|
|
72
|
+
const DEFAULT_FOCUS_ROLES = new Set([
|
|
73
|
+
'checkbox',
|
|
74
|
+
'radio',
|
|
75
|
+
'switch',
|
|
76
|
+
'slider',
|
|
77
|
+
'spinbutton',
|
|
78
|
+
'textbox',
|
|
79
|
+
'searchbox',
|
|
80
|
+
'combobox',
|
|
81
|
+
'listbox',
|
|
82
|
+
])
|
|
83
|
+
|
|
84
|
+
const INPUT_TYPES_WITHOUT_FOCUS_STATE = new Set(['hidden', 'button', 'submit', 'reset', 'image'])
|
|
85
|
+
|
|
68
86
|
const DISABLEABLE_TAGS = new Set(['button', 'input', 'select', 'textarea', 'fieldset'])
|
|
69
87
|
|
|
70
88
|
const DISABLEABLE_ROLES = new Set(['button', 'switch', 'checkbox', 'radio', 'menuitem', 'tab', 'option', 'link'])
|
|
@@ -102,6 +120,15 @@ function hasInteractiveSignal(tag: string, role: string | undefined, eventHandle
|
|
|
102
120
|
return eventHandlers.some((handler) => INTERACTIVE_EVENT_HANDLERS.has(handler))
|
|
103
121
|
}
|
|
104
122
|
|
|
123
|
+
function hasDefaultFocusSignal(element: ExtractedElement, tag: string, role: string | undefined): boolean {
|
|
124
|
+
if (role !== undefined) return DEFAULT_FOCUS_ROLES.has(role)
|
|
125
|
+
if (DEFAULT_FOCUS_TAGS.has(tag)) return true
|
|
126
|
+
if (tag !== 'input') return false
|
|
127
|
+
|
|
128
|
+
const inputType = element.attributes.type?.toLowerCase()
|
|
129
|
+
return inputType === undefined || !INPUT_TYPES_WITHOUT_FOCUS_STATE.has(inputType)
|
|
130
|
+
}
|
|
131
|
+
|
|
105
132
|
function hasDisableableSignal(tag: string, role: string | undefined): boolean {
|
|
106
133
|
if (DISABLEABLE_TAGS.has(tag)) return true
|
|
107
134
|
if (role !== undefined && DISABLEABLE_ROLES.has(role)) return true
|
|
@@ -114,14 +141,19 @@ function hasValidatableSignal(tag: string): boolean {
|
|
|
114
141
|
|
|
115
142
|
/**
|
|
116
143
|
* Decides which of the four native states the given element supports, based on
|
|
117
|
-
* its tag, role, and
|
|
144
|
+
* its tag, role, event handlers, and `authoredFocus` — a paired focus modifier
|
|
145
|
+
* the component author wrote in CSS, which opts an interactive element into the
|
|
146
|
+
* focus state it does not get by default.
|
|
118
147
|
*
|
|
119
148
|
* Two attribute-level overrides apply:
|
|
120
149
|
* - `role="presentation"` / `role="none"` opts the element out of every state.
|
|
121
150
|
* - `tabindex="-1"` suppresses `focus` only (the element is programmatically
|
|
122
151
|
* focusable but not via keyboard tab).
|
|
123
152
|
*/
|
|
124
|
-
export function inferSupportedNativeStates(
|
|
153
|
+
export function inferSupportedNativeStates(
|
|
154
|
+
element: ExtractedElement,
|
|
155
|
+
{ authoredFocus = false }: { authoredFocus?: boolean } = {},
|
|
156
|
+
): SupportedNativeStates {
|
|
125
157
|
const tag = element.tag.toLowerCase()
|
|
126
158
|
const role = getRole(element)
|
|
127
159
|
|
|
@@ -135,7 +167,7 @@ export function inferSupportedNativeStates(element: ExtractedElement): Supported
|
|
|
135
167
|
|
|
136
168
|
return {
|
|
137
169
|
hover: interactive,
|
|
138
|
-
focus: interactive && !isUntabbable,
|
|
170
|
+
focus: interactive && !isUntabbable && (hasDefaultFocusSignal(element, tag, role) || authoredFocus),
|
|
139
171
|
disabled: hasDisableableSignal(tag, role),
|
|
140
172
|
invalid: hasValidatableSignal(tag),
|
|
141
173
|
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import type { CssCustomPropertyItem, EditorElement, ElementItem } from '@wix/react-component-schema'
|
|
2
|
+
import { CSS_PROPERTIES, ELEMENTS } from '@wix/react-component-schema'
|
|
3
|
+
import { describe, expect, it } from 'vitest'
|
|
4
|
+
|
|
5
|
+
import { ValidationError } from '../errors'
|
|
6
|
+
import { validateEditorElement } from './editor-element-validator'
|
|
7
|
+
|
|
8
|
+
function buildMinimalEditorElement(overrides: Partial<EditorElement> = {}): EditorElement {
|
|
9
|
+
return {
|
|
10
|
+
selector: '.root',
|
|
11
|
+
displayName: 'Test Component',
|
|
12
|
+
...overrides,
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function buildInlineElementItem(overrides: Partial<ElementItem> = {}): ElementItem {
|
|
17
|
+
return {
|
|
18
|
+
elementType: ELEMENTS.ELEMENT_TYPE.inlineElement,
|
|
19
|
+
inlineElement: {
|
|
20
|
+
selector: '.child',
|
|
21
|
+
displayName: 'Child',
|
|
22
|
+
},
|
|
23
|
+
...overrides,
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function buildCssCustomPropertyItem(cssPropertyType: CssCustomPropertyItem['cssPropertyType']): CssCustomPropertyItem {
|
|
28
|
+
return { cssPropertyType }
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
describe('validateEditorElement', () => {
|
|
32
|
+
it('does not throw for a valid minimal editorElement', () => {
|
|
33
|
+
const editorElement = buildMinimalEditorElement()
|
|
34
|
+
expect(() => validateEditorElement(editorElement, 'TestComponent')).not.toThrow()
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
it('does not throw when cssCustomProperties have valid property types', () => {
|
|
38
|
+
const editorElement = buildMinimalEditorElement({
|
|
39
|
+
cssCustomProperties: {
|
|
40
|
+
color: buildCssCustomPropertyItem(CSS_PROPERTIES.CSS_PROPERTY_TYPE.color),
|
|
41
|
+
size: buildCssCustomPropertyItem(CSS_PROPERTIES.CSS_PROPERTY_TYPE.length),
|
|
42
|
+
},
|
|
43
|
+
})
|
|
44
|
+
expect(() => validateEditorElement(editorElement, 'TestComponent')).not.toThrow()
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it('throws ValidationError when a cssCustomProperty has UNKNOWN cssPropertyType', () => {
|
|
48
|
+
const editorElement = buildMinimalEditorElement({
|
|
49
|
+
cssCustomProperties: {
|
|
50
|
+
brokenProp: buildCssCustomPropertyItem(CSS_PROPERTIES.CSS_PROPERTY_TYPE.UNKNOWN_CssPropertyType),
|
|
51
|
+
},
|
|
52
|
+
})
|
|
53
|
+
expect(() => validateEditorElement(editorElement, 'TestComponent')).toThrow(ValidationError)
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
it('includes the field path in the error message for UNKNOWN cssPropertyType', () => {
|
|
57
|
+
const editorElement = buildMinimalEditorElement({
|
|
58
|
+
cssCustomProperties: {
|
|
59
|
+
brokenProp: buildCssCustomPropertyItem(CSS_PROPERTIES.CSS_PROPERTY_TYPE.UNKNOWN_CssPropertyType),
|
|
60
|
+
},
|
|
61
|
+
})
|
|
62
|
+
expect(() => validateEditorElement(editorElement, 'TestComponent')).toThrow(
|
|
63
|
+
'editorElement.cssCustomProperties.brokenProp',
|
|
64
|
+
)
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
it('throws ValidationError when a cssCustomProperty is missing cssPropertyType', () => {
|
|
68
|
+
const editorElement = buildMinimalEditorElement({
|
|
69
|
+
cssCustomProperties: {
|
|
70
|
+
missingTypeProp: {} as CssCustomPropertyItem,
|
|
71
|
+
},
|
|
72
|
+
})
|
|
73
|
+
expect(() => validateEditorElement(editorElement, 'TestComponent')).toThrow(ValidationError)
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
it('throws ValidationError when an elements entry has UNKNOWN elementType', () => {
|
|
77
|
+
const editorElement = buildMinimalEditorElement({
|
|
78
|
+
elements: {
|
|
79
|
+
child: {
|
|
80
|
+
elementType: ELEMENTS.ELEMENT_TYPE.UNKNOWN_ElementType,
|
|
81
|
+
inlineElement: { selector: '.child', displayName: 'Child' },
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
})
|
|
85
|
+
expect(() => validateEditorElement(editorElement, 'TestComponent')).toThrow(ValidationError)
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
it('throws ValidationError when an elements entry is missing elementType', () => {
|
|
89
|
+
const editorElement = buildMinimalEditorElement({
|
|
90
|
+
elements: {
|
|
91
|
+
child: buildInlineElementItem({ elementType: undefined }),
|
|
92
|
+
},
|
|
93
|
+
})
|
|
94
|
+
expect(() => validateEditorElement(editorElement, 'TestComponent')).toThrow(ValidationError)
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
it('collects all violations and throws once with all of them listed', () => {
|
|
98
|
+
const editorElement = buildMinimalEditorElement({
|
|
99
|
+
cssCustomProperties: {
|
|
100
|
+
badProp: buildCssCustomPropertyItem(CSS_PROPERTIES.CSS_PROPERTY_TYPE.UNKNOWN_CssPropertyType),
|
|
101
|
+
},
|
|
102
|
+
elements: {
|
|
103
|
+
child: {
|
|
104
|
+
elementType: ELEMENTS.ELEMENT_TYPE.UNKNOWN_ElementType,
|
|
105
|
+
inlineElement: { selector: '.child', displayName: 'Child' },
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
})
|
|
109
|
+
expect(() => validateEditorElement(editorElement, 'TestComponent')).toThrow(
|
|
110
|
+
/editorElement\.cssCustomProperties\.badProp.*\n.*editorElement\.elements\.child/s,
|
|
111
|
+
)
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
it('validates nested inline element cssCustomProperties recursively', () => {
|
|
115
|
+
const editorElement = buildMinimalEditorElement({
|
|
116
|
+
elements: {
|
|
117
|
+
child: {
|
|
118
|
+
elementType: ELEMENTS.ELEMENT_TYPE.inlineElement,
|
|
119
|
+
inlineElement: {
|
|
120
|
+
selector: '.child',
|
|
121
|
+
displayName: 'Child',
|
|
122
|
+
cssCustomProperties: {
|
|
123
|
+
nestedBad: buildCssCustomPropertyItem(CSS_PROPERTIES.CSS_PROPERTY_TYPE.UNKNOWN_CssPropertyType),
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
})
|
|
129
|
+
expect(() => validateEditorElement(editorElement, 'TestComponent')).toThrow(
|
|
130
|
+
'editorElement.elements.child.inlineElement.cssCustomProperties.nestedBad',
|
|
131
|
+
)
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
it('includes the component name in the error message', () => {
|
|
135
|
+
const editorElement = buildMinimalEditorElement({
|
|
136
|
+
cssCustomProperties: {
|
|
137
|
+
badProp: buildCssCustomPropertyItem(CSS_PROPERTIES.CSS_PROPERTY_TYPE.UNKNOWN_CssPropertyType),
|
|
138
|
+
},
|
|
139
|
+
})
|
|
140
|
+
expect(() => validateEditorElement(editorElement, 'MyButton')).toThrow('MyButton')
|
|
141
|
+
})
|
|
142
|
+
})
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
CssCustomPropertyItem,
|
|
3
|
+
EditorReactComponent,
|
|
4
|
+
ElementItem,
|
|
5
|
+
InlineElement,
|
|
6
|
+
} from '@wix/react-component-schema'
|
|
7
|
+
import { CSS_PROPERTIES, ELEMENTS } from '@wix/react-component-schema'
|
|
8
|
+
|
|
9
|
+
import { ValidationError } from '../errors'
|
|
10
|
+
|
|
11
|
+
type RawEditorElement = NonNullable<EditorReactComponent['editorElement']>
|
|
12
|
+
type AnyElement = RawEditorElement | InlineElement
|
|
13
|
+
|
|
14
|
+
export function validateEditorElement(editorElement: RawEditorElement, componentName: string): void {
|
|
15
|
+
const violations = collectElementViolations(editorElement, 'editorElement')
|
|
16
|
+
if (violations.length === 0) return
|
|
17
|
+
|
|
18
|
+
const violationList = violations.map((violation) => ` - ${violation}`).join('\n')
|
|
19
|
+
throw new ValidationError(`Component "${componentName}" produced an invalid editorElement:\n${violationList}`, {
|
|
20
|
+
props: { phase: 'conversion' },
|
|
21
|
+
})
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function collectElementViolations(element: AnyElement, elementPath: string): string[] {
|
|
25
|
+
return [
|
|
26
|
+
...validateCssCustomProperties(element.cssCustomProperties, elementPath),
|
|
27
|
+
...validateElementsMap(element.elements, elementPath),
|
|
28
|
+
]
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function validateCssCustomProperties(
|
|
32
|
+
cssCustomProperties: Record<string, CssCustomPropertyItem> | undefined,
|
|
33
|
+
elementPath: string,
|
|
34
|
+
): string[] {
|
|
35
|
+
if (!cssCustomProperties) return []
|
|
36
|
+
|
|
37
|
+
const violations: string[] = []
|
|
38
|
+
for (const [propKey, propValue] of Object.entries(cssCustomProperties)) {
|
|
39
|
+
const unknownType = CSS_PROPERTIES.CSS_PROPERTY_TYPE.UNKNOWN_CssPropertyType
|
|
40
|
+
if (!propValue.cssPropertyType || propValue.cssPropertyType === unknownType) {
|
|
41
|
+
violations.push(`${elementPath}.cssCustomProperties.${propKey}: cssPropertyType is not set or UNKNOWN`)
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return violations
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function validateElementsMap(elements: Record<string, ElementItem> | undefined, elementPath: string): string[] {
|
|
48
|
+
if (!elements) return []
|
|
49
|
+
|
|
50
|
+
const violations: string[] = []
|
|
51
|
+
for (const [elementKey, elementItem] of Object.entries(elements)) {
|
|
52
|
+
const itemPath = `${elementPath}.elements.${elementKey}`
|
|
53
|
+
const unknownType = ELEMENTS.ELEMENT_TYPE.UNKNOWN_ElementType
|
|
54
|
+
|
|
55
|
+
if (!elementItem.elementType || elementItem.elementType === unknownType) {
|
|
56
|
+
violations.push(`${itemPath}: elementType is not set or UNKNOWN`)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (elementItem.inlineElement) {
|
|
60
|
+
violations.push(...collectElementViolations(elementItem.inlineElement, `${itemPath}.inlineElement`))
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return violations
|
|
64
|
+
}
|