@remix-run/ui 0.0.0 → 0.1.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 (273) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +195 -2
  3. package/dist/animation/animate-layout-mixin.d.ts +12 -0
  4. package/dist/animation/animate-layout-mixin.js +214 -0
  5. package/dist/animation/animate-layout-mixin.js.map +1 -0
  6. package/dist/animation/animate-mixins.d.ts +29 -0
  7. package/dist/animation/animate-mixins.js +235 -0
  8. package/dist/animation/animate-mixins.js.map +1 -0
  9. package/dist/animation/index.d.ts +6 -0
  10. package/dist/animation/index.js +5 -0
  11. package/dist/animation/index.js.map +1 -0
  12. package/dist/animation/spring.d.ts +75 -0
  13. package/dist/animation/spring.js +183 -0
  14. package/dist/animation/spring.js.map +1 -0
  15. package/dist/animation/tween.d.ts +70 -0
  16. package/dist/animation/tween.js +93 -0
  17. package/dist/animation/tween.js.map +1 -0
  18. package/dist/components/accordion/accordion.d.ts +92 -0
  19. package/dist/components/accordion/accordion.js +337 -0
  20. package/dist/components/accordion/accordion.js.map +1 -0
  21. package/dist/components/anchor/anchor.d.ts +13 -0
  22. package/dist/components/anchor/anchor.js +558 -0
  23. package/dist/components/anchor/anchor.js.map +1 -0
  24. package/dist/components/breadcrumbs/breadcrumbs.d.ts +11 -0
  25. package/dist/components/breadcrumbs/breadcrumbs.js +78 -0
  26. package/dist/components/breadcrumbs/breadcrumbs.js.map +1 -0
  27. package/dist/components/button/button.d.ts +23 -0
  28. package/dist/components/button/button.js +147 -0
  29. package/dist/components/button/button.js.map +1 -0
  30. package/dist/components/combobox/combobox.d.ts +101 -0
  31. package/dist/components/combobox/combobox.js +708 -0
  32. package/dist/components/combobox/combobox.js.map +1 -0
  33. package/dist/components/glyph/glyph.d.ts +14 -0
  34. package/dist/components/glyph/glyph.js +65 -0
  35. package/dist/components/glyph/glyph.js.map +1 -0
  36. package/dist/components/listbox/listbox.d.ts +67 -0
  37. package/dist/components/listbox/listbox.js +340 -0
  38. package/dist/components/listbox/listbox.js.map +1 -0
  39. package/dist/components/menu/hover-aim.d.ts +5 -0
  40. package/dist/components/menu/hover-aim.js +308 -0
  41. package/dist/components/menu/hover-aim.js.map +1 -0
  42. package/dist/components/menu/menu.d.ts +164 -0
  43. package/dist/components/menu/menu.js +1106 -0
  44. package/dist/components/menu/menu.js.map +1 -0
  45. package/dist/components/popover/popover.d.ts +35 -0
  46. package/dist/components/popover/popover.js +138 -0
  47. package/dist/components/popover/popover.js.map +1 -0
  48. package/dist/components/select/select.d.ts +67 -0
  49. package/dist/components/select/select.js +397 -0
  50. package/dist/components/select/select.js.map +1 -0
  51. package/dist/components/separator/separator.d.ts +2 -0
  52. package/dist/components/separator/separator.js +15 -0
  53. package/dist/components/separator/separator.js.map +1 -0
  54. package/dist/components/tabs/tabs.d.ts +78 -0
  55. package/dist/components/tabs/tabs.js +341 -0
  56. package/dist/components/tabs/tabs.js.map +1 -0
  57. package/dist/index.d.ts +28 -0
  58. package/dist/index.js +21 -0
  59. package/dist/index.js.map +1 -0
  60. package/dist/interactions/keydown/keydown.d.ts +1 -0
  61. package/dist/interactions/keydown/keydown.js +8 -0
  62. package/dist/interactions/keydown/keydown.js.map +1 -0
  63. package/dist/interactions/outside-click/outside-click-mixin.d.ts +2 -0
  64. package/dist/interactions/outside-click/outside-click-mixin.js +29 -0
  65. package/dist/interactions/outside-click/outside-click-mixin.js.map +1 -0
  66. package/dist/interactions/typeahead/typeahead-mixin.d.ts +8 -0
  67. package/dist/interactions/typeahead/typeahead-mixin.js +59 -0
  68. package/dist/interactions/typeahead/typeahead-mixin.js.map +1 -0
  69. package/dist/jsx-runtime.d.ts +2 -0
  70. package/dist/jsx-runtime.js +3 -0
  71. package/dist/jsx-runtime.js.map +1 -0
  72. package/dist/runtime/client-entries.d.ts +91 -0
  73. package/dist/runtime/client-entries.js +40 -0
  74. package/dist/runtime/client-entries.js.map +1 -0
  75. package/dist/runtime/component.d.ts +223 -0
  76. package/dist/runtime/component.js +152 -0
  77. package/dist/runtime/component.js.map +1 -0
  78. package/dist/runtime/create-element.d.ts +10 -0
  79. package/dist/runtime/create-element.js +29 -0
  80. package/dist/runtime/create-element.js.map +1 -0
  81. package/dist/runtime/diff-dom.d.ts +2 -0
  82. package/dist/runtime/diff-dom.js +364 -0
  83. package/dist/runtime/diff-dom.js.map +1 -0
  84. package/dist/runtime/diff-props.d.ts +9 -0
  85. package/dist/runtime/diff-props.js +195 -0
  86. package/dist/runtime/diff-props.js.map +1 -0
  87. package/dist/runtime/document-state.d.ts +11 -0
  88. package/dist/runtime/document-state.js +106 -0
  89. package/dist/runtime/document-state.js.map +1 -0
  90. package/dist/runtime/dom.d.ts +3189 -0
  91. package/dist/runtime/dom.js +2 -0
  92. package/dist/runtime/dom.js.map +1 -0
  93. package/dist/runtime/error-event.d.ts +20 -0
  94. package/dist/runtime/error-event.js +19 -0
  95. package/dist/runtime/error-event.js.map +1 -0
  96. package/dist/runtime/event-listeners.d.ts +50 -0
  97. package/dist/runtime/event-listeners.js +31 -0
  98. package/dist/runtime/event-listeners.js.map +1 -0
  99. package/dist/runtime/frame.d.ts +119 -0
  100. package/dist/runtime/frame.js +915 -0
  101. package/dist/runtime/frame.js.map +1 -0
  102. package/dist/runtime/invariant.d.ts +23 -0
  103. package/dist/runtime/invariant.js +33 -0
  104. package/dist/runtime/invariant.js.map +1 -0
  105. package/dist/runtime/jsx.d.ts +320 -0
  106. package/dist/runtime/jsx.js +31 -0
  107. package/dist/runtime/jsx.js.map +1 -0
  108. package/dist/runtime/mixins/attrs-mixin.d.ts +9 -0
  109. package/dist/runtime/mixins/attrs-mixin.js +23 -0
  110. package/dist/runtime/mixins/attrs-mixin.js.map +1 -0
  111. package/dist/runtime/mixins/link-mixin.d.ts +17 -0
  112. package/dist/runtime/mixins/link-mixin.js +101 -0
  113. package/dist/runtime/mixins/link-mixin.js.map +1 -0
  114. package/dist/runtime/mixins/mixin.d.ts +145 -0
  115. package/dist/runtime/mixins/mixin.js +570 -0
  116. package/dist/runtime/mixins/mixin.js.map +1 -0
  117. package/dist/runtime/mixins/on-mixin.d.ts +16 -0
  118. package/dist/runtime/mixins/on-mixin.js +49 -0
  119. package/dist/runtime/mixins/on-mixin.js.map +1 -0
  120. package/dist/runtime/mixins/ref-mixin.d.ts +9 -0
  121. package/dist/runtime/mixins/ref-mixin.js +21 -0
  122. package/dist/runtime/mixins/ref-mixin.js.map +1 -0
  123. package/dist/runtime/navigation.d.ts +22 -0
  124. package/dist/runtime/navigation.js +102 -0
  125. package/dist/runtime/navigation.js.map +1 -0
  126. package/dist/runtime/reconcile.d.ts +11 -0
  127. package/dist/runtime/reconcile.js +1240 -0
  128. package/dist/runtime/reconcile.js.map +1 -0
  129. package/dist/runtime/render.d.ts +54 -0
  130. package/dist/runtime/render.js +50 -0
  131. package/dist/runtime/render.js.map +1 -0
  132. package/dist/runtime/run.d.ts +45 -0
  133. package/dist/runtime/run.js +68 -0
  134. package/dist/runtime/run.js.map +1 -0
  135. package/dist/runtime/scheduler.d.ts +30 -0
  136. package/dist/runtime/scheduler.js +185 -0
  137. package/dist/runtime/scheduler.js.map +1 -0
  138. package/dist/runtime/svg-attributes.d.ts +5 -0
  139. package/dist/runtime/svg-attributes.js +124 -0
  140. package/dist/runtime/svg-attributes.js.map +1 -0
  141. package/dist/runtime/to-vnode.d.ts +3 -0
  142. package/dist/runtime/to-vnode.js +43 -0
  143. package/dist/runtime/to-vnode.js.map +1 -0
  144. package/dist/runtime/typed-event-target.d.ts +50 -0
  145. package/dist/runtime/typed-event-target.js +6 -0
  146. package/dist/runtime/typed-event-target.js.map +1 -0
  147. package/dist/runtime/vdom.d.ts +55 -0
  148. package/dist/runtime/vdom.js +198 -0
  149. package/dist/runtime/vdom.js.map +1 -0
  150. package/dist/runtime/vnode.d.ts +79 -0
  151. package/dist/runtime/vnode.js +38 -0
  152. package/dist/runtime/vnode.js.map +1 -0
  153. package/dist/server/stream.d.ts +57 -0
  154. package/dist/server/stream.js +1007 -0
  155. package/dist/server/stream.js.map +1 -0
  156. package/dist/style/css-mixin.d.ts +8 -0
  157. package/dist/style/css-mixin.js +54 -0
  158. package/dist/style/css-mixin.js.map +1 -0
  159. package/dist/style/index.d.ts +5 -0
  160. package/dist/style/index.js +4 -0
  161. package/dist/style/index.js.map +1 -0
  162. package/dist/style/style.d.ts +24 -0
  163. package/dist/style/style.js +269 -0
  164. package/dist/style/style.js.map +1 -0
  165. package/dist/style/stylesheet.d.ts +11 -0
  166. package/dist/style/stylesheet.js +174 -0
  167. package/dist/style/stylesheet.js.map +1 -0
  168. package/dist/test.d.ts +2 -0
  169. package/dist/test.js +2 -0
  170. package/dist/test.js.map +1 -0
  171. package/dist/theme/contract.d.ts +259 -0
  172. package/dist/theme/contract.js +131 -0
  173. package/dist/theme/contract.js.map +1 -0
  174. package/dist/theme/glyph-contract.d.ts +13 -0
  175. package/dist/theme/glyph-contract.js +34 -0
  176. package/dist/theme/glyph-contract.js.map +1 -0
  177. package/dist/theme/presets/rmx-01/glyphs.d.ts +2 -0
  178. package/dist/theme/presets/rmx-01/glyphs.js +252 -0
  179. package/dist/theme/presets/rmx-01/glyphs.js.map +1 -0
  180. package/dist/theme/presets/rmx-01/index.d.ts +2 -0
  181. package/dist/theme/presets/rmx-01/index.js +119 -0
  182. package/dist/theme/presets/rmx-01/index.js.map +1 -0
  183. package/dist/theme/runtime.d.ts +2 -0
  184. package/dist/theme/runtime.js +81 -0
  185. package/dist/theme/runtime.js.map +1 -0
  186. package/dist/theme/theme.d.ts +6 -0
  187. package/dist/theme/theme.js +5 -0
  188. package/dist/theme/theme.js.map +1 -0
  189. package/dist/utils/flash-attribute.d.ts +1 -0
  190. package/dist/utils/flash-attribute.js +11 -0
  191. package/dist/utils/flash-attribute.js.map +1 -0
  192. package/dist/utils/scroll-lock.d.ts +3 -0
  193. package/dist/utils/scroll-lock.js +69 -0
  194. package/dist/utils/scroll-lock.js.map +1 -0
  195. package/dist/utils/wait-for-css-transition.d.ts +1 -0
  196. package/dist/utils/wait-for-css-transition.js +58 -0
  197. package/dist/utils/wait-for-css-transition.js.map +1 -0
  198. package/dist/utils/wait.d.ts +1 -0
  199. package/dist/utils/wait.js +6 -0
  200. package/dist/utils/wait.js.map +1 -0
  201. package/package.json +125 -5
  202. package/src/animation/animate-layout-mixin.ts +266 -0
  203. package/src/animation/animate-mixins.ts +292 -0
  204. package/src/animation/index.ts +6 -0
  205. package/src/animation/spring.ts +299 -0
  206. package/src/animation/tween.ts +134 -0
  207. package/src/components/accordion/accordion.tsx +565 -0
  208. package/src/components/anchor/anchor.ts +904 -0
  209. package/src/components/breadcrumbs/breadcrumbs.tsx +124 -0
  210. package/src/components/button/README.md +44 -0
  211. package/src/components/button/button.tsx +188 -0
  212. package/src/components/combobox/README.md +145 -0
  213. package/src/components/combobox/combobox.tsx +1014 -0
  214. package/src/components/glyph/glyph.tsx +110 -0
  215. package/src/components/listbox/listbox.ts +447 -0
  216. package/src/components/menu/hover-aim.ts +428 -0
  217. package/src/components/menu/menu.tsx +1547 -0
  218. package/src/components/popover/README.md +122 -0
  219. package/src/components/popover/popover.ts +201 -0
  220. package/src/components/select/select.tsx +585 -0
  221. package/src/components/separator/separator.ts +19 -0
  222. package/src/components/tabs/README.md +105 -0
  223. package/src/components/tabs/tabs.tsx +499 -0
  224. package/src/index.ts +72 -0
  225. package/src/interactions/keydown/keydown.ts +14 -0
  226. package/src/interactions/outside-click/outside-click-mixin.ts +55 -0
  227. package/src/interactions/typeahead/typeahead-mixin.ts +89 -0
  228. package/src/jsx-runtime.ts +2 -0
  229. package/src/runtime/client-entries.ts +137 -0
  230. package/src/runtime/component.ts +402 -0
  231. package/src/runtime/create-element.ts +38 -0
  232. package/src/runtime/diff-dom.ts +404 -0
  233. package/src/runtime/diff-props.ts +209 -0
  234. package/src/runtime/document-state.ts +130 -0
  235. package/src/runtime/dom.ts +3971 -0
  236. package/src/runtime/error-event.ts +26 -0
  237. package/src/runtime/event-listeners.ts +171 -0
  238. package/src/runtime/frame.ts +1211 -0
  239. package/src/runtime/invariant.ts +31 -0
  240. package/src/runtime/jsx.ts +398 -0
  241. package/src/runtime/mixins/attrs-mixin.ts +35 -0
  242. package/src/runtime/mixins/link-mixin.ts +131 -0
  243. package/src/runtime/mixins/mixin.ts +908 -0
  244. package/src/runtime/mixins/on-mixin.ts +89 -0
  245. package/src/runtime/mixins/ref-mixin.ts +32 -0
  246. package/src/runtime/navigation.ts +136 -0
  247. package/src/runtime/reconcile.ts +1722 -0
  248. package/src/runtime/render.ts +101 -0
  249. package/src/runtime/run.ts +100 -0
  250. package/src/runtime/scheduler.ts +262 -0
  251. package/src/runtime/svg-attributes.ts +133 -0
  252. package/src/runtime/to-vnode.ts +50 -0
  253. package/src/runtime/typed-event-target.ts +67 -0
  254. package/src/runtime/vdom.ts +295 -0
  255. package/src/runtime/vnode.ts +137 -0
  256. package/src/server/stream.ts +1342 -0
  257. package/src/style/css-mixin.ts +82 -0
  258. package/src/style/index.ts +10 -0
  259. package/src/style/style.ts +312 -0
  260. package/src/style/stylesheet.ts +209 -0
  261. package/src/test/setup.ts +7 -0
  262. package/src/test/utils.ts +45 -0
  263. package/src/test.ts +2 -0
  264. package/src/theme/contract.ts +183 -0
  265. package/src/theme/glyph-contract.ts +57 -0
  266. package/src/theme/presets/rmx-01/glyphs.tsx +308 -0
  267. package/src/theme/presets/rmx-01/index.ts +121 -0
  268. package/src/theme/runtime.ts +116 -0
  269. package/src/theme/theme.ts +15 -0
  270. package/src/utils/flash-attribute.ts +11 -0
  271. package/src/utils/scroll-lock.ts +97 -0
  272. package/src/utils/wait-for-css-transition.ts +66 -0
  273. package/src/utils/wait.ts +5 -0
