@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
@@ -8,7 +8,7 @@ Use it when the user should type draft text, filter a popup list, and still comm
8
8
 
9
9
  ```tsx
10
10
  import { css, type Handle } from 'remix/ui'
11
- import { Combobox, ComboboxOption, onComboboxChange } from 'remix/ui'
11
+ import { Combobox, ComboboxOption, onComboboxChange } from 'remix/ui/combobox'
12
12
 
13
13
  let airports = [
14
14
  {
@@ -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'
@@ -213,7 +214,9 @@ function getOptionSearchValue(option: {
213
214
  return option.searchValue ?? option.textValue ?? option.label
214
215
  }
215
216
 
216
- function ComboboxProvider(handle: Handle<ComboboxContextProps, ComboboxContextValue>) {
217
+ function ComboboxProvider(
218
+ handle: Handle<ComboboxContextProps, ComboboxContextValue>,
219
+ ): () => RemixNode {
217
220
  let inputRef: HTMLInputElement | undefined
218
221
  let listboxRef: listbox.ListboxRef | undefined
219
222
  let surfaceRef: HTMLElement | undefined
@@ -807,7 +810,11 @@ function getComboboxContext(handle: Handle | MixinHandle) {
807
810
  return handle.context.get(ComboboxProvider)
808
811
  }
809
812
 
810
- const inputMixin = createMixin<HTMLInputElement, [], ElementProps>((handle) => {
813
+ const inputMixin: MixinFactory<HTMLInputElement, [], ElementProps> = createMixin<
814
+ HTMLInputElement,
815
+ [],
816
+ ElementProps
817
+ >((handle) => {
811
818
  let context = getComboboxContext(handle)
812
819
 
813
820
  return (props) => [
@@ -882,7 +889,11 @@ const inputMixin = createMixin<HTMLInputElement, [], ElementProps>((handle) => {
882
889
  ]
883
890
  })
884
891
 
885
- const popoverMixin = createMixin<HTMLElement, [], ElementProps>((handle) => {
892
+ const popoverMixin: MixinFactory<HTMLElement, [], ElementProps> = createMixin<
893
+ HTMLElement,
894
+ [],
895
+ ElementProps
896
+ >((handle) => {
886
897
  let context = getComboboxContext(handle)
887
898
 
888
899
  return () => [
@@ -908,12 +919,20 @@ const popoverMixin = createMixin<HTMLElement, [], ElementProps>((handle) => {
908
919
  ]
909
920
  })
910
921
 
911
- const listMixin = createMixin<HTMLElement, [], ElementProps>((handle) => {
922
+ const listMixin: MixinFactory<HTMLElement, [], ElementProps> = createMixin<
923
+ HTMLElement,
924
+ [],
925
+ ElementProps
926
+ >((handle) => {
912
927
  let context = getComboboxContext(handle)
913
928
  return () => [attrs({ id: context.listId }), listbox.list()]
914
929
  })
915
930
 
916
- const hiddenInputMixin = createMixin<HTMLInputElement, [], ElementProps>((handle) => {
931
+ const hiddenInputMixin: MixinFactory<HTMLInputElement, [], ElementProps> = createMixin<
932
+ HTMLInputElement,
933
+ [],
934
+ ElementProps
935
+ >((handle) => {
917
936
  let context = getComboboxContext(handle)
918
937
  return () =>
919
938
  attrs({
@@ -924,8 +943,8 @@ const hiddenInputMixin = createMixin<HTMLInputElement, [], ElementProps>((handle
924
943
  })
925
944
  })
926
945
 
927
- const optionMixin = createMixin<HTMLElement, [options: ComboboxOptionOptions], ElementProps>(
928
- (handle) => {
946
+ const optionMixin: MixinFactory<HTMLElement, [options: ComboboxOptionOptions], ElementProps> =
947
+ createMixin<HTMLElement, [options: ComboboxOptionOptions], ElementProps>((handle) => {
929
948
  let context = getComboboxContext(handle)
930
949
 
931
950
  return (options) => {
@@ -946,8 +965,7 @@ const optionMixin = createMixin<HTMLElement, [options: ComboboxOptionOptions], E
946
965
  }),
947
966
  ]
948
967
  }
949
- },
950
- )
968
+ })
951
969
 
952
970
  export const Context = ComboboxProvider
953
971
  export const hiddenInput = hiddenInputMixin
@@ -968,11 +986,11 @@ const combobox = {
968
986
  export function onComboboxChange<target extends HTMLElement>(
969
987
  handler: ComboboxChangeHandler<target>,
970
988
  captureBoolean?: boolean,
971
- ) {
989
+ ): ReturnType<typeof on<target, typeof COMBOBOX_CHANGE_EVENT>> {
972
990
  return on(COMBOBOX_CHANGE_EVENT, handler, captureBoolean)
973
991
  }
974
992
 
975
- export function Combobox(handle: Handle<ComboboxProps>) {
993
+ export function Combobox(handle: Handle<ComboboxProps>): () => RemixNode {
976
994
  return () => {
977
995
  let { children, defaultValue, disabled, inputId, name, placeholder, ...divProps } = handle.props
978
996
 
@@ -995,7 +1013,7 @@ export function Combobox(handle: Handle<ComboboxProps>) {
995
1013
  }
996
1014
  }
997
1015
 
998
- export function ComboboxOption(handle: Handle<ComboboxOptionProps>) {
1016
+ export function ComboboxOption(handle: Handle<ComboboxOptionProps>): () => RemixNode {
999
1017
  return () => {
1000
1018
  let { children, disabled, label, mix, searchValue, value, ...divProps } = handle.props
1001
1019
 
@@ -31,7 +31,7 @@ export function createGlyphSheet(values: GlyphValues): GlyphSheetComponent {
31
31
  >,
32
32
  )
33
33
 
34
- function GlyphSheet(handle: Handle<GlyphSheetProps>) {
34
+ function GlyphSheet(handle: Handle<GlyphSheetProps>): () => RemixElement {
35
35
  return () => {
36
36
  let { style, ...svgProps } = handle.props
37
37
  let hiddenStyle = {
@@ -70,7 +70,7 @@ export function createGlyphSheet(values: GlyphValues): GlyphSheetComponent {
70
70
  })
71
71
  }
72
72
 
73
- export function Glyph(handle: Handle<GlyphProps>) {
73
+ export function Glyph(handle: Handle<GlyphProps>): () => RemixElement {
74
74
  return () => {
75
75
  let { fill, name, ...svgProps } = handle.props
76
76
  let glyphId = glyphContract[name].id
@@ -4,7 +4,9 @@ import {
4
4
  css,
5
5
  on,
6
6
  type CSSMixinDescriptor,
7
+ type ElementProps,
7
8
  type Handle,
9
+ type MixinFactory,
8
10
  type RemixNode,
9
11
  } from '@remix-run/ui'
10
12
  import { theme } from '../../theme/theme.ts'
@@ -149,7 +151,7 @@ export const optionStyle = itemCss
149
151
  export const glyphStyle = itemGlyphCss
150
152
  export const labelStyle = itemLabelCss
151
153
 
152
- function ListboxProvider(handle: Handle<ListboxProviderProps, ListboxContext>) {
154
+ function ListboxProvider(handle: Handle<ListboxProviderProps, ListboxContext>): () => RemixNode {
153
155
  let options: RegisteredOption[] = []
154
156
  let state: State = 'idle'
155
157
 
@@ -336,49 +338,51 @@ function ListboxProvider(handle: Handle<ListboxProviderProps, ListboxContext>) {
336
338
  }
337
339
  }
338
340
 
339
- const listMixin = createMixin<HTMLElement>((handle) => (props) => {
340
- let context = handle.context.get(ListboxProvider)
341
- return [
342
- attrs({
343
- tabIndex: props.tabIndex ?? -1,
344
- role: props.role ?? 'listbox',
345
- }),
346
- on('focus', () => {
347
- context.scrollActiveOptionIntoView()
348
- }),
349
- on('keydown', (event) => {
350
- switch (event.key) {
351
- case 'ArrowDown':
352
- event.preventDefault()
353
- context.navigate('next')
354
- break
355
- case 'ArrowUp':
356
- event.preventDefault()
357
- context.navigate('previous')
358
- break
359
- case 'Tab':
360
- event.preventDefault()
361
- context.navigate('first')
362
- break
363
- case 'Enter':
364
- case ' ':
365
- event.preventDefault()
366
- void context.select(context.activeValue)
367
- break
368
- case 'Home':
369
- event.preventDefault()
370
- context.navigate('first')
371
- break
372
- case 'End':
373
- event.preventDefault()
374
- context.navigate('last')
375
- }
376
- }),
377
- hiddenTypeahead((text) => {
378
- context.highlightSearchMatch(text)
379
- }),
380
- ]
381
- })
341
+ const listMixin: MixinFactory<HTMLElement, [], ElementProps> = createMixin<HTMLElement>(
342
+ (handle) => (props) => {
343
+ let context = handle.context.get(ListboxProvider)
344
+ return [
345
+ attrs({
346
+ tabIndex: props.tabIndex ?? -1,
347
+ role: props.role ?? 'listbox',
348
+ }),
349
+ on('focus', () => {
350
+ context.scrollActiveOptionIntoView()
351
+ }),
352
+ on('keydown', (event) => {
353
+ switch (event.key) {
354
+ case 'ArrowDown':
355
+ event.preventDefault()
356
+ context.navigate('next')
357
+ break
358
+ case 'ArrowUp':
359
+ event.preventDefault()
360
+ context.navigate('previous')
361
+ break
362
+ case 'Tab':
363
+ event.preventDefault()
364
+ context.navigate('first')
365
+ break
366
+ case 'Enter':
367
+ case ' ':
368
+ event.preventDefault()
369
+ void context.select(context.activeValue)
370
+ break
371
+ case 'Home':
372
+ event.preventDefault()
373
+ context.navigate('first')
374
+ break
375
+ case 'End':
376
+ event.preventDefault()
377
+ context.navigate('last')
378
+ }
379
+ }),
380
+ hiddenTypeahead((text) => {
381
+ context.highlightSearchMatch(text)
382
+ }),
383
+ ]
384
+ },
385
+ )
382
386
 
383
387
  export interface ListboxOption {
384
388
  id: string
@@ -388,50 +392,51 @@ export interface ListboxOption {
388
392
  textValue?: SearchValue
389
393
  }
390
394
 
391
- const optionMixin = createMixin<HTMLElement, [option: Omit<ListboxOption, 'id'>]>((handle) => {
392
- let optionRef: HTMLElement | undefined
393
-
394
- handle.queueTask((node) => {
395
- optionRef = node
396
- })
395
+ const optionMixin: MixinFactory<HTMLElement, [option: Omit<ListboxOption, 'id'>], ElementProps> =
396
+ createMixin<HTMLElement, [option: Omit<ListboxOption, 'id'>]>((handle) => {
397
+ let optionRef: HTMLElement | undefined
397
398
 
398
- return (option) => {
399
- let context = handle.context.get(ListboxProvider)
400
- context.registerOption({
401
- ...option,
402
- id: handle.id,
403
- get hidden() {
404
- return optionRef?.hidden === true
405
- },
406
- get node() {
407
- return optionRef as HTMLElement
408
- },
399
+ handle.queueTask((node) => {
400
+ optionRef = node
409
401
  })
410
402
 
411
- return [
412
- attrs({
413
- role: 'option',
403
+ return (option) => {
404
+ let context = handle.context.get(ListboxProvider)
405
+ context.registerOption({
406
+ ...option,
414
407
  id: handle.id,
415
- 'aria-selected': context.value === option.value ? 'true' : 'false',
416
- 'aria-disabled': option.disabled ? 'true' : 'false',
417
- 'data-highlighted': context.activeValue === option.value ? 'true' : 'false',
418
- }),
419
- !option.disabled && [
420
- on('click', () => {
421
- context.select(option.value)
408
+ get hidden() {
409
+ return optionRef?.hidden === true
410
+ },
411
+ get node() {
412
+ return optionRef as HTMLElement
413
+ },
414
+ })
415
+
416
+ return [
417
+ attrs({
418
+ role: 'option',
419
+ id: handle.id,
420
+ 'aria-selected': context.value === option.value ? 'true' : 'false',
421
+ 'aria-disabled': option.disabled ? 'true' : 'false',
422
+ 'data-highlighted': context.activeValue === option.value ? 'true' : 'false',
422
423
  }),
423
- on('mousemove', () => {
424
- if (context.activeValue === option.value) return
425
- context.highlight(option.value)
426
- }),
427
- on('mouseleave', () => {
428
- if (context.activeValue !== option.value) return
429
- context.highlight(null)
430
- }),
431
- ],
432
- ]
433
- }
434
- })
424
+ !option.disabled && [
425
+ on('click', () => {
426
+ context.select(option.value)
427
+ }),
428
+ on('mousemove', () => {
429
+ if (context.activeValue === option.value) return
430
+ context.highlight(option.value)
431
+ }),
432
+ on('mouseleave', () => {
433
+ if (context.activeValue !== option.value) return
434
+ context.highlight(null)
435
+ }),
436
+ ],
437
+ ]
438
+ }
439
+ })
435
440
 
436
441
  export const Context = ListboxProvider
437
442
  export const list = listMixin
@@ -9,6 +9,7 @@ import {
9
9
  type Dispatched,
10
10
  type ElementProps,
11
11
  type Handle,
12
+ type MixinFactory,
12
13
  type Props,
13
14
  type RemixNode,
14
15
  } from '@remix-run/ui'
@@ -416,7 +417,7 @@ function focusNode(node: HTMLElement | undefined) {
416
417
  }
417
418
  }
418
419
 
419
- function MenuProvider(handle: Handle<MenuProviderProps, MenuContextValue>) {
420
+ function MenuProvider(handle: Handle<MenuProviderProps, MenuContextValue>): () => RemixNode {
420
421
  let parent = handle.context.get(MenuProvider)
421
422
 
422
423
  let activeId: string | null = null
@@ -957,8 +958,8 @@ function MenuProvider(handle: Handle<MenuProviderProps, MenuContextValue>) {
957
958
  }
958
959
  }
959
960
 
960
- const triggerMixin = createMixin<HTMLElement, [options?: MenuTriggerOptions], ElementProps>(
961
- (handle) => {
961
+ const triggerMixin: MixinFactory<HTMLElement, [options?: MenuTriggerOptions], ElementProps> =
962
+ createMixin<HTMLElement, [options?: MenuTriggerOptions], ElementProps>((handle) => {
962
963
  let context = handle.context.get(MenuProvider)
963
964
 
964
965
  return (options = {}) => [
@@ -1005,10 +1006,13 @@ const triggerMixin = createMixin<HTMLElement, [options?: MenuTriggerOptions], El
1005
1006
  }
1006
1007
  }),
1007
1008
  ]
1008
- },
1009
- )
1009
+ })
1010
1010
 
1011
- const popoverMixin = createMixin<HTMLElement, [], ElementProps>((handle) => {
1011
+ const popoverMixin: MixinFactory<HTMLElement, [], ElementProps> = createMixin<
1012
+ HTMLElement,
1013
+ [],
1014
+ ElementProps
1015
+ >((handle) => {
1012
1016
  let context = handle.context.get(MenuProvider)
1013
1017
 
1014
1018
  return () => [
@@ -1045,7 +1049,11 @@ const popoverMixin = createMixin<HTMLElement, [], ElementProps>((handle) => {
1045
1049
  ]
1046
1050
  })
1047
1051
 
1048
- const listMixin = createMixin<HTMLElement, [], ElementProps>((handle) => {
1052
+ const listMixin: MixinFactory<HTMLElement, [], ElementProps> = createMixin<
1053
+ HTMLElement,
1054
+ [],
1055
+ ElementProps
1056
+ >((handle) => {
1049
1057
  let context = handle.context.get(MenuProvider)
1050
1058
 
1051
1059
  return () => [
@@ -1146,7 +1154,11 @@ const listMixin = createMixin<HTMLElement, [], ElementProps>((handle) => {
1146
1154
  ]
1147
1155
  })
1148
1156
 
1149
- const itemMixin = createMixin<HTMLElement, [options: MenuItemOptions], ElementProps>((handle) => {
1157
+ const itemMixin: MixinFactory<HTMLElement, [options: MenuItemOptions], ElementProps> = createMixin<
1158
+ HTMLElement,
1159
+ [options: MenuItemOptions],
1160
+ ElementProps
1161
+ >((handle) => {
1150
1162
  let itemRef: HTMLElement | undefined
1151
1163
 
1152
1164
  handle.queueTask((node) => {
@@ -1226,11 +1238,11 @@ const itemMixin = createMixin<HTMLElement, [options: MenuItemOptions], ElementPr
1226
1238
  }
1227
1239
  })
1228
1240
 
1229
- const submenuTriggerMixin = createMixin<
1241
+ const submenuTriggerMixin: MixinFactory<
1230
1242
  HTMLElement,
1231
1243
  [options: SubmenuTriggerOptions],
1232
1244
  ElementProps
1233
- >((handle) => {
1245
+ > = createMixin<HTMLElement, [options: SubmenuTriggerOptions], ElementProps>((handle) => {
1234
1246
  let itemRef: HTMLElement | undefined
1235
1247
  let openTimeoutId = 0
1236
1248
  let aborted = false
@@ -1400,7 +1412,7 @@ const menu = {
1400
1412
  export function onMenuSelect<target extends HTMLElement>(
1401
1413
  handler: MenuSelectHandler<target>,
1402
1414
  captureBoolean?: boolean,
1403
- ) {
1415
+ ): ReturnType<typeof on<target, typeof MENU_SELECT_EVENT>> {
1404
1416
  return on(MENU_SELECT_EVENT, handler, captureBoolean)
1405
1417
  }
1406
1418
 
@@ -1425,7 +1437,7 @@ export interface SubmenuProps
1425
1437
  menuLabel?: string
1426
1438
  }
1427
1439
 
1428
- export function Menu(handle: Handle<MenuProps>) {
1440
+ export function Menu(handle: Handle<MenuProps>): () => RemixNode {
1429
1441
  let buttonRef: HTMLButtonElement | undefined
1430
1442
 
1431
1443
  return () => {
@@ -1472,7 +1484,7 @@ export function Menu(handle: Handle<MenuProps>) {
1472
1484
  }
1473
1485
  }
1474
1486
 
1475
- export function MenuList(handle: Handle<MenuListProps>) {
1487
+ export function MenuList(handle: Handle<MenuListProps>): () => RemixNode {
1476
1488
  return () => {
1477
1489
  let { children, mix, ...divProps } = handle.props
1478
1490
 
@@ -1486,7 +1498,7 @@ export function MenuList(handle: Handle<MenuListProps>) {
1486
1498
  }
1487
1499
  }
1488
1500
 
1489
- export function MenuItem(handle: Handle<MenuItemProps>) {
1501
+ export function MenuItem(handle: Handle<MenuItemProps>): () => RemixNode {
1490
1502
  return () => {
1491
1503
  let { checked, children, disabled, label, mix, name, searchValue, type, value, ...divProps } =
1492
1504
  handle.props
@@ -1509,7 +1521,7 @@ export function MenuItem(handle: Handle<MenuItemProps>) {
1509
1521
  }
1510
1522
  }
1511
1523
 
1512
- export function Submenu(handle: Handle<SubmenuProps>) {
1524
+ export function Submenu(handle: Handle<SubmenuProps>): () => RemixNode {
1513
1525
  return () => {
1514
1526
  let { children, disabled, label, listProps, menuLabel, mix, searchValue, value, ...divProps } =
1515
1527
  handle.props
@@ -4,7 +4,9 @@ import {
4
4
  css,
5
5
  on,
6
6
  type CSSMixinDescriptor,
7
+ type ElementProps,
7
8
  type Handle,
9
+ type MixinFactory,
8
10
  type RemixNode,
9
11
  } from '@remix-run/ui'
10
12
  import { anchor as positionAnchor, type AnchorOptions } from '../anchor/anchor.ts'
@@ -75,7 +77,7 @@ const popoverSurfaceCss: CSSMixinDescriptor = css({
75
77
  export const contentStyle = popoverContentCss
76
78
  export const surfaceStyle = [popoverSurfaceCss, popoverSurfaceTransitionCss] as const
77
79
 
78
- function PopoverProvider(handle: Handle<PopoverProps, PopoverContext>) {
80
+ function PopoverProvider(handle: Handle<PopoverProps, PopoverContext>): () => RemixNode {
79
81
  handle.context.set({
80
82
  hideFocusTarget: null,
81
83
  showFocusTarget: null,
@@ -99,7 +101,10 @@ export interface PopoverHideRequest {
99
101
  target?: Node | null
100
102
  }
101
103
 
102
- const anchorMixin = createMixin<HTMLElement, [options: AnchorOptions]>((handle) => {
104
+ const anchorMixin: MixinFactory<HTMLElement, [options: AnchorOptions], ElementProps> = createMixin<
105
+ HTMLElement,
106
+ [options: AnchorOptions]
107
+ >((handle) => {
103
108
  let context = handle.context.get(PopoverProvider)
104
109
  return (options) => {
105
110
  handle.queueTask((node) => {
@@ -108,91 +113,97 @@ const anchorMixin = createMixin<HTMLElement, [options: AnchorOptions]>((handle)
108
113
  }
109
114
  })
110
115
 
111
- const surfaceMixin = createMixin<HTMLElement, [options: PopoverSurfaceOptions]>((handle) => {
112
- let openProp = false
113
- let cleanupAnchor = () => {}
114
- let unlockScroll = () => {}
115
- let context = handle.context.get(PopoverProvider)
116
-
117
- handle.queueTask((node, signal) => {
118
- context.surface = node
119
-
120
- signal.addEventListener('abort', () => {
121
- if (context.surface === node) {
122
- context.surface = null
123
- }
124
- })
125
- })
116
+ const surfaceMixin: MixinFactory<HTMLElement, [options: PopoverSurfaceOptions], ElementProps> =
117
+ createMixin<HTMLElement, [options: PopoverSurfaceOptions]>((handle) => {
118
+ let openProp = false
119
+ let cleanupAnchor = () => {}
120
+ let unlockScroll = () => {}
121
+ let context = handle.context.get(PopoverProvider)
126
122
 
127
- return (options) => {
128
- let wasOpen = openProp
129
- openProp = options.open
130
-
131
- handle.queueTask(async (node) => {
132
- if (openProp && !wasOpen) {
133
- node.showPopover()
134
- } else if (!openProp && wasOpen) {
135
- node.hidePopover()
136
- }
137
- })
123
+ handle.queueTask((node, signal) => {
124
+ context.surface = node
138
125
 
139
- return [
140
- attrs({ popover: 'manual' }),
141
-
142
- on('beforetoggle', (event) => {
143
- if (event.newState === 'open') {
144
- cleanupAnchor = positionAnchor(
145
- event.currentTarget,
146
- context.anchor!.node,
147
- context.anchor!.options,
148
- )
149
- unlockScroll = lockScroll()
150
- } else if (event.newState === 'closed') {
151
- cleanupAnchor()
152
- unlockScroll()
126
+ signal.addEventListener('abort', () => {
127
+ if (context.surface === node) {
128
+ context.surface = null
153
129
  }
154
- }),
130
+ })
131
+ })
155
132
 
156
- on('toggle', async (event) => {
157
- if (event.newState === 'open') {
158
- context.showFocusTarget?.focus()
159
- } else if (event.newState === 'closed' && options.restoreFocusOnHide !== false) {
160
- context.hideFocusTarget?.focus()
161
- }
162
- }),
133
+ return (options) => {
134
+ let wasOpen = openProp
135
+ openProp = options.open
163
136
 
164
- on('keydown', (event) => {
165
- if (event.key === 'Escape') {
166
- event.preventDefault()
167
- options.onHide({ reason: 'escape-key' })
137
+ handle.queueTask(async (node) => {
138
+ if (openProp && !wasOpen) {
139
+ node.showPopover()
140
+ } else if (!openProp && wasOpen) {
141
+ node.hidePopover()
168
142
  }
169
- }),
170
-
171
- onOutsideClick(
172
- openProp,
173
- (target) => {
174
- options.onHide({ reason: 'outside-click', target })
175
- },
176
- (target) => options.closeOnAnchorClick === false && !!context.anchor?.node.contains(target),
177
- options.stopOutsideClickPropagation ?? true,
178
- ),
179
- ]
180
- }
181
- })
182
-
183
- const focusOnShowMixin = createMixin<HTMLElement, []>((handle) => {
184
- handle.addEventListener('insert', (event) => {
185
- let context = handle.context.get(PopoverProvider)
186
- context.showFocusTarget = event.node
143
+ })
144
+
145
+ return [
146
+ attrs({ popover: 'manual' }),
147
+
148
+ on('beforetoggle', (event) => {
149
+ if (event.newState === 'open') {
150
+ cleanupAnchor = positionAnchor(
151
+ event.currentTarget,
152
+ context.anchor!.node,
153
+ context.anchor!.options,
154
+ )
155
+ unlockScroll = lockScroll()
156
+ } else if (event.newState === 'closed') {
157
+ cleanupAnchor()
158
+ unlockScroll()
159
+ }
160
+ }),
161
+
162
+ on('toggle', async (event) => {
163
+ if (event.newState === 'open') {
164
+ context.showFocusTarget?.focus()
165
+ } else if (event.newState === 'closed' && options.restoreFocusOnHide !== false) {
166
+ context.hideFocusTarget?.focus()
167
+ }
168
+ }),
169
+
170
+ on('keydown', (event) => {
171
+ if (event.key === 'Escape') {
172
+ event.preventDefault()
173
+ options.onHide({ reason: 'escape-key' })
174
+ }
175
+ }),
176
+
177
+ onOutsideClick(
178
+ openProp,
179
+ (target) => {
180
+ options.onHide({ reason: 'outside-click', target })
181
+ },
182
+ (target) =>
183
+ options.closeOnAnchorClick === false && !!context.anchor?.node.contains(target),
184
+ options.stopOutsideClickPropagation ?? true,
185
+ ),
186
+ ]
187
+ }
187
188
  })
188
- })
189
189
 
190
- const focusOnHideMixin = createMixin<HTMLElement, []>((handle) => {
191
- handle.addEventListener('insert', (event) => {
192
- let context = handle.context.get(PopoverProvider)
193
- context.hideFocusTarget = event.node
194
- })
195
- })
190
+ const focusOnShowMixin: MixinFactory<HTMLElement, [], ElementProps> = createMixin<HTMLElement, []>(
191
+ (handle) => {
192
+ handle.addEventListener('insert', (event) => {
193
+ let context = handle.context.get(PopoverProvider)
194
+ context.showFocusTarget = event.node
195
+ })
196
+ },
197
+ )
198
+
199
+ const focusOnHideMixin: MixinFactory<HTMLElement, [], ElementProps> = createMixin<HTMLElement, []>(
200
+ (handle) => {
201
+ handle.addEventListener('insert', (event) => {
202
+ let context = handle.context.get(PopoverProvider)
203
+ context.hideFocusTarget = event.node
204
+ })
205
+ },
206
+ )
196
207
 
197
208
  export const Context = PopoverProvider
198
209
  export const anchor = anchorMixin