@remix-run/ui 0.1.2 → 0.3.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 (318) hide show
  1. package/README.md +9 -7
  2. package/dist/animation/demos/animation.demo.d.ts +5 -0
  3. package/dist/animation/demos/animation.demo.js +92 -0
  4. package/dist/animation/demos/animation.demo.js.map +1 -0
  5. package/dist/animation/demos/aspect-ratio.d.ts +2 -0
  6. package/dist/animation/demos/aspect-ratio.js +69 -0
  7. package/dist/animation/demos/aspect-ratio.js.map +1 -0
  8. package/dist/animation/demos/bouncy-switch.d.ts +2 -0
  9. package/dist/animation/demos/bouncy-switch.js +36 -0
  10. package/dist/animation/demos/bouncy-switch.js.map +1 -0
  11. package/dist/animation/demos/color-interpolation.d.ts +1 -0
  12. package/dist/animation/demos/color-interpolation.js +53 -0
  13. package/dist/animation/demos/color-interpolation.js.map +1 -0
  14. package/dist/animation/demos/cube.d.ts +2 -0
  15. package/dist/animation/demos/cube.js +86 -0
  16. package/dist/animation/demos/cube.js.map +1 -0
  17. package/dist/animation/demos/default-animate.d.ts +2 -0
  18. package/dist/animation/demos/default-animate.js +89 -0
  19. package/dist/animation/demos/default-animate.js.map +1 -0
  20. package/dist/animation/demos/drag-release.d.ts +24 -0
  21. package/dist/animation/demos/drag-release.js +96 -0
  22. package/dist/animation/demos/drag-release.js.map +1 -0
  23. package/dist/animation/demos/enter.d.ts +1 -0
  24. package/dist/animation/demos/enter.js +19 -0
  25. package/dist/animation/demos/enter.js.map +1 -0
  26. package/dist/animation/demos/exit.d.ts +2 -0
  27. package/dist/animation/demos/exit.js +58 -0
  28. package/dist/animation/demos/exit.js.map +1 -0
  29. package/dist/animation/demos/flip-toggle.d.ts +2 -0
  30. package/dist/animation/demos/flip-toggle.js +37 -0
  31. package/dist/animation/demos/flip-toggle.js.map +1 -0
  32. package/dist/animation/demos/hold-to-confirm.d.ts +13 -0
  33. package/dist/animation/demos/hold-to-confirm.js +211 -0
  34. package/dist/animation/demos/hold-to-confirm.js.map +1 -0
  35. package/dist/animation/demos/html-content.d.ts +2 -0
  36. package/dist/animation/demos/html-content.js +30 -0
  37. package/dist/animation/demos/html-content.js.map +1 -0
  38. package/dist/animation/demos/interruptible-keyframes.d.ts +1 -0
  39. package/dist/animation/demos/interruptible-keyframes.js +48 -0
  40. package/dist/animation/demos/interruptible-keyframes.js.map +1 -0
  41. package/dist/animation/demos/keyframes.d.ts +1 -0
  42. package/dist/animation/demos/keyframes.js +24 -0
  43. package/dist/animation/demos/keyframes.js.map +1 -0
  44. package/dist/animation/demos/material-ripple.d.ts +2 -0
  45. package/dist/animation/demos/material-ripple.js +127 -0
  46. package/dist/animation/demos/material-ripple.js.map +1 -0
  47. package/dist/animation/demos/mixin-presence-list.d.ts +2 -0
  48. package/dist/animation/demos/mixin-presence-list.js +88 -0
  49. package/dist/animation/demos/mixin-presence-list.js.map +1 -0
  50. package/dist/animation/demos/mixin-reclaim.d.ts +2 -0
  51. package/dist/animation/demos/mixin-reclaim.js +90 -0
  52. package/dist/animation/demos/mixin-reclaim.js.map +1 -0
  53. package/dist/animation/demos/multi-state-badge.d.ts +2 -0
  54. package/dist/animation/demos/multi-state-badge.js +231 -0
  55. package/dist/animation/demos/multi-state-badge.js.map +1 -0
  56. package/dist/animation/demos/press.d.ts +2 -0
  57. package/dist/animation/demos/press.js +63 -0
  58. package/dist/animation/demos/press.js.map +1 -0
  59. package/dist/animation/demos/reordering.d.ts +2 -0
  60. package/dist/animation/demos/reordering.js +50 -0
  61. package/dist/animation/demos/reordering.js.map +1 -0
  62. package/dist/animation/demos/rolling-square.d.ts +2 -0
  63. package/dist/animation/demos/rolling-square.js +42 -0
  64. package/dist/animation/demos/rolling-square.js.map +1 -0
  65. package/dist/animation/demos/rotate.d.ts +1 -0
  66. package/dist/animation/demos/rotate.js +18 -0
  67. package/dist/animation/demos/rotate.js.map +1 -0
  68. package/dist/animation/demos/shared-layout.d.ts +2 -0
  69. package/dist/animation/demos/shared-layout.js +114 -0
  70. package/dist/animation/demos/shared-layout.js.map +1 -0
  71. package/dist/animation/demos/spring.demo.d.ts +6 -0
  72. package/dist/animation/demos/spring.demo.js +304 -0
  73. package/dist/animation/demos/spring.demo.js.map +1 -0
  74. package/dist/animation/demos/transition-options.d.ts +1 -0
  75. package/dist/animation/demos/transition-options.js +21 -0
  76. package/dist/animation/demos/transition-options.js.map +1 -0
  77. package/dist/animation/spring.d.ts +24 -31
  78. package/dist/animation/spring.js +31 -8
  79. package/dist/animation/spring.js.map +1 -1
  80. package/dist/components/accordion/accordion.d.ts +6 -6
  81. package/dist/components/accordion/accordion.js.map +1 -1
  82. package/dist/components/accordion/demos/card.demo.d.ts +1 -0
  83. package/dist/components/accordion/demos/card.demo.js +1 -0
  84. package/dist/components/accordion/demos/card.demo.js.map +1 -1
  85. package/dist/components/accordion/demos/multiple.demo.d.ts +1 -0
  86. package/dist/components/accordion/demos/multiple.demo.js +1 -0
  87. package/dist/components/accordion/demos/multiple.demo.js.map +1 -1
  88. package/dist/components/accordion/demos/overview.demo.d.ts +1 -0
  89. package/dist/components/accordion/demos/overview.demo.js +1 -0
  90. package/dist/components/accordion/demos/overview.demo.js.map +1 -1
  91. package/dist/components/anchor/anchor.d.ts +8 -1
  92. package/dist/components/anchor/anchor.js +30 -11
  93. package/dist/components/anchor/anchor.js.map +1 -1
  94. package/dist/components/breadcrumbs/{demos/basic.demo.d.ts → basic.demo.d.ts} +1 -0
  95. package/dist/components/breadcrumbs/{demos/basic.demo.js → basic.demo.js} +1 -0
  96. package/dist/components/breadcrumbs/basic.demo.js.map +1 -0
  97. package/dist/components/breadcrumbs/breadcrumbs.d.ts +1 -1
  98. package/dist/components/breadcrumbs/{demos/separator.demo.d.ts → separator.demo.d.ts} +1 -0
  99. package/dist/components/breadcrumbs/{demos/separator.demo.js → separator.demo.js} +1 -0
  100. package/dist/components/breadcrumbs/separator.demo.js.map +1 -0
  101. package/dist/components/button/button.d.ts +4 -4
  102. package/dist/components/button/button.js +3 -1
  103. package/dist/components/button/button.js.map +1 -1
  104. package/dist/components/button/demos/aliases.demo.d.ts +1 -0
  105. package/dist/components/button/demos/aliases.demo.js +1 -0
  106. package/dist/components/button/demos/aliases.demo.js.map +1 -1
  107. package/dist/components/button/demos/basic.demo.d.ts +1 -0
  108. package/dist/components/button/demos/basic.demo.js +1 -0
  109. package/dist/components/button/demos/basic.demo.js.map +1 -1
  110. package/dist/components/button/demos/states.demo.d.ts +1 -0
  111. package/dist/components/button/demos/states.demo.js +1 -0
  112. package/dist/components/button/demos/states.demo.js.map +1 -1
  113. package/dist/components/combobox/combobox.d.ts +10 -10
  114. package/dist/components/combobox/combobox.js.map +1 -1
  115. package/dist/components/combobox/{demos/overview.demo.d.ts → overview.demo.d.ts} +1 -0
  116. package/dist/components/combobox/{demos/overview.demo.js → overview.demo.js} +1 -0
  117. package/dist/components/combobox/overview.demo.js.map +1 -0
  118. package/dist/components/listbox/listbox.d.ts +3 -3
  119. package/dist/components/listbox/listbox.js.map +1 -1
  120. package/dist/components/listbox/{demos/overview.demo.d.ts → overview.demo.d.ts} +1 -0
  121. package/dist/components/listbox/{demos/overview.demo.js → overview.demo.js} +1 -0
  122. package/dist/components/listbox/overview.demo.js.map +1 -0
  123. package/dist/components/menu/demos/bubbling.demo.d.ts +1 -0
  124. package/dist/components/menu/demos/bubbling.demo.js +1 -0
  125. package/dist/components/menu/demos/bubbling.demo.js.map +1 -1
  126. package/dist/components/menu/demos/context-trigger.demo.d.ts +7 -0
  127. package/dist/components/menu/demos/context-trigger.demo.js +87 -0
  128. package/dist/components/menu/demos/context-trigger.demo.js.map +1 -0
  129. package/dist/components/menu/demos/overview.demo.d.ts +1 -0
  130. package/dist/components/menu/demos/overview.demo.js +1 -0
  131. package/dist/components/menu/demos/overview.demo.js.map +1 -1
  132. package/dist/components/menu/menu.d.ts +15 -12
  133. package/dist/components/menu/menu.js +55 -0
  134. package/dist/components/menu/menu.js.map +1 -1
  135. package/dist/components/popover/{demos/overview.demo.d.ts → overview.demo.d.ts} +1 -0
  136. package/dist/components/popover/{demos/overview.demo.js → overview.demo.js} +1 -0
  137. package/dist/components/popover/overview.demo.js.map +1 -0
  138. package/dist/components/popover/popover.d.ts +7 -7
  139. package/dist/components/popover/popover.js +11 -4
  140. package/dist/components/popover/popover.js.map +1 -1
  141. package/dist/components/select/{demos/deconstructed.demo.d.ts → deconstructed.demo.d.ts} +1 -0
  142. package/dist/components/select/{demos/deconstructed.demo.js → deconstructed.demo.js} +1 -0
  143. package/dist/components/select/deconstructed.demo.js.map +1 -0
  144. package/dist/components/select/{demos/overview.demo.d.ts → overview.demo.d.ts} +1 -0
  145. package/dist/components/select/{demos/overview.demo.js → overview.demo.js} +1 -0
  146. package/dist/components/select/overview.demo.js.map +1 -0
  147. package/dist/components/select/select.d.ts +10 -10
  148. package/dist/components/select/select.js +1 -1
  149. package/dist/components/select/select.js.map +1 -1
  150. package/dist/components/tabs/tabs.d.ts +3 -3
  151. package/dist/index.d.ts +1 -1
  152. package/dist/index.js.map +1 -1
  153. package/dist/interactions/keydown/keydown.d.ts +1 -1
  154. package/dist/interactions/outside-click/outside-click-mixin.d.ts +2 -1
  155. package/dist/interactions/typeahead/typeahead-mixin.d.ts +3 -1
  156. package/dist/runtime/client-entries.d.ts +1 -2
  157. package/dist/runtime/client-entries.js.map +1 -1
  158. package/dist/runtime/component.d.ts +20 -17
  159. package/dist/runtime/component.js +15 -6
  160. package/dist/runtime/component.js.map +1 -1
  161. package/dist/runtime/core/attributes.d.ts +3 -1
  162. package/dist/runtime/core/attributes.js +19 -1
  163. package/dist/runtime/core/attributes.js.map +1 -1
  164. package/dist/runtime/core/props.js +6 -5
  165. package/dist/runtime/core/props.js.map +1 -1
  166. package/dist/runtime/demos/basic.demo.d.ts +6 -0
  167. package/dist/runtime/demos/basic.demo.js +16 -0
  168. package/dist/runtime/demos/basic.demo.js.map +1 -0
  169. package/dist/runtime/demos/controlled-uncontrolled-values.demo.d.ts +6 -0
  170. package/dist/runtime/demos/controlled-uncontrolled-values.demo.js +77 -0
  171. package/dist/runtime/demos/controlled-uncontrolled-values.demo.js.map +1 -0
  172. package/dist/runtime/demos/keyed-list.demo.d.ts +6 -0
  173. package/dist/runtime/demos/keyed-list.demo.js +62 -0
  174. package/dist/runtime/demos/keyed-list.demo.js.map +1 -0
  175. package/dist/runtime/demos/readme.demo.d.ts +5 -0
  176. package/dist/runtime/demos/readme.demo.js +396 -0
  177. package/dist/runtime/demos/readme.demo.js.map +1 -0
  178. package/dist/runtime/diff-dom.js +194 -21
  179. package/dist/runtime/diff-dom.js.map +1 -1
  180. package/dist/runtime/dom.d.ts +16 -9
  181. package/dist/runtime/frame.d.ts +7 -0
  182. package/dist/runtime/frame.js +176 -41
  183. package/dist/runtime/frame.js.map +1 -1
  184. package/dist/runtime/jsx.d.ts +3 -4
  185. package/dist/runtime/jsx.js.map +1 -1
  186. package/dist/runtime/mixins/link-mixin.d.ts +5 -1
  187. package/dist/runtime/mixins/link-mixin.js.map +1 -1
  188. package/dist/runtime/mixins/mixin.d.ts +2 -1
  189. package/dist/runtime/mixins/mixin.js.map +1 -1
  190. package/dist/runtime/mixins/ref-mixin.d.ts +2 -1
  191. package/dist/runtime/mixins/ref-mixin.js.map +1 -1
  192. package/dist/runtime/reconcile.d.ts +1 -1
  193. package/dist/runtime/reconcile.js +19 -5
  194. package/dist/runtime/reconcile.js.map +1 -1
  195. package/dist/runtime/scheduler.d.ts +10 -10
  196. package/dist/runtime/scheduler.js +1 -1
  197. package/dist/runtime/scheduler.js.map +1 -1
  198. package/dist/runtime/svg-attributes.js +1 -0
  199. package/dist/runtime/svg-attributes.js.map +1 -1
  200. package/dist/runtime/to-vnode.js +2 -2
  201. package/dist/runtime/to-vnode.js.map +1 -1
  202. package/dist/runtime/vnode.d.ts +6 -1
  203. package/dist/runtime/vnode.js +4 -0
  204. package/dist/runtime/vnode.js.map +1 -1
  205. package/dist/server/stream.d.ts +2 -0
  206. package/dist/server/stream.js +82 -11
  207. package/dist/server/stream.js.map +1 -1
  208. package/dist/style/css-mixin.d.ts +2 -1
  209. package/dist/style/css-mixin.js.map +1 -1
  210. package/dist/style/index.d.ts +1 -1
  211. package/dist/style/style.js +27 -10
  212. package/dist/style/style.js.map +1 -1
  213. package/dist/style/stylesheet.d.ts +13 -13
  214. package/dist/style/stylesheet.js.map +1 -1
  215. package/dist/theme/contract.d.ts +24 -222
  216. package/dist/theme/contract.js +2 -2
  217. package/dist/theme/contract.js.map +1 -1
  218. package/dist/theme/glyph-contract.d.ts +1 -3
  219. package/dist/theme/glyph-contract.js.map +1 -1
  220. package/dist/theme/presets/rmx-01/index.d.ts +4 -2
  221. package/dist/theme/presets/rmx-01/index.js.map +1 -1
  222. package/dist/utils/scroll-lock.d.ts +3 -3
  223. package/dist/utils/scroll-lock.js.map +1 -1
  224. package/package.json +4 -4
  225. package/src/animation/README.md +7 -5
  226. package/src/animation/demos/animation.demo.tsx +194 -0
  227. package/src/animation/demos/aspect-ratio.tsx +120 -0
  228. package/src/animation/demos/bouncy-switch.tsx +47 -0
  229. package/src/animation/demos/color-interpolation.tsx +73 -0
  230. package/src/animation/demos/cube.tsx +116 -0
  231. package/src/animation/demos/default-animate.tsx +129 -0
  232. package/src/animation/demos/drag-release.ts +118 -0
  233. package/src/animation/demos/enter.tsx +22 -0
  234. package/src/animation/demos/exit.tsx +79 -0
  235. package/src/animation/demos/flip-toggle.tsx +46 -0
  236. package/src/animation/demos/hold-to-confirm.tsx +317 -0
  237. package/src/animation/demos/html-content.tsx +41 -0
  238. package/src/animation/demos/interruptible-keyframes.tsx +62 -0
  239. package/src/animation/demos/keyframes.tsx +29 -0
  240. package/src/animation/demos/material-ripple.tsx +158 -0
  241. package/src/animation/demos/mixin-presence-list.tsx +121 -0
  242. package/src/animation/demos/mixin-reclaim.tsx +125 -0
  243. package/src/animation/demos/multi-state-badge.tsx +426 -0
  244. package/src/animation/demos/press.tsx +67 -0
  245. package/src/animation/demos/reordering.tsx +65 -0
  246. package/src/animation/demos/rolling-square.tsx +57 -0
  247. package/src/animation/demos/rotate.tsx +21 -0
  248. package/src/animation/demos/shared-layout.tsx +211 -0
  249. package/src/animation/demos/spring.demo.tsx +385 -0
  250. package/src/animation/demos/transition-options.tsx +24 -0
  251. package/src/animation/spring.ts +43 -9
  252. package/src/components/accordion/accordion.tsx +7 -5
  253. package/src/components/accordion/demos/card.demo.tsx +1 -0
  254. package/src/components/accordion/demos/multiple.demo.tsx +1 -0
  255. package/src/components/accordion/demos/overview.demo.tsx +1 -0
  256. package/src/components/anchor/README.md +11 -3
  257. package/src/components/anchor/anchor.ts +54 -13
  258. package/src/components/breadcrumbs/{demos/basic.demo.tsx → basic.demo.tsx} +1 -0
  259. package/src/components/breadcrumbs/breadcrumbs.tsx +1 -1
  260. package/src/components/breadcrumbs/{demos/separator.demo.tsx → separator.demo.tsx} +1 -0
  261. package/src/components/button/button.tsx +30 -18
  262. package/src/components/button/demos/aliases.demo.tsx +1 -0
  263. package/src/components/button/demos/basic.demo.tsx +1 -0
  264. package/src/components/button/demos/states.demo.tsx +1 -0
  265. package/src/components/combobox/README.md +1 -1
  266. package/src/components/combobox/combobox.tsx +30 -12
  267. package/src/components/combobox/{demos/overview.demo.tsx → overview.demo.tsx} +1 -0
  268. package/src/components/glyph/glyph.tsx +2 -2
  269. package/src/components/listbox/listbox.ts +88 -83
  270. package/src/components/listbox/{demos/overview.demo.tsx → overview.demo.tsx} +1 -0
  271. package/src/components/menu/README.md +26 -2
  272. package/src/components/menu/demos/bubbling.demo.tsx +1 -0
  273. package/src/components/menu/demos/context-trigger.demo.tsx +133 -0
  274. package/src/components/menu/demos/overview.demo.tsx +1 -0
  275. package/src/components/menu/menu.tsx +100 -16
  276. package/src/components/popover/{demos/overview.demo.tsx → overview.demo.tsx} +1 -0
  277. package/src/components/popover/popover.ts +101 -81
  278. package/src/components/select/{demos/deconstructed.demo.tsx → deconstructed.demo.tsx} +1 -0
  279. package/src/components/select/{demos/overview.demo.tsx → overview.demo.tsx} +1 -0
  280. package/src/components/select/select.tsx +27 -10
  281. package/src/index.ts +1 -0
  282. package/src/runtime/client-entries.ts +1 -5
  283. package/src/runtime/component.ts +37 -15
  284. package/src/runtime/core/attributes.ts +21 -0
  285. package/src/runtime/core/props.ts +6 -4
  286. package/src/runtime/demos/basic.demo.tsx +21 -0
  287. package/src/runtime/demos/controlled-uncontrolled-values.demo.tsx +177 -0
  288. package/src/runtime/demos/keyed-list.demo.tsx +94 -0
  289. package/src/runtime/demos/readme.demo.tsx +702 -0
  290. package/src/runtime/diff-dom.ts +270 -20
  291. package/src/runtime/dom.ts +16 -9
  292. package/src/runtime/frame.ts +218 -41
  293. package/src/runtime/jsx.ts +7 -11
  294. package/src/runtime/mixins/link-mixin.ts +88 -86
  295. package/src/runtime/mixins/mixin.ts +9 -1
  296. package/src/runtime/mixins/ref-mixin.ts +4 -5
  297. package/src/runtime/reconcile.ts +23 -14
  298. package/src/runtime/scheduler.ts +20 -20
  299. package/src/runtime/svg-attributes.ts +1 -0
  300. package/src/runtime/to-vnode.ts +2 -2
  301. package/src/runtime/vnode.ts +10 -0
  302. package/src/server/README.md +4 -2
  303. package/src/server/stream.ts +84 -10
  304. package/src/style/css-mixin.ts +6 -1
  305. package/src/style/index.ts +1 -2
  306. package/src/style/style.ts +29 -10
  307. package/src/style/stylesheet.ts +14 -2
  308. package/src/theme/contract.ts +37 -5
  309. package/src/theme/glyph-contract.ts +3 -1
  310. package/src/theme/presets/rmx-01/index.ts +4 -2
  311. package/src/utils/scroll-lock.ts +7 -3
  312. package/dist/components/breadcrumbs/demos/basic.demo.js.map +0 -1
  313. package/dist/components/breadcrumbs/demos/separator.demo.js.map +0 -1
  314. package/dist/components/combobox/demos/overview.demo.js.map +0 -1
  315. package/dist/components/listbox/demos/overview.demo.js.map +0 -1
  316. package/dist/components/popover/demos/overview.demo.js.map +0 -1
  317. package/dist/components/select/demos/deconstructed.demo.js.map +0 -1
  318. package/dist/components/select/demos/overview.demo.js.map +0 -1
