@remix-run/ui 0.1.2 → 0.2.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.
Files changed (109) hide show
  1. package/README.md +9 -7
  2. package/dist/animation/spring.d.ts +6 -35
  3. package/dist/animation/spring.js +9 -8
  4. package/dist/animation/spring.js.map +1 -1
  5. package/dist/components/accordion/accordion.d.ts +6 -6
  6. package/dist/components/accordion/accordion.js.map +1 -1
  7. package/dist/components/breadcrumbs/breadcrumbs.d.ts +1 -1
  8. package/dist/components/button/button.d.ts +4 -4
  9. package/dist/components/button/button.js +3 -1
  10. package/dist/components/button/button.js.map +1 -1
  11. package/dist/components/combobox/combobox.d.ts +10 -10
  12. package/dist/components/combobox/combobox.js.map +1 -1
  13. package/dist/components/listbox/listbox.d.ts +3 -3
  14. package/dist/components/listbox/listbox.js.map +1 -1
  15. package/dist/components/menu/menu.d.ts +12 -12
  16. package/dist/components/menu/menu.js.map +1 -1
  17. package/dist/components/popover/popover.d.ts +5 -5
  18. package/dist/components/popover/popover.js.map +1 -1
  19. package/dist/components/select/select.d.ts +10 -10
  20. package/dist/components/select/select.js.map +1 -1
  21. package/dist/components/tabs/tabs.d.ts +3 -3
  22. package/dist/index.d.ts +1 -1
  23. package/dist/index.js.map +1 -1
  24. package/dist/interactions/keydown/keydown.d.ts +1 -1
  25. package/dist/interactions/outside-click/outside-click-mixin.d.ts +2 -1
  26. package/dist/interactions/typeahead/typeahead-mixin.d.ts +3 -1
  27. package/dist/runtime/component.d.ts +18 -15
  28. package/dist/runtime/component.js +14 -5
  29. package/dist/runtime/component.js.map +1 -1
  30. package/dist/runtime/core/attributes.d.ts +3 -1
  31. package/dist/runtime/core/attributes.js +19 -1
  32. package/dist/runtime/core/attributes.js.map +1 -1
  33. package/dist/runtime/core/props.js +6 -5
  34. package/dist/runtime/core/props.js.map +1 -1
  35. package/dist/runtime/dom.d.ts +16 -9
  36. package/dist/runtime/jsx.d.ts +1 -1
  37. package/dist/runtime/mixins/link-mixin.d.ts +5 -1
  38. package/dist/runtime/mixins/link-mixin.js.map +1 -1
  39. package/dist/runtime/mixins/mixin.d.ts +2 -1
  40. package/dist/runtime/mixins/mixin.js.map +1 -1
  41. package/dist/runtime/mixins/ref-mixin.d.ts +2 -1
  42. package/dist/runtime/mixins/ref-mixin.js.map +1 -1
  43. package/dist/runtime/reconcile.d.ts +1 -1
  44. package/dist/runtime/reconcile.js +19 -5
  45. package/dist/runtime/reconcile.js.map +1 -1
  46. package/dist/runtime/scheduler.d.ts +10 -10
  47. package/dist/runtime/scheduler.js +1 -1
  48. package/dist/runtime/scheduler.js.map +1 -1
  49. package/dist/runtime/svg-attributes.js +1 -0
  50. package/dist/runtime/svg-attributes.js.map +1 -1
  51. package/dist/runtime/to-vnode.js +2 -2
  52. package/dist/runtime/to-vnode.js.map +1 -1
  53. package/dist/runtime/vnode.d.ts +6 -1
  54. package/dist/runtime/vnode.js +4 -0
  55. package/dist/runtime/vnode.js.map +1 -1
  56. package/dist/server/stream.d.ts +2 -0
  57. package/dist/server/stream.js +79 -9
  58. package/dist/server/stream.js.map +1 -1
  59. package/dist/style/css-mixin.d.ts +2 -1
  60. package/dist/style/css-mixin.js.map +1 -1
  61. package/dist/style/index.d.ts +1 -1
  62. package/dist/style/stylesheet.d.ts +13 -13
  63. package/dist/style/stylesheet.js.map +1 -1
  64. package/dist/theme/contract.d.ts +24 -222
  65. package/dist/theme/contract.js +2 -2
  66. package/dist/theme/contract.js.map +1 -1
  67. package/dist/theme/glyph-contract.d.ts +1 -3
  68. package/dist/theme/glyph-contract.js.map +1 -1
  69. package/dist/theme/presets/rmx-01/index.d.ts +4 -2
  70. package/dist/theme/presets/rmx-01/index.js.map +1 -1
  71. package/dist/utils/scroll-lock.d.ts +3 -3
  72. package/dist/utils/scroll-lock.js.map +1 -1
  73. package/package.json +3 -3
  74. package/src/animation/README.md +7 -5
  75. package/src/animation/spring.ts +21 -9
  76. package/src/components/accordion/accordion.tsx +7 -5
  77. package/src/components/anchor/anchor.ts +1 -1
  78. package/src/components/breadcrumbs/breadcrumbs.tsx +1 -1
  79. package/src/components/button/button.tsx +30 -18
  80. package/src/components/combobox/README.md +1 -1
  81. package/src/components/combobox/combobox.tsx +30 -12
  82. package/src/components/glyph/glyph.tsx +2 -2
  83. package/src/components/listbox/listbox.ts +88 -83
  84. package/src/components/menu/menu.tsx +27 -15
  85. package/src/components/popover/popover.ts +90 -79
  86. package/src/components/select/select.tsx +26 -9
  87. package/src/index.ts +1 -0
  88. package/src/runtime/component.ts +34 -12
  89. package/src/runtime/core/attributes.ts +21 -0
  90. package/src/runtime/core/props.ts +6 -4
  91. package/src/runtime/dom.ts +16 -9
  92. package/src/runtime/jsx.ts +1 -1
  93. package/src/runtime/mixins/link-mixin.ts +88 -86
  94. package/src/runtime/mixins/mixin.ts +9 -1
  95. package/src/runtime/mixins/ref-mixin.ts +4 -5
  96. package/src/runtime/reconcile.ts +23 -14
  97. package/src/runtime/scheduler.ts +20 -20
  98. package/src/runtime/svg-attributes.ts +1 -0
  99. package/src/runtime/to-vnode.ts +2 -2
  100. package/src/runtime/vnode.ts +10 -0
  101. package/src/server/README.md +4 -2
  102. package/src/server/stream.ts +77 -8
  103. package/src/style/css-mixin.ts +6 -1
  104. package/src/style/index.ts +1 -2
  105. package/src/style/stylesheet.ts +14 -2
  106. package/src/theme/contract.ts +37 -5
  107. package/src/theme/glyph-contract.ts +3 -1
  108. package/src/theme/presets/rmx-01/index.ts +4 -2
  109. package/src/utils/scroll-lock.ts +7 -3
