@remix-run/ui 0.1.1 → 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 (243) hide show
  1. package/README.md +33 -10
  2. package/dist/animation/animate-layout-mixin.js +66 -66
  3. package/dist/animation/animate-layout-mixin.js.map +1 -1
  4. package/dist/animation/spring.d.ts +6 -35
  5. package/dist/animation/spring.js +9 -8
  6. package/dist/animation/spring.js.map +1 -1
  7. package/dist/components/accordion/accordion.d.ts +7 -7
  8. package/dist/components/accordion/accordion.js +0 -2
  9. package/dist/components/accordion/accordion.js.map +1 -1
  10. package/dist/components/accordion/demos/card.demo.d.ts +6 -0
  11. package/dist/components/accordion/demos/card.demo.js +58 -0
  12. package/dist/components/accordion/demos/card.demo.js.map +1 -0
  13. package/dist/components/accordion/demos/multiple.demo.d.ts +6 -0
  14. package/dist/components/accordion/demos/multiple.demo.js +19 -0
  15. package/dist/components/accordion/demos/multiple.demo.js.map +1 -0
  16. package/dist/components/accordion/demos/overview.demo.d.ts +6 -0
  17. package/dist/components/accordion/demos/overview.demo.js +19 -0
  18. package/dist/components/accordion/demos/overview.demo.js.map +1 -0
  19. package/dist/components/breadcrumbs/breadcrumbs.d.ts +1 -1
  20. package/dist/components/breadcrumbs/breadcrumbs.js +0 -2
  21. package/dist/components/breadcrumbs/breadcrumbs.js.map +1 -1
  22. package/dist/components/breadcrumbs/demos/basic.demo.d.ts +5 -0
  23. package/dist/components/breadcrumbs/demos/basic.demo.js +14 -0
  24. package/dist/components/breadcrumbs/demos/basic.demo.js.map +1 -0
  25. package/dist/components/breadcrumbs/demos/separator.demo.d.ts +5 -0
  26. package/dist/components/breadcrumbs/demos/separator.demo.js +14 -0
  27. package/dist/components/breadcrumbs/demos/separator.demo.js.map +1 -0
  28. package/dist/components/button/button.d.ts +86 -4
  29. package/dist/components/button/button.js +63 -3
  30. package/dist/components/button/button.js.map +1 -1
  31. package/dist/components/button/demos/aliases.demo.d.ts +6 -0
  32. package/dist/components/button/demos/aliases.demo.js +18 -0
  33. package/dist/components/button/demos/aliases.demo.js.map +1 -0
  34. package/dist/components/button/demos/basic.demo.d.ts +6 -0
  35. package/dist/components/button/demos/basic.demo.js +19 -0
  36. package/dist/components/button/demos/basic.demo.js.map +1 -0
  37. package/dist/components/button/demos/states.demo.d.ts +6 -0
  38. package/dist/components/button/demos/states.demo.js +29 -0
  39. package/dist/components/button/demos/states.demo.js.map +1 -0
  40. package/dist/components/combobox/combobox.d.ts +11 -11
  41. package/dist/components/combobox/combobox.js +0 -2
  42. package/dist/components/combobox/combobox.js.map +1 -1
  43. package/dist/components/combobox/demos/overview.demo.d.ts +6 -0
  44. package/dist/components/combobox/demos/overview.demo.js +153 -0
  45. package/dist/components/combobox/demos/overview.demo.js.map +1 -0
  46. package/dist/components/listbox/demos/overview.demo.d.ts +6 -0
  47. package/dist/components/listbox/demos/overview.demo.js +64 -0
  48. package/dist/components/listbox/demos/overview.demo.js.map +1 -0
  49. package/dist/components/listbox/listbox.d.ts +4 -9
  50. package/dist/components/listbox/listbox.js +21 -33
  51. package/dist/components/listbox/listbox.js.map +1 -1
  52. package/dist/components/menu/demos/bubbling.demo.d.ts +5 -0
  53. package/dist/components/menu/demos/bubbling.demo.js +15 -0
  54. package/dist/components/menu/demos/bubbling.demo.js.map +1 -0
  55. package/dist/components/menu/demos/overview.demo.d.ts +6 -0
  56. package/dist/components/menu/demos/overview.demo.js +32 -0
  57. package/dist/components/menu/demos/overview.demo.js.map +1 -0
  58. package/dist/components/menu/menu.d.ts +15 -24
  59. package/dist/components/menu/menu.js +23 -38
  60. package/dist/components/menu/menu.js.map +1 -1
  61. package/dist/components/popover/demos/overview.demo.d.ts +6 -0
  62. package/dist/components/popover/demos/overview.demo.js +64 -0
  63. package/dist/components/popover/demos/overview.demo.js.map +1 -0
  64. package/dist/components/popover/popover.d.ts +5 -5
  65. package/dist/components/popover/popover.js.map +1 -1
  66. package/dist/components/select/demos/deconstructed.demo.d.ts +6 -0
  67. package/dist/components/select/demos/deconstructed.demo.js +53 -0
  68. package/dist/components/select/demos/deconstructed.demo.js.map +1 -0
  69. package/dist/components/select/demos/overview.demo.d.ts +5 -0
  70. package/dist/components/select/demos/overview.demo.js +27 -0
  71. package/dist/components/select/demos/overview.demo.js.map +1 -0
  72. package/dist/components/select/select.d.ts +11 -11
  73. package/dist/components/select/select.js +17 -26
  74. package/dist/components/select/select.js.map +1 -1
  75. package/dist/components/tabs/tabs.d.ts +6 -6
  76. package/dist/components/tabs/tabs.js +0 -2
  77. package/dist/components/tabs/tabs.js.map +1 -1
  78. package/dist/index.d.ts +2 -2
  79. package/dist/index.js.map +1 -1
  80. package/dist/interactions/keydown/keydown.d.ts +1 -1
  81. package/dist/interactions/outside-click/outside-click-mixin.d.ts +2 -1
  82. package/dist/interactions/typeahead/typeahead-mixin.d.ts +3 -1
  83. package/dist/runtime/component.d.ts +18 -12
  84. package/dist/runtime/component.js +93 -80
  85. package/dist/runtime/component.js.map +1 -1
  86. package/dist/runtime/core/attributes.d.ts +14 -0
  87. package/dist/runtime/core/attributes.js +103 -0
  88. package/dist/runtime/core/attributes.js.map +1 -0
  89. package/dist/runtime/core/children.d.ts +7 -0
  90. package/dist/runtime/core/children.js +30 -0
  91. package/dist/runtime/core/children.js.map +1 -0
  92. package/dist/runtime/core/props.d.ts +9 -0
  93. package/dist/runtime/core/props.js +170 -0
  94. package/dist/runtime/core/props.js.map +1 -0
  95. package/dist/runtime/core/vnode.d.ts +3 -0
  96. package/dist/runtime/core/vnode.js +39 -0
  97. package/dist/runtime/core/vnode.js.map +1 -0
  98. package/dist/runtime/create-element.js +2 -1
  99. package/dist/runtime/create-element.js.map +1 -1
  100. package/dist/runtime/diff-dom.js +47 -14
  101. package/dist/runtime/diff-dom.js.map +1 -1
  102. package/dist/runtime/diff-props.d.ts +1 -2
  103. package/dist/runtime/diff-props.js +2 -181
  104. package/dist/runtime/diff-props.js.map +1 -1
  105. package/dist/runtime/dom.d.ts +20 -9
  106. package/dist/runtime/frame.d.ts +23 -2
  107. package/dist/runtime/frame.js +41 -47
  108. package/dist/runtime/frame.js.map +1 -1
  109. package/dist/runtime/jsx.d.ts +1 -1
  110. package/dist/runtime/jsx.js +2 -28
  111. package/dist/runtime/jsx.js.map +1 -1
  112. package/dist/runtime/mixins/link-mixin.d.ts +5 -1
  113. package/dist/runtime/mixins/link-mixin.js.map +1 -1
  114. package/dist/runtime/mixins/mixin.d.ts +2 -1
  115. package/dist/runtime/mixins/mixin.js +9 -4
  116. package/dist/runtime/mixins/mixin.js.map +1 -1
  117. package/dist/runtime/mixins/on-mixin.d.ts +15 -4
  118. package/dist/runtime/mixins/on-mixin.js +9 -2
  119. package/dist/runtime/mixins/on-mixin.js.map +1 -1
  120. package/dist/runtime/mixins/ref-mixin.d.ts +2 -1
  121. package/dist/runtime/mixins/ref-mixin.js.map +1 -1
  122. package/dist/runtime/navigation.js +9 -1
  123. package/dist/runtime/navigation.js.map +1 -1
  124. package/dist/runtime/reconcile.d.ts +1 -1
  125. package/dist/runtime/reconcile.js +452 -197
  126. package/dist/runtime/reconcile.js.map +1 -1
  127. package/dist/runtime/run.d.ts +17 -2
  128. package/dist/runtime/run.js.map +1 -1
  129. package/dist/runtime/scheduler.d.ts +10 -10
  130. package/dist/runtime/scheduler.js +1 -1
  131. package/dist/runtime/scheduler.js.map +1 -1
  132. package/dist/runtime/stream-protocol.d.ts +8 -0
  133. package/dist/runtime/stream-protocol.js +20 -0
  134. package/dist/runtime/stream-protocol.js.map +1 -0
  135. package/dist/runtime/svg-attributes.js +1 -0
  136. package/dist/runtime/svg-attributes.js.map +1 -1
  137. package/dist/runtime/to-vnode.js +7 -11
  138. package/dist/runtime/to-vnode.js.map +1 -1
  139. package/dist/runtime/vnode.d.ts +11 -5
  140. package/dist/runtime/vnode.js +6 -3
  141. package/dist/runtime/vnode.js.map +1 -1
  142. package/dist/server/stream.d.ts +2 -0
  143. package/dist/server/stream.js +154 -114
  144. package/dist/server/stream.js.map +1 -1
  145. package/dist/style/css-mixin.d.ts +2 -1
  146. package/dist/style/css-mixin.js.map +1 -1
  147. package/dist/style/index.d.ts +1 -1
  148. package/dist/style/layers.d.ts +2 -0
  149. package/dist/style/layers.js +3 -0
  150. package/dist/style/layers.js.map +1 -0
  151. package/dist/style/stylesheet.d.ts +13 -11
  152. package/dist/style/stylesheet.js +69 -8
  153. package/dist/style/stylesheet.js.map +1 -1
  154. package/dist/theme/contract.d.ts +24 -222
  155. package/dist/theme/contract.js +2 -2
  156. package/dist/theme/contract.js.map +1 -1
  157. package/dist/theme/glyph-contract.d.ts +1 -3
  158. package/dist/theme/glyph-contract.js.map +1 -1
  159. package/dist/theme/presets/rmx-01/index.d.ts +4 -2
  160. package/dist/theme/presets/rmx-01/index.js.map +1 -1
  161. package/dist/theme/runtime.js +4 -1
  162. package/dist/theme/runtime.js.map +1 -1
  163. package/dist/utils/scroll-lock.d.ts +3 -3
  164. package/dist/utils/scroll-lock.js.map +1 -1
  165. package/package.json +3 -3
  166. package/src/animation/README.md +316 -0
  167. package/src/animation/animate-layout-mixin.ts +79 -71
  168. package/src/animation/spring.ts +21 -9
  169. package/src/components/accordion/README.md +166 -0
  170. package/src/components/accordion/accordion.tsx +14 -14
  171. package/src/components/accordion/demos/card.demo.tsx +113 -0
  172. package/src/components/accordion/demos/multiple.demo.tsx +56 -0
  173. package/src/components/accordion/demos/overview.demo.tsx +56 -0
  174. package/src/components/anchor/README.md +153 -0
  175. package/src/components/anchor/anchor.ts +1 -1
  176. package/src/components/breadcrumbs/README.md +55 -0
  177. package/src/components/breadcrumbs/breadcrumbs.tsx +1 -3
  178. package/src/components/breadcrumbs/demos/basic.demo.tsx +17 -0
  179. package/src/components/breadcrumbs/demos/separator.demo.tsx +18 -0
  180. package/src/components/button/README.md +4 -4
  181. package/src/components/button/button.tsx +118 -20
  182. package/src/components/button/demos/aliases.demo.tsx +27 -0
  183. package/src/components/button/demos/basic.demo.tsx +30 -0
  184. package/src/components/button/demos/states.demo.tsx +52 -0
  185. package/src/components/combobox/README.md +1 -1
  186. package/src/components/combobox/combobox.tsx +37 -21
  187. package/src/components/combobox/demos/overview.demo.tsx +199 -0
  188. package/src/components/glyph/README.md +72 -0
  189. package/src/components/glyph/glyph.tsx +2 -2
  190. package/src/components/listbox/README.md +115 -0
  191. package/src/components/listbox/demos/overview.demo.tsx +101 -0
  192. package/src/components/listbox/listbox.ts +106 -110
  193. package/src/components/menu/README.md +96 -0
  194. package/src/components/menu/demos/bubbling.demo.tsx +34 -0
  195. package/src/components/menu/demos/overview.demo.tsx +76 -0
  196. package/src/components/menu/menu.tsx +61 -55
  197. package/src/components/popover/README.md +3 -3
  198. package/src/components/popover/demos/overview.demo.tsx +143 -0
  199. package/src/components/popover/popover.ts +90 -79
  200. package/src/components/select/README.md +107 -0
  201. package/src/components/select/demos/deconstructed.demo.tsx +95 -0
  202. package/src/components/select/demos/overview.demo.tsx +51 -0
  203. package/src/components/select/select.tsx +53 -36
  204. package/src/components/tabs/tabs.tsx +10 -5
  205. package/src/index.ts +2 -1
  206. package/src/runtime/component.ts +122 -88
  207. package/src/runtime/core/attributes.ts +118 -0
  208. package/src/runtime/core/children.ts +42 -0
  209. package/src/runtime/core/props.ts +187 -0
  210. package/src/runtime/core/vnode.ts +48 -0
  211. package/src/runtime/create-element.ts +2 -1
  212. package/src/runtime/diff-dom.ts +59 -12
  213. package/src/runtime/diff-props.ts +2 -187
  214. package/src/runtime/dom.ts +20 -9
  215. package/src/runtime/frame.ts +71 -55
  216. package/src/runtime/jsx.ts +3 -30
  217. package/src/runtime/mixins/link-mixin.ts +88 -86
  218. package/src/runtime/mixins/mixin.ts +18 -5
  219. package/src/runtime/mixins/on-mixin.ts +20 -12
  220. package/src/runtime/mixins/ref-mixin.ts +4 -5
  221. package/src/runtime/navigation.ts +10 -1
  222. package/src/runtime/reconcile.ts +563 -223
  223. package/src/runtime/run.ts +17 -1
  224. package/src/runtime/scheduler.ts +20 -20
  225. package/src/runtime/stream-protocol.ts +27 -0
  226. package/src/runtime/svg-attributes.ts +1 -0
  227. package/src/runtime/to-vnode.ts +7 -10
  228. package/src/runtime/vnode.ts +16 -9
  229. package/src/server/README.md +90 -0
  230. package/src/server/stream.ts +176 -124
  231. package/src/style/css-mixin.ts +6 -1
  232. package/src/style/index.ts +1 -2
  233. package/src/style/layers.ts +2 -0
  234. package/src/style/stylesheet.ts +88 -11
  235. package/src/test/README.md +107 -0
  236. package/src/test/utils.ts +3 -1
  237. package/src/theme/README.md +103 -0
  238. package/src/theme/contract.ts +37 -5
  239. package/src/theme/glyph-contract.ts +3 -1
  240. package/src/theme/presets/rmx-01/index.ts +4 -2
  241. package/src/theme/runtime.ts +4 -1
  242. package/src/utils/scroll-lock/README.md +33 -0
  243. package/src/utils/scroll-lock.ts +7 -3
