@ccheever/exact-renderer 0.1.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 +118 -0
- package/src/__tests__/adapter-window-state.test.tsx +190 -0
- package/src/__tests__/attrs.test.ts +157 -0
- package/src/__tests__/classname.test.ts +332 -0
- package/src/__tests__/color.test.ts +169 -0
- package/src/__tests__/dom-mirror.test.ts +682 -0
- package/src/__tests__/dom-shim.test.ts +274 -0
- package/src/__tests__/fixtures/SvelteCounter.svelte +7 -0
- package/src/__tests__/fixtures/SvelteInput.svelte +8 -0
- package/src/__tests__/host-config.test.ts +51 -0
- package/src/__tests__/host-ops.test.ts +2234 -0
- package/src/__tests__/image-source.test.ts +135 -0
- package/src/__tests__/liquid-glass.test.ts +72 -0
- package/src/__tests__/multi-root.test.ts +118 -0
- package/src/__tests__/native-view-events.test.ts +102 -0
- package/src/__tests__/nodes.test.ts +399 -0
- package/src/__tests__/normalize.test.ts +576 -0
- package/src/__tests__/paragraph-lowering.test.tsx +144 -0
- package/src/__tests__/props.test.ts +518 -0
- package/src/__tests__/protocol-encoder.test.ts +732 -0
- package/src/__tests__/protocol-fixture-bytes.test.ts +41 -0
- package/src/__tests__/reconciler.test.tsx +241 -0
- package/src/__tests__/svelte-adapter.test.ts +166 -0
- package/src/__tests__/svg-source.test.ts +71 -0
- package/src/__tests__/tags.test.ts +354 -0
- package/src/__tests__/toggle.test.ts +441 -0
- package/src/__tests__/transitions.test.ts +106 -0
- package/src/__tests__/web-primitives.test.tsx +454 -0
- package/src/__tests__/window-hooks.test.tsx +447 -0
- package/src/adapter-contract.ts +68 -0
- package/src/attrs.ts +596 -0
- package/src/classname-contract.ts +87 -0
- package/src/classname-resolve.ts +553 -0
- package/src/classname-runtime.ts +29 -0
- package/src/components.ts +214 -0
- package/src/css-variable-context.ts +83 -0
- package/src/dom-hydration.ts +160 -0
- package/src/dom-mirror.ts +1459 -0
- package/src/dom-shim.ts +1736 -0
- package/src/group-context.ts +69 -0
- package/src/host-config.ts +431 -0
- package/src/host-ops.ts +3167 -0
- package/src/image-source.native.ts +703 -0
- package/src/image-source.ts +554 -0
- package/src/index.ts +278 -0
- package/src/inspector-runtime.ts +244 -0
- package/src/inspector.ts +3570 -0
- package/src/jsx-augmentations.ts +54 -0
- package/src/keyboard-avoidance.ts +217 -0
- package/src/native-primitives.ts +43 -0
- package/src/native-view-events.ts +322 -0
- package/src/native-view.ts +60 -0
- package/src/nodes/index.ts +41 -0
- package/src/nodes/node.ts +531 -0
- package/src/peer-context.ts +100 -0
- package/src/primitives.native.ts +8 -0
- package/src/primitives.ts +8 -0
- package/src/props/index.ts +14 -0
- package/src/props/normalize.ts +816 -0
- package/src/protocol/encoder.ts +940 -0
- package/src/protocol/index.ts +33 -0
- package/src/reconciler.ts +581 -0
- package/src/runtime.ts +11 -0
- package/src/safe-area.ts +543 -0
- package/src/solid.ts +490 -0
- package/src/style/color.js +1 -0
- package/src/style/color.ts +15 -0
- package/src/style/index.js +1 -0
- package/src/style/index.ts +22 -0
- package/src/style/normalize.js +1 -0
- package/src/style/normalize.ts +1426 -0
- package/src/svelte.ts +349 -0
- package/src/svg-source.ts +222 -0
- package/src/tags/index.ts +21 -0
- package/src/tags/tag-map.ts +289 -0
- package/src/text/paragraph-lowering.ts +310 -0
- package/src/types.ts +1175 -0
- package/src/vue.ts +535 -0
- package/src/web-host.ts +19 -0
- package/src/web-primitives.ts +1654 -0
package/src/attrs.ts
ADDED
|
@@ -0,0 +1,596 @@
|
|
|
1
|
+
// @system @ref LLP 0202 §2.3 / LLP 0205 S1 — shared semantic DOM attr lowering.
|
|
2
|
+
|
|
3
|
+
export type DomTagName =
|
|
4
|
+
| 'div'
|
|
5
|
+
| 'span'
|
|
6
|
+
| 'a'
|
|
7
|
+
| 'button'
|
|
8
|
+
| 'input'
|
|
9
|
+
| 'textarea'
|
|
10
|
+
| 'img'
|
|
11
|
+
| 'main'
|
|
12
|
+
| 'nav'
|
|
13
|
+
| 'header'
|
|
14
|
+
| 'footer'
|
|
15
|
+
| 'aside'
|
|
16
|
+
| 'article'
|
|
17
|
+
| 'section'
|
|
18
|
+
| 'blockquote'
|
|
19
|
+
| 'pre'
|
|
20
|
+
| 'code'
|
|
21
|
+
| 'hr'
|
|
22
|
+
| 'ul'
|
|
23
|
+
| 'li'
|
|
24
|
+
| 'label'
|
|
25
|
+
| `h${1 | 2 | 3 | 4 | 5 | 6}`;
|
|
26
|
+
|
|
27
|
+
export interface HostAttrNode {
|
|
28
|
+
tag: string;
|
|
29
|
+
props?: Record<string, unknown>;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface HostSemanticAttrs {
|
|
33
|
+
tag?: string;
|
|
34
|
+
role?: string;
|
|
35
|
+
label?: string;
|
|
36
|
+
hint?: string;
|
|
37
|
+
headingLevel?: number;
|
|
38
|
+
live?: string;
|
|
39
|
+
labelledBy?: string;
|
|
40
|
+
describedBy?: string;
|
|
41
|
+
for?: string;
|
|
42
|
+
nativeID?: string;
|
|
43
|
+
testID?: string;
|
|
44
|
+
testId?: string;
|
|
45
|
+
focusable?: boolean;
|
|
46
|
+
inert?: boolean;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface LowerHostAttrsOptions {
|
|
50
|
+
defaultTag?: DomTagName;
|
|
51
|
+
defaultRole?: string;
|
|
52
|
+
inferTag?: boolean;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface LoweredHostAttrs {
|
|
56
|
+
tag: DomTagName;
|
|
57
|
+
attrs: Record<string, unknown>;
|
|
58
|
+
aria: Record<string, unknown>;
|
|
59
|
+
tabIndex?: number;
|
|
60
|
+
inert?: boolean;
|
|
61
|
+
role?: string;
|
|
62
|
+
labelAssociation: {
|
|
63
|
+
for?: string;
|
|
64
|
+
labelledBy?: string;
|
|
65
|
+
describedBy?: string;
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const NON_DOM_PROPS = new Set([
|
|
70
|
+
'testID',
|
|
71
|
+
'testId',
|
|
72
|
+
'style',
|
|
73
|
+
'children',
|
|
74
|
+
'key',
|
|
75
|
+
'ref',
|
|
76
|
+
'agentId',
|
|
77
|
+
'disabledReason',
|
|
78
|
+
'onTransitionEnd',
|
|
79
|
+
'onFocusIn',
|
|
80
|
+
'onFocusOut',
|
|
81
|
+
'numberOfLines',
|
|
82
|
+
'ellipsizeMode',
|
|
83
|
+
'selectable',
|
|
84
|
+
'selection',
|
|
85
|
+
'selectionCopyText',
|
|
86
|
+
'onSelectionChange',
|
|
87
|
+
'horizontal',
|
|
88
|
+
'focusScope',
|
|
89
|
+
'inert',
|
|
90
|
+
'scrollLocked',
|
|
91
|
+
'portalTarget',
|
|
92
|
+
'accessibilityRole',
|
|
93
|
+
'accessibilityLabel',
|
|
94
|
+
'accessibilityHeadingLevel',
|
|
95
|
+
'accessibilityHint',
|
|
96
|
+
'accessibilityModal',
|
|
97
|
+
'accessibilityExpanded',
|
|
98
|
+
'accessibilitySelected',
|
|
99
|
+
'accessibilityChecked',
|
|
100
|
+
'accessibilityDisabled',
|
|
101
|
+
'accessibilityLive',
|
|
102
|
+
'accessibilityValueNow',
|
|
103
|
+
'accessibilityValueMin',
|
|
104
|
+
'accessibilityValueMax',
|
|
105
|
+
'accessibilityValueText',
|
|
106
|
+
'focusable',
|
|
107
|
+
'nativeID',
|
|
108
|
+
'accessibilityFor',
|
|
109
|
+
'accessibilityLabelledBy',
|
|
110
|
+
'accessibilityDescribedBy',
|
|
111
|
+
'accessibilityBusy',
|
|
112
|
+
'showsVerticalScrollIndicator',
|
|
113
|
+
'showsHorizontalScrollIndicator',
|
|
114
|
+
'onScroll',
|
|
115
|
+
'contentContainerStyle',
|
|
116
|
+
'onPress',
|
|
117
|
+
'onPressIn',
|
|
118
|
+
'onPressOut',
|
|
119
|
+
'onLongPress',
|
|
120
|
+
'delayLongPress',
|
|
121
|
+
'onChangeText',
|
|
122
|
+
'onSubmitEditing',
|
|
123
|
+
'secureTextEntry',
|
|
124
|
+
'keyboardType',
|
|
125
|
+
'returnKeyType',
|
|
126
|
+
'placeholderTextColor',
|
|
127
|
+
'editable',
|
|
128
|
+
'source',
|
|
129
|
+
'resizeMode',
|
|
130
|
+
'objectFit',
|
|
131
|
+
'objectPosition',
|
|
132
|
+
'svgSource',
|
|
133
|
+
'alt',
|
|
134
|
+
'decorative',
|
|
135
|
+
'longDescription',
|
|
136
|
+
'loading',
|
|
137
|
+
'placeholder',
|
|
138
|
+
'onLoadStart',
|
|
139
|
+
'onLoad',
|
|
140
|
+
'onError',
|
|
141
|
+
'onDisplay',
|
|
142
|
+
'onValueChange',
|
|
143
|
+
'glassEffect',
|
|
144
|
+
'glass',
|
|
145
|
+
'glassVariant',
|
|
146
|
+
'glassTint',
|
|
147
|
+
'glassInteractive',
|
|
148
|
+
'tintColor',
|
|
149
|
+
'agentSemantics',
|
|
150
|
+
'colors',
|
|
151
|
+
'pixelDensity',
|
|
152
|
+
'__exactPresencePhase',
|
|
153
|
+
'__exactPortalLevel',
|
|
154
|
+
'__exactPortalPresentation',
|
|
155
|
+
'__exactDismissableLayer',
|
|
156
|
+
'__exactDismissAction',
|
|
157
|
+
'__exactFocusRestore',
|
|
158
|
+
'__exactAnchorTarget',
|
|
159
|
+
'__exactAnchorPlacement',
|
|
160
|
+
'__exactAnchorStrategy',
|
|
161
|
+
'__exactAnchorOffset',
|
|
162
|
+
'__exactComponentName',
|
|
163
|
+
'__exactComponentSlot',
|
|
164
|
+
'__exactSourceFilePath',
|
|
165
|
+
'__exactVariantProps',
|
|
166
|
+
'__exactInteractionState',
|
|
167
|
+
'__exactRenderMode',
|
|
168
|
+
'__exactSemanticTag',
|
|
169
|
+
]);
|
|
170
|
+
|
|
171
|
+
const URL_ATTRS = new Set(['href', 'src', 'action', 'formaction', 'xlink:href']);
|
|
172
|
+
|
|
173
|
+
const SAFE_PASSTHROUGH_ATTRS = new Set([
|
|
174
|
+
'id',
|
|
175
|
+
'class',
|
|
176
|
+
'className',
|
|
177
|
+
'htmlFor',
|
|
178
|
+
'for',
|
|
179
|
+
'role',
|
|
180
|
+
'title',
|
|
181
|
+
'href',
|
|
182
|
+
'target',
|
|
183
|
+
'rel',
|
|
184
|
+
'tabIndex',
|
|
185
|
+
'type',
|
|
186
|
+
'name',
|
|
187
|
+
'value',
|
|
188
|
+
'checked',
|
|
189
|
+
'disabled',
|
|
190
|
+
'placeholder',
|
|
191
|
+
'autoComplete',
|
|
192
|
+
'autocomplete',
|
|
193
|
+
'alt',
|
|
194
|
+
'src',
|
|
195
|
+
'width',
|
|
196
|
+
'height',
|
|
197
|
+
'loading',
|
|
198
|
+
'inert',
|
|
199
|
+
]);
|
|
200
|
+
|
|
201
|
+
function isSafeUrlAttrValue(value: unknown): boolean {
|
|
202
|
+
if (typeof value !== 'string') {
|
|
203
|
+
return true;
|
|
204
|
+
}
|
|
205
|
+
const trimmed = value.trim();
|
|
206
|
+
if (trimmed.length === 0) {
|
|
207
|
+
return true;
|
|
208
|
+
}
|
|
209
|
+
if (trimmed.startsWith('/') || trimmed.startsWith('#') || trimmed.startsWith('?')) {
|
|
210
|
+
return true;
|
|
211
|
+
}
|
|
212
|
+
try {
|
|
213
|
+
const url = new URL(trimmed, 'https://exact.invalid');
|
|
214
|
+
return ['http:', 'https:', 'mailto:', 'tel:'].includes(url.protocol);
|
|
215
|
+
} catch {
|
|
216
|
+
return false;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
export function isSafeHostDomAttr(name: string, value: unknown): boolean {
|
|
221
|
+
const lowerName = name.toLowerCase();
|
|
222
|
+
if (lowerName === 'style' || lowerName === 'srcdoc') {
|
|
223
|
+
return false;
|
|
224
|
+
}
|
|
225
|
+
if (lowerName.startsWith('on')) {
|
|
226
|
+
return typeof value === 'function';
|
|
227
|
+
}
|
|
228
|
+
if (lowerName.startsWith('aria-') || lowerName.startsWith('data-')) {
|
|
229
|
+
return true;
|
|
230
|
+
}
|
|
231
|
+
if (!SAFE_PASSTHROUGH_ATTRS.has(name) && !SAFE_PASSTHROUGH_ATTRS.has(lowerName)) {
|
|
232
|
+
return false;
|
|
233
|
+
}
|
|
234
|
+
if (URL_ATTRS.has(lowerName) && !isSafeUrlAttrValue(value)) {
|
|
235
|
+
return false;
|
|
236
|
+
}
|
|
237
|
+
return true;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const KNOWN_DOM_TAGS = new Set<DomTagName>([
|
|
241
|
+
'div',
|
|
242
|
+
'span',
|
|
243
|
+
'a',
|
|
244
|
+
'button',
|
|
245
|
+
'input',
|
|
246
|
+
'textarea',
|
|
247
|
+
'img',
|
|
248
|
+
'main',
|
|
249
|
+
'nav',
|
|
250
|
+
'header',
|
|
251
|
+
'footer',
|
|
252
|
+
'aside',
|
|
253
|
+
'article',
|
|
254
|
+
'section',
|
|
255
|
+
'blockquote',
|
|
256
|
+
'pre',
|
|
257
|
+
'code',
|
|
258
|
+
'hr',
|
|
259
|
+
'ul',
|
|
260
|
+
'li',
|
|
261
|
+
'label',
|
|
262
|
+
'h1',
|
|
263
|
+
'h2',
|
|
264
|
+
'h3',
|
|
265
|
+
'h4',
|
|
266
|
+
'h5',
|
|
267
|
+
'h6',
|
|
268
|
+
]);
|
|
269
|
+
|
|
270
|
+
function stringProp(props: Record<string, unknown>, key: string): string | undefined {
|
|
271
|
+
const value = props[key];
|
|
272
|
+
return typeof value === 'string' && value.length > 0 ? value : undefined;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function boolProp(props: Record<string, unknown>, key: string): boolean | undefined {
|
|
276
|
+
const value = props[key];
|
|
277
|
+
return typeof value === 'boolean' ? value : undefined;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function finiteNumberProp(props: Record<string, unknown>, key: string): number | undefined {
|
|
281
|
+
const value = props[key];
|
|
282
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function semanticProps(
|
|
286
|
+
props: Record<string, unknown>,
|
|
287
|
+
semantic: HostSemanticAttrs | undefined,
|
|
288
|
+
): Record<string, unknown> {
|
|
289
|
+
if (!semantic) {
|
|
290
|
+
return props;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const next = { ...props };
|
|
294
|
+
if (semantic.role !== undefined) next.accessibilityRole ??= semantic.role;
|
|
295
|
+
if (semantic.label !== undefined) next.accessibilityLabel ??= semantic.label;
|
|
296
|
+
if (semantic.hint !== undefined) next.accessibilityHint ??= semantic.hint;
|
|
297
|
+
if (semantic.headingLevel !== undefined) next.accessibilityHeadingLevel ??= semantic.headingLevel;
|
|
298
|
+
if (semantic.live !== undefined) next.accessibilityLive ??= semantic.live;
|
|
299
|
+
if (semantic.labelledBy !== undefined) next.accessibilityLabelledBy ??= semantic.labelledBy;
|
|
300
|
+
if (semantic.describedBy !== undefined) next.accessibilityDescribedBy ??= semantic.describedBy;
|
|
301
|
+
if (semantic.for !== undefined) next.accessibilityFor ??= semantic.for;
|
|
302
|
+
if (semantic.nativeID !== undefined) next.nativeID ??= semantic.nativeID;
|
|
303
|
+
if (semantic.testID !== undefined) next.testID ??= semantic.testID;
|
|
304
|
+
if (semantic.testId !== undefined) next.testId ??= semantic.testId;
|
|
305
|
+
if (semantic.focusable !== undefined) next.focusable ??= semantic.focusable;
|
|
306
|
+
if (semantic.inert !== undefined) next.inert ??= semantic.inert;
|
|
307
|
+
return next;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function coerceHeadingTag(level: number | undefined): DomTagName | undefined {
|
|
311
|
+
if (level === undefined) {
|
|
312
|
+
return undefined;
|
|
313
|
+
}
|
|
314
|
+
const clamped = Math.max(1, Math.min(6, Math.trunc(level))) as 1 | 2 | 3 | 4 | 5 | 6;
|
|
315
|
+
return `h${clamped}`;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function coerceDefaultTag(tag: unknown, fallback: DomTagName): DomTagName {
|
|
319
|
+
return typeof tag === 'string' && KNOWN_DOM_TAGS.has(tag as DomTagName)
|
|
320
|
+
? (tag as DomTagName)
|
|
321
|
+
: fallback;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function tagForSemanticTag(tag: string | undefined): DomTagName | undefined {
|
|
325
|
+
switch (tag) {
|
|
326
|
+
case 'main':
|
|
327
|
+
case 'nav':
|
|
328
|
+
case 'header':
|
|
329
|
+
case 'footer':
|
|
330
|
+
case 'aside':
|
|
331
|
+
case 'article':
|
|
332
|
+
case 'section':
|
|
333
|
+
case 'blockquote':
|
|
334
|
+
case 'pre':
|
|
335
|
+
case 'code':
|
|
336
|
+
case 'hr':
|
|
337
|
+
return tag;
|
|
338
|
+
case 'a':
|
|
339
|
+
case 'link':
|
|
340
|
+
return 'a';
|
|
341
|
+
case 'list':
|
|
342
|
+
return 'ul';
|
|
343
|
+
case 'listitem':
|
|
344
|
+
return 'li';
|
|
345
|
+
case 'label':
|
|
346
|
+
return 'label';
|
|
347
|
+
default:
|
|
348
|
+
return undefined;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function tagForRole(role: string | undefined): DomTagName | undefined {
|
|
353
|
+
switch (role) {
|
|
354
|
+
case 'main':
|
|
355
|
+
return 'main';
|
|
356
|
+
case 'navigation':
|
|
357
|
+
return 'nav';
|
|
358
|
+
case 'link':
|
|
359
|
+
return 'a';
|
|
360
|
+
case 'banner':
|
|
361
|
+
return 'header';
|
|
362
|
+
case 'contentinfo':
|
|
363
|
+
return 'footer';
|
|
364
|
+
case 'complementary':
|
|
365
|
+
return 'aside';
|
|
366
|
+
case 'article':
|
|
367
|
+
return 'article';
|
|
368
|
+
case 'list':
|
|
369
|
+
return 'ul';
|
|
370
|
+
case 'listitem':
|
|
371
|
+
return 'li';
|
|
372
|
+
default:
|
|
373
|
+
return undefined;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function implicitRoleForTag(tag: DomTagName): string | undefined {
|
|
378
|
+
switch (tag) {
|
|
379
|
+
case 'button':
|
|
380
|
+
return 'button';
|
|
381
|
+
case 'a':
|
|
382
|
+
return 'link';
|
|
383
|
+
case 'main':
|
|
384
|
+
return 'main';
|
|
385
|
+
case 'nav':
|
|
386
|
+
return 'navigation';
|
|
387
|
+
case 'header':
|
|
388
|
+
return 'banner';
|
|
389
|
+
case 'footer':
|
|
390
|
+
return 'contentinfo';
|
|
391
|
+
case 'aside':
|
|
392
|
+
return 'complementary';
|
|
393
|
+
case 'article':
|
|
394
|
+
return 'article';
|
|
395
|
+
case 'ul':
|
|
396
|
+
return 'list';
|
|
397
|
+
case 'li':
|
|
398
|
+
return 'listitem';
|
|
399
|
+
case 'input':
|
|
400
|
+
case 'textarea':
|
|
401
|
+
return 'textbox';
|
|
402
|
+
case 'h1':
|
|
403
|
+
case 'h2':
|
|
404
|
+
case 'h3':
|
|
405
|
+
case 'h4':
|
|
406
|
+
case 'h5':
|
|
407
|
+
case 'h6':
|
|
408
|
+
return 'heading';
|
|
409
|
+
default:
|
|
410
|
+
return undefined;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
function resolveDomTag(
|
|
415
|
+
node: HostAttrNode,
|
|
416
|
+
props: Record<string, unknown>,
|
|
417
|
+
semantic: HostSemanticAttrs | undefined,
|
|
418
|
+
options: LowerHostAttrsOptions,
|
|
419
|
+
): DomTagName {
|
|
420
|
+
const defaultTag = options.defaultTag ?? coerceDefaultTag(node.tag, 'div');
|
|
421
|
+
if (options.inferTag === false) {
|
|
422
|
+
return defaultTag;
|
|
423
|
+
}
|
|
424
|
+
const role = stringProp(props, 'accessibilityRole') ?? stringProp(props, 'role') ?? options.defaultRole;
|
|
425
|
+
const headingLevel = finiteNumberProp(props, 'accessibilityHeadingLevel');
|
|
426
|
+
|
|
427
|
+
return (
|
|
428
|
+
tagForSemanticTag(semantic?.tag) ??
|
|
429
|
+
(role === 'heading' ? coerceHeadingTag(headingLevel) : undefined) ??
|
|
430
|
+
tagForRole(role) ??
|
|
431
|
+
defaultTag
|
|
432
|
+
);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
export function lowerHostAttrs(
|
|
436
|
+
node: HostAttrNode,
|
|
437
|
+
semantic?: HostSemanticAttrs,
|
|
438
|
+
options: LowerHostAttrsOptions = {},
|
|
439
|
+
): LoweredHostAttrs {
|
|
440
|
+
const props = semanticProps(node.props ?? {}, semantic);
|
|
441
|
+
const tag = resolveDomTag(node, props, semantic, options);
|
|
442
|
+
const implicitRole = implicitRoleForTag(tag);
|
|
443
|
+
const role =
|
|
444
|
+
stringProp(props, 'accessibilityRole') ??
|
|
445
|
+
stringProp(props, 'role') ??
|
|
446
|
+
options.defaultRole;
|
|
447
|
+
const attrs: Record<string, unknown> = {};
|
|
448
|
+
const aria: Record<string, unknown> = {};
|
|
449
|
+
const labelAssociation: LoweredHostAttrs['labelAssociation'] = {};
|
|
450
|
+
|
|
451
|
+
const testId = stringProp(props, 'testID') ?? stringProp(props, 'testId');
|
|
452
|
+
if (testId) attrs['data-testid'] = testId;
|
|
453
|
+
|
|
454
|
+
const id = stringProp(props, 'nativeID') ?? stringProp(props, 'id');
|
|
455
|
+
if (id) attrs.id = id;
|
|
456
|
+
|
|
457
|
+
if (role && role !== implicitRole) {
|
|
458
|
+
attrs.role = role;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
const accessibilityLabel =
|
|
462
|
+
stringProp(props, 'accessibilityLabel') ?? stringProp(props, 'aria-label');
|
|
463
|
+
if (accessibilityLabel) {
|
|
464
|
+
aria['aria-label'] = accessibilityLabel;
|
|
465
|
+
attrs['aria-label'] = accessibilityLabel;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
const hint = stringProp(props, 'accessibilityHint');
|
|
469
|
+
if (hint) {
|
|
470
|
+
aria['aria-description'] = hint;
|
|
471
|
+
attrs['aria-description'] = hint;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
const modal = boolProp(props, 'accessibilityModal');
|
|
475
|
+
if (modal !== undefined) {
|
|
476
|
+
aria['aria-modal'] = modal;
|
|
477
|
+
attrs['aria-modal'] = modal;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
const expanded = boolProp(props, 'accessibilityExpanded');
|
|
481
|
+
if (expanded !== undefined) {
|
|
482
|
+
aria['aria-expanded'] = expanded;
|
|
483
|
+
attrs['aria-expanded'] = expanded;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
const selected = boolProp(props, 'accessibilitySelected');
|
|
487
|
+
if (selected !== undefined) {
|
|
488
|
+
aria['aria-selected'] = selected;
|
|
489
|
+
attrs['aria-selected'] = selected;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
if (typeof props.accessibilityChecked === 'boolean' || props.accessibilityChecked === 'mixed') {
|
|
493
|
+
aria['aria-checked'] = props.accessibilityChecked;
|
|
494
|
+
attrs['aria-checked'] = props.accessibilityChecked;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
const accessibilityDisabled = boolProp(props, 'accessibilityDisabled');
|
|
498
|
+
if (accessibilityDisabled !== undefined && attrs['aria-disabled'] === undefined) {
|
|
499
|
+
aria['aria-disabled'] = accessibilityDisabled;
|
|
500
|
+
attrs['aria-disabled'] = accessibilityDisabled;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
const live = stringProp(props, 'accessibilityLive');
|
|
504
|
+
if (live) {
|
|
505
|
+
aria['aria-live'] = live;
|
|
506
|
+
attrs['aria-live'] = live;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
for (const [prop, attr] of [
|
|
510
|
+
['accessibilityValueNow', 'aria-valuenow'],
|
|
511
|
+
['accessibilityValueMin', 'aria-valuemin'],
|
|
512
|
+
['accessibilityValueMax', 'aria-valuemax'],
|
|
513
|
+
] as const) {
|
|
514
|
+
const value = finiteNumberProp(props, prop);
|
|
515
|
+
if (value !== undefined) {
|
|
516
|
+
aria[attr] = value;
|
|
517
|
+
attrs[attr] = value;
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
const valueText = stringProp(props, 'accessibilityValueText');
|
|
522
|
+
if (valueText) {
|
|
523
|
+
aria['aria-valuetext'] = valueText;
|
|
524
|
+
attrs['aria-valuetext'] = valueText;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
const labelledBy = stringProp(props, 'accessibilityLabelledBy');
|
|
528
|
+
if (labelledBy) {
|
|
529
|
+
labelAssociation.labelledBy = labelledBy;
|
|
530
|
+
aria['aria-labelledby'] = labelledBy;
|
|
531
|
+
attrs['aria-labelledby'] = labelledBy;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
const describedBy = stringProp(props, 'accessibilityDescribedBy');
|
|
535
|
+
if (describedBy) {
|
|
536
|
+
labelAssociation.describedBy = describedBy;
|
|
537
|
+
aria['aria-describedby'] = describedBy;
|
|
538
|
+
attrs['aria-describedby'] = describedBy;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
const htmlFor = stringProp(props, 'accessibilityFor');
|
|
542
|
+
if (htmlFor) {
|
|
543
|
+
labelAssociation.for = htmlFor;
|
|
544
|
+
attrs.htmlFor = htmlFor;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
const busy = boolProp(props, 'accessibilityBusy');
|
|
548
|
+
if (busy !== undefined) {
|
|
549
|
+
aria['aria-busy'] = busy;
|
|
550
|
+
attrs['aria-busy'] = busy;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
const headingLevel = finiteNumberProp(props, 'accessibilityHeadingLevel');
|
|
554
|
+
if (headingLevel !== undefined && implicitRole !== 'heading') {
|
|
555
|
+
aria['aria-level'] = headingLevel;
|
|
556
|
+
attrs['aria-level'] = headingLevel;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
if (props.focusable === true && attrs.tabIndex === undefined) {
|
|
560
|
+
attrs.tabIndex = 0;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
if (props.inert === true) {
|
|
564
|
+
attrs.inert = true;
|
|
565
|
+
attrs['aria-hidden'] ??= true;
|
|
566
|
+
aria['aria-hidden'] ??= true;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
if (props.scrollLocked === true) {
|
|
570
|
+
attrs['data-exact-scroll-locked'] = 'true';
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
for (const key of Object.keys(props)) {
|
|
574
|
+
if (!NON_DOM_PROPS.has(key) && attrs[key] === undefined && isSafeHostDomAttr(key, props[key])) {
|
|
575
|
+
attrs[key] = props[key];
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
return {
|
|
580
|
+
tag,
|
|
581
|
+
attrs,
|
|
582
|
+
aria,
|
|
583
|
+
tabIndex: typeof attrs.tabIndex === 'number' ? attrs.tabIndex : undefined,
|
|
584
|
+
inert: attrs.inert === true,
|
|
585
|
+
role: typeof attrs.role === 'string' ? attrs.role : undefined,
|
|
586
|
+
labelAssociation,
|
|
587
|
+
};
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
/** Strip props that are not valid DOM attributes to avoid React warnings. */
|
|
591
|
+
export function filterDOMProps(props: Record<string, unknown>): Record<string, unknown> {
|
|
592
|
+
return lowerHostAttrs({ tag: 'div', props }, undefined, {
|
|
593
|
+
defaultTag: 'div',
|
|
594
|
+
inferTag: false,
|
|
595
|
+
}).attrs;
|
|
596
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import type { CanonicalStyle } from './types.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The runtime resolver and the server-side compiler intentionally share this
|
|
5
|
+
* file so the cross-boundary stylesheet artifact is explicit and easy to audit.
|
|
6
|
+
* Nothing here imports React, Lightning CSS, or Node-only modules.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export type SupportedPseudo =
|
|
10
|
+
| 'disabled'
|
|
11
|
+
| 'hover'
|
|
12
|
+
| 'focus'
|
|
13
|
+
| 'active'
|
|
14
|
+
| 'focus-visible'
|
|
15
|
+
| 'first-child'
|
|
16
|
+
| 'last-child';
|
|
17
|
+
|
|
18
|
+
export interface MediaCondition {
|
|
19
|
+
readonly feature:
|
|
20
|
+
| 'min-width'
|
|
21
|
+
| 'max-width'
|
|
22
|
+
| 'min-height'
|
|
23
|
+
| 'max-height'
|
|
24
|
+
| 'gt-width'
|
|
25
|
+
| 'gt-height'
|
|
26
|
+
| 'lt-width'
|
|
27
|
+
| 'lt-height'
|
|
28
|
+
| 'orientation'
|
|
29
|
+
| 'prefers-color-scheme'
|
|
30
|
+
| 'prefers-reduced-motion';
|
|
31
|
+
readonly value: number | string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export type SerializedFragmentCondition =
|
|
35
|
+
| { readonly type: 'pseudo'; readonly pseudo: SupportedPseudo }
|
|
36
|
+
| { readonly type: 'media'; readonly media: readonly MediaCondition[] }
|
|
37
|
+
| {
|
|
38
|
+
readonly type: 'group';
|
|
39
|
+
readonly groupName: string | null;
|
|
40
|
+
readonly pseudo: 'hover' | 'focus' | 'active';
|
|
41
|
+
}
|
|
42
|
+
| {
|
|
43
|
+
readonly type: 'peer';
|
|
44
|
+
readonly peerName: string | null;
|
|
45
|
+
readonly pseudo: 'hover' | 'focus' | 'active';
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export interface SerializedCompiledFragment {
|
|
49
|
+
readonly order: number;
|
|
50
|
+
readonly condition: SerializedFragmentCondition | null;
|
|
51
|
+
readonly mediaGate: readonly MediaCondition[] | null;
|
|
52
|
+
readonly style: CanonicalStyle;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface SerializedExactStyleSheet {
|
|
56
|
+
readonly classes: Record<string, SerializedCompiledFragment[]>;
|
|
57
|
+
readonly warnings: string[];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface InteractionState {
|
|
61
|
+
readonly hovered: boolean;
|
|
62
|
+
readonly focused: boolean;
|
|
63
|
+
readonly active: boolean;
|
|
64
|
+
readonly focusVisible: boolean;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export interface SiblingPosition {
|
|
68
|
+
readonly isFirst: boolean;
|
|
69
|
+
readonly isLast: boolean;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface MediaState {
|
|
73
|
+
readonly width: number;
|
|
74
|
+
readonly height: number;
|
|
75
|
+
readonly orientation: 'portrait' | 'landscape';
|
|
76
|
+
readonly colorScheme: 'light' | 'dark';
|
|
77
|
+
readonly reducedMotion: boolean;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export interface ClassNameResolutionContext {
|
|
81
|
+
readonly props?: Record<string, unknown>;
|
|
82
|
+
readonly interaction?: InteractionState;
|
|
83
|
+
readonly position?: SiblingPosition;
|
|
84
|
+
readonly groupStates?: ReadonlyMap<string | null, InteractionState>;
|
|
85
|
+
readonly peerStates?: { getState(name: string | null): InteractionState | undefined };
|
|
86
|
+
readonly cssVars?: Readonly<Record<string, string>>;
|
|
87
|
+
}
|