@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.
- package/README.md +24 -3
- package/dist/animation/animate-layout-mixin.js +66 -66
- package/dist/animation/animate-layout-mixin.js.map +1 -1
- package/dist/components/accordion/accordion.d.ts +3 -3
- package/dist/components/accordion/accordion.js +0 -2
- package/dist/components/accordion/accordion.js.map +1 -1
- package/dist/components/accordion/demos/card.demo.d.ts +6 -0
- package/dist/components/accordion/demos/card.demo.js +58 -0
- package/dist/components/accordion/demos/card.demo.js.map +1 -0
- package/dist/components/accordion/demos/multiple.demo.d.ts +6 -0
- package/dist/components/accordion/demos/multiple.demo.js +19 -0
- package/dist/components/accordion/demos/multiple.demo.js.map +1 -0
- package/dist/components/accordion/demos/overview.demo.d.ts +6 -0
- package/dist/components/accordion/demos/overview.demo.js +19 -0
- package/dist/components/accordion/demos/overview.demo.js.map +1 -0
- package/dist/components/breadcrumbs/breadcrumbs.js +0 -2
- package/dist/components/breadcrumbs/breadcrumbs.js.map +1 -1
- package/dist/components/breadcrumbs/demos/basic.demo.d.ts +5 -0
- package/dist/components/breadcrumbs/demos/basic.demo.js +14 -0
- package/dist/components/breadcrumbs/demos/basic.demo.js.map +1 -0
- package/dist/components/breadcrumbs/demos/separator.demo.d.ts +5 -0
- package/dist/components/breadcrumbs/demos/separator.demo.js +14 -0
- package/dist/components/breadcrumbs/demos/separator.demo.js.map +1 -0
- package/dist/components/button/button.d.ts +82 -0
- package/dist/components/button/button.js +60 -2
- package/dist/components/button/button.js.map +1 -1
- package/dist/components/button/demos/aliases.demo.d.ts +6 -0
- package/dist/components/button/demos/aliases.demo.js +18 -0
- package/dist/components/button/demos/aliases.demo.js.map +1 -0
- package/dist/components/button/demos/basic.demo.d.ts +6 -0
- package/dist/components/button/demos/basic.demo.js +19 -0
- package/dist/components/button/demos/basic.demo.js.map +1 -0
- package/dist/components/button/demos/states.demo.d.ts +6 -0
- package/dist/components/button/demos/states.demo.js +29 -0
- package/dist/components/button/demos/states.demo.js.map +1 -0
- package/dist/components/combobox/combobox.d.ts +3 -3
- package/dist/components/combobox/combobox.js +0 -2
- package/dist/components/combobox/combobox.js.map +1 -1
- package/dist/components/combobox/demos/overview.demo.d.ts +6 -0
- package/dist/components/combobox/demos/overview.demo.js +153 -0
- package/dist/components/combobox/demos/overview.demo.js.map +1 -0
- package/dist/components/listbox/demos/overview.demo.d.ts +6 -0
- package/dist/components/listbox/demos/overview.demo.js +64 -0
- package/dist/components/listbox/demos/overview.demo.js.map +1 -0
- package/dist/components/listbox/listbox.d.ts +1 -6
- package/dist/components/listbox/listbox.js +21 -33
- package/dist/components/listbox/listbox.js.map +1 -1
- package/dist/components/menu/demos/bubbling.demo.d.ts +5 -0
- package/dist/components/menu/demos/bubbling.demo.js +15 -0
- package/dist/components/menu/demos/bubbling.demo.js.map +1 -0
- package/dist/components/menu/demos/overview.demo.d.ts +6 -0
- package/dist/components/menu/demos/overview.demo.js +32 -0
- package/dist/components/menu/demos/overview.demo.js.map +1 -0
- package/dist/components/menu/menu.d.ts +5 -14
- package/dist/components/menu/menu.js +23 -38
- package/dist/components/menu/menu.js.map +1 -1
- package/dist/components/popover/demos/overview.demo.d.ts +6 -0
- package/dist/components/popover/demos/overview.demo.js +64 -0
- package/dist/components/popover/demos/overview.demo.js.map +1 -0
- package/dist/components/select/demos/deconstructed.demo.d.ts +6 -0
- package/dist/components/select/demos/deconstructed.demo.js +53 -0
- package/dist/components/select/demos/deconstructed.demo.js.map +1 -0
- package/dist/components/select/demos/overview.demo.d.ts +5 -0
- package/dist/components/select/demos/overview.demo.js +27 -0
- package/dist/components/select/demos/overview.demo.js.map +1 -0
- package/dist/components/select/select.d.ts +3 -3
- package/dist/components/select/select.js +17 -26
- package/dist/components/select/select.js.map +1 -1
- package/dist/components/tabs/tabs.d.ts +3 -3
- package/dist/components/tabs/tabs.js +0 -2
- package/dist/components/tabs/tabs.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js.map +1 -1
- package/dist/runtime/component.d.ts +8 -5
- package/dist/runtime/component.js +84 -80
- package/dist/runtime/component.js.map +1 -1
- package/dist/runtime/core/attributes.d.ts +12 -0
- package/dist/runtime/core/attributes.js +85 -0
- package/dist/runtime/core/attributes.js.map +1 -0
- package/dist/runtime/core/children.d.ts +7 -0
- package/dist/runtime/core/children.js +30 -0
- package/dist/runtime/core/children.js.map +1 -0
- package/dist/runtime/core/props.d.ts +9 -0
- package/dist/runtime/core/props.js +169 -0
- package/dist/runtime/core/props.js.map +1 -0
- package/dist/runtime/core/vnode.d.ts +3 -0
- package/dist/runtime/core/vnode.js +39 -0
- package/dist/runtime/core/vnode.js.map +1 -0
- package/dist/runtime/create-element.js +2 -1
- package/dist/runtime/create-element.js.map +1 -1
- package/dist/runtime/diff-dom.js +47 -14
- package/dist/runtime/diff-dom.js.map +1 -1
- package/dist/runtime/diff-props.d.ts +1 -2
- package/dist/runtime/diff-props.js +2 -181
- package/dist/runtime/diff-props.js.map +1 -1
- package/dist/runtime/dom.d.ts +4 -0
- package/dist/runtime/frame.d.ts +23 -2
- package/dist/runtime/frame.js +51 -52
- package/dist/runtime/frame.js.map +1 -1
- package/dist/runtime/jsx.js +2 -27
- package/dist/runtime/jsx.js.map +1 -1
- package/dist/runtime/mixins/mixin.js +9 -4
- package/dist/runtime/mixins/mixin.js.map +1 -1
- package/dist/runtime/mixins/on-mixin.d.ts +15 -4
- package/dist/runtime/mixins/on-mixin.js +9 -2
- package/dist/runtime/mixins/on-mixin.js.map +1 -1
- package/dist/runtime/navigation.d.ts +6 -0
- package/dist/runtime/navigation.js +16 -3
- package/dist/runtime/navigation.js.map +1 -1
- package/dist/runtime/reconcile.js +444 -181
- package/dist/runtime/reconcile.js.map +1 -1
- package/dist/runtime/run.d.ts +17 -2
- package/dist/runtime/run.js.map +1 -1
- package/dist/runtime/scheduler.js +10 -0
- package/dist/runtime/scheduler.js.map +1 -1
- package/dist/runtime/stream-protocol.d.ts +8 -0
- package/dist/runtime/stream-protocol.js +20 -0
- package/dist/runtime/stream-protocol.js.map +1 -0
- package/dist/runtime/to-vnode.js +17 -18
- package/dist/runtime/to-vnode.js.map +1 -1
- package/dist/runtime/vnode.d.ts +5 -4
- package/dist/runtime/vnode.js +2 -3
- package/dist/runtime/vnode.js.map +1 -1
- package/dist/server/stream.js +80 -106
- package/dist/server/stream.js.map +1 -1
- package/dist/style/layers.d.ts +2 -0
- package/dist/style/layers.js +3 -0
- package/dist/style/layers.js.map +1 -0
- package/dist/style/stylesheet.d.ts +3 -1
- package/dist/style/stylesheet.js +69 -8
- package/dist/style/stylesheet.js.map +1 -1
- package/dist/theme/glyph-contract.d.ts +1 -1
- package/dist/theme/runtime.js +4 -1
- package/dist/theme/runtime.js.map +1 -1
- package/package.json +6 -7
- package/src/animation/README.md +314 -0
- package/src/animation/animate-layout-mixin.ts +79 -71
- package/src/components/accordion/README.md +166 -0
- package/src/components/accordion/accordion.tsx +8 -10
- package/src/components/accordion/demos/card.demo.tsx +113 -0
- package/src/components/accordion/demos/multiple.demo.tsx +56 -0
- package/src/components/accordion/demos/overview.demo.tsx +56 -0
- package/src/components/anchor/README.md +153 -0
- package/src/components/breadcrumbs/README.md +55 -0
- package/src/components/breadcrumbs/breadcrumbs.tsx +0 -2
- package/src/components/breadcrumbs/demos/basic.demo.tsx +17 -0
- package/src/components/breadcrumbs/demos/separator.demo.tsx +18 -0
- package/src/components/button/README.md +4 -4
- package/src/components/button/button.tsx +88 -2
- package/src/components/button/demos/aliases.demo.tsx +27 -0
- package/src/components/button/demos/basic.demo.tsx +30 -0
- package/src/components/button/demos/states.demo.tsx +52 -0
- package/src/components/combobox/combobox.tsx +8 -10
- package/src/components/combobox/demos/overview.demo.tsx +199 -0
- package/src/components/glyph/README.md +72 -0
- package/src/components/listbox/README.md +115 -0
- package/src/components/listbox/demos/overview.demo.tsx +101 -0
- package/src/components/listbox/listbox.ts +23 -32
- package/src/components/menu/README.md +96 -0
- package/src/components/menu/demos/bubbling.demo.tsx +34 -0
- package/src/components/menu/demos/overview.demo.tsx +76 -0
- package/src/components/menu/menu.tsx +35 -41
- package/src/components/popover/README.md +3 -3
- package/src/components/popover/demos/overview.demo.tsx +143 -0
- package/src/components/select/README.md +107 -0
- package/src/components/select/demos/deconstructed.demo.tsx +95 -0
- package/src/components/select/demos/overview.demo.tsx +51 -0
- package/src/components/select/select.tsx +28 -28
- package/src/components/tabs/tabs.tsx +10 -5
- package/src/index.ts +1 -1
- package/src/runtime/component.ts +94 -82
- package/src/runtime/core/attributes.ts +97 -0
- package/src/runtime/core/children.ts +42 -0
- package/src/runtime/core/props.ts +185 -0
- package/src/runtime/core/vnode.ts +48 -0
- package/src/runtime/create-element.ts +2 -1
- package/src/runtime/diff-dom.ts +59 -12
- package/src/runtime/diff-props.ts +2 -187
- package/src/runtime/dom.ts +4 -0
- package/src/runtime/frame.ts +84 -61
- package/src/runtime/jsx.ts +2 -30
- package/src/runtime/mixins/mixin.ts +9 -4
- package/src/runtime/mixins/on-mixin.ts +20 -12
- package/src/runtime/navigation.ts +24 -3
- package/src/runtime/reconcile.ts +552 -186
- package/src/runtime/run.ts +17 -1
- package/src/runtime/scheduler.ts +7 -0
- package/src/runtime/stream-protocol.ts +27 -0
- package/src/runtime/to-vnode.ts +15 -17
- package/src/runtime/vnode.ts +6 -9
- package/src/server/README.md +88 -0
- package/src/server/stream.ts +105 -117
- package/src/style/layers.ts +2 -0
- package/src/style/stylesheet.ts +75 -10
- package/src/test/README.md +107 -0
- package/src/test/utils.ts +3 -1
- package/src/theme/README.md +103 -0
- package/src/theme/runtime.ts +4 -1
- package/src/utils/scroll-lock/README.md +33 -0
- 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 '
|
|
12
|
-
import { Glyph } from '
|
|
13
|
-
import { popover } from '
|
|
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 = (
|
|
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
|
-
|
|
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 =
|
|
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 !==
|
|
132
|
-
state =
|
|
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 !==
|
|
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 !==
|
|
169
|
-
state =
|
|
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 !==
|
|
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 !==
|
|
222
|
+
if (state !== 'open') return
|
|
226
223
|
|
|
227
224
|
let change = pendingChange
|
|
228
225
|
pendingChange = null
|
|
229
|
-
state =
|
|
226
|
+
state = 'selecting'
|
|
230
227
|
|
|
231
228
|
if (!surfaceRef) {
|
|
232
229
|
displayedLabel = selectedLabel
|
|
233
|
-
state =
|
|
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 =
|
|
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 !==
|
|
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 !==
|
|
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 ===
|
|
286
|
+
return state === 'open' || state === 'selecting'
|
|
290
287
|
},
|
|
291
288
|
|
|
292
289
|
get isOpen() {
|
|
293
|
-
return state ===
|
|
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 ===
|
|
356
|
+
if (state === 'initializing') {
|
|
360
357
|
selectedLabel = displayedLabel = handle.props.defaultLabel
|
|
361
358
|
value = handle.props.defaultValue ?? null
|
|
362
359
|
activeValue = value
|
|
363
|
-
state =
|
|
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
|
|
536
|
-
|
|
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 = (
|
|
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
|
|
450
|
-
|
|
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'
|