@remix-run/ui 0.1.0 → 0.1.2

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 (200) hide show
  1. package/README.md +24 -3
  2. package/dist/animation/animate-layout-mixin.js +66 -66
  3. package/dist/animation/animate-layout-mixin.js.map +1 -1
  4. package/dist/components/accordion/accordion.d.ts +3 -3
  5. package/dist/components/accordion/accordion.js +0 -2
  6. package/dist/components/accordion/accordion.js.map +1 -1
  7. package/dist/components/accordion/demos/card.demo.d.ts +6 -0
  8. package/dist/components/accordion/demos/card.demo.js +58 -0
  9. package/dist/components/accordion/demos/card.demo.js.map +1 -0
  10. package/dist/components/accordion/demos/multiple.demo.d.ts +6 -0
  11. package/dist/components/accordion/demos/multiple.demo.js +19 -0
  12. package/dist/components/accordion/demos/multiple.demo.js.map +1 -0
  13. package/dist/components/accordion/demos/overview.demo.d.ts +6 -0
  14. package/dist/components/accordion/demos/overview.demo.js +19 -0
  15. package/dist/components/accordion/demos/overview.demo.js.map +1 -0
  16. package/dist/components/breadcrumbs/breadcrumbs.js +0 -2
  17. package/dist/components/breadcrumbs/breadcrumbs.js.map +1 -1
  18. package/dist/components/breadcrumbs/demos/basic.demo.d.ts +5 -0
  19. package/dist/components/breadcrumbs/demos/basic.demo.js +14 -0
  20. package/dist/components/breadcrumbs/demos/basic.demo.js.map +1 -0
  21. package/dist/components/breadcrumbs/demos/separator.demo.d.ts +5 -0
  22. package/dist/components/breadcrumbs/demos/separator.demo.js +14 -0
  23. package/dist/components/breadcrumbs/demos/separator.demo.js.map +1 -0
  24. package/dist/components/button/button.d.ts +82 -0
  25. package/dist/components/button/button.js +60 -2
  26. package/dist/components/button/button.js.map +1 -1
  27. package/dist/components/button/demos/aliases.demo.d.ts +6 -0
  28. package/dist/components/button/demos/aliases.demo.js +18 -0
  29. package/dist/components/button/demos/aliases.demo.js.map +1 -0
  30. package/dist/components/button/demos/basic.demo.d.ts +6 -0
  31. package/dist/components/button/demos/basic.demo.js +19 -0
  32. package/dist/components/button/demos/basic.demo.js.map +1 -0
  33. package/dist/components/button/demos/states.demo.d.ts +6 -0
  34. package/dist/components/button/demos/states.demo.js +29 -0
  35. package/dist/components/button/demos/states.demo.js.map +1 -0
  36. package/dist/components/combobox/combobox.d.ts +3 -3
  37. package/dist/components/combobox/combobox.js +0 -2
  38. package/dist/components/combobox/combobox.js.map +1 -1
  39. package/dist/components/combobox/demos/overview.demo.d.ts +6 -0
  40. package/dist/components/combobox/demos/overview.demo.js +153 -0
  41. package/dist/components/combobox/demos/overview.demo.js.map +1 -0
  42. package/dist/components/listbox/demos/overview.demo.d.ts +6 -0
  43. package/dist/components/listbox/demos/overview.demo.js +64 -0
  44. package/dist/components/listbox/demos/overview.demo.js.map +1 -0
  45. package/dist/components/listbox/listbox.d.ts +1 -6
  46. package/dist/components/listbox/listbox.js +21 -33
  47. package/dist/components/listbox/listbox.js.map +1 -1
  48. package/dist/components/menu/demos/bubbling.demo.d.ts +5 -0
  49. package/dist/components/menu/demos/bubbling.demo.js +15 -0
  50. package/dist/components/menu/demos/bubbling.demo.js.map +1 -0
  51. package/dist/components/menu/demos/overview.demo.d.ts +6 -0
  52. package/dist/components/menu/demos/overview.demo.js +32 -0
  53. package/dist/components/menu/demos/overview.demo.js.map +1 -0
  54. package/dist/components/menu/menu.d.ts +5 -14
  55. package/dist/components/menu/menu.js +23 -38
  56. package/dist/components/menu/menu.js.map +1 -1
  57. package/dist/components/popover/demos/overview.demo.d.ts +6 -0
  58. package/dist/components/popover/demos/overview.demo.js +64 -0
  59. package/dist/components/popover/demos/overview.demo.js.map +1 -0
  60. package/dist/components/select/demos/deconstructed.demo.d.ts +6 -0
  61. package/dist/components/select/demos/deconstructed.demo.js +53 -0
  62. package/dist/components/select/demos/deconstructed.demo.js.map +1 -0
  63. package/dist/components/select/demos/overview.demo.d.ts +5 -0
  64. package/dist/components/select/demos/overview.demo.js +27 -0
  65. package/dist/components/select/demos/overview.demo.js.map +1 -0
  66. package/dist/components/select/select.d.ts +3 -3
  67. package/dist/components/select/select.js +17 -26
  68. package/dist/components/select/select.js.map +1 -1
  69. package/dist/components/tabs/tabs.d.ts +3 -3
  70. package/dist/components/tabs/tabs.js +0 -2
  71. package/dist/components/tabs/tabs.js.map +1 -1
  72. package/dist/index.d.ts +1 -1
  73. package/dist/index.js.map +1 -1
  74. package/dist/runtime/component.d.ts +8 -5
  75. package/dist/runtime/component.js +84 -80
  76. package/dist/runtime/component.js.map +1 -1
  77. package/dist/runtime/core/attributes.d.ts +12 -0
  78. package/dist/runtime/core/attributes.js +85 -0
  79. package/dist/runtime/core/attributes.js.map +1 -0
  80. package/dist/runtime/core/children.d.ts +7 -0
  81. package/dist/runtime/core/children.js +30 -0
  82. package/dist/runtime/core/children.js.map +1 -0
  83. package/dist/runtime/core/props.d.ts +9 -0
  84. package/dist/runtime/core/props.js +169 -0
  85. package/dist/runtime/core/props.js.map +1 -0
  86. package/dist/runtime/core/vnode.d.ts +3 -0
  87. package/dist/runtime/core/vnode.js +39 -0
  88. package/dist/runtime/core/vnode.js.map +1 -0
  89. package/dist/runtime/create-element.js +2 -1
  90. package/dist/runtime/create-element.js.map +1 -1
  91. package/dist/runtime/diff-dom.js +47 -14
  92. package/dist/runtime/diff-dom.js.map +1 -1
  93. package/dist/runtime/diff-props.d.ts +1 -2
  94. package/dist/runtime/diff-props.js +2 -181
  95. package/dist/runtime/diff-props.js.map +1 -1
  96. package/dist/runtime/dom.d.ts +4 -0
  97. package/dist/runtime/frame.d.ts +23 -2
  98. package/dist/runtime/frame.js +51 -52
  99. package/dist/runtime/frame.js.map +1 -1
  100. package/dist/runtime/jsx.js +2 -27
  101. package/dist/runtime/jsx.js.map +1 -1
  102. package/dist/runtime/mixins/mixin.js +9 -4
  103. package/dist/runtime/mixins/mixin.js.map +1 -1
  104. package/dist/runtime/mixins/on-mixin.d.ts +15 -4
  105. package/dist/runtime/mixins/on-mixin.js +9 -2
  106. package/dist/runtime/mixins/on-mixin.js.map +1 -1
  107. package/dist/runtime/navigation.d.ts +6 -0
  108. package/dist/runtime/navigation.js +16 -3
  109. package/dist/runtime/navigation.js.map +1 -1
  110. package/dist/runtime/reconcile.js +444 -181
  111. package/dist/runtime/reconcile.js.map +1 -1
  112. package/dist/runtime/run.d.ts +17 -2
  113. package/dist/runtime/run.js.map +1 -1
  114. package/dist/runtime/scheduler.js +10 -0
  115. package/dist/runtime/scheduler.js.map +1 -1
  116. package/dist/runtime/stream-protocol.d.ts +8 -0
  117. package/dist/runtime/stream-protocol.js +20 -0
  118. package/dist/runtime/stream-protocol.js.map +1 -0
  119. package/dist/runtime/to-vnode.js +17 -18
  120. package/dist/runtime/to-vnode.js.map +1 -1
  121. package/dist/runtime/vnode.d.ts +5 -4
  122. package/dist/runtime/vnode.js +2 -3
  123. package/dist/runtime/vnode.js.map +1 -1
  124. package/dist/server/stream.js +80 -106
  125. package/dist/server/stream.js.map +1 -1
  126. package/dist/style/layers.d.ts +2 -0
  127. package/dist/style/layers.js +3 -0
  128. package/dist/style/layers.js.map +1 -0
  129. package/dist/style/stylesheet.d.ts +3 -1
  130. package/dist/style/stylesheet.js +69 -8
  131. package/dist/style/stylesheet.js.map +1 -1
  132. package/dist/theme/glyph-contract.d.ts +1 -1
  133. package/dist/theme/runtime.js +4 -1
  134. package/dist/theme/runtime.js.map +1 -1
  135. package/package.json +6 -7
  136. package/src/animation/README.md +314 -0
  137. package/src/animation/animate-layout-mixin.ts +79 -71
  138. package/src/components/accordion/README.md +166 -0
  139. package/src/components/accordion/accordion.tsx +8 -10
  140. package/src/components/accordion/demos/card.demo.tsx +113 -0
  141. package/src/components/accordion/demos/multiple.demo.tsx +56 -0
  142. package/src/components/accordion/demos/overview.demo.tsx +56 -0
  143. package/src/components/anchor/README.md +153 -0
  144. package/src/components/breadcrumbs/README.md +55 -0
  145. package/src/components/breadcrumbs/breadcrumbs.tsx +0 -2
  146. package/src/components/breadcrumbs/demos/basic.demo.tsx +17 -0
  147. package/src/components/breadcrumbs/demos/separator.demo.tsx +18 -0
  148. package/src/components/button/README.md +4 -4
  149. package/src/components/button/button.tsx +88 -2
  150. package/src/components/button/demos/aliases.demo.tsx +27 -0
  151. package/src/components/button/demos/basic.demo.tsx +30 -0
  152. package/src/components/button/demos/states.demo.tsx +52 -0
  153. package/src/components/combobox/combobox.tsx +8 -10
  154. package/src/components/combobox/demos/overview.demo.tsx +199 -0
  155. package/src/components/glyph/README.md +72 -0
  156. package/src/components/listbox/README.md +115 -0
  157. package/src/components/listbox/demos/overview.demo.tsx +101 -0
  158. package/src/components/listbox/listbox.ts +23 -32
  159. package/src/components/menu/README.md +96 -0
  160. package/src/components/menu/demos/bubbling.demo.tsx +34 -0
  161. package/src/components/menu/demos/overview.demo.tsx +76 -0
  162. package/src/components/menu/menu.tsx +35 -41
  163. package/src/components/popover/README.md +3 -3
  164. package/src/components/popover/demos/overview.demo.tsx +143 -0
  165. package/src/components/select/README.md +107 -0
  166. package/src/components/select/demos/deconstructed.demo.tsx +95 -0
  167. package/src/components/select/demos/overview.demo.tsx +51 -0
  168. package/src/components/select/select.tsx +28 -28
  169. package/src/components/tabs/tabs.tsx +10 -5
  170. package/src/index.ts +1 -1
  171. package/src/runtime/component.ts +94 -82
  172. package/src/runtime/core/attributes.ts +97 -0
  173. package/src/runtime/core/children.ts +42 -0
  174. package/src/runtime/core/props.ts +185 -0
  175. package/src/runtime/core/vnode.ts +48 -0
  176. package/src/runtime/create-element.ts +2 -1
  177. package/src/runtime/diff-dom.ts +59 -12
  178. package/src/runtime/diff-props.ts +2 -187
  179. package/src/runtime/dom.ts +4 -0
  180. package/src/runtime/frame.ts +84 -61
  181. package/src/runtime/jsx.ts +2 -30
  182. package/src/runtime/mixins/mixin.ts +9 -4
  183. package/src/runtime/mixins/on-mixin.ts +20 -12
  184. package/src/runtime/navigation.ts +24 -3
  185. package/src/runtime/reconcile.ts +552 -186
  186. package/src/runtime/run.ts +17 -1
  187. package/src/runtime/scheduler.ts +7 -0
  188. package/src/runtime/stream-protocol.ts +27 -0
  189. package/src/runtime/to-vnode.ts +15 -17
  190. package/src/runtime/vnode.ts +6 -9
  191. package/src/server/README.md +88 -0
  192. package/src/server/stream.ts +105 -117
  193. package/src/style/layers.ts +2 -0
  194. package/src/style/stylesheet.ts +75 -10
  195. package/src/test/README.md +107 -0
  196. package/src/test/utils.ts +3 -1
  197. package/src/theme/README.md +103 -0
  198. package/src/theme/runtime.ts +4 -1
  199. package/src/utils/scroll-lock/README.md +33 -0
  200. package/src/test/setup.ts +0 -7
