@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,585 @@
1
+ // @jsxRuntime classic
2
+ // @jsx createElement
3
+ import {
4
+ attrs,
5
+ createElement,
6
+ createMixin,
7
+ css,
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
+ import * as button from '../button/button.tsx'
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 { hiddenTypeahead } from '../../interactions/typeahead/typeahead-mixin.ts'
23
+ import { onKeyDown } from '../../interactions/keydown/keydown.ts'
24
+ import { waitForCssTransition } from '../../utils/wait-for-css-transition.ts'
25
+ import { wait } from '../../utils/wait.ts'
26
+ import type { AnchorOptions } from '../anchor/anchor.ts'
27
+
28
+ const SELECT_CHANGE_EVENT = 'rmx:select-change' as const
29
+ const LABEL_SWAP_DELAY_MS = 75
30
+
31
+ type SelectChangeHandler = (event: SelectChangeEvent, signal: AbortSignal) => void | Promise<void>
32
+
33
+ declare global {
34
+ interface HTMLElementEventMap {
35
+ [SELECT_CHANGE_EVENT]: SelectChangeEvent
36
+ }
37
+ }
38
+
39
+ export class SelectChangeEvent extends Event {
40
+ readonly label: string | null
41
+ readonly optionId: string | null
42
+ readonly value: string | null
43
+
44
+ constructor({
45
+ label,
46
+ optionId,
47
+ value,
48
+ }: {
49
+ label: string | null
50
+ optionId: string | null
51
+ value: string | null
52
+ }) {
53
+ super(SELECT_CHANGE_EVENT, { bubbles: true })
54
+ this.label = label
55
+ this.optionId = optionId
56
+ this.value = value
57
+ }
58
+ }
59
+
60
+ export interface SelectContextProps {
61
+ children?: RemixNode
62
+ defaultLabel: string
63
+ defaultValue?: string | null
64
+ disabled?: boolean
65
+ name?: string
66
+ }
67
+
68
+ export interface SelectProps extends Omit<Props<'button'>, 'children' | 'name'> {
69
+ children?: RemixNode
70
+ defaultLabel: string
71
+ defaultValue?: string | null
72
+ name?: string
73
+ }
74
+
75
+ export type SelectOptionProps = Props<'div'> & Omit<listbox.ListboxOption, 'id'>
76
+
77
+ enum State {
78
+ Initializing = 'initializing',
79
+ Closed = 'closed',
80
+ Open = 'open',
81
+ Selecting = 'selecting',
82
+ }
83
+
84
+ type PendingChange = {
85
+ label: string | null
86
+ optionId: string | null
87
+ value: string | null
88
+ } | null
89
+
90
+ interface SelectContextValue {
91
+ readonly activeId: string | undefined
92
+ readonly disabled: boolean
93
+ readonly displayedLabel: string
94
+ readonly isExpanded: boolean
95
+ readonly isOpen: boolean
96
+ readonly listId: string
97
+ readonly name: string | undefined
98
+ readonly selectedId: string | undefined
99
+ readonly value: listbox.ListboxValue
100
+ close: () => void
101
+ open: () => void
102
+ registerPopoverContext: (context: popover.PopoverContext) => void
103
+ registerSurface: (node: HTMLElement) => void
104
+ registerTrigger: (node: HTMLButtonElement) => void
105
+ selectTypeaheadMatch: (text: string) => void
106
+ syncPopoverMinWidth: () => void
107
+ unregisterPopoverContext: (context: popover.PopoverContext) => void
108
+ unregisterSurface: (node: HTMLElement) => void
109
+ unregisterTrigger: (node: HTMLButtonElement) => void
110
+ }
111
+
112
+ function SelectProvider(handle: Handle<SelectContextProps, SelectContextValue>) {
113
+ let triggerRef: HTMLButtonElement | undefined
114
+ let listboxRef: listbox.ListboxRef | undefined
115
+ let surfaceRef: HTMLElement | undefined
116
+ let popoverContextRef: popover.PopoverContext | undefined
117
+
118
+ let state: State = State.Initializing
119
+
120
+ let value: listbox.ListboxValue = null
121
+ let activeValue: listbox.ListboxValue = null
122
+ let activeId: string | undefined = undefined
123
+ let selectedId: string | undefined = undefined
124
+ let selectedLabel = ''
125
+ let displayedLabel = ''
126
+ let pendingChange: PendingChange = null
127
+
128
+ let listId = `${handle.id}-list`
129
+
130
+ function open() {
131
+ if (state !== State.Closed || handle.props.disabled) return
132
+ state = State.Open
133
+ activeValue = value
134
+ handle.update()
135
+ }
136
+
137
+ function syncPopoverMinWidth() {
138
+ if (state !== State.Open || !surfaceRef || !triggerRef) {
139
+ return
140
+ }
141
+
142
+ surfaceRef.style.minWidth = `${triggerRef.offsetWidth}px`
143
+ }
144
+
145
+ function getPopoverAnchorOptions(): AnchorOptions {
146
+ return {
147
+ placement: 'left',
148
+ inset: true,
149
+ relativeTo: selectedId ? `#${selectedId}` : '[role="option"]',
150
+ }
151
+ }
152
+
153
+ function syncPopoverContext() {
154
+ if (!popoverContextRef) {
155
+ return
156
+ }
157
+
158
+ popoverContextRef.hideFocusTarget = triggerRef ?? null
159
+ popoverContextRef.anchor = triggerRef
160
+ ? {
161
+ node: triggerRef,
162
+ options: getPopoverAnchorOptions(),
163
+ }
164
+ : null
165
+ }
166
+
167
+ function close() {
168
+ if (state !== State.Open) return
169
+ state = State.Closed
170
+ handle.update()
171
+ }
172
+
173
+ function setSelectedOption(
174
+ nextValue: listbox.ListboxValue,
175
+ option: listbox.ListboxOption | undefined,
176
+ syncDisplayedLabel = false,
177
+ ) {
178
+ value = nextValue
179
+ activeValue = nextValue
180
+ activeId = option?.id
181
+ selectedId = option?.id
182
+ selectedLabel = option ? option.label : handle.props.defaultLabel
183
+ syncPopoverContext()
184
+
185
+ if (syncDisplayedLabel) {
186
+ displayedLabel = selectedLabel
187
+ }
188
+ }
189
+
190
+ function getPendingChange(
191
+ nextValue: listbox.ListboxValue,
192
+ option: listbox.ListboxOption | undefined,
193
+ ): PendingChange {
194
+ if (!option || value === nextValue) {
195
+ return null
196
+ }
197
+
198
+ return {
199
+ label: option.label,
200
+ optionId: option.id,
201
+ value: option.value,
202
+ }
203
+ }
204
+
205
+ function dispatchChange(change: PendingChange) {
206
+ if (!change) {
207
+ return
208
+ }
209
+
210
+ let target = triggerRef ?? surfaceRef
211
+ target?.dispatchEvent(new SelectChangeEvent(change))
212
+ }
213
+
214
+ function selectOption(
215
+ nextValue: listbox.ListboxValue,
216
+ option: listbox.ListboxOption | undefined,
217
+ ) {
218
+ if (state !== State.Open) return
219
+ pendingChange = getPendingChange(nextValue, option)
220
+ setSelectedOption(nextValue, option)
221
+ handle.update()
222
+ }
223
+
224
+ async function settleSelectedOption() {
225
+ if (state !== State.Open) return
226
+
227
+ let change = pendingChange
228
+ pendingChange = null
229
+ state = State.Selecting
230
+
231
+ if (!surfaceRef) {
232
+ displayedLabel = selectedLabel
233
+ state = State.Closed
234
+ let signal = await handle.update()
235
+ if (signal.aborted) return
236
+ dispatchChange(change)
237
+ return
238
+ }
239
+
240
+ await Promise.all([handle.update(), waitForCssTransition(surfaceRef, handle.signal)])
241
+ await wait(LABEL_SWAP_DELAY_MS) // UX delay label swap for clear value change
242
+ if (handle.signal.aborted) return
243
+ displayedLabel = selectedLabel
244
+ state = State.Closed
245
+ let signal = await handle.update()
246
+ if (signal.aborted) return
247
+ dispatchChange(change)
248
+ }
249
+
250
+ function selectTypeaheadMatch(text: string) {
251
+ if (state !== State.Closed || handle.props.disabled) return
252
+
253
+ let option = listboxRef?.matchSearchText(text, value)
254
+ if (!option) return
255
+
256
+ let change = getPendingChange(option.value, option)
257
+ pendingChange = null
258
+ setSelectedOption(option.value, option, true)
259
+ void handle.update().then((signal) => {
260
+ if (signal.aborted) return
261
+ dispatchChange(change)
262
+ })
263
+ }
264
+
265
+ function highlightOption(
266
+ nextActiveValue: listbox.ListboxValue,
267
+ option: listbox.ListboxOption | undefined,
268
+ ) {
269
+ if (state !== State.Open) return
270
+ activeValue = nextActiveValue
271
+ activeId = option?.id
272
+ handle.update()
273
+ }
274
+
275
+ handle.context.set({
276
+ get activeId() {
277
+ return activeId
278
+ },
279
+
280
+ get disabled() {
281
+ return !!handle.props.disabled
282
+ },
283
+
284
+ get displayedLabel() {
285
+ return displayedLabel
286
+ },
287
+
288
+ get isExpanded() {
289
+ return state === State.Open || state === State.Selecting
290
+ },
291
+
292
+ get isOpen() {
293
+ return state === State.Open
294
+ },
295
+
296
+ get listId() {
297
+ return listId
298
+ },
299
+
300
+ get name() {
301
+ return handle.props.name
302
+ },
303
+
304
+ get selectedId() {
305
+ return selectedId
306
+ },
307
+
308
+ get value() {
309
+ return value
310
+ },
311
+
312
+ close,
313
+
314
+ open,
315
+
316
+ registerSurface(node) {
317
+ surfaceRef = node
318
+ },
319
+
320
+ registerTrigger(node) {
321
+ triggerRef = node
322
+ syncPopoverContext()
323
+ },
324
+
325
+ registerPopoverContext(context) {
326
+ popoverContextRef = context
327
+ syncPopoverContext()
328
+ },
329
+
330
+ selectTypeaheadMatch,
331
+
332
+ syncPopoverMinWidth,
333
+
334
+ unregisterSurface(node) {
335
+ if (surfaceRef === node) {
336
+ surfaceRef = undefined
337
+ }
338
+ },
339
+
340
+ unregisterPopoverContext(context) {
341
+ if (popoverContextRef !== context) {
342
+ return
343
+ }
344
+
345
+ popoverContextRef.anchor = null
346
+ popoverContextRef.hideFocusTarget = null
347
+ popoverContextRef = undefined
348
+ },
349
+
350
+ unregisterTrigger(node) {
351
+ if (triggerRef === node) {
352
+ triggerRef = undefined
353
+ syncPopoverContext()
354
+ }
355
+ },
356
+ })
357
+
358
+ return () => {
359
+ if (state === State.Initializing) {
360
+ selectedLabel = displayedLabel = handle.props.defaultLabel
361
+ value = handle.props.defaultValue ?? null
362
+ activeValue = value
363
+ state = State.Closed
364
+
365
+ handle.queueTask(() => {
366
+ if (selectedId || !surfaceRef) {
367
+ return
368
+ }
369
+
370
+ let selected = surfaceRef.querySelector(`[aria-selected="true"]`)
371
+ if (selected && !selectedId) {
372
+ selectedId = selected.id
373
+ syncPopoverContext()
374
+ handle.update()
375
+ }
376
+ })
377
+ }
378
+
379
+ return (
380
+ <listbox.Context
381
+ flashSelection
382
+ ref={(nextListboxRef: listbox.ListboxRef) => {
383
+ listboxRef = nextListboxRef
384
+ }}
385
+ value={value}
386
+ activeValue={activeValue}
387
+ onSelectSettled={settleSelectedOption}
388
+ onSelect={selectOption}
389
+ onHighlight={highlightOption}
390
+ selectionFlashAttribute="data-select-flash"
391
+ >
392
+ {handle.props.children}
393
+ </listbox.Context>
394
+ )
395
+ }
396
+ }
397
+
398
+ function getSelectContext(handle: Handle | MixinHandle) {
399
+ return handle.context.get(SelectProvider)
400
+ }
401
+
402
+ const triggerMixin = createMixin<HTMLButtonElement, [], ElementProps>((handle) => {
403
+ let context = getSelectContext(handle)
404
+
405
+ return (props) => [
406
+ attrs({
407
+ 'aria-haspopup': 'listbox',
408
+ 'aria-expanded': context.isExpanded ? 'true' : 'false',
409
+ 'aria-controls': context.listId,
410
+ 'aria-describedby': context.selectedId,
411
+ disabled: context.disabled ? true : props.disabled,
412
+ }),
413
+ ref((node: HTMLButtonElement, signal) => {
414
+ context.registerTrigger(node)
415
+ signal.addEventListener('abort', () => {
416
+ context.unregisterTrigger(node)
417
+ })
418
+ }),
419
+ hiddenTypeahead((text) => {
420
+ context.selectTypeaheadMatch(text)
421
+ }),
422
+ on('click', () => {
423
+ context.open()
424
+ }),
425
+ onKeyDown('ArrowDown', () => {
426
+ context.open()
427
+ }),
428
+ onKeyDown('ArrowUp', () => {
429
+ context.open()
430
+ }),
431
+ ]
432
+ })
433
+
434
+ const popoverMixin = createMixin<HTMLElement, [], ElementProps>((handle) => {
435
+ let context = getSelectContext(handle)
436
+ let popoverState = handle.context.get(popover.Context)
437
+
438
+ return () => [
439
+ ref((node: HTMLElement, signal) => {
440
+ context.registerSurface(node)
441
+ context.registerPopoverContext(popoverState)
442
+ signal.addEventListener('abort', () => {
443
+ context.unregisterSurface(node)
444
+ context.unregisterPopoverContext(popoverState)
445
+ })
446
+ }),
447
+ popover.surface({
448
+ open: context.isOpen,
449
+ onHide: context.close,
450
+ }),
451
+ on('beforetoggle', (event) => {
452
+ if (event.newState === 'open') {
453
+ context.syncPopoverMinWidth()
454
+ }
455
+ }),
456
+ ]
457
+ })
458
+
459
+ const listMixin = createMixin<HTMLElement, [], ElementProps>((handle) => {
460
+ let context = getSelectContext(handle)
461
+
462
+ return () => [
463
+ attrs({
464
+ id: context.listId,
465
+ 'aria-activedescendant': context.activeId,
466
+ }),
467
+ popover.focusOnShow(),
468
+ listbox.list(),
469
+ ]
470
+ })
471
+
472
+ const hiddenInputMixin = createMixin<HTMLInputElement, [], ElementProps>((handle) => {
473
+ let context = getSelectContext(handle)
474
+
475
+ return () =>
476
+ attrs({
477
+ disabled: context.disabled ? true : undefined,
478
+ name: context.name,
479
+ type: 'hidden',
480
+ value: context.value ?? '',
481
+ })
482
+ })
483
+
484
+ const selectTriggerCss: CSSMixinDescriptor = css({
485
+ minHeight: theme.control.height.sm,
486
+ width: '100%',
487
+ paddingInline: theme.space.md,
488
+ paddingInlineEnd: theme.space.sm,
489
+ display: 'grid',
490
+ gridTemplateColumns: 'minmax(0, 1fr) auto',
491
+ alignItems: 'center',
492
+ gap: theme.space.sm,
493
+ borderRadius: theme.radius.md,
494
+ backgroundImage: 'none',
495
+ border: '0.5px solid transparent',
496
+ boxShadow: 'none',
497
+ fontSize: theme.fontSize.xs,
498
+ textAlign: 'left',
499
+ backgroundColor: theme.surface.lvl3,
500
+ color: theme.colors.text.secondary,
501
+ '&:hover, &:focus-visible, &[aria-expanded="true"], &[aria-expanded="true"]:hover, &[aria-expanded="true"]:focus-visible':
502
+ {
503
+ backgroundColor: theme.surface.lvl4,
504
+ color: theme.colors.text.primary,
505
+ },
506
+ '&:active': {
507
+ backgroundColor: theme.surface.lvl3,
508
+ },
509
+ '&:focus-visible': {
510
+ outline: `2px solid ${theme.colors.focus.ring}`,
511
+ outlineOffset: '2px',
512
+ },
513
+ '&:disabled': {
514
+ opacity: 0.6,
515
+ },
516
+ })
517
+
518
+ export const triggerStyle = selectTriggerCss
519
+ export const Context = SelectProvider
520
+ export const hiddenInput = hiddenInputMixin
521
+ export const list = listMixin
522
+ export const option = listbox.option
523
+ export { popoverMixin as popover }
524
+ export const trigger = triggerMixin
525
+
526
+ const select = {
527
+ Context,
528
+ hiddenInput,
529
+ list,
530
+ option,
531
+ popover: popoverMixin,
532
+ trigger,
533
+ } as const
534
+
535
+ export function onSelectChange(handler: SelectChangeHandler, captureBoolean?: boolean) {
536
+ return on<HTMLElement, typeof SELECT_CHANGE_EVENT>(SELECT_CHANGE_EVENT, handler, captureBoolean)
537
+ }
538
+
539
+ function SelectLabel(handle: Handle) {
540
+ let context = getSelectContext(handle)
541
+
542
+ return () => <span mix={button.labelStyle}>{context.displayedLabel}</span>
543
+ }
544
+
545
+ export function Select(handle: Handle<SelectProps>) {
546
+ return () => {
547
+ let { children, defaultLabel, defaultValue, disabled, name, mix, ...buttonProps } = handle.props
548
+
549
+ return (
550
+ <select.Context
551
+ defaultLabel={defaultLabel}
552
+ defaultValue={defaultValue}
553
+ disabled={disabled}
554
+ name={name}
555
+ >
556
+ <button {...buttonProps} mix={[button.baseStyle, triggerStyle, select.trigger(), mix]}>
557
+ <SelectLabel />
558
+ <Glyph mix={button.iconStyle} name="chevronVertical" />
559
+ </button>
560
+ <popover.Context>
561
+ <div mix={[popover.surfaceStyle, select.popover()]}>
562
+ <div mix={[popover.contentStyle, listbox.listStyle, select.list()]}>{children}</div>
563
+ </div>
564
+ </popover.Context>
565
+ {name && <input mix={select.hiddenInput()} />}
566
+ </select.Context>
567
+ )
568
+ }
569
+ }
570
+
571
+ export function Option(handle: Handle<SelectOptionProps>) {
572
+ return () => {
573
+ let { label, value, disabled, textValue, children, mix, ...divProps } = handle.props
574
+
575
+ return (
576
+ <div
577
+ {...divProps}
578
+ mix={[listbox.optionStyle, select.option({ value, label, disabled, textValue }), mix]}
579
+ >
580
+ <Glyph mix={listbox.glyphStyle} name="check" />
581
+ <span mix={listbox.labelStyle}>{children ?? handle.props.label}</span>
582
+ </div>
583
+ )
584
+ }
585
+ }
@@ -0,0 +1,19 @@
1
+ import { css, type CSSMixinDescriptor } from '@remix-run/ui'
2
+
3
+ import { theme } from '../../theme/theme.ts'
4
+
5
+ const itemInset = `var(--rmx-ui-item-inset, ${theme.space.sm})`
6
+ const itemIndicatorHalfInset =
7
+ 'calc((var(--rmx-ui-item-indicator-width, 0px) + var(--rmx-ui-item-indicator-gap, 0px)) / 2)'
8
+
9
+ const separatorCss: CSSMixinDescriptor = css({
10
+ flexShrink: 0,
11
+ margin: 0,
12
+ marginBlock: theme.space.xs,
13
+ marginInlineStart: `calc(${itemInset} + ${itemIndicatorHalfInset})`,
14
+ marginInlineEnd: itemInset,
15
+ border: 0,
16
+ borderTop: `1px solid ${theme.colors.border.subtle}`,
17
+ })
18
+
19
+ export const separatorStyle = separatorCss
@@ -0,0 +1,105 @@
1
+ # Tabs
2
+
3
+ `tabs` is a primitive for building accessible tabs with automatic activation and simple default styling.
4
+
5
+ Use it when you need a tablist, tab triggers, and tab panels. `tabs.listStyle` and `tabs.triggerStyle` style the tab row and triggers, but panel presentation stays app-owned.
6
+
7
+ ## Usage
8
+
9
+ ```tsx
10
+ import { css, type Handle } from 'remix/ui'
11
+ import { Tab, Tabs, TabsList, TabsPanel } from 'remix/ui/tabs'
12
+
13
+ let panel = css({
14
+ padding: '16px',
15
+ border: '1px solid #d6d6d6',
16
+ borderRadius: '16px',
17
+ })
18
+
19
+ export function ProjectTabs(_handle: Handle) {
20
+ return () => (
21
+ <Tabs defaultValue="overview">
22
+ <TabsList aria-label="Project sections">
23
+ <Tab value="overview">Overview</Tab>
24
+ <Tab value="analytics">Analytics</Tab>
25
+ <Tab value="reports">Reports</Tab>
26
+ <Tab value="settings">Settings</Tab>
27
+ </TabsList>
28
+
29
+ <TabsPanel mix={panel} value="overview">
30
+ View your key metrics and recent project activity.
31
+ </TabsPanel>
32
+ <TabsPanel mix={panel} value="analytics">
33
+ Track trends and compare performance over time.
34
+ </TabsPanel>
35
+ <TabsPanel mix={panel} value="reports">
36
+ Export and review your latest reports.
37
+ </TabsPanel>
38
+ <TabsPanel mix={panel} value="settings">
39
+ Manage project preferences and defaults.
40
+ </TabsPanel>
41
+ </Tabs>
42
+ )
43
+ }
44
+ ```
45
+
46
+ ## `tabs.*`
47
+
48
+ ### `tabs.Context`
49
+
50
+ Provides the shared coordinator for one tabs instance.
51
+
52
+ - Owns the selected tab value for uncontrolled usage.
53
+ - Accepts controlled `value` plus `onValueChange` when the app owns selection state.
54
+ - Accepts `orientation="vertical"` when the tablist should use `ArrowUp` and `ArrowDown`.
55
+
56
+ ### `tabs.list()`
57
+
58
+ Turns the host into the tablist.
59
+
60
+ - Wires `role="tablist"`.
61
+ - Adds `aria-orientation="vertical"` when needed.
62
+
63
+ ### `tabs.trigger({ value, disabled })`
64
+
65
+ Registers one tab trigger.
66
+
67
+ - Wires `role="tab"`, `aria-selected`, `aria-controls`, and roving `tabIndex`.
68
+ - Selects on press.
69
+ - Supports `ArrowLeft` and `ArrowRight` for horizontal lists.
70
+ - Supports `ArrowUp` and `ArrowDown` for vertical lists.
71
+ - Supports `Home` and `End`.
72
+
73
+ ### `tabs.panel({ value })`
74
+
75
+ Turns the host into the panel for one tab value.
76
+
77
+ - Wires `role="tabpanel"` and `aria-labelledby`.
78
+ - Hides inactive panels with `hidden`.
79
+
80
+ ### `onTabsChange(...)`
81
+
82
+ The listener mixin for bubbled tab selection changes.
83
+
84
+ - `event.value` is the newly selected tab value.
85
+ - `event.previousValue` is the previously selected value, or `null`.
86
+
87
+ ## Convenience Components
88
+
89
+ - `Tabs` is a thin wrapper around `tabs.Context`.
90
+ - `TabsList` applies `tabs.list()` with `tabs.listStyle`.
91
+ - `Tab` applies `tabs.trigger(...)` with `button.baseStyle` and `tabs.triggerStyle`.
92
+ - `TabsPanel` applies `tabs.panel(...)` without adding panel styling.
93
+
94
+ ## Behavior Notes
95
+
96
+ - When you do not provide `defaultValue` or controlled `value`, the first enabled tab becomes selected automatically.
97
+ - Arrow-key navigation wraps and skips disabled tabs.
98
+ - Selection follows focus during arrow-key navigation.
99
+ - The default panel contract is behavioral only. Apps own spacing, borders, backgrounds, and layout for tab panels.
100
+
101
+ ## When To Use Something Else
102
+
103
+ - Use `accordion` when sections should expand and collapse independently instead of switching a single active panel.
104
+ - Use `select` or `listbox` when the control chooses a form value instead of changing visible page sections.
105
+ - Use `menu` for action lists and command surfaces.