@@ -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'
@@ -17,17 +19,8 @@ import { flashAttribute } from '../../utils/flash-attribute.ts'
17
19
 
18
20
  export type ListboxValue = string | null
19
21
 
20
- enum NavigationStrategy {
21
- Next = 0,
22
- Previous = 1,
23
- First = 2,
24
- Last = 3,
25
- }
26
-
27
- enum State {
28
- Idle = 'idle',
29
- Selecting = 'selecting',
30
- }
22
+ type NavigationStrategy = 'next' | 'previous' | 'first' | 'last'
23
+ type State = 'idle' | 'selecting'
31
24
 
32
25
  interface Values {
33
26
  value: ListboxValue
@@ -158,9 +151,9 @@ export const optionStyle = itemCss
158
151
  export const glyphStyle = itemGlyphCss
159
152
  export const labelStyle = itemLabelCss
160
153
 
161
- function ListboxProvider(handle: Handle<ListboxProviderProps, ListboxContext>) {
154
+ function ListboxProvider(handle: Handle<ListboxProviderProps, ListboxContext>): () => RemixNode {
162
155
  let options: RegisteredOption[] = []
163
- let state = State.Idle
156
+ let state: State = 'idle'
164
157
 
165
158
  function getOption(value: ListboxValue) {
166
159
  return options.find((option) => option.value === value)
@@ -221,16 +214,16 @@ function ListboxProvider(handle: Handle<ListboxProviderProps, ListboxContext>) {
221
214
  return findSearchMatch(text, fromValue)
222
215
  },
223
216
  navigateFirst() {
224
- context.navigate(NavigationStrategy.First)
217
+ context.navigate('first')
225
218
  },
226
219
  navigateLast() {
227
- context.navigate(NavigationStrategy.Last)
220
+ context.navigate('last')
228
221
  },
229
222
  navigateNext() {
230
- context.navigate(NavigationStrategy.Next)
223
+ context.navigate('next')
231
224
  },
232
225
  navigatePrevious() {
233
- context.navigate(NavigationStrategy.Previous)
226
+ context.navigate('previous')
234
227
  },
235
228
  scrollActiveOptionIntoView() {
236
229
  context.scrollActiveOptionIntoView()
@@ -261,13 +254,13 @@ function ListboxProvider(handle: Handle<ListboxProviderProps, ListboxContext>) {
261
254
  },
262
255
 
263
256
  async select(value) {
264
- if (state === State.Selecting) {
257
+ if (state === 'selecting') {
265
258
  return
266
259
  }
267
- state = State.Selecting
260
+ state = 'selecting'
268
261
  let option = getOption(value)
269
262
  if (!isInteractableOption(option)) {
270
- state = State.Idle
263
+ state = 'idle'
271
264
  return
272
265
  }
273
266
  handle.props.onSelect(value, option)
@@ -279,17 +272,17 @@ function ListboxProvider(handle: Handle<ListboxProviderProps, ListboxContext>) {
279
272
  )
280
273
  }
281
274
  await handle.props.onSelectSettled?.(value, option)
282
- state = State.Idle
275
+ state = 'idle'
283
276
  },
284
277
 
285
278
  highlight(value) {
286
- if (state === State.Selecting) return
279
+ if (state === 'selecting') return
287
280
  let option = getOption(value)
288
281
  handle.props.onHighlight(value, option)
289
282
  },
290
283
 
291
284
  highlightSearchMatch(text) {
292
- if (state === State.Selecting) return
285
+ if (state === 'selecting') return
293
286
 
294
287
  let option = findSearchMatch(text, handle.props.activeValue)
295
288
 
@@ -300,7 +293,7 @@ function ListboxProvider(handle: Handle<ListboxProviderProps, ListboxContext>) {
300
293
  },
301
294
 
302
295
  navigate(strategy: NavigationStrategy) {
303
- if (state === State.Selecting) return
296
+ if (state === 'selecting') return
304
297
 
305
298
  let option: RegisteredOption | undefined
306
299
  let interactableOptions = getInteractableOptions()
@@ -309,19 +302,19 @@ function ListboxProvider(handle: Handle<ListboxProviderProps, ListboxContext>) {
309
302
  )
310
303
 
311
304
  switch (strategy) {
312
- case NavigationStrategy.Next:
305
+ case 'next':
313
306
  option = interactableOptions[activeIndex + 1] ?? interactableOptions[0]
314
307
  break
315
- case NavigationStrategy.Previous:
308
+ case 'previous':
316
309
  option =
317
310
  activeIndex === -1
318
311
  ? interactableOptions[interactableOptions.length - 1]
319
312
  : interactableOptions[activeIndex - 1]
320
313
  break
321
- case NavigationStrategy.First:
314
+ case 'first':
322
315
  option = interactableOptions[0]
323
316
  break
324
- case NavigationStrategy.Last:
317
+ case 'last':
325
318
  option = interactableOptions[interactableOptions.length - 1]
326
319
  break
327
320
  }
@@ -345,49 +338,51 @@ function ListboxProvider(handle: Handle<ListboxProviderProps, ListboxContext>) {
345
338
  }
346
339
  }
347
340
 
348
- const listMixin = createMixin<HTMLElement>((handle) => (props) => {
349
- let context = handle.context.get(ListboxProvider)
350
- return [
351
- attrs({
352
- tabIndex: props.tabIndex ?? -1,
353
- role: props.role ?? 'listbox',
354
- }),
355
- on('focus', () => {
356
- context.scrollActiveOptionIntoView()
357
- }),
358
- on('keydown', (event) => {
359
- switch (event.key) {
360
- case 'ArrowDown':
361
- event.preventDefault()
362
- context.navigate(NavigationStrategy.Next)
363
- break
364
- case 'ArrowUp':
365
- event.preventDefault()
366
- context.navigate(NavigationStrategy.Previous)
367
- break
368
- case 'Tab':
369
- event.preventDefault()
370
- context.navigate(NavigationStrategy.First)
371
- break
372
- case 'Enter':
373
- case ' ':
374
- event.preventDefault()
375
- void context.select(context.activeValue)
376
- break
377
- case 'Home':
378
- event.preventDefault()
379
- context.navigate(NavigationStrategy.First)
380
- break
381
- case 'End':
382
- event.preventDefault()
383
- context.navigate(NavigationStrategy.Last)
384
- }
385
- }),
386
- hiddenTypeahead((text) => {
387
- context.highlightSearchMatch(text)
388
- }),
389
- ]
390
- })
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
+ )
391
386
 
392
387
  export interface ListboxOption {
393
388
  id: string
@@ -397,50 +392,51 @@ export interface ListboxOption {
397
392
  textValue?: SearchValue
398
393
  }
399
394
 
400
- const optionMixin = createMixin<HTMLElement, [option: Omit<ListboxOption, 'id'>]>((handle) => {
401
- let optionRef: HTMLElement | undefined
395
+ const optionMixin: MixinFactory<HTMLElement, [option: Omit<ListboxOption, 'id'>], ElementProps> =
396
+ createMixin<HTMLElement, [option: Omit<ListboxOption, 'id'>]>((handle) => {
397
+ let optionRef: HTMLElement | undefined
402
398
 
403
- handle.queueTask((node) => {
404
- optionRef = node
405
- })
406
-
407
- return (option) => {
408
- let context = handle.context.get(ListboxProvider)
409
- context.registerOption({
410
- ...option,
411
- id: handle.id,
412
- get hidden() {
413
- return optionRef?.hidden === true
414
- },
415
- get node() {
416
- return optionRef as HTMLElement
417
- },
399
+ handle.queueTask((node) => {
400
+ optionRef = node
418
401
  })
419
402
 
420
- return [
421
- attrs({
422
- role: 'option',
403
+ return (option) => {
404
+ let context = handle.context.get(ListboxProvider)
405
+ context.registerOption({
406
+ ...option,
423
407
  id: handle.id,
424
- 'aria-selected': context.value === option.value ? 'true' : 'false',
425
- 'aria-disabled': option.disabled ? 'true' : 'false',
426
- 'data-highlighted': context.activeValue === option.value ? 'true' : 'false',
427
- }),
428
- !option.disabled && [
429
- on('click', () => {
430
- context.select(option.value)
431
- }),
432
- on('mousemove', () => {
433
- if (context.activeValue === option.value) return
434
- context.highlight(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',
435
423
  }),
436
- on('mouseleave', () => {
437
- if (context.activeValue !== option.value) return
438
- context.highlight(null)
439
- }),
440
- ],
441
- ]
442
- }
443
- })
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
+ })
444
440
 
445
441
  export const Context = ListboxProvider
446
442
  export const list = listMixin
@@ -0,0 +1,96 @@
1
+ # menu
2
+
3
+ `Menu` renders a button-triggered menu with keyboard navigation, checked items, selection events, and nested submenus. Use it for action menus and command groups.
4
+
5
+ ## Usage
6
+
7
+ ```tsx
8
+ import type { Handle } from 'remix/ui'
9
+ import { Menu, MenuItem, Submenu, onMenuSelect } from 'remix/ui/menu'
10
+ import { separatorStyle } from 'remix/ui/separator'
11
+
12
+ export function ViewMenu(handle: Handle) {
13
+ let wordWrap = false
14
+ let density = 'comfortable'
15
+
16
+ return () => (
17
+ <Menu
18
+ label="View"
19
+ mix={onMenuSelect((event) => {
20
+ if (event.item.name === 'wordWrap') {
21
+ wordWrap = event.item.checked ?? false
22
+ } else if (event.item.name === 'density' && event.item.value) {
23
+ density = event.item.value
24
+ }
25
+
26
+ void handle.update()
27
+ })}
28
+ >
29
+ <MenuItem checked={wordWrap} name="wordWrap" type="checkbox">
30
+ Word wrap
31
+ </MenuItem>
32
+ <MenuItem disabled name="minimap">
33
+ Minimap
34
+ </MenuItem>
35
+ <hr mix={separatorStyle} />
36
+ <MenuItem checked={density === 'compact'} name="density" type="radio" value="compact">
37
+ Compact
38
+ </MenuItem>
39
+ <MenuItem checked={density === 'comfortable'} name="density" type="radio" value="comfortable">
40
+ Comfortable
41
+ </MenuItem>
42
+ <Submenu label="Zoom">
43
+ <MenuItem name="zoomIn" value="zoom-in">
44
+ Zoom in
45
+ </MenuItem>
46
+ <MenuItem name="zoomOut" value="zoom-out">
47
+ Zoom out
48
+ </MenuItem>
49
+ </Submenu>
50
+ </Menu>
51
+ )
52
+ }
53
+ ```
54
+
55
+ Use `label` or `searchValue` when the rendered item content is not the text that should be used for event labels or typeahead.
56
+
57
+ ```tsx
58
+ <MenuItem label="Open command palette" name="commandPalette" searchValue="palette">
59
+ Command palette
60
+ </MenuItem>
61
+ ```
62
+
63
+ Use `menuLabel` when the menu surface needs a different accessible label from the visible trigger.
64
+
65
+ ```tsx
66
+ <Menu label="..." menuLabel="Project actions">
67
+ <MenuItem name="rename">Rename project</MenuItem>
68
+ </Menu>
69
+ ```
70
+
71
+ ## `menu.*`
72
+
73
+ - `Menu`: composed trigger, popover, and list component for the common menu case.
74
+ - `MenuItem`: menu item wrapper. Supports regular, checkbox, and radio item roles through `type`, `checked`, `name`, `value`, `label`, `disabled`, and `searchValue`.
75
+ - `Submenu`: nested menu wrapper with its own trigger and child menu surface.
76
+ - `MenuList`: lower-level list wrapper for custom composition.
77
+ - `onMenuSelect(...)`: event mixin for the bubbling `MenuSelectEvent`.
78
+ - `MenuSelectEvent`: bubbling event class whose `item` describes the selected item.
79
+ - `MenuSelectItem`: selected item shape with `checked`, `id`, `label`, `name`, `type`, and `value`.
80
+ - `menu.Context`, `menu.trigger()`, `menu.popover()`, `menu.list()`, `menu.item(...)`, and `menu.submenuTrigger(...)`: lower-level composition primitives.
81
+ - `buttonStyle`, `popoverStyle`, `listStyle`, `itemStyle`, `itemSlotStyle`, `itemLabelStyle`, `itemGlyphStyle`, and `triggerGlyphStyle`: flat style mixins used by the wrappers.
82
+ - `MenuProps`, `MenuItemProps`, `MenuListProps`, `MenuProviderProps`, `MenuTriggerOptions`, `MenuItemOptions`, and `SubmenuProps`: public TypeScript props and option types.
83
+
84
+ ## Behavior Notes
85
+
86
+ - Click opens the root menu and focuses the list; clicking the trigger again closes it and restores focus.
87
+ - `ArrowDown` opens from the trigger at the first enabled item. `ArrowUp` opens at the last enabled item. Enter and Space open the menu with focus on the list.
88
+ - Keyboard navigation skips disabled items and does not wrap past the first or last enabled item.
89
+ - `Home` and `End` move to the first and last enabled item. Enter and Space activate the highlighted item.
90
+ - Printable keys use typeahead. Typeahead matches `searchValue` when provided and otherwise uses the item label.
91
+ - Submenus open with `ArrowRight`, close with `ArrowLeft`, and are anchored to the submenu trigger with `right-start` placement.
92
+ - Pointer movement highlights enabled items. Submenus open after a short focus/pointer delay and use hover aim so they stay open while moving toward the child surface.
93
+ - Selecting a regular item flashes that item. Selecting a checkbox or radio item flashes the committed checked state.
94
+ - Selection dispatches one bubbled `MenuSelectEvent`, closes the full menu tree, and restores focus to the root trigger.
95
+ - The composed `Menu` re-dispatches selection from its trigger so handlers on the `Menu` button and shared ancestors see the event once.
96
+ - `menuLabel`, `Submenu.menuLabel`, and `Submenu.listProps` let composed menus label and customize menu surfaces separately from trigger content.
@@ -0,0 +1,34 @@
1
+ import { on } from '@remix-run/ui'
2
+ import { Menu, MenuItem, onMenuSelect } from '@remix-run/ui/menu'
3
+
4
+ /**
5
+ * @name Menu Event Bubbling
6
+ * @description Item-level handlers fire before the root handler, letting individual items intercept events while others bubble up.
7
+ */
8
+ export default function Example() {
9
+ return () => (
10
+ <Menu
11
+ label="Project"
12
+ menuLabel="Project actions"
13
+ mix={onMenuSelect((event) => {
14
+ console.log('Menu root handler:', event.item)
15
+ })}
16
+ >
17
+ <MenuItem name="open" value="open-project">
18
+ Open project
19
+ </MenuItem>
20
+ <MenuItem
21
+ name="rename"
22
+ value="rename-project"
23
+ mix={onMenuSelect((event) => {
24
+ console.log('Menu item handler:', event.item)
25
+ })}
26
+ >
27
+ Rename project
28
+ </MenuItem>
29
+ <MenuItem name="duplicate" value="duplicate-project">
30
+ Duplicate project
31
+ </MenuItem>
32
+ </Menu>
33
+ )
34
+ }
@@ -0,0 +1,76 @@
1
+ import { type Handle } from '@remix-run/ui'
2
+ import { Menu, MenuItem, onMenuSelect, Submenu } from '@remix-run/ui/menu'
3
+ import { separatorStyle } from '@remix-run/ui/separator'
4
+
5
+ /**
6
+ * @name Menu Overview
7
+ * @description A hierarchical menu with checkboxes, radio groups, submenus, and separators.
8
+ */
9
+ export default function Example(handle: Handle) {
10
+ type Density = 'comfortable' | 'compact'
11
+ let wordWrap = true
12
+ let minimap = false
13
+ let showGutter = true
14
+ let density: Density = 'comfortable'
15
+
16
+ return () => (
17
+ <Menu
18
+ label="View"
19
+ menuLabel="View options"
20
+ mix={onMenuSelect((event) => {
21
+ switch (event.item.name) {
22
+ case 'wordWrap':
23
+ wordWrap = !wordWrap
24
+ break
25
+ case 'minimap':
26
+ minimap = !minimap
27
+ break
28
+ case 'showGutter':
29
+ showGutter = !showGutter
30
+ break
31
+ case 'density':
32
+ density = event.item.value as Density
33
+ break
34
+ }
35
+ handle.update()
36
+ })}
37
+ >
38
+ <MenuItem name="wordWrap" type="checkbox" checked={wordWrap}>
39
+ Word wrap
40
+ </MenuItem>
41
+ <MenuItem name="minimap" type="checkbox" checked={minimap}>
42
+ Minimap
43
+ </MenuItem>
44
+ <MenuItem name="showGutter" type="checkbox" checked={showGutter}>
45
+ Show gutter
46
+ </MenuItem>
47
+
48
+ <hr mix={separatorStyle} />
49
+ <Submenu label="Zoom">
50
+ <MenuItem name="zoomIn" value="zoom-in">
51
+ Zoom In
52
+ </MenuItem>
53
+ <MenuItem name="zoomOut" value="zoom-out">
54
+ Zoom Out
55
+ </MenuItem>
56
+ <MenuItem name="resetZoom" value="reset-zoom">
57
+ Reset Zoom
58
+ </MenuItem>
59
+ </Submenu>
60
+
61
+ <Submenu label="Density">
62
+ <MenuItem
63
+ name="density"
64
+ type="radio"
65
+ value="comfortable"
66
+ checked={density === 'comfortable'}
67
+ >
68
+ Comfortable
69
+ </MenuItem>
70
+ <MenuItem name="density" type="radio" value="compact" checked={density === 'compact'}>
71
+ Compact
72
+ </MenuItem>
73
+ </Submenu>
74
+ </Menu>
75
+ )
76
+ }