@@ -8,9 +8,9 @@ Use it for custom surfaces like filters, inspectors, and view options. Higher-le
8
8
 
9
9
  ```tsx
10
10
  import { css, on, type Handle } from 'remix/ui'
11
- import { Button } from '@remix-run/ui/button'
12
- import { Glyph } from '@remix-run/ui/glyph'
13
- import { popover } from '@remix-run/ui/popover'
11
+ import { Button } from 'remix/ui/button'
12
+ import { Glyph } from 'remix/ui/glyph'
13
+ import { popover } from 'remix/ui/popover'
14
14
 
15
15
  export function ViewOptions(handle: Handle) {
16
16
  let open = false
@@ -0,0 +1,143 @@
1
+ import { css, on, type Handle } from '@remix-run/ui'
2
+ import { Button } from '@remix-run/ui/button'
3
+ import { Glyph } from '@remix-run/ui/glyph'
4
+ import * as popover from '@remix-run/ui/popover'
5
+ import { Option, Select } from '@remix-run/ui/select'
6
+ import { theme } from '@remix-run/ui/theme'
7
+
8
+ /**
9
+ * @name Popover Overview
10
+ * @description A popover panel anchored to a trigger button, containing form controls and a close action.
11
+ */
12
+ export default function Example(handle: Handle) {
13
+ let open = false
14
+
15
+ function closePopover() {
16
+ open = false
17
+ void handle.update()
18
+ }
19
+
20
+ return () => (
21
+ <popover.Context>
22
+ <div mix={buttonRowCss}>
23
+ <Button
24
+ endIcon={<Glyph name="chevronDown" />}
25
+ mix={[
26
+ popover.anchor({ placement: 'bottom' }),
27
+ popover.focusOnHide(),
28
+ on('click', () => {
29
+ open = !open
30
+ void handle.update()
31
+ }),
32
+ ]}
33
+ tone="secondary"
34
+ >
35
+ View options
36
+ </Button>
37
+ </div>
38
+
39
+ <div
40
+ mix={[
41
+ popover.surfaceStyle,
42
+ popover.surface({
43
+ closeOnAnchorClick: false,
44
+ open,
45
+ onHide() {
46
+ closePopover()
47
+ },
48
+ }),
49
+ ]}
50
+ >
51
+ <div mix={[popover.contentStyle, panelCss]}>
52
+ <div mix={fieldCss}>
53
+ <label for={`${handle.id}-grouping`} mix={labelCss}>
54
+ Grouping
55
+ </label>
56
+ <Select
57
+ id={`${handle.id}-grouping`}
58
+ defaultLabel="No grouping"
59
+ defaultValue="none"
60
+ mix={[fieldSelectCss, popover.focusOnShow()]}
61
+ >
62
+ <Option label="No grouping" value="none" />
63
+ <Option label="Status" value="status" />
64
+ <Option label="Priority" value="priority" />
65
+ </Select>
66
+ </div>
67
+
68
+ <div mix={fieldCss}>
69
+ <label for={`${handle.id}-ordering`} mix={labelCss}>
70
+ Ordering
71
+ </label>
72
+ <Select
73
+ id={`${handle.id}-ordering`}
74
+ defaultLabel="Manual"
75
+ defaultValue="manual"
76
+ mix={fieldSelectCss}
77
+ >
78
+ <Option label="Manual" value="manual" />
79
+ <Option label="Newest first" value="newest" />
80
+ <Option label="Oldest first" value="oldest" />
81
+ </Select>
82
+ </div>
83
+
84
+ <div mix={fieldCss}>
85
+ <label for={`${handle.id}-closed-projects`} mix={labelCss}>
86
+ Show closed projects
87
+ </label>
88
+ <Select
89
+ id={`${handle.id}-closed-projects`}
90
+ defaultLabel="All"
91
+ defaultValue="all"
92
+ mix={fieldSelectCss}
93
+ >
94
+ <Option label="All" value="all" />
95
+ <Option label="Open only" value="open" />
96
+ <Option label="Closed only" value="closed" />
97
+ </Select>
98
+ </div>
99
+
100
+ <div mix={actionsCss}>
101
+ <Button mix={on('click', closePopover)} tone="ghost">
102
+ Done
103
+ </Button>
104
+ </div>
105
+ </div>
106
+ </div>
107
+ </popover.Context>
108
+ )
109
+ }
110
+
111
+ const buttonRowCss = css({
112
+ display: 'flex',
113
+ justifyContent: 'flex-start',
114
+ })
115
+
116
+ const panelCss = css({
117
+ display: 'grid',
118
+ gap: theme.space.md,
119
+ width: '22rem',
120
+ padding: theme.space.lg,
121
+ })
122
+
123
+ const fieldCss = css({
124
+ display: 'grid',
125
+ gap: theme.space.px,
126
+ })
127
+
128
+ const labelCss = css({
129
+ margin: 0,
130
+ fontSize: theme.fontSize.xs,
131
+ fontWeight: theme.fontWeight.semibold,
132
+ color: theme.colors.text.primary,
133
+ })
134
+
135
+ const fieldSelectCss = css({
136
+ width: '100%',
137
+ })
138
+
139
+ const actionsCss = css({
140
+ display: 'flex',
141
+ justifyContent: 'flex-end',
142
+ paddingTop: theme.space.xs,
143
+ })
@@ -0,0 +1,107 @@
1
+ # select
2
+
3
+ `Select` is a button-triggered popup value picker backed by `listbox` and `popover`. Use it when the user should choose one stable string value from a finite set.
4
+
5
+ ## Usage
6
+
7
+ ```tsx
8
+ import { Option, Select, onSelectChange } from 'remix/ui/select'
9
+
10
+ export function FrameworkSelect() {
11
+ return (
12
+ <Select
13
+ defaultLabel="Select a framework"
14
+ defaultValue="remix"
15
+ name="framework"
16
+ mix={onSelectChange((event) => {
17
+ console.log(event.value, event.label, event.optionId)
18
+ })}
19
+ >
20
+ <Option label="Remix framework" value="remix">
21
+ Remix
22
+ </Option>
23
+ <Option disabled label="React Router framework" value="react-router">
24
+ React Router
25
+ </Option>
26
+ <Option label="React framework" value="react">
27
+ React
28
+ </Option>
29
+ </Select>
30
+ )
31
+ }
32
+ ```
33
+
34
+ Use `textValue` when closed-trigger typeahead should match a different string from the visible label.
35
+
36
+ ```tsx
37
+ <Select defaultLabel="Select an environment">
38
+ <Option label="Production environment" value="production">
39
+ Production
40
+ </Option>
41
+ <Option label="Staging environment" textValue="beta" value="staging">
42
+ Staging
43
+ </Option>
44
+ </Select>
45
+ ```
46
+
47
+ Use the lower-level primitives when the trigger or popup structure needs to be owned by another component. Keep the same provider, trigger, popover, list, option, and hidden-input relationship.
48
+
49
+ ```tsx
50
+ import type { Handle } from 'remix/ui'
51
+ import * as button from 'remix/ui/button'
52
+ import * as listbox from 'remix/ui/listbox'
53
+ import * as popover from 'remix/ui/popover'
54
+ import * as select from 'remix/ui/select'
55
+
56
+ function SelectValue(handle: Handle) {
57
+ let context = handle.context.get(select.Context)
58
+
59
+ return () => <span mix={button.labelStyle}>{context.displayedLabel}</span>
60
+ }
61
+
62
+ function IssueTypeSelect() {
63
+ return () => (
64
+ <select.Context defaultLabel="Select a type" name="issueType">
65
+ <button type="button" mix={[button.baseStyle, select.triggerStyle, select.trigger()]}>
66
+ <SelectValue />
67
+ </button>
68
+ <popover.Context>
69
+ <div mix={[popover.surfaceStyle, select.popover()]}>
70
+ <div mix={[popover.contentStyle, listbox.listStyle, select.list()]}>
71
+ <div mix={[listbox.optionStyle, select.option({ label: 'Bug', value: 'bug' })]}>
72
+ Bug
73
+ </div>
74
+ <div mix={[listbox.optionStyle, select.option({ label: 'Feature', value: 'feature' })]}>
75
+ Feature
76
+ </div>
77
+ </div>
78
+ </div>
79
+ </popover.Context>
80
+ <input mix={select.hiddenInput()} />
81
+ </select.Context>
82
+ )
83
+ }
84
+ ```
85
+
86
+ ## `select.*`
87
+
88
+ - `Select`: composed trigger, popover, listbox, option list, and optional hidden input for form participation. Accepts `defaultLabel`, `defaultValue`, `disabled`, `name`, and button props.
89
+ - `Option`: option wrapper that renders the standard check glyph and label slot. Accepts `label`, `value`, optional `disabled`, and optional `textValue`.
90
+ - `onSelectChange(...)`: event mixin for the bubbling `SelectChangeEvent`.
91
+ - `Context`, `trigger()`, `popover()`, `list()`, `option(...)`, and `hiddenInput()`: lower-level composition primitives.
92
+ - `triggerStyle`: standard select trigger style.
93
+ - `SelectChangeEvent`: event with `value`, `label`, and `optionId`.
94
+ - `SelectProps`, `SelectContextProps`, and `SelectOptionProps`: public TypeScript props for the composed and lower-level APIs.
95
+
96
+ ## Behavior Notes
97
+
98
+ - `defaultLabel` is displayed before selection settles. `defaultValue` selects the matching option without replacing the trigger label until a new selection commits.
99
+ - `Option.label` is the committed display label and event label. `children` are the rendered option contents.
100
+ - Click, `ArrowDown`, and `ArrowUp` open the popup. Focus moves into the list and Escape restores focus to the trigger through popover behavior.
101
+ - Reopening highlights the current selected value.
102
+ - The popup min-width syncs to the trigger width before opening.
103
+ - Closed-trigger typeahead selects a matching option immediately and supports option `textValue`.
104
+ - Selecting an option flashes it with `data-select-flash`, waits for the close transition and label delay, updates the displayed label, and dispatches `SelectChangeEvent`.
105
+ - Selecting the already-selected value updates state but does not dispatch a change event.
106
+ - `SelectChangeEvent` bubbles from the trigger when a trigger exists. It includes `value`, `label`, and `optionId`.
107
+ - Passing `name` renders a hidden input so the selected value participates in `FormData`; disabled selects disable the trigger and hidden input.
@@ -0,0 +1,95 @@
1
+ import { css, type Handle } from '@remix-run/ui'
2
+ import * as button from '@remix-run/ui/button'
3
+ import { Glyph } from '@remix-run/ui/glyph'
4
+ import * as listbox from '@remix-run/ui/listbox'
5
+ import * as popover from '@remix-run/ui/popover'
6
+ import * as select from '@remix-run/ui/select'
7
+ import { theme } from '@remix-run/ui/theme'
8
+
9
+ /**
10
+ * @name Select Deconstructed
11
+ * @description Build a fully custom select by composing the select, popover, and listbox primitives directly.
12
+ */
13
+ export default function Example(handle: Handle) {
14
+ let label = 'Local'
15
+ let value = 'local'
16
+ let triggerId = `${handle.id}-trigger`
17
+
18
+ return () => (
19
+ <div
20
+ mix={[
21
+ stackCss,
22
+ select.onSelectChange((event) => {
23
+ label = event.label ?? 'Select an environment'
24
+ value = event.value ?? 'null'
25
+ void handle.update()
26
+ }),
27
+ ]}
28
+ >
29
+ <p mix={labelCss}>Environment</p>
30
+
31
+ <select.Context defaultLabel="Local" defaultValue="local" name="environment">
32
+ <button
33
+ id={triggerId}
34
+ type="button"
35
+ mix={[button.baseStyle, select.triggerStyle, select.trigger(), selectCss]}
36
+ >
37
+ <span mix={button.labelStyle}>{label}</span>
38
+ <Glyph mix={button.iconStyle} name="chevronVertical" />
39
+ </button>
40
+
41
+ <popover.Context>
42
+ <div mix={[popover.surfaceStyle, select.popover()]}>
43
+ <div
44
+ aria-labelledby={triggerId}
45
+ mix={[popover.contentStyle, listbox.listStyle, select.list()]}
46
+ >
47
+ {environmentOptions.map((option) => (
48
+ <div key={option.value} mix={[listbox.optionStyle, select.option(option)]}>
49
+ <Glyph mix={listbox.glyphStyle} name="check" />
50
+ <span mix={listbox.labelStyle}>{option.label}</span>
51
+ </div>
52
+ ))}
53
+ </div>
54
+ </div>
55
+ </popover.Context>
56
+
57
+ <input mix={select.hiddenInput()} />
58
+ </select.Context>
59
+
60
+ <p mix={valueCss}>{`value=${value}`}</p>
61
+ </div>
62
+ )
63
+ }
64
+
65
+ const environmentOptions = [
66
+ { label: 'Local', value: 'local' },
67
+ { label: 'Staging', value: 'staging' },
68
+ { label: 'Production', value: 'production' },
69
+ { disabled: true, label: 'Archived', value: 'archived' },
70
+ ] as const
71
+
72
+ const selectCss = css({
73
+ width: '16rem',
74
+ })
75
+
76
+ const stackCss = css({
77
+ display: 'flex',
78
+ flexDirection: 'column',
79
+ gap: theme.space.sm,
80
+ width: '100%',
81
+ })
82
+
83
+ const labelCss = css({
84
+ margin: 0,
85
+ fontSize: theme.fontSize.xs,
86
+ fontWeight: theme.fontWeight.semibold,
87
+ color: theme.colors.text.primary,
88
+ })
89
+
90
+ const valueCss = css({
91
+ margin: 0,
92
+ fontFamily: theme.fontFamily.mono,
93
+ fontSize: theme.fontSize.xs,
94
+ color: theme.colors.text.secondary,
95
+ })
@@ -0,0 +1,51 @@
1
+ import { css } from '@remix-run/ui'
2
+ import { Option, Select } from '@remix-run/ui/select'
3
+ import { theme } from '@remix-run/ui/theme'
4
+
5
+ /**
6
+ * @name Select Overview
7
+ * @description A styled select control with a searchable dropdown and accessible label.
8
+ */
9
+ export default function Example() {
10
+ return () => (
11
+ <div mix={stackCss}>
12
+ <label for="fruit-select" mix={labelCss}>
13
+ Choose a fruit
14
+ </label>
15
+ <Select
16
+ id="fruit-select"
17
+ defaultLabel="Banana"
18
+ defaultValue="banana"
19
+ name="fruit"
20
+ mix={selectCss}
21
+ >
22
+ <Option label="Apple" value="apple" />
23
+ <Option label="Apricot" value="apricot" />
24
+ <Option label="Banana" value="banana" />
25
+ <Option label="Blackberry" value="blackberry" />
26
+ <Option label="Blackcurrant" value="blackcurrant" />
27
+ <Option label="Blueberry" value="blueberry" />
28
+ <Option label="Boysenberry" value="boysenberry" />
29
+ <Option label="Cantaloupe" value="cantaloupe" />
30
+ </Select>
31
+ </div>
32
+ )
33
+ }
34
+
35
+ const selectCss = css({
36
+ width: '16rem',
37
+ })
38
+
39
+ const stackCss = css({
40
+ display: 'flex',
41
+ flexDirection: 'column',
42
+ gap: theme.space.sm,
43
+ width: '100%',
44
+ })
45
+
46
+ const labelCss = css({
47
+ margin: 0,
48
+ fontSize: theme.fontSize.xs,
49
+ fontWeight: theme.fontWeight.semibold,
50
+ color: theme.colors.text.primary,
51
+ })
@@ -1,5 +1,3 @@
1
- // @jsxRuntime classic
2
- // @jsx createElement
3
1
  import {
4
2
  attrs,
5
3
  createElement,
@@ -8,6 +6,7 @@ import {
8
6
  on,
9
7
  ref,
10
8
  type CSSMixinDescriptor,
9
+ type Dispatched,
11
10
  type ElementProps,
12
11
  type Handle,
13
12
  type MixinHandle,
@@ -28,7 +27,10 @@ import type { AnchorOptions } from '../anchor/anchor.ts'
28
27
  const SELECT_CHANGE_EVENT = 'rmx:select-change' as const
29
28
  const LABEL_SWAP_DELAY_MS = 75
30
29
 
31
- type SelectChangeHandler = (event: SelectChangeEvent, signal: AbortSignal) => void | Promise<void>
30
+ type SelectChangeHandler<target extends HTMLElement> = (
31
+ event: Dispatched<SelectChangeEvent, target>,
32
+ signal: AbortSignal,
33
+ ) => void | Promise<void>
32
34
 
33
35
  declare global {
34
36
  interface HTMLElementEventMap {
@@ -74,12 +76,7 @@ export interface SelectProps extends Omit<Props<'button'>, 'children' | 'name'>
74
76
 
75
77
  export type SelectOptionProps = Props<'div'> & Omit<listbox.ListboxOption, 'id'>
76
78
 
77
- enum State {
78
- Initializing = 'initializing',
79
- Closed = 'closed',
80
- Open = 'open',
81
- Selecting = 'selecting',
82
- }
79
+ type State = 'initializing' | 'closed' | 'open' | 'selecting'
83
80
 
84
81
  type PendingChange = {
85
82
  label: string | null
@@ -115,7 +112,7 @@ function SelectProvider(handle: Handle<SelectContextProps, SelectContextValue>)
115
112
  let surfaceRef: HTMLElement | undefined
116
113
  let popoverContextRef: popover.PopoverContext | undefined
117
114
 
118
- let state: State = State.Initializing
115
+ let state: State = 'initializing'
119
116
 
120
117
  let value: listbox.ListboxValue = null
121
118
  let activeValue: listbox.ListboxValue = null
@@ -128,14 +125,14 @@ function SelectProvider(handle: Handle<SelectContextProps, SelectContextValue>)
128
125
  let listId = `${handle.id}-list`
129
126
 
130
127
  function open() {
131
- if (state !== State.Closed || handle.props.disabled) return
132
- state = State.Open
128
+ if (state !== 'closed' || handle.props.disabled) return
129
+ state = 'open'
133
130
  activeValue = value
134
131
  handle.update()
135
132
  }
136
133
 
137
134
  function syncPopoverMinWidth() {
138
- if (state !== State.Open || !surfaceRef || !triggerRef) {
135
+ if (state !== 'open' || !surfaceRef || !triggerRef) {
139
136
  return
140
137
  }
141
138
 
@@ -165,8 +162,8 @@ function SelectProvider(handle: Handle<SelectContextProps, SelectContextValue>)
165
162
  }
166
163
 
167
164
  function close() {
168
- if (state !== State.Open) return
169
- state = State.Closed
165
+ if (state !== 'open') return
166
+ state = 'closed'
170
167
  handle.update()
171
168
  }
172
169
 
@@ -215,22 +212,22 @@ function SelectProvider(handle: Handle<SelectContextProps, SelectContextValue>)
215
212
  nextValue: listbox.ListboxValue,
216
213
  option: listbox.ListboxOption | undefined,
217
214
  ) {
218
- if (state !== State.Open) return
215
+ if (state !== 'open') return
219
216
  pendingChange = getPendingChange(nextValue, option)
220
217
  setSelectedOption(nextValue, option)
221
218
  handle.update()
222
219
  }
223
220
 
224
221
  async function settleSelectedOption() {
225
- if (state !== State.Open) return
222
+ if (state !== 'open') return
226
223
 
227
224
  let change = pendingChange
228
225
  pendingChange = null
229
- state = State.Selecting
226
+ state = 'selecting'
230
227
 
231
228
  if (!surfaceRef) {
232
229
  displayedLabel = selectedLabel
233
- state = State.Closed
230
+ state = 'closed'
234
231
  let signal = await handle.update()
235
232
  if (signal.aborted) return
236
233
  dispatchChange(change)
@@ -241,14 +238,14 @@ function SelectProvider(handle: Handle<SelectContextProps, SelectContextValue>)
241
238
  await wait(LABEL_SWAP_DELAY_MS) // UX delay label swap for clear value change
242
239
  if (handle.signal.aborted) return
243
240
  displayedLabel = selectedLabel
244
- state = State.Closed
241
+ state = 'closed'
245
242
  let signal = await handle.update()
246
243
  if (signal.aborted) return
247
244
  dispatchChange(change)
248
245
  }
249
246
 
250
247
  function selectTypeaheadMatch(text: string) {
251
- if (state !== State.Closed || handle.props.disabled) return
248
+ if (state !== 'closed' || handle.props.disabled) return
252
249
 
253
250
  let option = listboxRef?.matchSearchText(text, value)
254
251
  if (!option) return
@@ -266,7 +263,7 @@ function SelectProvider(handle: Handle<SelectContextProps, SelectContextValue>)
266
263
  nextActiveValue: listbox.ListboxValue,
267
264
  option: listbox.ListboxOption | undefined,
268
265
  ) {
269
- if (state !== State.Open) return
266
+ if (state !== 'open') return
270
267
  activeValue = nextActiveValue
271
268
  activeId = option?.id
272
269
  handle.update()
@@ -286,11 +283,11 @@ function SelectProvider(handle: Handle<SelectContextProps, SelectContextValue>)
286
283
  },
287
284
 
288
285
  get isExpanded() {
289
- return state === State.Open || state === State.Selecting
286
+ return state === 'open' || state === 'selecting'
290
287
  },
291
288
 
292
289
  get isOpen() {
293
- return state === State.Open
290
+ return state === 'open'
294
291
  },
295
292
 
296
293
  get listId() {
@@ -356,11 +353,11 @@ function SelectProvider(handle: Handle<SelectContextProps, SelectContextValue>)
356
353
  })
357
354
 
358
355
  return () => {
359
- if (state === State.Initializing) {
356
+ if (state === 'initializing') {
360
357
  selectedLabel = displayedLabel = handle.props.defaultLabel
361
358
  value = handle.props.defaultValue ?? null
362
359
  activeValue = value
363
- state = State.Closed
360
+ state = 'closed'
364
361
 
365
362
  handle.queueTask(() => {
366
363
  if (selectedId || !surfaceRef) {
@@ -532,8 +529,11 @@ const select = {
532
529
  trigger,
533
530
  } as const
534
531
 
535
- export function onSelectChange(handler: SelectChangeHandler, captureBoolean?: boolean) {
536
- return on<HTMLElement, typeof SELECT_CHANGE_EVENT>(SELECT_CHANGE_EVENT, handler, captureBoolean)
532
+ export function onSelectChange<target extends HTMLElement>(
533
+ handler: SelectChangeHandler<target>,
534
+ captureBoolean?: boolean,
535
+ ) {
536
+ return on(SELECT_CHANGE_EVENT, handler, captureBoolean)
537
537
  }
538
538
 
539
539
  function SelectLabel(handle: Handle) {
@@ -1,5 +1,3 @@
1
- // @jsxRuntime classic
2
- // @jsx createElement
3
1
  import {
4
2
  attrs,
5
3
  css,
@@ -7,6 +5,7 @@ import {
7
5
  createMixin,
8
6
  on,
9
7
  type CSSMixinDescriptor,
8
+ type Dispatched,
10
9
  type ElementProps,
11
10
  type Handle,
12
11
  type MixinHandle,
@@ -18,7 +17,10 @@ import { theme } from '../../theme/theme.ts'
18
17
 
19
18
  const TABS_CHANGE_EVENT = 'rmx:tabs-change' as const
20
19
 
21
- type TabsChangeHandler = (event: TabsChangeEvent, signal: AbortSignal) => void | Promise<void>
20
+ type TabsChangeHandler<target extends HTMLElement> = (
21
+ event: Dispatched<TabsChangeEvent, target>,
22
+ signal: AbortSignal,
23
+ ) => void | Promise<void>
22
24
 
23
25
  const tabsListCss: CSSMixinDescriptor = css({
24
26
  display: 'inline-flex',
@@ -446,8 +448,11 @@ export const trigger = triggerMixin
446
448
 
447
449
  const tabs = { Context, list, panel, trigger } as const
448
450
 
449
- export function onTabsChange(handler: TabsChangeHandler, captureBoolean?: boolean) {
450
- return on<HTMLElement, typeof TABS_CHANGE_EVENT>(TABS_CHANGE_EVENT, handler, captureBoolean)
451
+ export function onTabsChange<target extends HTMLElement>(
452
+ handler: TabsChangeHandler<target>,
453
+ captureBoolean?: boolean,
454
+ ) {
455
+ return on(TABS_CHANGE_EVENT, handler, captureBoolean)
451
456
  }
452
457
 
453
458
  export function Tabs(handle: Handle<TabsProps>) {
package/src/index.ts CHANGED
@@ -58,8 +58,8 @@ export type {
58
58
  } from './runtime/mixins/mixin.ts'
59
59
  export { TypedEventTarget } from './runtime/typed-event-target.ts'
60
60
  export { addEventListeners } from './runtime/event-listeners.ts'
61
+ export type { Dispatched } from './runtime/event-listeners.ts'
61
62
  export { on } from './runtime/mixins/on-mixin.ts'
62
- export type { Dispatched } from './runtime/mixins/on-mixin.ts'
63
63
  export { link } from './runtime/mixins/link-mixin.ts'
64
64
  export { ref } from './runtime/mixins/ref-mixin.ts'
65
65
  export type { RefCallback } from './runtime/mixins/ref-mixin.ts'