@@ -10,6 +10,7 @@ import {
10
10
  type ElementProps,
11
11
  type Handle,
12
12
  type MixinHandle,
13
+ type MixinFactory,
13
14
  type Props,
14
15
  type RemixNode,
15
16
  } from '@remix-run/ui'
@@ -106,7 +107,7 @@ interface SelectContextValue {
106
107
  unregisterTrigger: (node: HTMLButtonElement) => void
107
108
  }
108
109
 
109
- function SelectProvider(handle: Handle<SelectContextProps, SelectContextValue>) {
110
+ function SelectProvider(handle: Handle<SelectContextProps, SelectContextValue>): () => RemixNode {
110
111
  let triggerRef: HTMLButtonElement | undefined
111
112
  let listboxRef: listbox.ListboxRef | undefined
112
113
  let surfaceRef: HTMLElement | undefined
@@ -396,7 +397,11 @@ function getSelectContext(handle: Handle | MixinHandle) {
396
397
  return handle.context.get(SelectProvider)
397
398
  }
398
399
 
399
- const triggerMixin = createMixin<HTMLButtonElement, [], ElementProps>((handle) => {
400
+ const triggerMixin: MixinFactory<HTMLButtonElement, [], ElementProps> = createMixin<
401
+ HTMLButtonElement,
402
+ [],
403
+ ElementProps
404
+ >((handle) => {
400
405
  let context = getSelectContext(handle)
401
406
 
402
407
  return (props) => [
@@ -428,7 +433,11 @@ const triggerMixin = createMixin<HTMLButtonElement, [], ElementProps>((handle) =
428
433
  ]
429
434
  })
430
435
 
431
- const popoverMixin = createMixin<HTMLElement, [], ElementProps>((handle) => {
436
+ const popoverMixin: MixinFactory<HTMLElement, [], ElementProps> = createMixin<
437
+ HTMLElement,
438
+ [],
439
+ ElementProps
440
+ >((handle) => {
432
441
  let context = getSelectContext(handle)
433
442
  let popoverState = handle.context.get(popover.Context)
434
443
 
@@ -453,7 +462,11 @@ const popoverMixin = createMixin<HTMLElement, [], ElementProps>((handle) => {
453
462
  ]
454
463
  })
455
464
 
456
- const listMixin = createMixin<HTMLElement, [], ElementProps>((handle) => {
465
+ const listMixin: MixinFactory<HTMLElement, [], ElementProps> = createMixin<
466
+ HTMLElement,
467
+ [],
468
+ ElementProps
469
+ >((handle) => {
457
470
  let context = getSelectContext(handle)
458
471
 
459
472
  return () => [
@@ -466,7 +479,11 @@ const listMixin = createMixin<HTMLElement, [], ElementProps>((handle) => {
466
479
  ]
467
480
  })
468
481
 
469
- const hiddenInputMixin = createMixin<HTMLInputElement, [], ElementProps>((handle) => {
482
+ const hiddenInputMixin: MixinFactory<HTMLInputElement, [], ElementProps> = createMixin<
483
+ HTMLInputElement,
484
+ [],
485
+ ElementProps
486
+ >((handle) => {
470
487
  let context = getSelectContext(handle)
471
488
 
472
489
  return () =>
@@ -532,17 +549,17 @@ const select = {
532
549
  export function onSelectChange<target extends HTMLElement>(
533
550
  handler: SelectChangeHandler<target>,
534
551
  captureBoolean?: boolean,
535
- ) {
552
+ ): ReturnType<typeof on<target, typeof SELECT_CHANGE_EVENT>> {
536
553
  return on(SELECT_CHANGE_EVENT, handler, captureBoolean)
537
554
  }
538
555
 
539
- function SelectLabel(handle: Handle) {
556
+ function SelectLabel(handle: Handle): () => RemixNode {
540
557
  let context = getSelectContext(handle)
541
558
 
542
559
  return () => <span mix={button.labelStyle}>{context.displayedLabel}</span>
543
560
  }
544
561
 
545
- export function Select(handle: Handle<SelectProps>) {
562
+ export function Select(handle: Handle<SelectProps>): () => RemixNode {
546
563
  return () => {
547
564
  let { children, defaultLabel, defaultValue, disabled, name, mix, ...buttonProps } = handle.props
548
565
 
@@ -568,7 +585,7 @@ export function Select(handle: Handle<SelectProps>) {
568
585
  }
569
586
  }
570
587
 
571
- export function Option(handle: Handle<SelectOptionProps>) {
588
+ export function Option(handle: Handle<SelectOptionProps>): () => RemixNode {
572
589
  return () => {
573
590
  let { label, value, disabled, textValue, children, mix, ...divProps } = handle.props
574
591
 
package/src/index.ts CHANGED
@@ -51,6 +51,7 @@ export type { HostProps, LayoutAnimationConfig } from './runtime/dom.ts'
51
51
  export { createMixin } from './runtime/mixins/mixin.ts'
52
52
  export type {
53
53
  MixinDescriptor,
54
+ MixinFactory,
54
55
  MixinHandle,
55
56
  MixinType,
56
57
  MixInput,
@@ -118,13 +118,18 @@ export type ContextFrom<ComponentType> =
118
118
 
119
119
  /**
120
120
  * Context storage API exposed on component handles.
121
+ *
122
+ * Context values are keyed by provider component identity. `get(Component)`
123
+ * reads the nearest ancestor instance whose component function is exactly
124
+ * `Component`, so nested instances of the same provider shadow outer instances
125
+ * while different component types remain independent.
121
126
  */
122
127
  export interface Context<C> {
123
128
  /** Replaces the current context value for this component instance. */
124
129
  set(values: C): void
125
- /** Reads the context value associated with the given component type. */
130
+ /** Reads the context value from the nearest ancestor instance of the given component type. */
126
131
  get<ComponentType>(component: ComponentType): ContextFrom<ComponentType>
127
- /** Reads the context value associated with the given component key. */
132
+ /** Reads an unknown context value for an untyped lookup. */
128
133
  get(component: ElementType | symbol): unknown | undefined
129
134
  }
130
135
 
@@ -218,7 +223,14 @@ type ComponentConfig = {
218
223
  /**
219
224
  * Runtime handle returned by {@link createComponent}.
220
225
  */
221
- export type ComponentHandle = ReturnType<typeof createComponent>
226
+ export interface ComponentHandle<C = NoContext> {
227
+ frame: FrameHandle
228
+ render(nextProps: ElementProps): [RemixNode, Array<() => void>]
229
+ remove(): Array<() => void>
230
+ setScheduleUpdate(nextScheduleUpdate: () => void): void
231
+ getContextValue(): C | undefined
232
+ isRemoved(): boolean
233
+ }
222
234
 
223
235
  /**
224
236
  * Creates the internal runtime wrapper for a component instance.
@@ -226,11 +238,11 @@ export type ComponentHandle = ReturnType<typeof createComponent>
226
238
  * @param config Component runtime configuration.
227
239
  * @returns Component runtime helpers used by the reconciler.
228
240
  */
229
- export function createComponent<C = NoContext>(config: ComponentConfig) {
241
+ export function createComponent<C = NoContext>(config: ComponentConfig): ComponentHandle<C> {
230
242
  return new ComponentRuntime<C>(config)
231
243
  }
232
244
 
233
- class ComponentRuntime<C = NoContext> {
245
+ class ComponentRuntime<C = NoContext> implements ComponentHandle<C> {
234
246
  frame: FrameHandle
235
247
 
236
248
  #config: ComponentConfig
@@ -240,6 +252,7 @@ class ComponentRuntime<C = NoContext> {
240
252
  #props = {} as ElementProps
241
253
  #renderController: AbortController | undefined
242
254
  #renderFn: RenderFn | undefined
255
+ #removed = false
243
256
  #scheduleUpdate: () => void = () => {
244
257
  throw new Error('scheduleUpdate not implemented')
245
258
  }
@@ -252,7 +265,7 @@ class ComponentRuntime<C = NoContext> {
252
265
  }
253
266
 
254
267
  render = (nextProps: ElementProps): [RemixNode, Array<() => void>] => {
255
- if (this.#connectedController?.signal.aborted) {
268
+ if (this.#removed) {
256
269
  console.warn('render called after component was removed, potential application memory leak')
257
270
  return [null, []]
258
271
  }
@@ -278,9 +291,11 @@ class ComponentRuntime<C = NoContext> {
278
291
  }
279
292
 
280
293
  remove = (): Array<() => void> => {
294
+ if (this.#removed) return []
295
+ this.#removed = true
281
296
  this.#connectedController?.abort()
282
297
  this.#abortRenderSignal()
283
- return this.#dequeueTasks()
298
+ return this.#dequeueTasks(AbortSignal.abort())
284
299
  }
285
300
 
286
301
  setScheduleUpdate = (nextScheduleUpdate: () => void): void => {
@@ -289,6 +304,8 @@ class ComponentRuntime<C = NoContext> {
289
304
 
290
305
  getContextValue = (): C | undefined => this.#contextValue
291
306
 
307
+ isRemoved = (): boolean => this.#removed
308
+
292
309
  #createHandle(): Handle<ElementProps, C> {
293
310
  let component = this
294
311
  let context: Context<C> = {
@@ -303,6 +320,11 @@ class ComponentRuntime<C = NoContext> {
303
320
  props: this.#props,
304
321
  update: () =>
305
322
  new Promise((resolve) => {
323
+ if (component.#removed) {
324
+ resolve(AbortSignal.abort())
325
+ return
326
+ }
327
+
306
328
  this.#tasks.push((signal) => resolve(signal))
307
329
  this.#scheduleUpdate()
308
330
  }),
@@ -335,14 +357,14 @@ class ComponentRuntime<C = NoContext> {
335
357
  this.#renderController = undefined
336
358
  }
337
359
 
338
- #dequeueTasks(): Array<() => void> {
339
- let needsSignal = this.#tasks.some((task) => task.length >= 1)
360
+ #dequeueTasks(signal?: AbortSignal): Array<() => void> {
361
+ let needsSignal = signal === undefined && this.#tasks.some((task) => task.length >= 1)
340
362
 
341
363
  if (needsSignal) {
342
364
  this.#renderController ??= new AbortController()
343
365
  }
344
366
 
345
- let signal = this.#renderController?.signal
367
+ signal ??= this.#renderController?.signal
346
368
  let tasks = this.#tasks.splice(0, this.#tasks.length)
347
369
  return tasks.map((task) => () => task(signal!))
348
370
  }
@@ -366,7 +388,7 @@ function syncProps(target: ElementProps, next: ElementProps): void {
366
388
  * @param handle Component handle for the frame instance.
367
389
  * @returns A placeholder render function handled by the reconciler.
368
390
  */
369
- export function Frame(handle: Handle<FrameProps, FrameHandle>) {
391
+ export function Frame(handle: Handle<FrameProps, FrameHandle>): RenderFn {
370
392
  void handle
371
393
  return () => null // reconciler renders
372
394
  }
@@ -377,7 +399,7 @@ export function Frame(handle: Handle<FrameProps, FrameHandle>) {
377
399
  * @param handle Component handle for the fragment instance.
378
400
  * @returns A placeholder render function handled by the reconciler.
379
401
  */
380
- export function Fragment(handle: Handle<FragmentProps>) {
402
+ export function Fragment(handle: Handle<FragmentProps>): RenderFn {
381
403
  void handle
382
404
  return () => null // reconciler renders
383
405
  }
@@ -14,6 +14,17 @@ const ATTRIBUTE_FALLBACK_NAMES = new Set([
14
14
  'colSpan',
15
15
  'role',
16
16
  'popover',
17
+ 'translate',
18
+ ])
19
+
20
+ const BOOLEANISH_STRING_ATTRIBUTES = new Set([
21
+ 'autoReverse',
22
+ 'contenteditable',
23
+ 'draggable',
24
+ 'externalResourcesRequired',
25
+ 'focusable',
26
+ 'preserveAlpha',
27
+ 'spellcheck',
17
28
  ])
18
29
 
19
30
  export const FRAMEWORK_PROPS = new Set(['children', 'mix', 'key', 'animate', 'innerHTML', 'on'])
@@ -43,9 +54,11 @@ export function canUseProperty(
43
54
  element: Element,
44
55
  name: string,
45
56
  isSvg: boolean,
57
+ attr: string,
46
58
  ): element is Element & Record<string, unknown> {
47
59
  if (isSvg) return false
48
60
  if (ATTRIBUTE_FALLBACK_NAMES.has(name)) return false
61
+ if (isBooleanishStringAttribute(attr)) return false
49
62
  return name in element
50
63
  }
51
64
 
@@ -68,6 +81,14 @@ export function normalizeAttributeName(
68
81
  return normalizeSvgAttribute(name)
69
82
  }
70
83
 
84
+ export function isBooleanishStringAttribute(name: string): boolean {
85
+ return BOOLEANISH_STRING_ATTRIBUTES.has(name)
86
+ }
87
+
88
+ export function shouldStringifyBooleanAttribute(name: string): boolean {
89
+ return isBooleanishStringAttribute(name) || name === 'value'
90
+ }
91
+
71
92
  export function serializeStyleObject(style: Record<string, unknown>): string {
72
93
  let parts: string[] = []
73
94
 
@@ -2,6 +2,7 @@ import type { ElementProps } from '../jsx.ts'
2
2
  import {
3
3
  canUseProperty,
4
4
  getMergedClassName,
5
+ isBooleanishStringAttribute,
5
6
  normalizeAttributeName,
6
7
  serializeStyleObject,
7
8
  toKebabCase,
@@ -68,11 +69,11 @@ export function patchHostProps(curr: ElementProps, next: ElementProps, dom: Elem
68
69
  if (name === 'class' || name === 'className') continue
69
70
  if (name in next && next[name] != null) continue
70
71
 
71
- if (canUseProperty(dom, name, isSvg)) {
72
+ let { ns, attr } = normalizeAttributeName(name, isSvg)
73
+ if (canUseProperty(dom, name, isSvg, attr)) {
72
74
  clearRuntimePropertyOnRemoval(dom, name)
73
75
  }
74
76
 
75
- let { ns, attr } = normalizeAttributeName(name, isSvg)
76
77
  if (ns) dom.removeAttributeNS(ns, toLocalName(attr))
77
78
  else dom.removeAttribute(attr)
78
79
  }
@@ -114,7 +115,7 @@ function patchHostProp(dom: Element, name: string, value: unknown, isSvg: boolea
114
115
  return
115
116
  }
116
117
 
117
- if (canUseProperty(dom, name, isSvg)) {
118
+ if (canUseProperty(dom, name, isSvg, attr)) {
118
119
  try {
119
120
  dom[name] = value == null ? '' : value
120
121
  return
@@ -124,7 +125,8 @@ function patchHostProp(dom: Element, name: string, value: unknown, isSvg: boolea
124
125
  if (typeof value === 'function') return
125
126
 
126
127
  let isAriaOrData = name.startsWith('aria-') || name.startsWith('data-')
127
- if (value != null && (value !== false || isAriaOrData)) {
128
+ let isBooleanishString = isBooleanishStringAttribute(attr)
129
+ if (value != null && (value !== false || isAriaOrData || isBooleanishString)) {
128
130
  let attrValue = name === 'popover' && value === true ? '' : String(value)
129
131
  if (ns) dom.setAttributeNS(ns, attr, attrValue)
130
132
  else dom.setAttribute(attr, attrValue)
@@ -9,6 +9,7 @@ import type { MixInput } from './mixins/mixin.ts'
9
9
  * - Copyright (c) 2015-present Jason Miller
10
10
  */
11
11
  type Booleanish = boolean | 'true' | 'false'
12
+ type TranslateValue = 'yes' | 'no'
12
13
 
13
14
  /**
14
15
  * Layout animation configuration for FLIP-based position animations.
@@ -109,6 +110,8 @@ export interface SVGProps<eventTarget extends EventTarget = SVGElement>
109
110
  attributeName?: Trackable<string | undefined>
110
111
  /** The `attributeType` SVG attribute. */
111
112
  attributeType?: Trackable<string | undefined>
113
+ /** The `autoReverse` SVG attribute. */
114
+ autoReverse?: Trackable<Booleanish | undefined>
112
115
  /** The `azimuth` SVG attribute. */
113
116
  azimuth?: Trackable<number | string | undefined>
114
117
  /** The `baseFrequency` SVG attribute. */
@@ -212,7 +215,7 @@ export interface SVGProps<eventTarget extends EventTarget = SVGElement>
212
215
  /** The `exponent` SVG attribute. */
213
216
  exponent?: Trackable<number | string | undefined>
214
217
  /** The `externalResourcesRequired` SVG attribute. */
215
- externalResourcesRequired?: Trackable<number | string | undefined>
218
+ externalResourcesRequired?: Trackable<Booleanish | undefined>
216
219
  /** The `fill` SVG attribute. */
217
220
  fill?: Trackable<string | undefined>
218
221
  /** The `fillOpacity` SVG attribute. */
@@ -238,7 +241,7 @@ export interface SVGProps<eventTarget extends EventTarget = SVGElement>
238
241
  /** The `flood-opacity` SVG attribute. */
239
242
  'flood-opacity'?: Trackable<number | string | undefined>
240
243
  /** The `focusable` SVG attribute. */
241
- focusable?: Trackable<number | string | undefined>
244
+ focusable?: Trackable<Booleanish | undefined>
242
245
  /** The `fontFamily` SVG attribute. */
243
246
  fontFamily?: Trackable<string | undefined>
244
247
  /** The `font-family` SVG attribute. */
@@ -446,7 +449,7 @@ export interface SVGProps<eventTarget extends EventTarget = SVGElement>
446
449
  /** The `pointsAtZ` SVG attribute. */
447
450
  pointsAtZ?: Trackable<number | string | undefined>
448
451
  /** The `preserveAlpha` SVG attribute. */
449
- preserveAlpha?: Trackable<number | string | undefined>
452
+ preserveAlpha?: Trackable<Booleanish | undefined>
450
453
  /** The `preserveAspectRatio` SVG attribute. */
451
454
  preserveAspectRatio?: Trackable<string | undefined>
452
455
  /** The `primitiveUnits` SVG attribute. */
@@ -1263,7 +1266,7 @@ export interface AllHTMLProps<eventTarget extends EventTarget = EventTarget>
1263
1266
  /** The `decoding` HTML attribute. */
1264
1267
  decoding?: Trackable<'sync' | 'async' | 'auto' | undefined>
1265
1268
  /** The `draggable` HTML attribute. */
1266
- draggable?: Trackable<boolean | undefined>
1269
+ draggable?: Trackable<Booleanish | undefined>
1267
1270
  /** The `encType` HTML attribute. */
1268
1271
  encType?: Trackable<string | undefined>
1269
1272
  /** The `enctype` HTML attribute. */
@@ -1487,7 +1490,9 @@ export interface AllHTMLProps<eventTarget extends EventTarget = EventTarget>
1487
1490
  /** The `span` HTML attribute. */
1488
1491
  span?: Trackable<number | undefined>
1489
1492
  /** The `spellcheck` HTML attribute. */
1490
- spellcheck?: Trackable<boolean | undefined>
1493
+ spellcheck?: Trackable<Booleanish | undefined>
1494
+ /** The `spellCheck` HTML attribute. */
1495
+ spellCheck?: Trackable<Booleanish | undefined>
1491
1496
  /** The `src` HTML attribute. */
1492
1497
  src?: Trackable<string | undefined>
1493
1498
  /** The `srcDoc` HTML attribute. */
@@ -1551,7 +1556,7 @@ export interface AllHTMLProps<eventTarget extends EventTarget = EventTarget>
1551
1556
  /** The `results` HTML attribute. */
1552
1557
  results?: Trackable<number | undefined>
1553
1558
  /** The `translate` HTML attribute. */
1554
- translate?: Trackable<boolean | undefined>
1559
+ translate?: Trackable<TranslateValue | undefined>
1555
1560
 
1556
1561
  // RDFa Attributes
1557
1562
  /** The `about` HTML attribute. */
@@ -1632,7 +1637,7 @@ export interface HTMLProps<eventTarget extends EventTarget = EventTarget>
1632
1637
  /** The `dir` HTML attribute. */
1633
1638
  dir?: Trackable<'auto' | 'rtl' | 'ltr' | undefined>
1634
1639
  /** The `draggable` HTML attribute. */
1635
- draggable?: Trackable<boolean | undefined>
1640
+ draggable?: Trackable<Booleanish | undefined>
1636
1641
  /** The `enterkeyhint` HTML attribute. */
1637
1642
  enterkeyhint?: Trackable<
1638
1643
  'enter' | 'done' | 'go' | 'next' | 'previous' | 'search' | 'send' | undefined
@@ -1662,7 +1667,9 @@ export interface HTMLProps<eventTarget extends EventTarget = EventTarget>
1662
1667
  /** The `slot` HTML attribute. */
1663
1668
  slot?: Trackable<string | undefined>
1664
1669
  /** The `spellcheck` HTML attribute. */
1665
- spellcheck?: Trackable<boolean | undefined>
1670
+ spellcheck?: Trackable<Booleanish | undefined>
1671
+ /** The `spellCheck` HTML attribute. */
1672
+ spellCheck?: Trackable<Booleanish | undefined>
1666
1673
  /** The `style` HTML attribute. */
1667
1674
  style?: Trackable<string | StyleProps | undefined>
1668
1675
  /** The `tabindex` HTML attribute. */
@@ -1672,7 +1679,7 @@ export interface HTMLProps<eventTarget extends EventTarget = EventTarget>
1672
1679
  /** The `title` HTML attribute. */
1673
1680
  title?: Trackable<string | undefined>
1674
1681
  /** The `translate` HTML attribute. */
1675
- translate?: Trackable<boolean | undefined>
1682
+ translate?: Trackable<TranslateValue | undefined>
1676
1683
 
1677
1684
  // WAI-ARIA Attributes
1678
1685
  // Most elements only allow a subset of roles and so this
@@ -44,7 +44,7 @@ export type Renderable = RemixElement | string | number | bigint | boolean | nul
44
44
  * Particularly useful for `props.children`.
45
45
  *
46
46
  * ```tsx
47
- * function MyComponent({ children }: { children: RemixNode }) {}
47
+ * function MyComponent(handle: Handle<{ children: RemixNode }>) {}
48
48
  * ```
49
49
  */
50
50
  export type RemixNode = Renderable | RemixNode[]
@@ -1,4 +1,4 @@
1
- import { createMixin, renderMixinElement } from './mixin.ts'
1
+ import { createMixin, renderMixinElement, type MixinFactory } from './mixin.ts'
2
2
  import { navigate } from '../navigation.ts'
3
3
  import { on } from './on-mixin.ts'
4
4
  import type { ElementProps } from '../jsx.ts'
@@ -21,108 +21,110 @@ const nativeLinkHostTypes = new Set(['a', 'area'])
21
21
  /**
22
22
  * Adds client-side navigation behavior to anchor-like elements.
23
23
  */
24
- export const link = createMixin<
24
+ export const link: MixinFactory<
25
25
  HTMLElement,
26
26
  [href: string, options?: NavigationOptions],
27
27
  LinkCurrentProps
28
- >((handle, hostType) => {
29
- let suppressKeyboardClick = false
28
+ > = createMixin<HTMLElement, [href: string, options?: NavigationOptions], LinkCurrentProps>(
29
+ (handle, hostType) => {
30
+ let suppressKeyboardClick = false
30
31
 
31
- return (href, options, props: LinkCurrentProps) => {
32
- if (nativeLinkHostTypes.has(hostType)) {
33
- return renderMixinElement(handle.element, {
34
- ...(props ?? {}),
35
- href,
36
- ...(options?.target == null ? {} : { 'rmx-target': options.target }),
37
- ...(options?.src == null ? {} : { 'rmx-src': options.src }),
38
- ...(options?.resetScroll === false ? { 'rmx-reset-scroll': 'false' } : {}),
39
- })
40
- }
41
-
42
- let nextProps = { ...props }
43
- if (nextProps.role == null) {
44
- nextProps.role = 'link'
45
- }
46
- if (nextProps.disabled === true && nextProps['aria-disabled'] == null) {
47
- nextProps['aria-disabled'] = 'true'
48
- }
49
- if (hostType === 'button' && nextProps.type == null) {
50
- nextProps.type = 'button'
51
- }
52
- if (
53
- hostType !== 'button' &&
54
- nextProps.tabIndex == null &&
55
- nextProps.tabindex == null &&
56
- nextProps.contentEditable == null &&
57
- nextProps.contenteditable == null
58
- ) {
59
- nextProps.tabIndex = 0
60
- }
32
+ return (href, options, props: LinkCurrentProps) => {
33
+ if (nativeLinkHostTypes.has(hostType)) {
34
+ return renderMixinElement(handle.element, {
35
+ ...(props ?? {}),
36
+ href,
37
+ ...(options?.target == null ? {} : { 'rmx-target': options.target }),
38
+ ...(options?.src == null ? {} : { 'rmx-src': options.src }),
39
+ ...(options?.resetScroll === false ? { 'rmx-reset-scroll': 'false' } : {}),
40
+ })
41
+ }
61
42
 
62
- return renderMixinElement(handle.element, {
63
- ...nextProps,
64
- mix: [
65
- on('click', (event) => {
66
- if (event.detail === 0 && suppressKeyboardClick) {
67
- suppressKeyboardClick = false
68
- event.preventDefault()
69
- return
70
- }
43
+ let nextProps = { ...props }
44
+ if (nextProps.role == null) {
45
+ nextProps.role = 'link'
46
+ }
47
+ if (nextProps.disabled === true && nextProps['aria-disabled'] == null) {
48
+ nextProps['aria-disabled'] = 'true'
49
+ }
50
+ if (hostType === 'button' && nextProps.type == null) {
51
+ nextProps.type = 'button'
52
+ }
53
+ if (
54
+ hostType !== 'button' &&
55
+ nextProps.tabIndex == null &&
56
+ nextProps.tabindex == null &&
57
+ nextProps.contentEditable == null &&
58
+ nextProps.contenteditable == null
59
+ ) {
60
+ nextProps.tabIndex = 0
61
+ }
71
62
 
72
- suppressKeyboardClick = false
73
- if (isDisabledElement(event.currentTarget)) {
74
- event.preventDefault()
75
- return
76
- }
77
- if (event.button !== 0) return
63
+ return renderMixinElement(handle.element, {
64
+ ...nextProps,
65
+ mix: [
66
+ on('click', (event) => {
67
+ if (event.detail === 0 && suppressKeyboardClick) {
68
+ suppressKeyboardClick = false
69
+ event.preventDefault()
70
+ return
71
+ }
78
72
 
79
- event.preventDefault()
80
- if (event.metaKey || event.ctrlKey) {
81
- globalThis.open(href, '_blank')
82
- return
83
- }
73
+ suppressKeyboardClick = false
74
+ if (isDisabledElement(event.currentTarget)) {
75
+ event.preventDefault()
76
+ return
77
+ }
78
+ if (event.button !== 0) return
84
79
 
85
- void navigate(href, options)
86
- }),
87
- on('auxclick', (event) => {
88
- suppressKeyboardClick = false
89
- if (isDisabledElement(event.currentTarget)) {
90
80
  event.preventDefault()
91
- return
92
- }
93
- if (event.button !== 1) return
81
+ if (event.metaKey || event.ctrlKey) {
82
+ globalThis.open(href, '_blank')
83
+ return
84
+ }
94
85
 
95
- event.preventDefault()
96
- globalThis.open(href, '_blank')
97
- }),
98
- on('keydown', (event) => {
99
- if (event.key === 'Enter') {
100
- if (event.repeat) return
86
+ void navigate(href, options)
87
+ }),
88
+ on('auxclick', (event) => {
89
+ suppressKeyboardClick = false
101
90
  if (isDisabledElement(event.currentTarget)) {
102
91
  event.preventDefault()
103
92
  return
104
93
  }
94
+ if (event.button !== 1) return
105
95
 
106
- suppressKeyboardClick = hostType === 'button'
107
96
  event.preventDefault()
108
- void navigate(href, options)
109
- return
110
- }
97
+ globalThis.open(href, '_blank')
98
+ }),
99
+ on('keydown', (event) => {
100
+ if (event.key === 'Enter') {
101
+ if (event.repeat) return
102
+ if (isDisabledElement(event.currentTarget)) {
103
+ event.preventDefault()
104
+ return
105
+ }
111
106
 
112
- if (hostType === 'button' && event.key === ' ') {
113
- suppressKeyboardClick = true
114
- event.preventDefault()
115
- }
116
- }),
117
- on('keyup', (event) => {
118
- if (hostType === 'button' && event.key === ' ') {
119
- event.preventDefault()
120
- }
121
- }),
122
- ],
123
- })
124
- }
125
- })
107
+ suppressKeyboardClick = hostType === 'button'
108
+ event.preventDefault()
109
+ void navigate(href, options)
110
+ return
111
+ }
112
+
113
+ if (hostType === 'button' && event.key === ' ') {
114
+ suppressKeyboardClick = true
115
+ event.preventDefault()
116
+ }
117
+ }),
118
+ on('keyup', (event) => {
119
+ if (hostType === 'button' && event.key === ' ') {
120
+ event.preventDefault()
121
+ }
122
+ }),
123
+ ],
124
+ })
125
+ }
126
+ },
127
+ )
126
128
 
127
129
  function isDisabledElement(node: Element) {
128
130
  return (
@@ -130,6 +130,14 @@ export type MixinDescriptor<
130
130
  readonly __node?: (node: node) => void
131
131
  }
132
132
 
133
+ export type MixinFactory<
134
+ node extends EventTarget = Element,
135
+ args extends unknown[] = [],
136
+ props extends ElementProps = ElementProps,
137
+ > = <boundNode extends node = node>(
138
+ ...args: RebindTuple<args, node, boundNode>
139
+ ) => MixinDescriptor<boundNode, RebindTuple<args, node, boundNode>, props>
140
+
133
141
  type PreviousMixDepth = [0, 0, 1, 2, 3, 4]
134
142
  type FalsyMixValue = false | 0 | 0n | '' | null | undefined
135
143
  type NullableMixValue<descriptor> = descriptor | FalsyMixValue
@@ -244,7 +252,7 @@ export function createMixin<
244
252
  node extends EventTarget = Element,
245
253
  args extends unknown[] = [],
246
254
  props extends ElementProps = ElementProps,
247
- >(type: MixinType<node, args, props>) {
255
+ >(type: MixinType<node, args, props>): MixinFactory<node, args, props> {
248
256
  return <boundNode extends node = node>(
249
257
  ...args: RebindTuple<args, node, boundNode>
250
258
  ): MixinDescriptor<boundNode, RebindTuple<args, node, boundNode>, props> => ({