@@ -0,0 +1,110 @@
1
+ import { createElement } from '@remix-run/ui'
2
+ import type { Handle, Props, RemixElement } from '@remix-run/ui'
3
+
4
+ import {
5
+ glyphContract,
6
+ glyphNames,
7
+ type GlyphName,
8
+ type GlyphValues,
9
+ } from '../../theme/glyph-contract.ts'
10
+
11
+ export type { GlyphName, GlyphSymbol, GlyphValues } from '../../theme/glyph-contract.ts'
12
+
13
+ export type GlyphSheetProps = Omit<Props<'svg'>, 'children'>
14
+
15
+ type GlyphSheetRenderer = (handle: Handle<GlyphSheetProps>) => () => RemixElement
16
+
17
+ export type GlyphSheetComponent = GlyphSheetRenderer & {
18
+ ids: Readonly<Record<GlyphName, string>>
19
+ values: GlyphValues
20
+ }
21
+
22
+ export type GlyphProps = Omit<Props<'svg'>, 'children'> & {
23
+ name: GlyphName
24
+ }
25
+
26
+ export function createGlyphSheet(values: GlyphValues): GlyphSheetComponent {
27
+ let ids = Object.freeze(
28
+ Object.fromEntries(glyphNames.map((name) => [name, glyphContract[name].id])) as Record<
29
+ GlyphName,
30
+ string
31
+ >,
32
+ )
33
+
34
+ function GlyphSheet(handle: Handle<GlyphSheetProps>) {
35
+ return () => {
36
+ let { style, ...svgProps } = handle.props
37
+ let hiddenStyle = {
38
+ position: 'absolute',
39
+ width: '0',
40
+ height: '0',
41
+ overflow: 'hidden',
42
+ pointerEvents: 'none',
43
+ }
44
+ let nextStyle =
45
+ typeof style === 'object' && style !== null ? { ...hiddenStyle, ...style } : hiddenStyle
46
+
47
+ return createElement(
48
+ 'svg',
49
+ {
50
+ ...svgProps,
51
+ 'aria-hidden': handle.props['aria-hidden'] ?? true,
52
+ focusable: handle.props.focusable ?? 'false',
53
+ height: handle.props.height ?? '0',
54
+ style: nextStyle,
55
+ width: handle.props.width ?? '0',
56
+ xmlns: 'http://www.w3.org/2000/svg',
57
+ },
58
+ glyphNames.map((name) => {
59
+ let glyph = values[name]
60
+
61
+ return cloneGlyphSymbol(name, glyph, ids[name])
62
+ }),
63
+ )
64
+ }
65
+ }
66
+
67
+ return Object.assign(GlyphSheet, {
68
+ ids,
69
+ values,
70
+ })
71
+ }
72
+
73
+ export function Glyph(handle: Handle<GlyphProps>) {
74
+ return () => {
75
+ let { fill, name, ...svgProps } = handle.props
76
+ let glyphId = glyphContract[name].id
77
+ let hiddenByDefault =
78
+ handle.props['aria-hidden'] === undefined &&
79
+ handle.props['aria-label'] === undefined &&
80
+ handle.props['aria-labelledby'] === undefined
81
+
82
+ return createElement(
83
+ 'svg',
84
+ {
85
+ ...svgProps,
86
+ 'aria-hidden': hiddenByDefault ? true : handle.props['aria-hidden'],
87
+ fill: fill ?? 'none',
88
+ xmlns: 'http://www.w3.org/2000/svg',
89
+ },
90
+ createElement('use', {
91
+ xlinkHref: `#${glyphId}`,
92
+ }),
93
+ )
94
+ }
95
+ }
96
+
97
+ function cloneGlyphSymbol(name: GlyphName, glyph: RemixElement, id: string): RemixElement {
98
+ if (glyph.type !== 'symbol') {
99
+ throw new TypeError(`Expected glyph "${name}" to be a <symbol> element`)
100
+ }
101
+
102
+ return {
103
+ ...glyph,
104
+ key: name,
105
+ props: {
106
+ ...glyph.props,
107
+ id,
108
+ },
109
+ }
110
+ }
@@ -0,0 +1,447 @@
1
+ import {
2
+ attrs,
3
+ createMixin,
4
+ css,
5
+ on,
6
+ type CSSMixinDescriptor,
7
+ type Handle,
8
+ type RemixNode,
9
+ } from '@remix-run/ui'
10
+ import { theme } from '../../theme/theme.ts'
11
+ import {
12
+ hiddenTypeahead,
13
+ matchNextItemBySearchText,
14
+ type SearchValue,
15
+ } from '../../interactions/typeahead/typeahead-mixin.ts'
16
+ import { flashAttribute } from '../../utils/flash-attribute.ts'
17
+
18
+ export type ListboxValue = string | null
19
+
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
+ }
31
+
32
+ interface Values {
33
+ value: ListboxValue
34
+ activeValue: ListboxValue
35
+ }
36
+
37
+ export interface ListboxContext extends Values {
38
+ registerOption: (option: RegisteredOption) => void
39
+ select: (value: ListboxValue) => Promise<void>
40
+ highlight: (value: ListboxValue) => void
41
+ highlightSearchMatch: (text: string) => void
42
+ navigate: (direction: NavigationStrategy) => void
43
+ scrollActiveOptionIntoView: () => void
44
+ }
45
+
46
+ export interface ListboxProviderProps extends Values {
47
+ children?: RemixNode
48
+ ref?: (ref: ListboxRef) => void
49
+ flashSelection?: boolean
50
+ selectionFlashAttribute?: string
51
+ onSelect: (value: ListboxValue, option?: ListboxRegisteredOption) => void
52
+ onSelectSettled?: (value: ListboxValue, option?: ListboxRegisteredOption) => void | Promise<void>
53
+ onHighlight: (value: ListboxValue, option?: ListboxRegisteredOption) => void
54
+ }
55
+
56
+ export interface ListboxRef {
57
+ active: ListboxRegisteredOption | undefined
58
+ options: ReadonlyArray<ListboxRegisteredOption>
59
+ selected: ListboxRegisteredOption | undefined
60
+ highlight: (value: ListboxValue) => void
61
+ highlightSearchMatch: (text: string) => void
62
+ matchSearchText: (text: string, fromValue?: ListboxValue) => ListboxRegisteredOption | null
63
+ navigateFirst: () => void
64
+ navigateLast: () => void
65
+ navigateNext: () => void
66
+ navigatePrevious: () => void
67
+ scrollActiveOptionIntoView: () => void
68
+ select: (value: ListboxValue) => Promise<void>
69
+ selectActive: () => Promise<void>
70
+ }
71
+
72
+ export interface ListboxRegisteredOption extends ListboxOption {
73
+ readonly hidden: boolean
74
+ readonly node: HTMLElement
75
+ }
76
+
77
+ interface RegisteredOption extends ListboxRegisteredOption {}
78
+
79
+ const listCss: CSSMixinDescriptor = css({
80
+ display: 'flex',
81
+ flexDirection: 'column',
82
+ outline: 'none',
83
+ userSelect: 'none',
84
+ WebkitUserSelect: 'none',
85
+ '--rmx-ui-item-inset': theme.space.sm,
86
+ '--rmx-ui-item-indicator-gap': theme.space.xs,
87
+ '--rmx-ui-item-indicator-width': theme.fontSize.sm,
88
+ })
89
+
90
+ const itemCss: CSSMixinDescriptor = css({
91
+ display: 'grid',
92
+ gridTemplateColumns: 'max-content minmax(0, 1fr)',
93
+ alignItems: 'center',
94
+ width: '100%',
95
+ minHeight: theme.control.height.md,
96
+ padding: `${theme.space.xs} ${theme.space.sm}`,
97
+ borderRadius: theme.radius.md,
98
+ backgroundColor: 'transparent',
99
+ color: theme.colors.text.primary,
100
+ fontFamily: theme.fontFamily.sans,
101
+ fontSize: theme.fontSize.sm,
102
+ fontWeight: theme.fontWeight.normal,
103
+ lineHeight: theme.lineHeight.normal,
104
+ textAlign: 'left',
105
+ userSelect: 'none',
106
+ WebkitUserSelect: 'none',
107
+ '&:focus': {
108
+ outline: 'none',
109
+ },
110
+ '&[data-highlighted="true"]': {
111
+ backgroundColor: theme.colors.action.primary.background,
112
+ color: theme.colors.action.primary.foreground,
113
+ },
114
+ '&[aria-disabled="true"]': {
115
+ opacity: 0.5,
116
+ },
117
+ scrollMarginBlock: theme.space.xs,
118
+ '--rmx-listbox-option-indicator-opacity': '0',
119
+ '&[hidden]': {
120
+ display: 'none',
121
+ },
122
+ '&[data-listbox-flash="true"], &[data-select-flash="true"], &[data-combobox-flash="true"]': {
123
+ backgroundColor: 'transparent',
124
+ color: theme.colors.text.primary,
125
+ },
126
+ '&[aria-selected="true"]': {
127
+ '--rmx-listbox-option-indicator-opacity': '1',
128
+ },
129
+ })
130
+
131
+ const itemGlyphCss: CSSMixinDescriptor = css({
132
+ gridColumn: '1',
133
+ display: 'inline-flex',
134
+ alignItems: 'center',
135
+ justifyContent: 'center',
136
+ width: '1em',
137
+ height: '1em',
138
+ color: 'currentColor',
139
+ flexShrink: 0,
140
+ opacity: 'var(--rmx-listbox-option-indicator-opacity)',
141
+ '& > svg': {
142
+ display: 'block',
143
+ width: '100%',
144
+ height: '100%',
145
+ },
146
+ })
147
+
148
+ const itemLabelCss: CSSMixinDescriptor = css({
149
+ display: 'inline-flex',
150
+ alignItems: 'center',
151
+ minWidth: 0,
152
+ paddingInline: theme.space.xs,
153
+ WebkitUserSelect: 'none',
154
+ })
155
+
156
+ export const listStyle = listCss
157
+ export const optionStyle = itemCss
158
+ export const glyphStyle = itemGlyphCss
159
+ export const labelStyle = itemLabelCss
160
+
161
+ function ListboxProvider(handle: Handle<ListboxProviderProps, ListboxContext>) {
162
+ let options: RegisteredOption[] = []
163
+ let state = State.Idle
164
+
165
+ function getOption(value: ListboxValue) {
166
+ return options.find((option) => option.value === value)
167
+ }
168
+
169
+ function isVisibleOption(option: RegisteredOption | undefined): option is RegisteredOption {
170
+ return !!option?.node?.isConnected && !option.hidden
171
+ }
172
+
173
+ function isInteractableOption(option: RegisteredOption | undefined) {
174
+ return isVisibleOption(option) && !option?.disabled
175
+ }
176
+
177
+ function getInteractableOptions() {
178
+ return options.filter(isInteractableOption)
179
+ }
180
+
181
+ function scrollOptionIntoView(option: RegisteredOption | undefined) {
182
+ if (!isVisibleOption(option)) {
183
+ return
184
+ }
185
+
186
+ option.node.scrollIntoView({
187
+ block: 'nearest',
188
+ inline: 'nearest',
189
+ })
190
+ }
191
+
192
+ function findSearchMatch(text: string, fromValue = handle.props.activeValue) {
193
+ let interactableOptions = getInteractableOptions()
194
+ let fromIndex = interactableOptions.findIndex((option) => option.value === fromValue)
195
+
196
+ return matchNextItemBySearchText(text, interactableOptions, {
197
+ fromIndex,
198
+ getSearchValues: (option) => option.textValue ?? option.label,
199
+ })
200
+ }
201
+
202
+ let context: ListboxContext
203
+
204
+ let ref: ListboxRef = {
205
+ get active() {
206
+ return getOption(handle.props.activeValue)
207
+ },
208
+ get options() {
209
+ return options
210
+ },
211
+ get selected() {
212
+ return getOption(handle.props.value)
213
+ },
214
+ highlight(value) {
215
+ context.highlight(value)
216
+ },
217
+ highlightSearchMatch(text) {
218
+ context.highlightSearchMatch(text)
219
+ },
220
+ matchSearchText(text, fromValue = handle.props.activeValue) {
221
+ return findSearchMatch(text, fromValue)
222
+ },
223
+ navigateFirst() {
224
+ context.navigate(NavigationStrategy.First)
225
+ },
226
+ navigateLast() {
227
+ context.navigate(NavigationStrategy.Last)
228
+ },
229
+ navigateNext() {
230
+ context.navigate(NavigationStrategy.Next)
231
+ },
232
+ navigatePrevious() {
233
+ context.navigate(NavigationStrategy.Previous)
234
+ },
235
+ scrollActiveOptionIntoView() {
236
+ context.scrollActiveOptionIntoView()
237
+ },
238
+ select(value) {
239
+ return context.select(value)
240
+ },
241
+ selectActive() {
242
+ return context.select(handle.props.activeValue)
243
+ },
244
+ }
245
+
246
+ handle.queueTask(() => {
247
+ handle.props.ref?.(ref)
248
+ })
249
+
250
+ context = {
251
+ get value() {
252
+ return handle.props.value
253
+ },
254
+
255
+ get activeValue() {
256
+ return handle.props.activeValue
257
+ },
258
+
259
+ registerOption(option) {
260
+ options.push(option)
261
+ },
262
+
263
+ async select(value) {
264
+ if (state === State.Selecting) {
265
+ return
266
+ }
267
+ state = State.Selecting
268
+ let option = getOption(value)
269
+ if (!isInteractableOption(option)) {
270
+ state = State.Idle
271
+ return
272
+ }
273
+ handle.props.onSelect(value, option)
274
+ if (option && handle.props.flashSelection) {
275
+ await flashAttribute(
276
+ option.node,
277
+ handle.props.selectionFlashAttribute ?? 'data-listbox-flash',
278
+ 60,
279
+ )
280
+ }
281
+ await handle.props.onSelectSettled?.(value, option)
282
+ state = State.Idle
283
+ },
284
+
285
+ highlight(value) {
286
+ if (state === State.Selecting) return
287
+ let option = getOption(value)
288
+ handle.props.onHighlight(value, option)
289
+ },
290
+
291
+ highlightSearchMatch(text) {
292
+ if (state === State.Selecting) return
293
+
294
+ let option = findSearchMatch(text, handle.props.activeValue)
295
+
296
+ if (option) {
297
+ handle.props.onHighlight(option.value, option)
298
+ scrollOptionIntoView(option)
299
+ }
300
+ },
301
+
302
+ navigate(strategy: NavigationStrategy) {
303
+ if (state === State.Selecting) return
304
+
305
+ let option: RegisteredOption | undefined
306
+ let interactableOptions = getInteractableOptions()
307
+ let activeIndex = interactableOptions.findIndex(
308
+ (option) => option.value === handle.props.activeValue,
309
+ )
310
+
311
+ switch (strategy) {
312
+ case NavigationStrategy.Next:
313
+ option = interactableOptions[activeIndex + 1] ?? interactableOptions[0]
314
+ break
315
+ case NavigationStrategy.Previous:
316
+ option =
317
+ activeIndex === -1
318
+ ? interactableOptions[interactableOptions.length - 1]
319
+ : interactableOptions[activeIndex - 1]
320
+ break
321
+ case NavigationStrategy.First:
322
+ option = interactableOptions[0]
323
+ break
324
+ case NavigationStrategy.Last:
325
+ option = interactableOptions[interactableOptions.length - 1]
326
+ break
327
+ }
328
+
329
+ if (option) {
330
+ handle.props.onHighlight(option.value, option)
331
+ scrollOptionIntoView(option)
332
+ }
333
+ },
334
+
335
+ scrollActiveOptionIntoView() {
336
+ scrollOptionIntoView(getOption(handle.props.activeValue))
337
+ },
338
+ }
339
+
340
+ handle.context.set(context)
341
+
342
+ return () => {
343
+ options = []
344
+ return handle.props.children
345
+ }
346
+ }
347
+
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
+ })
391
+
392
+ export interface ListboxOption {
393
+ id: string
394
+ value: string
395
+ label: string
396
+ disabled?: boolean
397
+ textValue?: SearchValue
398
+ }
399
+
400
+ const optionMixin = createMixin<HTMLElement, [option: Omit<ListboxOption, 'id'>]>((handle) => {
401
+ let optionRef: HTMLElement | undefined
402
+
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
+ },
418
+ })
419
+
420
+ return [
421
+ attrs({
422
+ role: 'option',
423
+ 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)
435
+ }),
436
+ on('mouseleave', () => {
437
+ if (context.activeValue !== option.value) return
438
+ context.highlight(null)
439
+ }),
440
+ ],
441
+ ]
442
+ }
443
+ })
444
+
445
+ export const Context = ListboxProvider
446
+ export const list = listMixin
447
+ export const option = optionMixin