@remix-run/ui 0.1.1 → 0.2.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.
- package/README.md +33 -10
- package/dist/animation/animate-layout-mixin.js +66 -66
- package/dist/animation/animate-layout-mixin.js.map +1 -1
- package/dist/animation/spring.d.ts +6 -35
- package/dist/animation/spring.js +9 -8
- package/dist/animation/spring.js.map +1 -1
- package/dist/components/accordion/accordion.d.ts +7 -7
- 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.d.ts +1 -1
- 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 +86 -4
- package/dist/components/button/button.js +63 -3
- 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 +11 -11
- 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 +4 -9
- 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 +15 -24
- 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/popover/popover.d.ts +5 -5
- package/dist/components/popover/popover.js.map +1 -1
- 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 +11 -11
- package/dist/components/select/select.js +17 -26
- package/dist/components/select/select.js.map +1 -1
- package/dist/components/tabs/tabs.d.ts +6 -6
- package/dist/components/tabs/tabs.js +0 -2
- package/dist/components/tabs/tabs.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js.map +1 -1
- package/dist/interactions/keydown/keydown.d.ts +1 -1
- package/dist/interactions/outside-click/outside-click-mixin.d.ts +2 -1
- package/dist/interactions/typeahead/typeahead-mixin.d.ts +3 -1
- package/dist/runtime/component.d.ts +18 -12
- package/dist/runtime/component.js +93 -80
- package/dist/runtime/component.js.map +1 -1
- package/dist/runtime/core/attributes.d.ts +14 -0
- package/dist/runtime/core/attributes.js +103 -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 +170 -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 +20 -9
- package/dist/runtime/frame.d.ts +23 -2
- package/dist/runtime/frame.js +41 -47
- package/dist/runtime/frame.js.map +1 -1
- package/dist/runtime/jsx.d.ts +1 -1
- package/dist/runtime/jsx.js +2 -28
- package/dist/runtime/jsx.js.map +1 -1
- package/dist/runtime/mixins/link-mixin.d.ts +5 -1
- package/dist/runtime/mixins/link-mixin.js.map +1 -1
- package/dist/runtime/mixins/mixin.d.ts +2 -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/mixins/ref-mixin.d.ts +2 -1
- package/dist/runtime/mixins/ref-mixin.js.map +1 -1
- package/dist/runtime/navigation.js +9 -1
- package/dist/runtime/navigation.js.map +1 -1
- package/dist/runtime/reconcile.d.ts +1 -1
- package/dist/runtime/reconcile.js +452 -197
- 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.d.ts +10 -10
- package/dist/runtime/scheduler.js +1 -1
- 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/svg-attributes.js +1 -0
- package/dist/runtime/svg-attributes.js.map +1 -1
- package/dist/runtime/to-vnode.js +7 -11
- package/dist/runtime/to-vnode.js.map +1 -1
- package/dist/runtime/vnode.d.ts +11 -5
- package/dist/runtime/vnode.js +6 -3
- package/dist/runtime/vnode.js.map +1 -1
- package/dist/server/stream.d.ts +2 -0
- package/dist/server/stream.js +154 -114
- package/dist/server/stream.js.map +1 -1
- package/dist/style/css-mixin.d.ts +2 -1
- package/dist/style/css-mixin.js.map +1 -1
- package/dist/style/index.d.ts +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 +13 -11
- package/dist/style/stylesheet.js +69 -8
- package/dist/style/stylesheet.js.map +1 -1
- package/dist/theme/contract.d.ts +24 -222
- package/dist/theme/contract.js +2 -2
- package/dist/theme/contract.js.map +1 -1
- package/dist/theme/glyph-contract.d.ts +1 -3
- package/dist/theme/glyph-contract.js.map +1 -1
- package/dist/theme/presets/rmx-01/index.d.ts +4 -2
- package/dist/theme/presets/rmx-01/index.js.map +1 -1
- package/dist/theme/runtime.js +4 -1
- package/dist/theme/runtime.js.map +1 -1
- package/dist/utils/scroll-lock.d.ts +3 -3
- package/dist/utils/scroll-lock.js.map +1 -1
- package/package.json +3 -3
- package/src/animation/README.md +316 -0
- package/src/animation/animate-layout-mixin.ts +79 -71
- package/src/animation/spring.ts +21 -9
- package/src/components/accordion/README.md +166 -0
- package/src/components/accordion/accordion.tsx +14 -14
- 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/anchor/anchor.ts +1 -1
- package/src/components/breadcrumbs/README.md +55 -0
- package/src/components/breadcrumbs/breadcrumbs.tsx +1 -3
- 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 +118 -20
- 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/README.md +1 -1
- package/src/components/combobox/combobox.tsx +37 -21
- package/src/components/combobox/demos/overview.demo.tsx +199 -0
- package/src/components/glyph/README.md +72 -0
- package/src/components/glyph/glyph.tsx +2 -2
- package/src/components/listbox/README.md +115 -0
- package/src/components/listbox/demos/overview.demo.tsx +101 -0
- package/src/components/listbox/listbox.ts +106 -110
- 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 +61 -55
- package/src/components/popover/README.md +3 -3
- package/src/components/popover/demos/overview.demo.tsx +143 -0
- package/src/components/popover/popover.ts +90 -79
- 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 +53 -36
- package/src/components/tabs/tabs.tsx +10 -5
- package/src/index.ts +2 -1
- package/src/runtime/component.ts +122 -88
- package/src/runtime/core/attributes.ts +118 -0
- package/src/runtime/core/children.ts +42 -0
- package/src/runtime/core/props.ts +187 -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 +20 -9
- package/src/runtime/frame.ts +71 -55
- package/src/runtime/jsx.ts +3 -30
- package/src/runtime/mixins/link-mixin.ts +88 -86
- package/src/runtime/mixins/mixin.ts +18 -5
- package/src/runtime/mixins/on-mixin.ts +20 -12
- package/src/runtime/mixins/ref-mixin.ts +4 -5
- package/src/runtime/navigation.ts +10 -1
- package/src/runtime/reconcile.ts +563 -223
- package/src/runtime/run.ts +17 -1
- package/src/runtime/scheduler.ts +20 -20
- package/src/runtime/stream-protocol.ts +27 -0
- package/src/runtime/svg-attributes.ts +1 -0
- package/src/runtime/to-vnode.ts +7 -10
- package/src/runtime/vnode.ts +16 -9
- package/src/server/README.md +90 -0
- package/src/server/stream.ts +176 -124
- package/src/style/css-mixin.ts +6 -1
- package/src/style/index.ts +1 -2
- package/src/style/layers.ts +2 -0
- package/src/style/stylesheet.ts +88 -11
- package/src/test/README.md +107 -0
- package/src/test/utils.ts +3 -1
- package/src/theme/README.md +103 -0
- package/src/theme/contract.ts +37 -5
- package/src/theme/glyph-contract.ts +3 -1
- package/src/theme/presets/rmx-01/index.ts +4 -2
- package/src/theme/runtime.ts +4 -1
- package/src/utils/scroll-lock/README.md +33 -0
- package/src/utils/scroll-lock.ts +7 -3
|
@@ -4,7 +4,9 @@ import {
|
|
|
4
4
|
css,
|
|
5
5
|
on,
|
|
6
6
|
type CSSMixinDescriptor,
|
|
7
|
+
type ElementProps,
|
|
7
8
|
type Handle,
|
|
9
|
+
type MixinFactory,
|
|
8
10
|
type RemixNode,
|
|
9
11
|
} from '@remix-run/ui'
|
|
10
12
|
import { anchor as positionAnchor, type AnchorOptions } from '../anchor/anchor.ts'
|
|
@@ -75,7 +77,7 @@ const popoverSurfaceCss: CSSMixinDescriptor = css({
|
|
|
75
77
|
export const contentStyle = popoverContentCss
|
|
76
78
|
export const surfaceStyle = [popoverSurfaceCss, popoverSurfaceTransitionCss] as const
|
|
77
79
|
|
|
78
|
-
function PopoverProvider(handle: Handle<PopoverProps, PopoverContext>) {
|
|
80
|
+
function PopoverProvider(handle: Handle<PopoverProps, PopoverContext>): () => RemixNode {
|
|
79
81
|
handle.context.set({
|
|
80
82
|
hideFocusTarget: null,
|
|
81
83
|
showFocusTarget: null,
|
|
@@ -99,7 +101,10 @@ export interface PopoverHideRequest {
|
|
|
99
101
|
target?: Node | null
|
|
100
102
|
}
|
|
101
103
|
|
|
102
|
-
const anchorMixin
|
|
104
|
+
const anchorMixin: MixinFactory<HTMLElement, [options: AnchorOptions], ElementProps> = createMixin<
|
|
105
|
+
HTMLElement,
|
|
106
|
+
[options: AnchorOptions]
|
|
107
|
+
>((handle) => {
|
|
103
108
|
let context = handle.context.get(PopoverProvider)
|
|
104
109
|
return (options) => {
|
|
105
110
|
handle.queueTask((node) => {
|
|
@@ -108,91 +113,97 @@ const anchorMixin = createMixin<HTMLElement, [options: AnchorOptions]>((handle)
|
|
|
108
113
|
}
|
|
109
114
|
})
|
|
110
115
|
|
|
111
|
-
const surfaceMixin
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
handle.queueTask((node, signal) => {
|
|
118
|
-
context.surface = node
|
|
119
|
-
|
|
120
|
-
signal.addEventListener('abort', () => {
|
|
121
|
-
if (context.surface === node) {
|
|
122
|
-
context.surface = null
|
|
123
|
-
}
|
|
124
|
-
})
|
|
125
|
-
})
|
|
116
|
+
const surfaceMixin: MixinFactory<HTMLElement, [options: PopoverSurfaceOptions], ElementProps> =
|
|
117
|
+
createMixin<HTMLElement, [options: PopoverSurfaceOptions]>((handle) => {
|
|
118
|
+
let openProp = false
|
|
119
|
+
let cleanupAnchor = () => {}
|
|
120
|
+
let unlockScroll = () => {}
|
|
121
|
+
let context = handle.context.get(PopoverProvider)
|
|
126
122
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
openProp = options.open
|
|
130
|
-
|
|
131
|
-
handle.queueTask(async (node) => {
|
|
132
|
-
if (openProp && !wasOpen) {
|
|
133
|
-
node.showPopover()
|
|
134
|
-
} else if (!openProp && wasOpen) {
|
|
135
|
-
node.hidePopover()
|
|
136
|
-
}
|
|
137
|
-
})
|
|
123
|
+
handle.queueTask((node, signal) => {
|
|
124
|
+
context.surface = node
|
|
138
125
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
on('beforetoggle', (event) => {
|
|
143
|
-
if (event.newState === 'open') {
|
|
144
|
-
cleanupAnchor = positionAnchor(
|
|
145
|
-
event.currentTarget,
|
|
146
|
-
context.anchor!.node,
|
|
147
|
-
context.anchor!.options,
|
|
148
|
-
)
|
|
149
|
-
unlockScroll = lockScroll()
|
|
150
|
-
} else if (event.newState === 'closed') {
|
|
151
|
-
cleanupAnchor()
|
|
152
|
-
unlockScroll()
|
|
126
|
+
signal.addEventListener('abort', () => {
|
|
127
|
+
if (context.surface === node) {
|
|
128
|
+
context.surface = null
|
|
153
129
|
}
|
|
154
|
-
})
|
|
130
|
+
})
|
|
131
|
+
})
|
|
155
132
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
} else if (event.newState === 'closed' && options.restoreFocusOnHide !== false) {
|
|
160
|
-
context.hideFocusTarget?.focus()
|
|
161
|
-
}
|
|
162
|
-
}),
|
|
133
|
+
return (options) => {
|
|
134
|
+
let wasOpen = openProp
|
|
135
|
+
openProp = options.open
|
|
163
136
|
|
|
164
|
-
|
|
165
|
-
if (
|
|
166
|
-
|
|
167
|
-
|
|
137
|
+
handle.queueTask(async (node) => {
|
|
138
|
+
if (openProp && !wasOpen) {
|
|
139
|
+
node.showPopover()
|
|
140
|
+
} else if (!openProp && wasOpen) {
|
|
141
|
+
node.hidePopover()
|
|
168
142
|
}
|
|
169
|
-
})
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
return [
|
|
146
|
+
attrs({ popover: 'manual' }),
|
|
147
|
+
|
|
148
|
+
on('beforetoggle', (event) => {
|
|
149
|
+
if (event.newState === 'open') {
|
|
150
|
+
cleanupAnchor = positionAnchor(
|
|
151
|
+
event.currentTarget,
|
|
152
|
+
context.anchor!.node,
|
|
153
|
+
context.anchor!.options,
|
|
154
|
+
)
|
|
155
|
+
unlockScroll = lockScroll()
|
|
156
|
+
} else if (event.newState === 'closed') {
|
|
157
|
+
cleanupAnchor()
|
|
158
|
+
unlockScroll()
|
|
159
|
+
}
|
|
160
|
+
}),
|
|
161
|
+
|
|
162
|
+
on('toggle', async (event) => {
|
|
163
|
+
if (event.newState === 'open') {
|
|
164
|
+
context.showFocusTarget?.focus()
|
|
165
|
+
} else if (event.newState === 'closed' && options.restoreFocusOnHide !== false) {
|
|
166
|
+
context.hideFocusTarget?.focus()
|
|
167
|
+
}
|
|
168
|
+
}),
|
|
169
|
+
|
|
170
|
+
on('keydown', (event) => {
|
|
171
|
+
if (event.key === 'Escape') {
|
|
172
|
+
event.preventDefault()
|
|
173
|
+
options.onHide({ reason: 'escape-key' })
|
|
174
|
+
}
|
|
175
|
+
}),
|
|
176
|
+
|
|
177
|
+
onOutsideClick(
|
|
178
|
+
openProp,
|
|
179
|
+
(target) => {
|
|
180
|
+
options.onHide({ reason: 'outside-click', target })
|
|
181
|
+
},
|
|
182
|
+
(target) =>
|
|
183
|
+
options.closeOnAnchorClick === false && !!context.anchor?.node.contains(target),
|
|
184
|
+
options.stopOutsideClickPropagation ?? true,
|
|
185
|
+
),
|
|
186
|
+
]
|
|
187
|
+
}
|
|
187
188
|
})
|
|
188
|
-
})
|
|
189
189
|
|
|
190
|
-
const
|
|
191
|
-
handle
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
})
|
|
190
|
+
const focusOnShowMixin: MixinFactory<HTMLElement, [], ElementProps> = createMixin<HTMLElement, []>(
|
|
191
|
+
(handle) => {
|
|
192
|
+
handle.addEventListener('insert', (event) => {
|
|
193
|
+
let context = handle.context.get(PopoverProvider)
|
|
194
|
+
context.showFocusTarget = event.node
|
|
195
|
+
})
|
|
196
|
+
},
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
const focusOnHideMixin: MixinFactory<HTMLElement, [], ElementProps> = createMixin<HTMLElement, []>(
|
|
200
|
+
(handle) => {
|
|
201
|
+
handle.addEventListener('insert', (event) => {
|
|
202
|
+
let context = handle.context.get(PopoverProvider)
|
|
203
|
+
context.hideFocusTarget = event.node
|
|
204
|
+
})
|
|
205
|
+
},
|
|
206
|
+
)
|
|
196
207
|
|
|
197
208
|
export const Context = PopoverProvider
|
|
198
209
|
export const anchor = anchorMixin
|
|
@@ -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
|
+
})
|