@@ -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
@@ -7,6 +7,7 @@ import { theme } from '@remix-run/ui/theme'
7
7
  /**
8
8
  * @name Listbox Overview
9
9
  * @description A keyboard-navigable listbox with selection, highlighting, and an optional flash-selection animation.
10
+ * @layout center
10
11
  */
11
12
  export default function Example(handle: Handle) {
12
13
  let value: ListboxValue = options[0]!.value
@@ -68,6 +68,29 @@ Use `menuLabel` when the menu surface needs a different accessible label from th
68
68
  </Menu>
69
69
  ```
70
70
 
71
+ Use `menu.contextTrigger()` with `menu.Context` and `MenuList` when a menu should open at the right-click location of an element.
72
+
73
+ ```tsx
74
+ import * as menu from 'remix/ui/menu'
75
+ import { MenuItem, MenuList } from 'remix/ui/menu'
76
+
77
+ export function FileContextMenu(handle: Handle) {
78
+ return () => (
79
+ <menu.Context label="File actions">
80
+ <div mix={menu.contextTrigger()} tabIndex={0}>
81
+ File.txt
82
+ </div>
83
+ <MenuList>
84
+ <MenuItem name="rename">Rename</MenuItem>
85
+ <MenuItem name="delete">Delete</MenuItem>
86
+ </MenuList>
87
+ </menu.Context>
88
+ )
89
+ }
90
+ ```
91
+
92
+ Attach `onMenuSelect(...)` to `MenuList` or a shared ancestor when using lower-level context menu composition.
93
+
71
94
  ## `menu.*`
72
95
 
73
96
  - `Menu`: composed trigger, popover, and list component for the common menu case.
@@ -77,13 +100,14 @@ Use `menuLabel` when the menu surface needs a different accessible label from th
77
100
  - `onMenuSelect(...)`: event mixin for the bubbling `MenuSelectEvent`.
78
101
  - `MenuSelectEvent`: bubbling event class whose `item` describes the selected item.
79
102
  - `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.
103
+ - `menu.Context`, `menu.trigger()`, `menu.contextTrigger()`, `menu.popover()`, `menu.list()`, `menu.item(...)`, and `menu.submenuTrigger(...)`: lower-level composition primitives.
81
104
  - `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.
105
+ - `MenuProps`, `MenuItemProps`, `MenuListProps`, `MenuProviderProps`, `MenuTriggerOptions`, `MenuContextTriggerOptions`, `MenuItemOptions`, and `SubmenuProps`: public TypeScript props and option types.
83
106
 
84
107
  ## Behavior Notes
85
108
 
86
109
  - Click opens the root menu and focuses the list; clicking the trigger again closes it and restores focus.
110
+ - `menu.contextTrigger()` opens the root menu from a `contextmenu` event at the pointer coordinates and supports keyboard opening with the Context Menu key or Shift+F10.
87
111
  - `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
112
  - Keyboard navigation skips disabled items and does not wrap past the first or last enabled item.
89
113
  - `Home` and `End` move to the first and last enabled item. Enter and Space activate the highlighted item.
@@ -4,6 +4,7 @@ import { Menu, MenuItem, onMenuSelect } from '@remix-run/ui/menu'
4
4
  /**
5
5
  * @name Menu Event Bubbling
6
6
  * @description Item-level handlers fire before the root handler, letting individual items intercept events while others bubble up.
7
+ * @layout center
7
8
  */
8
9
  export default function Example() {
9
10
  return () => (
@@ -0,0 +1,133 @@
1
+ import { css, type Handle } from '@remix-run/ui'
2
+ import * as menu from '@remix-run/ui/menu'
3
+ import { MenuItem, MenuList, onMenuSelect, Submenu } from '@remix-run/ui/menu'
4
+ import { separatorStyle } from '@remix-run/ui/separator'
5
+ import { theme } from '@remix-run/ui/theme'
6
+
7
+ type FileAction = 'copyPath' | 'duplicate' | 'move' | 'rename' | 'reveal' | 'trash'
8
+
9
+ const actionLabelByName: Record<FileAction, string> = {
10
+ copyPath: 'Copied path',
11
+ duplicate: 'Duplicated file',
12
+ move: 'Moved file',
13
+ rename: 'Renamed file',
14
+ reveal: 'Revealed in Finder',
15
+ trash: 'Moved to trash',
16
+ }
17
+
18
+ /**
19
+ * @name Context Menu Trigger
20
+ * @description A lower-level menu composition that opens from right-click coordinates while keeping standard menu selection and submenu behavior.
21
+ * @layout center
22
+ */
23
+ export default function Example(handle: Handle) {
24
+ let latestAction = 'Right-click the card.'
25
+
26
+ return () => (
27
+ <menu.Context label="File actions">
28
+ <div mix={layoutCss}>
29
+ <div tabIndex={0} mix={[fileCardCss, menu.contextTrigger()]}>
30
+ <span mix={fileIconCss}>TS</span>
31
+ <span mix={fileTextCss}>
32
+ <strong mix={fileNameCss}>context-menu.tsx</strong>
33
+ <span mix={fileMetaCss}>Right-click or press Shift+F10</span>
34
+ </span>
35
+ </div>
36
+
37
+ <p aria-live="polite" mix={statusCss}>
38
+ {latestAction}
39
+ </p>
40
+ </div>
41
+
42
+ <MenuList
43
+ mix={onMenuSelect((event) => {
44
+ latestAction =
45
+ actionLabelByName[event.item.name as FileAction] ?? `Selected ${event.item.label}`
46
+ void handle.update()
47
+ })}
48
+ >
49
+ <MenuItem name="rename">Rename</MenuItem>
50
+ <MenuItem name="duplicate">Duplicate</MenuItem>
51
+ <MenuItem name="copyPath">Copy path</MenuItem>
52
+ <hr mix={separatorStyle} />
53
+ <Submenu label="Move to">
54
+ <MenuItem name="move" value="drafts">
55
+ Drafts
56
+ </MenuItem>
57
+ <MenuItem name="move" value="archive">
58
+ Archive
59
+ </MenuItem>
60
+ </Submenu>
61
+ <MenuItem name="reveal">Reveal in Finder</MenuItem>
62
+ <hr mix={separatorStyle} />
63
+ <MenuItem name="trash">Move to trash</MenuItem>
64
+ </MenuList>
65
+ </menu.Context>
66
+ )
67
+ }
68
+
69
+ const layoutCss = css({
70
+ display: 'grid',
71
+ justifyItems: 'start',
72
+ gap: theme.space.md,
73
+ })
74
+
75
+ const fileCardCss = css({
76
+ display: 'grid',
77
+ gridTemplateColumns: 'auto minmax(0, 1fr)',
78
+ alignItems: 'center',
79
+ gap: theme.space.md,
80
+ width: 'min(100%, 21rem)',
81
+ padding: theme.space.md,
82
+ border: `1px solid ${theme.colors.border.subtle}`,
83
+ borderRadius: theme.radius.lg,
84
+ backgroundColor: theme.surface.lvl1,
85
+ color: theme.colors.text.primary,
86
+ boxShadow: theme.shadow.xs,
87
+ cursor: 'context-menu',
88
+ userSelect: 'none',
89
+ '&:focus-visible': {
90
+ outline: `2px solid ${theme.colors.focus.ring}`,
91
+ outlineOffset: '2px',
92
+ },
93
+ })
94
+
95
+ const fileIconCss = css({
96
+ display: 'inline-grid',
97
+ placeItems: 'center',
98
+ width: theme.control.height.lg,
99
+ height: theme.control.height.lg,
100
+ borderRadius: theme.radius.md,
101
+ backgroundColor: theme.colors.action.primary.background,
102
+ color: theme.colors.action.primary.foreground,
103
+ fontSize: theme.fontSize.xs,
104
+ fontWeight: theme.fontWeight.bold,
105
+ })
106
+
107
+ const fileTextCss = css({
108
+ display: 'grid',
109
+ gap: theme.space.px,
110
+ minWidth: 0,
111
+ })
112
+
113
+ const fileNameCss = css({
114
+ overflow: 'hidden',
115
+ textOverflow: 'ellipsis',
116
+ whiteSpace: 'nowrap',
117
+ fontSize: theme.fontSize.sm,
118
+ lineHeight: theme.lineHeight.normal,
119
+ })
120
+
121
+ const fileMetaCss = css({
122
+ color: theme.colors.text.secondary,
123
+ fontSize: theme.fontSize.xs,
124
+ lineHeight: theme.lineHeight.normal,
125
+ })
126
+
127
+ const statusCss = css({
128
+ margin: 0,
129
+ minHeight: theme.lineHeight.normal,
130
+ color: theme.colors.text.secondary,
131
+ fontSize: theme.fontSize.sm,
132
+ lineHeight: theme.lineHeight.normal,
133
+ })
@@ -5,6 +5,7 @@ import { separatorStyle } from '@remix-run/ui/separator'
5
5
  /**
6
6
  * @name Menu Overview
7
7
  * @description A hierarchical menu with checkboxes, radio groups, submenus, and separators.
8
+ * @layout center
8
9
  */
9
10
  export default function Example(handle: Handle) {
10
11
  type Density = 'comfortable' | 'compact'
@@ -9,10 +9,11 @@ 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'
15
- import { type AnchorOptions } from '../anchor/anchor.ts'
16
+ import { type AnchorOptions, type AnchorPoint } from '../anchor/anchor.ts'
16
17
  import * as button from '../button/button.tsx'
17
18
  import { Glyph } from '../glyph/glyph.tsx'
18
19
  import * as popover from '../popover/popover.ts'
@@ -249,6 +250,8 @@ export interface MenuProviderProps {
249
250
 
250
251
  export interface MenuTriggerOptions extends AnchorOptions {}
251
252
 
253
+ export interface MenuContextTriggerOptions extends AnchorOptions {}
254
+
252
255
  export interface MenuItemOptions {
253
256
  checked?: boolean
254
257
  disabled?: boolean
@@ -416,7 +419,7 @@ function focusNode(node: HTMLElement | undefined) {
416
419
  }
417
420
  }
418
421
 
419
- function MenuProvider(handle: Handle<MenuProviderProps, MenuContextValue>) {
422
+ function MenuProvider(handle: Handle<MenuProviderProps, MenuContextValue>): () => RemixNode {
420
423
  let parent = handle.context.get(MenuProvider)
421
424
 
422
425
  let activeId: string | null = null
@@ -957,8 +960,8 @@ function MenuProvider(handle: Handle<MenuProviderProps, MenuContextValue>) {
957
960
  }
958
961
  }
959
962
 
960
- const triggerMixin = createMixin<HTMLElement, [options?: MenuTriggerOptions], ElementProps>(
961
- (handle) => {
963
+ const triggerMixin: MixinFactory<HTMLElement, [options?: MenuTriggerOptions], ElementProps> =
964
+ createMixin<HTMLElement, [options?: MenuTriggerOptions], ElementProps>((handle) => {
962
965
  let context = handle.context.get(MenuProvider)
963
966
 
964
967
  return (options = {}) => [
@@ -1005,10 +1008,81 @@ const triggerMixin = createMixin<HTMLElement, [options?: MenuTriggerOptions], El
1005
1008
  }
1006
1009
  }),
1007
1010
  ]
1008
- },
1009
- )
1011
+ })
1012
+
1013
+ const contextTriggerMixin: MixinFactory<
1014
+ HTMLElement,
1015
+ [options?: MenuContextTriggerOptions],
1016
+ ElementProps
1017
+ > = createMixin<HTMLElement, [options?: MenuContextTriggerOptions], ElementProps>((handle) => {
1018
+ let context = handle.context.get(MenuProvider)
1019
+ let popoverContext = handle.context.get(popover.Context)
1020
+ let anchorPoint: AnchorPoint = { x: 0, y: 0, width: 0, height: 0 }
1021
+
1022
+ function setAnchorPoint(
1023
+ { x, y, width = 0, height = 0 }: AnchorPoint,
1024
+ options: MenuContextTriggerOptions,
1025
+ ) {
1026
+ anchorPoint.x = x
1027
+ anchorPoint.y = y
1028
+ anchorPoint.width = width
1029
+ anchorPoint.height = height
1030
+ popoverContext.anchor = {
1031
+ target: anchorPoint,
1032
+ options: {
1033
+ placement: 'bottom-start',
1034
+ ...options,
1035
+ },
1036
+ }
1037
+ }
1038
+
1039
+ return (options = {}) => [
1040
+ attrs({
1041
+ id: handle.id,
1042
+ 'aria-controls': context.listId,
1043
+ 'aria-expanded': context.isOpen ? 'true' : 'false',
1044
+ 'aria-haspopup': 'menu',
1045
+ }),
1046
+ ref((node: HTMLElement, signal) => {
1047
+ context.registerTrigger(node, handle.id)
1048
+ signal.addEventListener('abort', () => {
1049
+ context.unregisterTrigger(node)
1050
+ })
1051
+ }),
1052
+ on('contextmenu', (event) => {
1053
+ event.preventDefault()
1054
+ event.stopPropagation()
1055
+ setAnchorPoint({ x: event.clientX, y: event.clientY }, options)
1056
+ void context.open({ strategy: 'list' })
1057
+ }),
1058
+ on('keydown', (event) => {
1059
+ if (event.key !== 'ContextMenu' && !(event.shiftKey && event.key === 'F10')) {
1060
+ return
1061
+ }
1062
+
1063
+ event.preventDefault()
1064
+ event.stopPropagation()
1065
+
1066
+ let rect = event.currentTarget.getBoundingClientRect()
1067
+ setAnchorPoint(
1068
+ {
1069
+ x: rect.left,
1070
+ y: rect.top,
1071
+ width: rect.width,
1072
+ height: rect.height,
1073
+ },
1074
+ options,
1075
+ )
1076
+ void context.open({ strategy: 'list' })
1077
+ }),
1078
+ ]
1079
+ })
1010
1080
 
1011
- const popoverMixin = createMixin<HTMLElement, [], ElementProps>((handle) => {
1081
+ const popoverMixin: MixinFactory<HTMLElement, [], ElementProps> = createMixin<
1082
+ HTMLElement,
1083
+ [],
1084
+ ElementProps
1085
+ >((handle) => {
1012
1086
  let context = handle.context.get(MenuProvider)
1013
1087
 
1014
1088
  return () => [
@@ -1045,7 +1119,11 @@ const popoverMixin = createMixin<HTMLElement, [], ElementProps>((handle) => {
1045
1119
  ]
1046
1120
  })
1047
1121
 
1048
- const listMixin = createMixin<HTMLElement, [], ElementProps>((handle) => {
1122
+ const listMixin: MixinFactory<HTMLElement, [], ElementProps> = createMixin<
1123
+ HTMLElement,
1124
+ [],
1125
+ ElementProps
1126
+ >((handle) => {
1049
1127
  let context = handle.context.get(MenuProvider)
1050
1128
 
1051
1129
  return () => [
@@ -1146,7 +1224,11 @@ const listMixin = createMixin<HTMLElement, [], ElementProps>((handle) => {
1146
1224
  ]
1147
1225
  })
1148
1226
 
1149
- const itemMixin = createMixin<HTMLElement, [options: MenuItemOptions], ElementProps>((handle) => {
1227
+ const itemMixin: MixinFactory<HTMLElement, [options: MenuItemOptions], ElementProps> = createMixin<
1228
+ HTMLElement,
1229
+ [options: MenuItemOptions],
1230
+ ElementProps
1231
+ >((handle) => {
1150
1232
  let itemRef: HTMLElement | undefined
1151
1233
 
1152
1234
  handle.queueTask((node) => {
@@ -1226,11 +1308,11 @@ const itemMixin = createMixin<HTMLElement, [options: MenuItemOptions], ElementPr
1226
1308
  }
1227
1309
  })
1228
1310
 
1229
- const submenuTriggerMixin = createMixin<
1311
+ const submenuTriggerMixin: MixinFactory<
1230
1312
  HTMLElement,
1231
1313
  [options: SubmenuTriggerOptions],
1232
1314
  ElementProps
1233
- >((handle) => {
1315
+ > = createMixin<HTMLElement, [options: SubmenuTriggerOptions], ElementProps>((handle) => {
1234
1316
  let itemRef: HTMLElement | undefined
1235
1317
  let openTimeoutId = 0
1236
1318
  let aborted = false
@@ -1382,6 +1464,7 @@ type MenuListChildProps = Omit<
1382
1464
  >
1383
1465
 
1384
1466
  export const Context = MenuProvider
1467
+ export const contextTrigger = contextTriggerMixin
1385
1468
  export const item = itemMixin
1386
1469
  export const list = listMixin
1387
1470
  export { popoverMixin as popover }
@@ -1390,6 +1473,7 @@ export const trigger = triggerMixin
1390
1473
 
1391
1474
  const menu = {
1392
1475
  Context,
1476
+ contextTrigger,
1393
1477
  item,
1394
1478
  list,
1395
1479
  popover: popoverMixin,
@@ -1400,7 +1484,7 @@ const menu = {
1400
1484
  export function onMenuSelect<target extends HTMLElement>(
1401
1485
  handler: MenuSelectHandler<target>,
1402
1486
  captureBoolean?: boolean,
1403
- ) {
1487
+ ): ReturnType<typeof on<target, typeof MENU_SELECT_EVENT>> {
1404
1488
  return on(MENU_SELECT_EVENT, handler, captureBoolean)
1405
1489
  }
1406
1490
 
@@ -1425,7 +1509,7 @@ export interface SubmenuProps
1425
1509
  menuLabel?: string
1426
1510
  }
1427
1511
 
1428
- export function Menu(handle: Handle<MenuProps>) {
1512
+ export function Menu(handle: Handle<MenuProps>): () => RemixNode {
1429
1513
  let buttonRef: HTMLButtonElement | undefined
1430
1514
 
1431
1515
  return () => {
@@ -1472,7 +1556,7 @@ export function Menu(handle: Handle<MenuProps>) {
1472
1556
  }
1473
1557
  }
1474
1558
 
1475
- export function MenuList(handle: Handle<MenuListProps>) {
1559
+ export function MenuList(handle: Handle<MenuListProps>): () => RemixNode {
1476
1560
  return () => {
1477
1561
  let { children, mix, ...divProps } = handle.props
1478
1562
 
@@ -1486,7 +1570,7 @@ export function MenuList(handle: Handle<MenuListProps>) {
1486
1570
  }
1487
1571
  }
1488
1572
 
1489
- export function MenuItem(handle: Handle<MenuItemProps>) {
1573
+ export function MenuItem(handle: Handle<MenuItemProps>): () => RemixNode {
1490
1574
  return () => {
1491
1575
  let { checked, children, disabled, label, mix, name, searchValue, type, value, ...divProps } =
1492
1576
  handle.props
@@ -1509,7 +1593,7 @@ export function MenuItem(handle: Handle<MenuItemProps>) {
1509
1593
  }
1510
1594
  }
1511
1595
 
1512
- export function Submenu(handle: Handle<SubmenuProps>) {
1596
+ export function Submenu(handle: Handle<SubmenuProps>): () => RemixNode {
1513
1597
  return () => {
1514
1598
  let { children, disabled, label, listProps, menuLabel, mix, searchValue, value, ...divProps } =
1515
1599
  handle.props
@@ -8,6 +8,7 @@ import { theme } from '@remix-run/ui/theme'
8
8
  /**
9
9
  * @name Popover Overview
10
10
  * @description A popover panel anchored to a trigger button, containing form controls and a close action.
11
+ * @layout center
11
12
  */
12
13
  export default function Example(handle: Handle) {
13
14
  let open = false