@wix/zero-config-implementation 1.91.0 → 1.93.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/package.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "registry": "https://registry.npmjs.org/",
5
5
  "access": "public"
6
6
  },
7
- "version": "1.91.0",
7
+ "version": "1.93.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",
@@ -106,5 +106,5 @@
106
106
  ]
107
107
  }
108
108
  },
109
- "falconPackageHash": "7232f116f870e20fe94caa49ba503a0a945ec4eed14c9f9b55544590"
109
+ "falconPackageHash": "d7c036bb07aaafe708985efa56e88c2eefb34077d2d892938112978f"
110
110
  }
@@ -166,6 +166,71 @@ describe('toEditorReactComponent display conversion', () => {
166
166
  })
167
167
  })
168
168
 
169
+ describe('toEditorReactComponent selector conversion', () => {
170
+ it.each([
171
+ ['a', 'a___'],
172
+ ['div', 'div_'],
173
+ ['span', 'span'],
174
+ ])('pads the root selector %j to at least four characters', (tag, expectedSelector) => {
175
+ const rootElement = createElementWithDisplayMatcher(tag, matcherDataFromCss(''))
176
+
177
+ const editorElement = toEditorReactComponent(createComponent(rootElement)).editorElement
178
+
179
+ expect(editorElement?.selector).toBe(expectedSelector)
180
+ })
181
+
182
+ it('pads a short semantic class selector', () => {
183
+ const rootElement = createSemanticElement({
184
+ traceId: 'trace-root',
185
+ name: 'root',
186
+ className: 'a',
187
+ })
188
+
189
+ const editorElement = toEditorReactComponent(createComponent(rootElement)).editorElement
190
+
191
+ expect(editorElement?.selector).toBe('.a__')
192
+ })
193
+
194
+ it('pads short inline and ref element selectors', () => {
195
+ const inlineElement: ExtractedElement = {
196
+ traceId: 'trace-link',
197
+ name: 'link',
198
+ tag: 'a',
199
+ attributes: {},
200
+ extractorData: new Map<string, unknown>(),
201
+ children: [],
202
+ }
203
+ const refElement: ExtractedElement = {
204
+ traceId: 'trace-action-button',
205
+ name: 'actionButton',
206
+ tag: 'div',
207
+ attributes: {},
208
+ extractorData: new Map<string, unknown>([
209
+ [
210
+ 'ref-element',
211
+ {
212
+ elementPropsPath: 'actionButton',
213
+ refComponentType: 'test.Button',
214
+ selector: '.a',
215
+ },
216
+ ],
217
+ ]),
218
+ children: [],
219
+ }
220
+ const rootElement = createSemanticElement({
221
+ traceId: 'trace-root',
222
+ name: 'root',
223
+ className: 'root',
224
+ children: [inlineElement, refElement],
225
+ })
226
+
227
+ const elements = toEditorReactComponent(createComponent(rootElement)).editorElement?.elements
228
+
229
+ expect(elements?.link?.inlineElement?.selector).toBe('a___')
230
+ expect(elements?.actionButton?.refElement?.selector).toBe('.a__')
231
+ })
232
+ })
233
+
169
234
  function cssInfoFromStylesheet(cssString: string): ExtractedCssInfo {
170
235
  const api = parseCss(cssString)
171
236
  const properties = new Map<string, string>()
@@ -34,6 +34,8 @@ import type { StateClassTrigger } from './state-class-trigger'
34
34
  import { buildStatesBlock } from './states-builder'
35
35
  import { formatDisplayName } from './utils'
36
36
 
37
+ const MINIMUM_SELECTOR_LENGTH = 4
38
+
37
39
  /**
38
40
  * Merges native and custom state blocks. Native states take precedence on any
39
41
  * key collision (e.g. an author wrapping `isDisabled` in `ElementState<>` does not
@@ -140,9 +142,13 @@ function getSemanticBlockName(element: ExtractedElement | undefined): string | u
140
142
  function buildSelector(rootElement?: ExtractedElement): string {
141
143
  if (rootElement?.attributes.class) {
142
144
  const semanticClass = findPreferredSemanticClass(normalizeClassNames(rootElement.attributes.class))
143
- if (semanticClass) return `.${semanticClass}`
145
+ if (semanticClass) return padSelector(`.${semanticClass}`)
144
146
  }
145
- return rootElement?.tag ?? ''
147
+ return padSelector(rootElement?.tag ?? '')
148
+ }
149
+
150
+ function padSelector(selector: string): string {
151
+ return selector.padEnd(MINIMUM_SELECTOR_LENGTH, '_')
146
152
  }
147
153
 
148
154
  function buildInnerElementDisplayName(element: ExtractedElement, ancestorSemanticClasses: string[]): string {
@@ -206,7 +212,7 @@ function buildElements(
206
212
  result[buildRefElementManifestKey(refElementMatch.elementPropsPath, parentElementPropsPath)] = {
207
213
  elementType: ELEMENTS.ELEMENT_TYPE.refElement,
208
214
  refElement: {
209
- selector: refElementMatch.selector,
215
+ selector: padSelector(refElementMatch.selector),
210
216
  type: refElementMatch.refComponentType,
211
217
  },
212
218
  }
@@ -38,7 +38,7 @@ function createElementWithCssProperties(
38
38
  ): ExtractedElement {
39
39
  const extractorData = new Map<string, unknown>()
40
40
  extractorData.set('css-matcher', matcherDataFromCss(declarations))
41
- extractorData.set('css-properties', { relevant: getCssPropertiesForTag(tag, attributes.role) })
41
+ extractorData.set('css-properties', { relevant: getCssPropertiesForTag(tag, attributes) })
42
42
 
43
43
  return {
44
44
  traceId: 'trace-1',
@@ -173,11 +173,38 @@ describe('getCssPropertiesForTag', () => {
173
173
  expect(relevantProperties).not.toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.font)
174
174
  })
175
175
 
176
- it('derives box properties from default display for controls', () => {
177
- const relevantProperties = getCssPropertiesForTag('button')
176
+ it('gives interactive controls both box and text properties', () => {
177
+ for (const tag of ['button', 'select', 'textarea']) {
178
+ const relevantProperties = getCssPropertiesForTag(tag)
179
+
180
+ expect(relevantProperties).toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.paddingTop)
181
+ expect(relevantProperties).toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.font)
182
+ expect(relevantProperties).toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.color)
183
+ }
184
+ })
185
+
186
+ it('treats an input with no type as text-bearing', () => {
187
+ const relevantProperties = getCssPropertiesForTag('input')
178
188
 
179
189
  expect(relevantProperties).toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.paddingTop)
180
- expect(relevantProperties).not.toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.font)
190
+ expect(relevantProperties).toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.font)
191
+ })
192
+
193
+ it('treats text-entry and button-like input types as text-bearing', () => {
194
+ for (const inputType of ['text', 'email', 'password', 'search', 'number', 'submit', 'button', 'reset']) {
195
+ const relevantProperties = getCssPropertiesForTag('input', { type: inputType })
196
+
197
+ expect(relevantProperties).toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.font)
198
+ }
199
+ })
200
+
201
+ it('keeps typography off input types that render no text of their own', () => {
202
+ for (const inputType of ['checkbox', 'radio', 'range', 'color', 'file', 'image', 'hidden']) {
203
+ const relevantProperties = getCssPropertiesForTag('input', { type: inputType })
204
+
205
+ expect(relevantProperties).toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.paddingTop)
206
+ expect(relevantProperties).not.toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.font)
207
+ }
181
208
  })
182
209
 
183
210
  it('keeps media elements out of box properties even when their default display is non-inline', () => {
@@ -203,7 +230,7 @@ describe('getCssPropertiesForTag', () => {
203
230
  })
204
231
 
205
232
  it('keeps role-based heading text-only by default', () => {
206
- const relevantProperties = getCssPropertiesForTag('div', 'heading')
233
+ const relevantProperties = getCssPropertiesForTag('div', { role: 'heading' })
207
234
 
208
235
  expect(relevantProperties).toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.font)
209
236
  expect(relevantProperties).not.toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.paddingTop)
@@ -19,12 +19,18 @@ export interface CssPropertiesData {
19
19
  relevant: string[]
20
20
  }
21
21
 
22
+ export interface SemanticAttributes {
23
+ role?: string
24
+ type?: string
25
+ }
26
+
22
27
  // ─────────────────────────────────────────────────────────────────────────────
23
28
  // Semantic Traits
24
29
  // ─────────────────────────────────────────────────────────────────────────────
25
30
 
26
31
  /**
27
- * Text & Typography - elements where readability and hierarchy are primary
32
+ * Text & Typography - elements where readability and hierarchy are primary.
33
+ * Form controls render their own text; `input` varies by type (see below).
28
34
  */
29
35
  const TEXT_TAGS = new Set([
30
36
  'h1',
@@ -42,8 +48,14 @@ const TEXT_TAGS = new Set([
42
48
  'em',
43
49
  'b',
44
50
  'i',
51
+ 'button',
52
+ 'select',
53
+ 'textarea',
45
54
  ])
46
55
 
56
+ /** Anything else - including a missing or invalid `type` - defaults to text. */
57
+ const NON_TEXT_INPUT_TYPES = new Set(['checkbox', 'radio', 'range', 'color', 'file', 'image', 'hidden'])
58
+
47
59
  const DEFAULT_NON_BOX_TEXT_TAGS = new Set(['p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6'])
48
60
 
49
61
  /**
@@ -107,13 +119,15 @@ const MEDIA_CSS_PROPERTIES = [
107
119
  CSS_PROPERTY_TYPE.boxShadow,
108
120
  ]
109
121
 
110
- function hasTextSemantics(tag: string, role?: string): boolean {
122
+ function hasTextSemantics(tag: string, attributes: SemanticAttributes = {}): boolean {
123
+ const normalizedRole = attributes.role?.toLowerCase()
124
+ if (normalizedRole === 'heading' || normalizedRole === 'paragraph') {
125
+ return true
126
+ }
127
+
111
128
  const normalizedTag = tag.toLowerCase()
112
- if (role) {
113
- const normalizedRole = role.toLowerCase()
114
- if (normalizedRole === 'heading' || normalizedRole === 'paragraph') {
115
- return true
116
- }
129
+ if (normalizedTag === 'input') {
130
+ return !NON_TEXT_INPUT_TYPES.has((attributes.type ?? 'text').toLowerCase())
117
131
  }
118
132
  return TEXT_TAGS.has(normalizedTag)
119
133
  }
@@ -135,11 +149,11 @@ function hasMediaSemantics(tag: string, role?: string): boolean {
135
149
  * Note: hasTextContent determination is deferred to tree building since
136
150
  * we can't know during createElement if a child will have text content.
137
151
  */
138
- export function getCssPropertiesForTag(tag: string, role?: string): string[] {
152
+ export function getCssPropertiesForTag(tag: string, attributes: SemanticAttributes = {}): string[] {
139
153
  const properties: string[] = []
140
- const hasTextProperties = hasTextSemantics(tag, role)
141
- const hasMediaProperties = hasMediaSemantics(tag, role)
142
- const hasBoxProperties = hasDefaultBoxProperties(tag, role) && !hasMediaProperties
154
+ const hasTextProperties = hasTextSemantics(tag, attributes)
155
+ const hasMediaProperties = hasMediaSemantics(tag, attributes.role)
156
+ const hasBoxProperties = hasDefaultBoxProperties(tag, attributes.role) && !hasMediaProperties
143
157
 
144
158
  if (hasTextProperties) {
145
159
  properties.push(...TEXT_CSS_PROPERTIES)
@@ -465,12 +479,11 @@ export function enrichGapProperties(elements: ExtractedElement[]): ExtractedElem
465
479
  */
466
480
  export function enrichContainerProperties(elements: ExtractedElement[]): ExtractedElement[] {
467
481
  return elements.map((element) => {
468
- const role = element.attributes.role
469
- if (hasTextSemantics(element.tag, role)) {
482
+ if (hasTextSemantics(element.tag, element.attributes)) {
470
483
  const matcherData = element.extractorData.get('css-matcher') as MatchedCssData | undefined
471
484
  const cssData = element.extractorData.get('css-properties') as CssPropertiesData | undefined
472
485
 
473
- if (cssData && hasEffectiveContainerDisplay(element.tag, role, matcherData)) {
486
+ if (cssData && hasEffectiveContainerDisplay(element.tag, element.attributes.role, matcherData)) {
474
487
  element.extractorData.set('css-properties', {
475
488
  relevant: addBoxProperties(cssData.relevant),
476
489
  })
@@ -500,9 +513,10 @@ export function createCssPropertiesExtractor(): ReactExtractor {
500
513
  if (!event.isDomElement || !event.tag || !event.traceId) return
501
514
 
502
515
  const { tag, props, traceId, store } = event
503
- const role = props.role as string | undefined
504
-
505
- const relevant = getCssPropertiesForTag(tag, role)
516
+ const relevant = getCssPropertiesForTag(tag, {
517
+ role: props.role as string | undefined,
518
+ type: props.type as string | undefined,
519
+ })
506
520
  const data: CssPropertiesData = { relevant }
507
521
 
508
522
  store.set(traceId, 'css-properties', data)