@remix-run/ui 0.1.0 → 0.1.2
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 +24 -3
- package/dist/animation/animate-layout-mixin.js +66 -66
- package/dist/animation/animate-layout-mixin.js.map +1 -1
- package/dist/components/accordion/accordion.d.ts +3 -3
- package/dist/components/accordion/accordion.js +0 -2
- package/dist/components/accordion/accordion.js.map +1 -1
- package/dist/components/accordion/demos/card.demo.d.ts +6 -0
- package/dist/components/accordion/demos/card.demo.js +58 -0
- package/dist/components/accordion/demos/card.demo.js.map +1 -0
- package/dist/components/accordion/demos/multiple.demo.d.ts +6 -0
- package/dist/components/accordion/demos/multiple.demo.js +19 -0
- package/dist/components/accordion/demos/multiple.demo.js.map +1 -0
- package/dist/components/accordion/demos/overview.demo.d.ts +6 -0
- package/dist/components/accordion/demos/overview.demo.js +19 -0
- package/dist/components/accordion/demos/overview.demo.js.map +1 -0
- package/dist/components/breadcrumbs/breadcrumbs.js +0 -2
- package/dist/components/breadcrumbs/breadcrumbs.js.map +1 -1
- package/dist/components/breadcrumbs/demos/basic.demo.d.ts +5 -0
- package/dist/components/breadcrumbs/demos/basic.demo.js +14 -0
- package/dist/components/breadcrumbs/demos/basic.demo.js.map +1 -0
- package/dist/components/breadcrumbs/demos/separator.demo.d.ts +5 -0
- package/dist/components/breadcrumbs/demos/separator.demo.js +14 -0
- package/dist/components/breadcrumbs/demos/separator.demo.js.map +1 -0
- package/dist/components/button/button.d.ts +82 -0
- package/dist/components/button/button.js +60 -2
- package/dist/components/button/button.js.map +1 -1
- package/dist/components/button/demos/aliases.demo.d.ts +6 -0
- package/dist/components/button/demos/aliases.demo.js +18 -0
- package/dist/components/button/demos/aliases.demo.js.map +1 -0
- package/dist/components/button/demos/basic.demo.d.ts +6 -0
- package/dist/components/button/demos/basic.demo.js +19 -0
- package/dist/components/button/demos/basic.demo.js.map +1 -0
- package/dist/components/button/demos/states.demo.d.ts +6 -0
- package/dist/components/button/demos/states.demo.js +29 -0
- package/dist/components/button/demos/states.demo.js.map +1 -0
- package/dist/components/combobox/combobox.d.ts +3 -3
- package/dist/components/combobox/combobox.js +0 -2
- package/dist/components/combobox/combobox.js.map +1 -1
- package/dist/components/combobox/demos/overview.demo.d.ts +6 -0
- package/dist/components/combobox/demos/overview.demo.js +153 -0
- package/dist/components/combobox/demos/overview.demo.js.map +1 -0
- package/dist/components/listbox/demos/overview.demo.d.ts +6 -0
- package/dist/components/listbox/demos/overview.demo.js +64 -0
- package/dist/components/listbox/demos/overview.demo.js.map +1 -0
- package/dist/components/listbox/listbox.d.ts +1 -6
- package/dist/components/listbox/listbox.js +21 -33
- package/dist/components/listbox/listbox.js.map +1 -1
- package/dist/components/menu/demos/bubbling.demo.d.ts +5 -0
- package/dist/components/menu/demos/bubbling.demo.js +15 -0
- package/dist/components/menu/demos/bubbling.demo.js.map +1 -0
- package/dist/components/menu/demos/overview.demo.d.ts +6 -0
- package/dist/components/menu/demos/overview.demo.js +32 -0
- package/dist/components/menu/demos/overview.demo.js.map +1 -0
- package/dist/components/menu/menu.d.ts +5 -14
- package/dist/components/menu/menu.js +23 -38
- package/dist/components/menu/menu.js.map +1 -1
- package/dist/components/popover/demos/overview.demo.d.ts +6 -0
- package/dist/components/popover/demos/overview.demo.js +64 -0
- package/dist/components/popover/demos/overview.demo.js.map +1 -0
- package/dist/components/select/demos/deconstructed.demo.d.ts +6 -0
- package/dist/components/select/demos/deconstructed.demo.js +53 -0
- package/dist/components/select/demos/deconstructed.demo.js.map +1 -0
- package/dist/components/select/demos/overview.demo.d.ts +5 -0
- package/dist/components/select/demos/overview.demo.js +27 -0
- package/dist/components/select/demos/overview.demo.js.map +1 -0
- package/dist/components/select/select.d.ts +3 -3
- package/dist/components/select/select.js +17 -26
- package/dist/components/select/select.js.map +1 -1
- package/dist/components/tabs/tabs.d.ts +3 -3
- package/dist/components/tabs/tabs.js +0 -2
- package/dist/components/tabs/tabs.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js.map +1 -1
- package/dist/runtime/component.d.ts +8 -5
- package/dist/runtime/component.js +84 -80
- package/dist/runtime/component.js.map +1 -1
- package/dist/runtime/core/attributes.d.ts +12 -0
- package/dist/runtime/core/attributes.js +85 -0
- package/dist/runtime/core/attributes.js.map +1 -0
- package/dist/runtime/core/children.d.ts +7 -0
- package/dist/runtime/core/children.js +30 -0
- package/dist/runtime/core/children.js.map +1 -0
- package/dist/runtime/core/props.d.ts +9 -0
- package/dist/runtime/core/props.js +169 -0
- package/dist/runtime/core/props.js.map +1 -0
- package/dist/runtime/core/vnode.d.ts +3 -0
- package/dist/runtime/core/vnode.js +39 -0
- package/dist/runtime/core/vnode.js.map +1 -0
- package/dist/runtime/create-element.js +2 -1
- package/dist/runtime/create-element.js.map +1 -1
- package/dist/runtime/diff-dom.js +47 -14
- package/dist/runtime/diff-dom.js.map +1 -1
- package/dist/runtime/diff-props.d.ts +1 -2
- package/dist/runtime/diff-props.js +2 -181
- package/dist/runtime/diff-props.js.map +1 -1
- package/dist/runtime/dom.d.ts +4 -0
- package/dist/runtime/frame.d.ts +23 -2
- package/dist/runtime/frame.js +51 -52
- package/dist/runtime/frame.js.map +1 -1
- package/dist/runtime/jsx.js +2 -27
- package/dist/runtime/jsx.js.map +1 -1
- package/dist/runtime/mixins/mixin.js +9 -4
- package/dist/runtime/mixins/mixin.js.map +1 -1
- package/dist/runtime/mixins/on-mixin.d.ts +15 -4
- package/dist/runtime/mixins/on-mixin.js +9 -2
- package/dist/runtime/mixins/on-mixin.js.map +1 -1
- package/dist/runtime/navigation.d.ts +6 -0
- package/dist/runtime/navigation.js +16 -3
- package/dist/runtime/navigation.js.map +1 -1
- package/dist/runtime/reconcile.js +444 -181
- package/dist/runtime/reconcile.js.map +1 -1
- package/dist/runtime/run.d.ts +17 -2
- package/dist/runtime/run.js.map +1 -1
- package/dist/runtime/scheduler.js +10 -0
- package/dist/runtime/scheduler.js.map +1 -1
- package/dist/runtime/stream-protocol.d.ts +8 -0
- package/dist/runtime/stream-protocol.js +20 -0
- package/dist/runtime/stream-protocol.js.map +1 -0
- package/dist/runtime/to-vnode.js +17 -18
- package/dist/runtime/to-vnode.js.map +1 -1
- package/dist/runtime/vnode.d.ts +5 -4
- package/dist/runtime/vnode.js +2 -3
- package/dist/runtime/vnode.js.map +1 -1
- package/dist/server/stream.js +80 -106
- package/dist/server/stream.js.map +1 -1
- package/dist/style/layers.d.ts +2 -0
- package/dist/style/layers.js +3 -0
- package/dist/style/layers.js.map +1 -0
- package/dist/style/stylesheet.d.ts +3 -1
- package/dist/style/stylesheet.js +69 -8
- package/dist/style/stylesheet.js.map +1 -1
- package/dist/theme/glyph-contract.d.ts +1 -1
- package/dist/theme/runtime.js +4 -1
- package/dist/theme/runtime.js.map +1 -1
- package/package.json +6 -7
- package/src/animation/README.md +314 -0
- package/src/animation/animate-layout-mixin.ts +79 -71
- package/src/components/accordion/README.md +166 -0
- package/src/components/accordion/accordion.tsx +8 -10
- package/src/components/accordion/demos/card.demo.tsx +113 -0
- package/src/components/accordion/demos/multiple.demo.tsx +56 -0
- package/src/components/accordion/demos/overview.demo.tsx +56 -0
- package/src/components/anchor/README.md +153 -0
- package/src/components/breadcrumbs/README.md +55 -0
- package/src/components/breadcrumbs/breadcrumbs.tsx +0 -2
- package/src/components/breadcrumbs/demos/basic.demo.tsx +17 -0
- package/src/components/breadcrumbs/demos/separator.demo.tsx +18 -0
- package/src/components/button/README.md +4 -4
- package/src/components/button/button.tsx +88 -2
- package/src/components/button/demos/aliases.demo.tsx +27 -0
- package/src/components/button/demos/basic.demo.tsx +30 -0
- package/src/components/button/demos/states.demo.tsx +52 -0
- package/src/components/combobox/combobox.tsx +8 -10
- package/src/components/combobox/demos/overview.demo.tsx +199 -0
- package/src/components/glyph/README.md +72 -0
- package/src/components/listbox/README.md +115 -0
- package/src/components/listbox/demos/overview.demo.tsx +101 -0
- package/src/components/listbox/listbox.ts +23 -32
- package/src/components/menu/README.md +96 -0
- package/src/components/menu/demos/bubbling.demo.tsx +34 -0
- package/src/components/menu/demos/overview.demo.tsx +76 -0
- package/src/components/menu/menu.tsx +35 -41
- package/src/components/popover/README.md +3 -3
- package/src/components/popover/demos/overview.demo.tsx +143 -0
- package/src/components/select/README.md +107 -0
- package/src/components/select/demos/deconstructed.demo.tsx +95 -0
- package/src/components/select/demos/overview.demo.tsx +51 -0
- package/src/components/select/select.tsx +28 -28
- package/src/components/tabs/tabs.tsx +10 -5
- package/src/index.ts +1 -1
- package/src/runtime/component.ts +94 -82
- package/src/runtime/core/attributes.ts +97 -0
- package/src/runtime/core/children.ts +42 -0
- package/src/runtime/core/props.ts +185 -0
- package/src/runtime/core/vnode.ts +48 -0
- package/src/runtime/create-element.ts +2 -1
- package/src/runtime/diff-dom.ts +59 -12
- package/src/runtime/diff-props.ts +2 -187
- package/src/runtime/dom.ts +4 -0
- package/src/runtime/frame.ts +84 -61
- package/src/runtime/jsx.ts +2 -30
- package/src/runtime/mixins/mixin.ts +9 -4
- package/src/runtime/mixins/on-mixin.ts +20 -12
- package/src/runtime/navigation.ts +24 -3
- package/src/runtime/reconcile.ts +552 -186
- package/src/runtime/run.ts +17 -1
- package/src/runtime/scheduler.ts +7 -0
- package/src/runtime/stream-protocol.ts +27 -0
- package/src/runtime/to-vnode.ts +15 -17
- package/src/runtime/vnode.ts +6 -9
- package/src/server/README.md +88 -0
- package/src/server/stream.ts +105 -117
- package/src/style/layers.ts +2 -0
- package/src/style/stylesheet.ts +75 -10
- package/src/test/README.md +107 -0
- package/src/test/utils.ts +3 -1
- package/src/theme/README.md +103 -0
- package/src/theme/runtime.ts +4 -1
- package/src/utils/scroll-lock/README.md +33 -0
- package/src/test/setup.ts +0 -7
package/src/runtime/component.ts
CHANGED
|
@@ -227,113 +227,125 @@ export type ComponentHandle = ReturnType<typeof createComponent>
|
|
|
227
227
|
* @returns Component runtime helpers used by the reconciler.
|
|
228
228
|
*/
|
|
229
229
|
export function createComponent<C = NoContext>(config: ComponentConfig) {
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
let connectedCtrl: AbortController | null = null
|
|
233
|
-
let contextValue: C | undefined = undefined
|
|
234
|
-
|
|
235
|
-
function getConnectedSignal() {
|
|
236
|
-
if (!connectedCtrl) connectedCtrl = new AbortController()
|
|
237
|
-
return connectedCtrl.signal
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
let getContent: null | RenderFn = null
|
|
241
|
-
let scheduleUpdate: () => void = () => {
|
|
242
|
-
throw new Error('scheduleUpdate not implemented')
|
|
243
|
-
}
|
|
244
|
-
let props = {} as ElementProps
|
|
230
|
+
return new ComponentRuntime<C>(config)
|
|
231
|
+
}
|
|
245
232
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
contextValue = value
|
|
249
|
-
},
|
|
250
|
-
get: (type: Component) => config.getContext(type),
|
|
251
|
-
}
|
|
233
|
+
class ComponentRuntime<C = NoContext> {
|
|
234
|
+
frame: FrameHandle
|
|
252
235
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
taskQueue.push(task)
|
|
263
|
-
},
|
|
264
|
-
frame: config.frame,
|
|
265
|
-
frames: {
|
|
266
|
-
get top() {
|
|
267
|
-
return config.getTopFrame?.() ?? config.frame
|
|
268
|
-
},
|
|
269
|
-
get(name: string) {
|
|
270
|
-
return config.getFrameByName(name)
|
|
271
|
-
},
|
|
272
|
-
},
|
|
273
|
-
context: context,
|
|
274
|
-
get signal() {
|
|
275
|
-
return config.signal ?? getConnectedSignal()
|
|
276
|
-
},
|
|
236
|
+
#config: ComponentConfig
|
|
237
|
+
#connectedController: AbortController | undefined
|
|
238
|
+
#contextValue: C | undefined
|
|
239
|
+
#handle: Handle<ElementProps, C>
|
|
240
|
+
#props = {} as ElementProps
|
|
241
|
+
#renderController: AbortController | undefined
|
|
242
|
+
#renderFn: RenderFn | undefined
|
|
243
|
+
#scheduleUpdate: () => void = () => {
|
|
244
|
+
throw new Error('scheduleUpdate not implemented')
|
|
277
245
|
}
|
|
246
|
+
#tasks: Task[] = []
|
|
278
247
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
renderCtrl = new AbortController()
|
|
284
|
-
}
|
|
285
|
-
let signal = renderCtrl?.signal
|
|
286
|
-
return taskQueue.splice(0, taskQueue.length).map((task) => () => task(signal!))
|
|
248
|
+
constructor(config: ComponentConfig) {
|
|
249
|
+
this.#config = config
|
|
250
|
+
this.frame = config.frame
|
|
251
|
+
this.#handle = this.#createHandle()
|
|
287
252
|
}
|
|
288
253
|
|
|
289
|
-
|
|
290
|
-
if (
|
|
254
|
+
render = (nextProps: ElementProps): [RemixNode, Array<() => void>] => {
|
|
255
|
+
if (this.#connectedController?.signal.aborted) {
|
|
291
256
|
console.warn('render called after component was removed, potential application memory leak')
|
|
292
257
|
return [null, []]
|
|
293
258
|
}
|
|
294
259
|
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
renderCtrl.abort()
|
|
298
|
-
renderCtrl = null
|
|
299
|
-
}
|
|
260
|
+
this.#abortRenderSignal()
|
|
261
|
+
syncProps(this.#props, nextProps)
|
|
300
262
|
|
|
301
|
-
|
|
263
|
+
let renderFn = this.#renderFn
|
|
264
|
+
|
|
265
|
+
if (renderFn === undefined) {
|
|
266
|
+
let result = this.#config.type(this.#handle)
|
|
302
267
|
|
|
303
|
-
let renderContent = getContent
|
|
304
|
-
if (!renderContent) {
|
|
305
|
-
let result = config.type(handle)
|
|
306
268
|
if (typeof result !== 'function') {
|
|
307
|
-
let name = config.type.name || 'Anonymous'
|
|
269
|
+
let name = this.#config.type.name || 'Anonymous'
|
|
308
270
|
throw new Error(`${name} must return a render function, received ${typeof result}`)
|
|
309
|
-
} else {
|
|
310
|
-
getContent = result
|
|
311
|
-
renderContent = result
|
|
312
271
|
}
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
272
|
+
|
|
273
|
+
renderFn = result as RenderFn
|
|
274
|
+
this.#renderFn = renderFn
|
|
316
275
|
}
|
|
317
276
|
|
|
318
|
-
|
|
319
|
-
return [node, dequeueTasks()]
|
|
277
|
+
return [renderFn(this.#props), this.#dequeueTasks()]
|
|
320
278
|
}
|
|
321
279
|
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
return dequeueTasks()
|
|
280
|
+
remove = (): Array<() => void> => {
|
|
281
|
+
this.#connectedController?.abort()
|
|
282
|
+
this.#abortRenderSignal()
|
|
283
|
+
return this.#dequeueTasks()
|
|
326
284
|
}
|
|
327
285
|
|
|
328
|
-
|
|
329
|
-
scheduleUpdate = nextScheduleUpdate
|
|
286
|
+
setScheduleUpdate = (nextScheduleUpdate: () => void): void => {
|
|
287
|
+
this.#scheduleUpdate = nextScheduleUpdate
|
|
330
288
|
}
|
|
331
289
|
|
|
332
|
-
|
|
333
|
-
|
|
290
|
+
getContextValue = (): C | undefined => this.#contextValue
|
|
291
|
+
|
|
292
|
+
#createHandle(): Handle<ElementProps, C> {
|
|
293
|
+
let component = this
|
|
294
|
+
let context: Context<C> = {
|
|
295
|
+
set: (value: C) => {
|
|
296
|
+
this.#contextValue = value
|
|
297
|
+
},
|
|
298
|
+
get: (type: ElementType | symbol) => this.#config.getContext(type as Component),
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
return {
|
|
302
|
+
id: this.#config.id,
|
|
303
|
+
props: this.#props,
|
|
304
|
+
update: () =>
|
|
305
|
+
new Promise((resolve) => {
|
|
306
|
+
this.#tasks.push((signal) => resolve(signal))
|
|
307
|
+
this.#scheduleUpdate()
|
|
308
|
+
}),
|
|
309
|
+
queueTask: (task: Task) => {
|
|
310
|
+
this.#tasks.push(task)
|
|
311
|
+
},
|
|
312
|
+
frame: this.#config.frame,
|
|
313
|
+
frames: {
|
|
314
|
+
get top() {
|
|
315
|
+
return component.#config.getTopFrame?.() ?? component.#config.frame
|
|
316
|
+
},
|
|
317
|
+
get(name: string) {
|
|
318
|
+
return component.#config.getFrameByName(name)
|
|
319
|
+
},
|
|
320
|
+
},
|
|
321
|
+
context,
|
|
322
|
+
get signal() {
|
|
323
|
+
return component.#config.signal ?? component.#connectedSignal()
|
|
324
|
+
},
|
|
325
|
+
}
|
|
334
326
|
}
|
|
335
327
|
|
|
336
|
-
|
|
328
|
+
#connectedSignal(): AbortSignal {
|
|
329
|
+
this.#connectedController ??= new AbortController()
|
|
330
|
+
return this.#connectedController.signal
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
#abortRenderSignal(): void {
|
|
334
|
+
this.#renderController?.abort()
|
|
335
|
+
this.#renderController = undefined
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
#dequeueTasks(): Array<() => void> {
|
|
339
|
+
let needsSignal = this.#tasks.some((task) => task.length >= 1)
|
|
340
|
+
|
|
341
|
+
if (needsSignal) {
|
|
342
|
+
this.#renderController ??= new AbortController()
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
let signal = this.#renderController?.signal
|
|
346
|
+
let tasks = this.#tasks.splice(0, this.#tasks.length)
|
|
347
|
+
return tasks.map((task) => () => task(signal!))
|
|
348
|
+
}
|
|
337
349
|
}
|
|
338
350
|
|
|
339
351
|
function syncProps(target: ElementProps, next: ElementProps): void {
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { normalizeCssValue } from '../../style/style.ts'
|
|
2
|
+
import { normalizeSvgAttribute } from '../svg-attributes.ts'
|
|
3
|
+
import type { ElementProps } from '../jsx.ts'
|
|
4
|
+
|
|
5
|
+
const ATTRIBUTE_FALLBACK_NAMES = new Set([
|
|
6
|
+
'width',
|
|
7
|
+
'height',
|
|
8
|
+
'href',
|
|
9
|
+
'list',
|
|
10
|
+
'form',
|
|
11
|
+
'tabIndex',
|
|
12
|
+
'download',
|
|
13
|
+
'rowSpan',
|
|
14
|
+
'colSpan',
|
|
15
|
+
'role',
|
|
16
|
+
'popover',
|
|
17
|
+
])
|
|
18
|
+
|
|
19
|
+
export const FRAMEWORK_PROPS = new Set(['children', 'mix', 'key', 'animate', 'innerHTML', 'on'])
|
|
20
|
+
|
|
21
|
+
export const SELF_CLOSING_TAGS = new Set([
|
|
22
|
+
'area',
|
|
23
|
+
'base',
|
|
24
|
+
'br',
|
|
25
|
+
'col',
|
|
26
|
+
'embed',
|
|
27
|
+
'hr',
|
|
28
|
+
'img',
|
|
29
|
+
'input',
|
|
30
|
+
'link',
|
|
31
|
+
'meta',
|
|
32
|
+
'param',
|
|
33
|
+
'source',
|
|
34
|
+
'track',
|
|
35
|
+
'wbr',
|
|
36
|
+
])
|
|
37
|
+
|
|
38
|
+
export function isChildlessElement(name: string): boolean {
|
|
39
|
+
return SELF_CLOSING_TAGS.has(name)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function canUseProperty(
|
|
43
|
+
element: Element,
|
|
44
|
+
name: string,
|
|
45
|
+
isSvg: boolean,
|
|
46
|
+
): element is Element & Record<string, unknown> {
|
|
47
|
+
if (isSvg) return false
|
|
48
|
+
if (ATTRIBUTE_FALLBACK_NAMES.has(name)) return false
|
|
49
|
+
return name in element
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function normalizeAttributeName(
|
|
53
|
+
name: string,
|
|
54
|
+
isSvg: boolean,
|
|
55
|
+
): { ns?: string; attr: string } {
|
|
56
|
+
if (name.startsWith('aria-') || name.startsWith('data-')) return { attr: name }
|
|
57
|
+
|
|
58
|
+
if (name === 'className') return { attr: 'class' }
|
|
59
|
+
|
|
60
|
+
if (!isSvg) {
|
|
61
|
+
if (name === 'htmlFor') return { attr: 'for' }
|
|
62
|
+
if (name === 'tabIndex') return { attr: 'tabindex' }
|
|
63
|
+
if (name === 'acceptCharset') return { attr: 'accept-charset' }
|
|
64
|
+
if (name === 'httpEquiv') return { attr: 'http-equiv' }
|
|
65
|
+
return { attr: name.toLowerCase() }
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return normalizeSvgAttribute(name)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function serializeStyleObject(style: Record<string, unknown>): string {
|
|
72
|
+
let parts: string[] = []
|
|
73
|
+
|
|
74
|
+
for (let [key, value] of Object.entries(style)) {
|
|
75
|
+
if (value == null) continue
|
|
76
|
+
if (typeof value === 'boolean') continue
|
|
77
|
+
if (typeof value === 'number' && !Number.isFinite(value)) continue
|
|
78
|
+
|
|
79
|
+
let cssKey = toKebabCase(key)
|
|
80
|
+
let cssValue = Array.isArray(value) ? value.join(', ') : normalizeCssValue(key, value)
|
|
81
|
+
|
|
82
|
+
parts.push(`${cssKey}: ${cssValue};`)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return parts.join(' ')
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function getMergedClassName(props: ElementProps): string | undefined {
|
|
89
|
+
let classAttr = typeof props.class === 'string' ? props.class : ''
|
|
90
|
+
let className = typeof props.className === 'string' ? props.className : ''
|
|
91
|
+
let merged = classAttr && className ? `${classAttr} ${className}` : classAttr || className
|
|
92
|
+
return merged || undefined
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function toKebabCase(value: string): string {
|
|
96
|
+
return value.replace(/[A-Z]/g, (match) => `-${match.toLowerCase()}`)
|
|
97
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { RemixNode } from '../jsx.ts'
|
|
2
|
+
|
|
3
|
+
export type EmptyChild = boolean | null | undefined
|
|
4
|
+
export type PrimitiveChild = string | number | bigint
|
|
5
|
+
|
|
6
|
+
export function isEmptyChild(value: RemixNode): value is EmptyChild {
|
|
7
|
+
return value == null || typeof value === 'boolean'
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function isPrimitiveChild(value: RemixNode): value is PrimitiveChild {
|
|
11
|
+
let type = typeof value
|
|
12
|
+
return type === 'string' || type === 'number' || type === 'bigint'
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function normalizeChildren(children: readonly RemixNode[]): RemixNode[] {
|
|
16
|
+
for (let child of children) {
|
|
17
|
+
if (Array.isArray(child)) {
|
|
18
|
+
return (children as unknown[]).flat(Infinity) as RemixNode[]
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return children as RemixNode[]
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function packChildren(children: readonly RemixNode[]): RemixNode | undefined {
|
|
26
|
+
if (children.length === 0) {
|
|
27
|
+
return undefined
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (children.length === 1) {
|
|
31
|
+
let child = children[0]
|
|
32
|
+
|
|
33
|
+
if (child === undefined || isEmptyChild(child)) {
|
|
34
|
+
return undefined
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return Array.isArray(child) ? normalizeChildren(child) : child
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
let normalized = normalizeChildren(children)
|
|
41
|
+
return normalized.length === 0 ? undefined : normalized
|
|
42
|
+
}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import type { ElementProps } from '../jsx.ts'
|
|
2
|
+
import {
|
|
3
|
+
canUseProperty,
|
|
4
|
+
getMergedClassName,
|
|
5
|
+
normalizeAttributeName,
|
|
6
|
+
serializeStyleObject,
|
|
7
|
+
toKebabCase,
|
|
8
|
+
} from './attributes.ts'
|
|
9
|
+
import { normalizeCssValue } from '../../style/style.ts'
|
|
10
|
+
|
|
11
|
+
const SVG_NS = 'http://www.w3.org/2000/svg'
|
|
12
|
+
|
|
13
|
+
function isFrameworkProp(name: string): boolean {
|
|
14
|
+
return (
|
|
15
|
+
name === 'children' ||
|
|
16
|
+
name === 'mix' ||
|
|
17
|
+
name === 'key' ||
|
|
18
|
+
name === 'animate' ||
|
|
19
|
+
name === 'innerHTML' ||
|
|
20
|
+
name === 'on'
|
|
21
|
+
)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function toLocalName(attrName: string): string {
|
|
25
|
+
let separatorIndex = attrName.indexOf(':')
|
|
26
|
+
if (separatorIndex === -1) return attrName
|
|
27
|
+
return attrName.slice(separatorIndex + 1)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function clearRuntimePropertyOnRemoval(dom: Element & Record<string, unknown>, name: string): void {
|
|
31
|
+
try {
|
|
32
|
+
if (name === 'value' || name === 'defaultValue') {
|
|
33
|
+
dom[name] = ''
|
|
34
|
+
return
|
|
35
|
+
}
|
|
36
|
+
if (name === 'checked' || name === 'defaultChecked' || name === 'selected') {
|
|
37
|
+
dom[name] = false
|
|
38
|
+
return
|
|
39
|
+
}
|
|
40
|
+
if (name === 'selectedIndex') {
|
|
41
|
+
dom[name] = -1
|
|
42
|
+
}
|
|
43
|
+
} catch {}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Patches DOM attributes and selected properties from old props to new props.
|
|
48
|
+
*
|
|
49
|
+
* @param curr Previous host props.
|
|
50
|
+
* @param next Next host props.
|
|
51
|
+
* @param dom Element to patch.
|
|
52
|
+
*/
|
|
53
|
+
export function patchHostProps(curr: ElementProps, next: ElementProps, dom: Element): void {
|
|
54
|
+
let isSvg = dom.namespaceURI === SVG_NS
|
|
55
|
+
let currClassName = getMergedClassName(curr)
|
|
56
|
+
let nextClassName = getMergedClassName(next)
|
|
57
|
+
|
|
58
|
+
if (currClassName !== nextClassName) {
|
|
59
|
+
if (nextClassName) {
|
|
60
|
+
dom.setAttribute('class', nextClassName)
|
|
61
|
+
} else {
|
|
62
|
+
dom.removeAttribute('class')
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
for (let name in curr) {
|
|
67
|
+
if (isFrameworkProp(name)) continue
|
|
68
|
+
if (name === 'class' || name === 'className') continue
|
|
69
|
+
if (name in next && next[name] != null) continue
|
|
70
|
+
|
|
71
|
+
if (canUseProperty(dom, name, isSvg)) {
|
|
72
|
+
clearRuntimePropertyOnRemoval(dom, name)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
let { ns, attr } = normalizeAttributeName(name, isSvg)
|
|
76
|
+
if (ns) dom.removeAttributeNS(ns, toLocalName(attr))
|
|
77
|
+
else dom.removeAttribute(attr)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
for (let name in next) {
|
|
81
|
+
if (isFrameworkProp(name)) continue
|
|
82
|
+
if (name === 'class' || name === 'className') continue
|
|
83
|
+
|
|
84
|
+
let nextValue = next[name]
|
|
85
|
+
if (nextValue == null) continue
|
|
86
|
+
|
|
87
|
+
let prevValue = curr[name]
|
|
88
|
+
if (prevValue === nextValue) continue
|
|
89
|
+
|
|
90
|
+
if (name === 'style' && isStyleObject(nextValue)) {
|
|
91
|
+
if (isStyleObject(prevValue)) {
|
|
92
|
+
patchStyleObject(dom, prevValue, nextValue)
|
|
93
|
+
} else {
|
|
94
|
+
dom.removeAttribute('style')
|
|
95
|
+
patchStyleObject(dom, undefined, nextValue)
|
|
96
|
+
}
|
|
97
|
+
continue
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
patchHostProp(dom, name, nextValue, isSvg)
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function patchHostProp(dom: Element, name: string, value: unknown, isSvg: boolean): void {
|
|
105
|
+
let { ns, attr } = normalizeAttributeName(name, isSvg)
|
|
106
|
+
|
|
107
|
+
if (attr === 'style' && isStyleObject(value)) {
|
|
108
|
+
patchStyleObject(dom, undefined, value)
|
|
109
|
+
return
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (attr === 'style' && typeof value === 'string') {
|
|
113
|
+
dom.setAttribute('style', value)
|
|
114
|
+
return
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (canUseProperty(dom, name, isSvg)) {
|
|
118
|
+
try {
|
|
119
|
+
dom[name] = value == null ? '' : value
|
|
120
|
+
return
|
|
121
|
+
} catch {}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (typeof value === 'function') return
|
|
125
|
+
|
|
126
|
+
let isAriaOrData = name.startsWith('aria-') || name.startsWith('data-')
|
|
127
|
+
if (value != null && (value !== false || isAriaOrData)) {
|
|
128
|
+
let attrValue = name === 'popover' && value === true ? '' : String(value)
|
|
129
|
+
if (ns) dom.setAttributeNS(ns, attr, attrValue)
|
|
130
|
+
else dom.setAttribute(attr, attrValue)
|
|
131
|
+
} else {
|
|
132
|
+
if (ns) dom.removeAttributeNS(ns, toLocalName(attr))
|
|
133
|
+
else dom.removeAttribute(attr)
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function isStyleObject(value: unknown): value is Record<string, unknown> {
|
|
138
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function patchStyleObject(
|
|
142
|
+
dom: Element,
|
|
143
|
+
curr: Record<string, unknown> | undefined,
|
|
144
|
+
next: Record<string, unknown>,
|
|
145
|
+
): void {
|
|
146
|
+
if (!(dom instanceof HTMLElement || dom instanceof SVGElement)) {
|
|
147
|
+
dom.setAttribute('style', serializeStyleObject(next))
|
|
148
|
+
return
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
let style = dom.style
|
|
152
|
+
if (curr) {
|
|
153
|
+
for (let name in curr) {
|
|
154
|
+
let nextCssValue = styleValueToCss(name, next[name])
|
|
155
|
+
if (nextCssValue !== undefined) continue
|
|
156
|
+
|
|
157
|
+
let prevCssValue = styleValueToCss(name, curr[name])
|
|
158
|
+
if (prevCssValue === undefined) continue
|
|
159
|
+
|
|
160
|
+
style.removeProperty(toKebabCase(name))
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
for (let name in next) {
|
|
165
|
+
let nextCssValue = styleValueToCss(name, next[name])
|
|
166
|
+
if (nextCssValue === undefined) continue
|
|
167
|
+
|
|
168
|
+
let prevCssValue = curr ? styleValueToCss(name, curr[name]) : undefined
|
|
169
|
+
if (prevCssValue === nextCssValue) continue
|
|
170
|
+
|
|
171
|
+
style.setProperty(toKebabCase(name), nextCssValue)
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function styleValueToCss(name: string, value: unknown): string | undefined {
|
|
176
|
+
if (value == null) return undefined
|
|
177
|
+
if (typeof value === 'boolean') return undefined
|
|
178
|
+
if (typeof value === 'number' && !Number.isFinite(value)) return undefined
|
|
179
|
+
|
|
180
|
+
if (Array.isArray(value)) {
|
|
181
|
+
return value.join(', ')
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return normalizeCssValue(name, value)
|
|
185
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { ElementProps, ElementType, RemixElement, RemixNode } from '../jsx.ts'
|
|
2
|
+
|
|
3
|
+
export function createRemixElement(
|
|
4
|
+
type: ElementType,
|
|
5
|
+
props: ElementProps | null | undefined,
|
|
6
|
+
key?: string,
|
|
7
|
+
): RemixElement {
|
|
8
|
+
return {
|
|
9
|
+
$rmx: true,
|
|
10
|
+
key,
|
|
11
|
+
props: normalizeElementProps(props),
|
|
12
|
+
type,
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function isRemixElement(node: RemixNode): node is RemixElement {
|
|
17
|
+
return typeof node === 'object' && node !== null && '$rmx' in node
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function normalizeElementProps(props: ElementProps | null | undefined): ElementProps {
|
|
21
|
+
if (!props) return {}
|
|
22
|
+
if (!('mix' in props)) return props
|
|
23
|
+
|
|
24
|
+
let { mix, ...rest } = props
|
|
25
|
+
let normalizedMix = normalizeMixValue(mix)
|
|
26
|
+
return normalizedMix === undefined ? rest : { ...rest, mix: normalizedMix }
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function normalizeMixValue(mix: unknown): unknown[] | undefined {
|
|
30
|
+
if (!mix) return undefined
|
|
31
|
+
|
|
32
|
+
let normalizedMix: unknown[] = []
|
|
33
|
+
flattenMixValue(mix, normalizedMix)
|
|
34
|
+
return normalizedMix.length === 0 ? undefined : normalizedMix
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function flattenMixValue(mix: unknown, out: unknown[]): void {
|
|
38
|
+
if (!mix) return
|
|
39
|
+
|
|
40
|
+
if (!Array.isArray(mix)) {
|
|
41
|
+
out.push(mix)
|
|
42
|
+
return
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
for (let item of mix) {
|
|
46
|
+
flattenMixValue(item, out)
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { jsx } from './jsx.ts'
|
|
2
2
|
import type { ElementType, RemixElement } from './jsx.ts'
|
|
3
|
+
import { normalizeChildren } from './core/children.ts'
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* Creates a Remix virtual element from a JSX-like call signature.
|
|
@@ -20,7 +21,7 @@ export function createElement(
|
|
|
20
21
|
console.error(new Error('mixin elements must not receive children'))
|
|
21
22
|
}
|
|
22
23
|
} else {
|
|
23
|
-
nextProps.children = children
|
|
24
|
+
nextProps.children = normalizeChildren(children)
|
|
24
25
|
}
|
|
25
26
|
|
|
26
27
|
if (nextProps.key != null) {
|