@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,1014 @@
1
+ // @jsxRuntime classic
2
+ // @jsx createElement
3
+ import {
4
+ attrs,
5
+ css,
6
+ createElement,
7
+ createMixin,
8
+ on,
9
+ ref,
10
+ type CSSMixinDescriptor,
11
+ type ElementProps,
12
+ type Handle,
13
+ type MixinHandle,
14
+ type Props,
15
+ type RemixNode,
16
+ } from '@remix-run/ui'
17
+
18
+ import { Glyph } from '../glyph/glyph.tsx'
19
+ import * as listbox from '../listbox/listbox.ts'
20
+ import * as popover from '../popover/popover.ts'
21
+ import { theme } from '../../theme/theme.ts'
22
+ import { type SearchValue } from '../../interactions/typeahead/typeahead-mixin.ts'
23
+ import { waitForCssTransition } from '../../utils/wait-for-css-transition.ts'
24
+ import { wait } from '../../utils/wait.ts'
25
+
26
+ const COMBOBOX_CHANGE_EVENT = 'rmx:combobox-change' as const
27
+ const INPUT_COMMIT_DELAY_MS = 50
28
+
29
+ type ComboboxChangeHandler = (
30
+ event: ComboboxChangeEvent,
31
+ signal: AbortSignal,
32
+ ) => void | Promise<void>
33
+
34
+ type ShowReason = 'hint' | 'nav'
35
+
36
+ const comboboxPopoverCss: CSSMixinDescriptor = css({
37
+ opacity: 0,
38
+ '&:popover-open': {
39
+ opacity: 1,
40
+ },
41
+ '&:not(:popover-open)': {
42
+ pointerEvents: 'none',
43
+ },
44
+ '&[data-show-reason="nav"]:not(:popover-open)': {
45
+ transition: 'opacity 180ms ease-in, overlay 180ms ease-in, display 180ms ease-in',
46
+ transitionBehavior: 'allow-discrete',
47
+ },
48
+ '&[data-show-reason="hint"]:not(:popover-open)': {
49
+ transition: 'none',
50
+ transitionBehavior: 'normal',
51
+ },
52
+ })
53
+
54
+ const comboboxInputCss: CSSMixinDescriptor = css({
55
+ minHeight: theme.control.height.sm,
56
+ width: '100%',
57
+ paddingInline: theme.space.sm,
58
+ border: `0.5px solid ${theme.colors.border.default}`,
59
+ borderRadius: theme.radius.md,
60
+ backgroundColor: theme.surface.lvl0,
61
+ color: theme.colors.text.primary,
62
+ fontFamily: theme.fontFamily.sans,
63
+ fontSize: theme.fontSize.sm,
64
+ lineHeight: theme.lineHeight.normal,
65
+ boxShadow: 'inset 0 1px 0 rgb(255 255 255 / 0.7)',
66
+ '&:focus-visible': {
67
+ outline: `2px solid ${theme.colors.focus.ring}`,
68
+ outlineOffset: theme.space.none,
69
+ },
70
+ '&[data-surface-visible="true"][aria-activedescendant]:focus-visible': {
71
+ outline: 'none',
72
+ },
73
+ })
74
+
75
+ export const inputStyle = comboboxInputCss
76
+ export const popoverStyle = comboboxPopoverCss
77
+
78
+ declare global {
79
+ interface HTMLElementEventMap {
80
+ [COMBOBOX_CHANGE_EVENT]: ComboboxChangeEvent
81
+ }
82
+ }
83
+
84
+ export class ComboboxChangeEvent extends Event {
85
+ readonly label: string | null
86
+ readonly optionId: string | null
87
+ readonly value: string | null
88
+
89
+ constructor({
90
+ label,
91
+ optionId,
92
+ value,
93
+ }: {
94
+ label: string | null
95
+ optionId: string | null
96
+ value: string | null
97
+ }) {
98
+ super(COMBOBOX_CHANGE_EVENT, { bubbles: true })
99
+ this.label = label
100
+ this.optionId = optionId
101
+ this.value = value
102
+ }
103
+ }
104
+
105
+ export type ComboboxOpenStrategy = 'selected' | 'selected-or-none' | 'first' | 'last'
106
+
107
+ export interface ComboboxHandle {
108
+ readonly activeOptionId: string | null
109
+ readonly id: string
110
+ readonly inputText: string
111
+ readonly isOpen: boolean
112
+ readonly label: string | null
113
+ readonly value: string | null
114
+ close: () => void
115
+ open: (strategy?: ComboboxOpenStrategy) => Promise<void>
116
+ }
117
+
118
+ export interface ComboboxContextProps {
119
+ children?: RemixNode
120
+ defaultValue?: string | null
121
+ disabled?: boolean
122
+ name?: string
123
+ ref?: (handle: ComboboxHandle) => void
124
+ }
125
+
126
+ export interface ComboboxProps extends Omit<Props<'div'>, 'children'> {
127
+ children?: RemixNode
128
+ defaultValue?: string | null
129
+ disabled?: boolean
130
+ inputId?: string
131
+ name?: string
132
+ placeholder?: string
133
+ }
134
+
135
+ export interface ComboboxOptionOptions {
136
+ disabled?: boolean
137
+ label: string
138
+ searchValue?: SearchValue
139
+ value: string
140
+ }
141
+
142
+ export interface ComboboxOptionProps extends Omit<Props<'div'>, 'children'> {
143
+ children?: RemixNode
144
+ disabled?: boolean
145
+ label: string
146
+ searchValue?: SearchValue
147
+ value: string
148
+ }
149
+
150
+ interface ComboboxContextValue {
151
+ readonly activeId: string | undefined
152
+ readonly disabled: boolean
153
+ readonly filterText: string
154
+ readonly inputText: string
155
+ readonly isOpen: boolean
156
+ readonly listId: string
157
+ readonly name: string | undefined
158
+ readonly showReason: ShowReason
159
+ readonly surfaceVisible: boolean
160
+ readonly value: listbox.ListboxValue
161
+ clearInputSelection: () => void
162
+ close: () => void
163
+ handleBlur: () => void
164
+ handleEscape: () => void
165
+ navigateNext: () => void
166
+ navigatePrevious: () => void
167
+ open: (strategy?: ComboboxOpenStrategy) => Promise<void>
168
+ openFromArrow: (direction: 'first' | 'last') => Promise<void>
169
+ openFromInputActivation: () => Promise<void>
170
+ registerInput: (node: HTMLInputElement) => void
171
+ registerSurface: (node: HTMLElement) => void
172
+ setInputText: (text: string) => Promise<void>
173
+ syncPopoverMinWidth: () => void
174
+ selectActive: () => Promise<void>
175
+ unregisterInput: (node: HTMLInputElement) => void
176
+ unregisterSurface: (node: HTMLElement) => void
177
+ }
178
+
179
+ type PendingChange = {
180
+ label: string
181
+ optionId: string
182
+ value: string
183
+ } | null
184
+
185
+ function getSearchValues(value: SearchValue) {
186
+ return Array.isArray(value) ? value : [value]
187
+ }
188
+
189
+ function matchesSearchValue(value: SearchValue, text: string) {
190
+ if (text === '') {
191
+ return true
192
+ }
193
+
194
+ let normalizedText = text.toLowerCase()
195
+ return getSearchValues(value).some((candidate) =>
196
+ candidate.toLowerCase().startsWith(normalizedText),
197
+ )
198
+ }
199
+
200
+ function matchesExactSearchValue(value: SearchValue, text: string) {
201
+ if (text === '') {
202
+ return false
203
+ }
204
+
205
+ let normalizedText = text.toLowerCase()
206
+ return getSearchValues(value).some((candidate) => candidate.toLowerCase() === normalizedText)
207
+ }
208
+
209
+ function getOptionSearchValue(option: {
210
+ label: string
211
+ searchValue?: SearchValue
212
+ textValue?: SearchValue
213
+ }) {
214
+ return option.searchValue ?? option.textValue ?? option.label
215
+ }
216
+
217
+ function ComboboxProvider(handle: Handle<ComboboxContextProps, ComboboxContextValue>) {
218
+ let inputRef: HTMLInputElement | undefined
219
+ let listboxRef: listbox.ListboxRef | undefined
220
+ let surfaceRef: HTMLElement | undefined
221
+
222
+ let hasInitialized = false
223
+ let hasProvidedRef = false
224
+
225
+ let activeValue: listbox.ListboxValue = null
226
+ let activeId: string | undefined = undefined
227
+ let value: listbox.ListboxValue = null
228
+ let selectedId: string | undefined = undefined
229
+ let selectedLabel = ''
230
+ let inputText = ''
231
+ let filterText = ''
232
+ let open = false
233
+ let surfaceVisible = false
234
+ let showReason: ShowReason = 'nav'
235
+ let pendingInputValue: string | null = null
236
+ let pendingChange: PendingChange = null
237
+ let selectInputAfterClose = false
238
+ let closeSequenceId = 0
239
+
240
+ let listId = `${handle.id}-combobox-list`
241
+
242
+ function getOptions() {
243
+ return listboxRef?.options ?? []
244
+ }
245
+
246
+ function getSelectedOption() {
247
+ return getOptions().find((option) => option.value === value)
248
+ }
249
+
250
+ function getMatchingOptions(text: string) {
251
+ if (text === '') {
252
+ return getOptions()
253
+ }
254
+
255
+ return getOptions().filter((option) =>
256
+ matchesSearchValue(option.textValue ?? option.label, text),
257
+ )
258
+ }
259
+
260
+ function getEnabledMatchingOptions(text: string) {
261
+ return getMatchingOptions(text).filter((option) => !option.disabled)
262
+ }
263
+
264
+ function getExactInputMatch(text = inputText) {
265
+ return (
266
+ getOptions().find(
267
+ (option) =>
268
+ !option.disabled && matchesExactSearchValue(option.textValue ?? option.label, text),
269
+ ) ?? null
270
+ )
271
+ }
272
+
273
+ function getOpenFilterText() {
274
+ if (inputText === '') {
275
+ return ''
276
+ }
277
+
278
+ if (getExactInputMatch(inputText)) {
279
+ return ''
280
+ }
281
+
282
+ if (getMatchingOptions(inputText).length === 0) {
283
+ return ''
284
+ }
285
+
286
+ return inputText
287
+ }
288
+
289
+ function resolveOpenOption(strategy: ComboboxOpenStrategy, nextFilterText: string) {
290
+ let enabledMatchingOptions = getEnabledMatchingOptions(nextFilterText)
291
+ if (enabledMatchingOptions.length === 0) {
292
+ return null
293
+ }
294
+
295
+ if (strategy === 'selected-or-none') {
296
+ let selectedOption = getSelectedOption()
297
+ if (
298
+ selectedOption &&
299
+ !selectedOption.disabled &&
300
+ matchesSearchValue(selectedOption.textValue ?? selectedOption.label, nextFilterText)
301
+ ) {
302
+ return selectedOption
303
+ }
304
+
305
+ return null
306
+ }
307
+
308
+ if (strategy === 'selected') {
309
+ let exactInputMatch = getExactInputMatch(inputText)
310
+ if (
311
+ exactInputMatch &&
312
+ matchesSearchValue(exactInputMatch.textValue ?? exactInputMatch.label, nextFilterText)
313
+ ) {
314
+ return exactInputMatch
315
+ }
316
+
317
+ let selectedOption = getSelectedOption()
318
+ if (
319
+ selectedOption &&
320
+ !selectedOption.disabled &&
321
+ matchesSearchValue(selectedOption.textValue ?? selectedOption.label, nextFilterText)
322
+ ) {
323
+ return selectedOption
324
+ }
325
+ }
326
+
327
+ return strategy === 'last'
328
+ ? enabledMatchingOptions[enabledMatchingOptions.length - 1]
329
+ : enabledMatchingOptions[0]
330
+ }
331
+
332
+ function setInputValue(nextValue: string) {
333
+ if (!inputRef || inputRef.value === nextValue) {
334
+ return
335
+ }
336
+
337
+ inputRef.value = nextValue
338
+ }
339
+
340
+ function clearInputSelection() {
341
+ if (!inputRef || inputRef.selectionStart === null || inputRef.selectionEnd === null) {
342
+ return
343
+ }
344
+
345
+ let cursor = inputRef.value.length
346
+ if (inputRef.selectionStart === cursor && inputRef.selectionEnd === cursor) {
347
+ return
348
+ }
349
+
350
+ inputRef.setSelectionRange(cursor, cursor)
351
+ }
352
+
353
+ function syncPopoverMinWidth() {
354
+ if (!surfaceRef || !inputRef) {
355
+ return
356
+ }
357
+
358
+ let width = inputRef.offsetWidth
359
+ if (width <= 0) {
360
+ return
361
+ }
362
+
363
+ surfaceRef.style.minWidth = `${width}px`
364
+ }
365
+
366
+ function dispatchChange(selection: {
367
+ label: string | null
368
+ optionId: string | null
369
+ value: string | null
370
+ }) {
371
+ let target = inputRef ?? surfaceRef
372
+ target?.dispatchEvent(
373
+ new ComboboxChangeEvent({
374
+ label: selection.label,
375
+ optionId: selection.optionId,
376
+ value: selection.value,
377
+ }),
378
+ )
379
+ }
380
+
381
+ function clearCommittedSelection() {
382
+ let selectionChanged = value !== null || selectedId !== undefined
383
+ value = null
384
+ selectedId = undefined
385
+ selectedLabel = ''
386
+ return selectionChanged
387
+ }
388
+
389
+ function clearInputAndSelection() {
390
+ closeSequenceId++
391
+ pendingInputValue = null
392
+ pendingChange = null
393
+ selectInputAfterClose = false
394
+
395
+ inputText = ''
396
+ activeValue = null
397
+ activeId = undefined
398
+ if (!open) {
399
+ filterText = ''
400
+ }
401
+
402
+ let selectionChanged = clearCommittedSelection()
403
+ setInputValue('')
404
+
405
+ if (selectionChanged) {
406
+ dispatchChange({ label: null, optionId: null, value: null })
407
+ }
408
+ }
409
+
410
+ async function closePopover() {
411
+ if (!open) {
412
+ return
413
+ }
414
+
415
+ let closeId = ++closeSequenceId
416
+ let shouldWaitForTransition = showReason === 'nav'
417
+ open = false
418
+
419
+ if (!shouldWaitForTransition) {
420
+ surfaceVisible = false
421
+ filterText = ''
422
+ activeValue = null
423
+ activeId = undefined
424
+ await handle.update()
425
+ return
426
+ }
427
+
428
+ let signal = await handle.update()
429
+ if (signal.aborted || closeId !== closeSequenceId || open) {
430
+ return
431
+ }
432
+
433
+ if (surfaceRef?.isConnected) {
434
+ await waitForCssTransition(surfaceRef, signal)
435
+ }
436
+
437
+ if (signal.aborted || closeId !== closeSequenceId || open) {
438
+ return
439
+ }
440
+
441
+ surfaceVisible = false
442
+ filterText = ''
443
+ activeValue = null
444
+ activeId = undefined
445
+
446
+ signal = await handle.update()
447
+ if (signal.aborted || closeId !== closeSequenceId || open) {
448
+ return
449
+ }
450
+
451
+ if (pendingInputValue !== null) {
452
+ let nextValue = pendingInputValue
453
+ await wait(INPUT_COMMIT_DELAY_MS)
454
+ if (
455
+ signal.aborted ||
456
+ closeId !== closeSequenceId ||
457
+ open ||
458
+ pendingInputValue !== nextValue
459
+ ) {
460
+ return
461
+ }
462
+
463
+ setInputValue(nextValue)
464
+ pendingInputValue = null
465
+ }
466
+
467
+ if (selectInputAfterClose && inputRef?.isConnected && document.activeElement === inputRef) {
468
+ inputRef.select()
469
+ }
470
+
471
+ selectInputAfterClose = false
472
+ }
473
+
474
+ async function openPopover(
475
+ strategy: ComboboxOpenStrategy = 'selected',
476
+ nextShowReason: ShowReason = 'nav',
477
+ ) {
478
+ if (handle.props.disabled) {
479
+ return
480
+ }
481
+
482
+ if (pendingInputValue !== null) {
483
+ setInputValue(pendingInputValue)
484
+ pendingInputValue = null
485
+ }
486
+
487
+ closeSequenceId++
488
+ selectInputAfterClose = false
489
+
490
+ let nextFilterText = getOpenFilterText()
491
+ let matchingOptions = getMatchingOptions(nextFilterText)
492
+ if (matchingOptions.length === 0) {
493
+ return
494
+ }
495
+
496
+ let activeOption = resolveOpenOption(strategy, nextFilterText)
497
+
498
+ filterText = nextFilterText
499
+ showReason = nextShowReason
500
+ open = true
501
+ surfaceVisible = true
502
+ activeValue = activeOption?.value ?? null
503
+ activeId = activeOption?.id
504
+
505
+ let signal = await handle.update()
506
+ if (signal.aborted) {
507
+ return
508
+ }
509
+
510
+ listboxRef?.scrollActiveOptionIntoView()
511
+ }
512
+
513
+ function beginSelection(
514
+ nextValue: listbox.ListboxValue,
515
+ option: listbox.ListboxRegisteredOption | undefined,
516
+ ) {
517
+ if (!option) {
518
+ return
519
+ }
520
+
521
+ let selectionChanged = value !== nextValue
522
+ value = nextValue
523
+ selectedId = option.id
524
+ selectedLabel = option.label
525
+ activeValue = nextValue
526
+ activeId = option.id
527
+ inputText = option.label
528
+ showReason = 'nav'
529
+ pendingInputValue = inputRef?.value === option.label ? null : option.label
530
+ pendingChange = selectionChanged
531
+ ? {
532
+ label: option.label,
533
+ optionId: option.id,
534
+ value: option.value,
535
+ }
536
+ : null
537
+ selectInputAfterClose = true
538
+ void handle.update()
539
+ }
540
+
541
+ async function finishSelection() {
542
+ let change = pendingChange
543
+ pendingChange = null
544
+
545
+ if (change) {
546
+ dispatchChange(change)
547
+ }
548
+
549
+ await closePopover()
550
+ }
551
+
552
+ async function setInputText(nextText: string) {
553
+ closeSequenceId++
554
+ pendingInputValue = null
555
+ pendingChange = null
556
+ selectInputAfterClose = false
557
+
558
+ let selectionChanged = clearCommittedSelection()
559
+
560
+ inputText = nextText
561
+ showReason = 'hint'
562
+ activeValue = null
563
+ activeId = undefined
564
+
565
+ let nextFilterText = nextText !== '' || !open ? nextText : filterText
566
+ let matchingOptions = getMatchingOptions(nextText)
567
+ filterText = nextFilterText
568
+
569
+ if (selectionChanged) {
570
+ dispatchChange({ label: null, optionId: null, value: null })
571
+ }
572
+
573
+ if (nextText === '' || matchingOptions.length === 0) {
574
+ if (open) {
575
+ open = false
576
+ surfaceVisible = false
577
+ } else {
578
+ filterText = ''
579
+ }
580
+
581
+ await handle.update()
582
+ return
583
+ }
584
+
585
+ open = true
586
+ surfaceVisible = true
587
+ await handle.update()
588
+ }
589
+
590
+ function handleBlur() {
591
+ let exactMatch = getExactInputMatch(inputText)
592
+ if (!exactMatch) {
593
+ clearInputAndSelection()
594
+ } else {
595
+ let selectionChanged = value !== exactMatch.value
596
+ value = exactMatch.value
597
+ selectedId = exactMatch.id
598
+ selectedLabel = exactMatch.label
599
+ activeValue = exactMatch.value
600
+ activeId = exactMatch.id
601
+
602
+ if (selectionChanged) {
603
+ dispatchChange({
604
+ label: exactMatch.label,
605
+ optionId: exactMatch.id,
606
+ value: exactMatch.value,
607
+ })
608
+ }
609
+ }
610
+
611
+ void closePopover()
612
+ }
613
+
614
+ function handleEscape() {
615
+ if (!getExactInputMatch(inputText)) {
616
+ clearInputAndSelection()
617
+ }
618
+
619
+ if (open) {
620
+ void closePopover()
621
+ }
622
+ }
623
+
624
+ function openFromArrow(direction: 'first' | 'last') {
625
+ let strategy: ComboboxOpenStrategy = getExactInputMatch(inputText) ? 'selected' : direction
626
+ clearInputSelection()
627
+ return openPopover(strategy)
628
+ }
629
+
630
+ function openFromInputActivation() {
631
+ clearInputSelection()
632
+ return openPopover('selected-or-none')
633
+ }
634
+
635
+ function navigateNext() {
636
+ clearInputSelection()
637
+ listboxRef?.navigateNext()
638
+ }
639
+
640
+ function navigatePrevious() {
641
+ clearInputSelection()
642
+ listboxRef?.navigatePrevious()
643
+ }
644
+
645
+ function selectActive() {
646
+ return listboxRef?.selectActive() ?? Promise.resolve()
647
+ }
648
+
649
+ function registerInput(node: HTMLInputElement) {
650
+ inputRef = node
651
+ setInputValue(inputText)
652
+ }
653
+
654
+ function unregisterInput(node: HTMLInputElement) {
655
+ if (inputRef === node) {
656
+ inputRef = undefined
657
+ }
658
+ }
659
+
660
+ function registerSurface(node: HTMLElement) {
661
+ surfaceRef = node
662
+ }
663
+
664
+ function unregisterSurface(node: HTMLElement) {
665
+ if (surfaceRef === node) {
666
+ surfaceRef = undefined
667
+ }
668
+ }
669
+
670
+ let publicHandle: ComboboxHandle = {
671
+ get activeOptionId() {
672
+ return activeId ?? null
673
+ },
674
+ get id() {
675
+ return listId
676
+ },
677
+ get inputText() {
678
+ return inputText
679
+ },
680
+ get isOpen() {
681
+ return open
682
+ },
683
+ get label() {
684
+ return selectedLabel || null
685
+ },
686
+ get value() {
687
+ return value
688
+ },
689
+ close() {
690
+ void closePopover()
691
+ },
692
+ open(strategy = 'selected') {
693
+ return openPopover(strategy)
694
+ },
695
+ }
696
+
697
+ handle.context.set({
698
+ get activeId() {
699
+ return activeId
700
+ },
701
+ get disabled() {
702
+ return handle.props.disabled === true
703
+ },
704
+ get filterText() {
705
+ return filterText
706
+ },
707
+ get inputText() {
708
+ return inputText
709
+ },
710
+ get isOpen() {
711
+ return open
712
+ },
713
+ get listId() {
714
+ return listId
715
+ },
716
+ get name() {
717
+ return handle.props.name
718
+ },
719
+ get showReason() {
720
+ return showReason
721
+ },
722
+ get surfaceVisible() {
723
+ return surfaceVisible
724
+ },
725
+ get value() {
726
+ return value
727
+ },
728
+ clearInputSelection,
729
+ close() {
730
+ void closePopover()
731
+ },
732
+ handleBlur,
733
+ handleEscape,
734
+ navigateNext,
735
+ navigatePrevious,
736
+ open(strategy = 'selected') {
737
+ return openPopover(strategy)
738
+ },
739
+ openFromArrow,
740
+ openFromInputActivation,
741
+ registerInput,
742
+ registerSurface,
743
+ setInputText,
744
+ selectActive,
745
+ syncPopoverMinWidth,
746
+ unregisterInput,
747
+ unregisterSurface,
748
+ })
749
+
750
+ return () => {
751
+ if (!hasInitialized) {
752
+ value = handle.props.defaultValue ?? null
753
+ inputText = handle.props.defaultValue ?? ''
754
+ hasInitialized = true
755
+ }
756
+
757
+ handle.queueTask(() => {
758
+ let selectedOption = getSelectedOption()
759
+ if (selectedOption) {
760
+ selectedId = selectedOption.id
761
+ selectedLabel = selectedOption.label
762
+ } else if (value === null) {
763
+ selectedId = undefined
764
+ selectedLabel = ''
765
+ }
766
+ })
767
+
768
+ if (!hasProvidedRef) {
769
+ handle.queueTask(() => {
770
+ handle.props.ref?.(publicHandle)
771
+ })
772
+ hasProvidedRef = true
773
+ }
774
+
775
+ return (
776
+ <popover.Context>
777
+ <listbox.Context
778
+ activeValue={activeValue}
779
+ flashSelection
780
+ onHighlight={(nextActiveValue, option) => {
781
+ if (!open) {
782
+ return
783
+ }
784
+
785
+ activeValue = nextActiveValue
786
+ activeId = option?.id
787
+ if (option) {
788
+ showReason = 'nav'
789
+ }
790
+ void handle.update()
791
+ }}
792
+ onSelect={beginSelection}
793
+ onSelectSettled={finishSelection}
794
+ ref={(ref) => {
795
+ listboxRef = ref
796
+ }}
797
+ selectionFlashAttribute="data-combobox-flash"
798
+ value={value}
799
+ >
800
+ {handle.props.children}
801
+ </listbox.Context>
802
+ </popover.Context>
803
+ )
804
+ }
805
+ }
806
+
807
+ function getComboboxContext(handle: Handle | MixinHandle) {
808
+ return handle.context.get(ComboboxProvider)
809
+ }
810
+
811
+ const inputMixin = createMixin<HTMLInputElement, [], ElementProps>((handle) => {
812
+ let context = getComboboxContext(handle)
813
+
814
+ return (props) => [
815
+ attrs({
816
+ 'aria-activedescendant': context.activeId,
817
+ 'aria-autocomplete': 'list',
818
+ 'aria-controls': context.listId,
819
+ 'aria-expanded': context.isOpen ? 'true' : 'false',
820
+ 'aria-haspopup': 'listbox',
821
+ autocomplete: props.autocomplete ?? 'off',
822
+ 'data-surface-visible': context.surfaceVisible ? 'true' : undefined,
823
+ disabled: context.disabled ? true : props.disabled,
824
+ role: 'combobox',
825
+ type: props.type ?? 'text',
826
+ }),
827
+ ref((node: HTMLInputElement, signal) => {
828
+ context.registerInput(node)
829
+ signal.addEventListener('abort', () => {
830
+ context.unregisterInput(node)
831
+ })
832
+ }),
833
+ popover.anchor({ placement: 'bottom-start' }),
834
+ on('click', () => {
835
+ if (context.isOpen) {
836
+ return
837
+ }
838
+
839
+ void context.openFromInputActivation()
840
+ }),
841
+ on('input', (event) => {
842
+ void context.setInputText(event.currentTarget.value)
843
+ }),
844
+ on('keydown', (event) => {
845
+ switch (event.key) {
846
+ case 'ArrowDown':
847
+ event.preventDefault()
848
+ if (context.isOpen) {
849
+ context.navigateNext()
850
+ } else {
851
+ void context.openFromArrow('first')
852
+ }
853
+ return
854
+ case 'ArrowUp':
855
+ event.preventDefault()
856
+ if (context.isOpen) {
857
+ context.navigatePrevious()
858
+ } else {
859
+ void context.openFromArrow('last')
860
+ }
861
+ return
862
+ case 'Enter':
863
+ if (!context.isOpen) {
864
+ return
865
+ }
866
+
867
+ event.preventDefault()
868
+ void context.selectActive()
869
+ return
870
+ case 'Escape':
871
+ if (!context.isOpen && context.inputText === '') {
872
+ return
873
+ }
874
+
875
+ event.preventDefault()
876
+ context.handleEscape()
877
+ return
878
+ }
879
+ }),
880
+ on('blur', () => {
881
+ context.handleBlur()
882
+ }),
883
+ ]
884
+ })
885
+
886
+ const popoverMixin = createMixin<HTMLElement, [], ElementProps>((handle) => {
887
+ let context = getComboboxContext(handle)
888
+
889
+ return () => [
890
+ attrs({ 'data-show-reason': context.showReason }),
891
+ ref((node: HTMLElement, signal) => {
892
+ context.registerSurface(node)
893
+ signal.addEventListener('abort', () => {
894
+ context.unregisterSurface(node)
895
+ })
896
+ }),
897
+ popover.surface({
898
+ open: context.isOpen,
899
+ onHide() {
900
+ context.close()
901
+ },
902
+ closeOnAnchorClick: false,
903
+ }),
904
+ on('beforetoggle', (event) => {
905
+ if (event.newState === 'open') {
906
+ context.syncPopoverMinWidth()
907
+ }
908
+ }),
909
+ ]
910
+ })
911
+
912
+ const listMixin = createMixin<HTMLElement, [], ElementProps>((handle) => {
913
+ let context = getComboboxContext(handle)
914
+ return () => [attrs({ id: context.listId }), listbox.list()]
915
+ })
916
+
917
+ const hiddenInputMixin = createMixin<HTMLInputElement, [], ElementProps>((handle) => {
918
+ let context = getComboboxContext(handle)
919
+ return () =>
920
+ attrs({
921
+ disabled: context.disabled ? true : undefined,
922
+ name: context.name,
923
+ type: 'hidden',
924
+ value: context.value ?? '',
925
+ })
926
+ })
927
+
928
+ const optionMixin = createMixin<HTMLElement, [options: ComboboxOptionOptions], ElementProps>(
929
+ (handle) => {
930
+ let context = getComboboxContext(handle)
931
+
932
+ return (options) => {
933
+ let hidden = !matchesSearchValue(getOptionSearchValue(options), context.filterText)
934
+
935
+ return [
936
+ attrs({ hidden: hidden ? true : undefined }),
937
+ on('pointerdown', (event) => {
938
+ if (event.button === 0) {
939
+ event.preventDefault()
940
+ }
941
+ }),
942
+ listbox.option({
943
+ disabled: options.disabled,
944
+ label: options.label,
945
+ textValue: options.searchValue,
946
+ value: options.value,
947
+ }),
948
+ ]
949
+ }
950
+ },
951
+ )
952
+
953
+ export const Context = ComboboxProvider
954
+ export const hiddenInput = hiddenInputMixin
955
+ export const input = inputMixin
956
+ export const list = listMixin
957
+ export const option = optionMixin
958
+ export { popoverMixin as popover }
959
+
960
+ const combobox = {
961
+ Context,
962
+ hiddenInput,
963
+ input,
964
+ list,
965
+ option,
966
+ popover: popoverMixin,
967
+ } as const
968
+
969
+ export function onComboboxChange(handler: ComboboxChangeHandler, captureBoolean?: boolean) {
970
+ return on<HTMLElement, typeof COMBOBOX_CHANGE_EVENT>(
971
+ COMBOBOX_CHANGE_EVENT,
972
+ handler,
973
+ captureBoolean,
974
+ )
975
+ }
976
+
977
+ export function Combobox(handle: Handle<ComboboxProps>) {
978
+ return () => {
979
+ let { children, defaultValue, disabled, inputId, name, placeholder, ...divProps } = handle.props
980
+
981
+ return (
982
+ <combobox.Context defaultValue={defaultValue} disabled={disabled} name={name}>
983
+ <div {...divProps}>
984
+ <input
985
+ defaultValue={defaultValue ?? undefined}
986
+ id={inputId}
987
+ mix={[inputStyle, combobox.input()]}
988
+ placeholder={placeholder}
989
+ />
990
+ <div mix={[popover.surfaceStyle, popoverStyle, combobox.popover()]}>
991
+ <div mix={[popover.contentStyle, listbox.listStyle, combobox.list()]}>{children}</div>
992
+ </div>
993
+ {name && <input mix={combobox.hiddenInput()} />}
994
+ </div>
995
+ </combobox.Context>
996
+ )
997
+ }
998
+ }
999
+
1000
+ export function ComboboxOption(handle: Handle<ComboboxOptionProps>) {
1001
+ return () => {
1002
+ let { children, disabled, label, mix, searchValue, value, ...divProps } = handle.props
1003
+
1004
+ return (
1005
+ <div
1006
+ {...divProps}
1007
+ mix={[listbox.optionStyle, combobox.option({ disabled, label, searchValue, value }), mix]}
1008
+ >
1009
+ <Glyph mix={listbox.glyphStyle} name="check" />
1010
+ <span mix={listbox.labelStyle}>{children ?? label}</span>
1011
+ </div>
1012
+ )
1013
+ }
1014
+ }