@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
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
# button
|
|
2
2
|
|
|
3
|
-
`button` is the shared button styling contract for
|
|
3
|
+
`button` is the shared button styling contract for `remix/ui`. Use `Button` for ordinary action buttons, or compose flat `button.*Style` exports directly when a higher-level control needs button structure without a wrapper.
|
|
4
4
|
|
|
5
5
|
## Usage
|
|
6
6
|
|
|
7
7
|
```tsx
|
|
8
|
-
import { Button } from '
|
|
9
|
-
import * as button from '
|
|
10
|
-
import { Glyph } from '
|
|
8
|
+
import { Button } from 'remix/ui/button'
|
|
9
|
+
import * as button from 'remix/ui/button'
|
|
10
|
+
import { Glyph } from 'remix/ui/glyph'
|
|
11
11
|
|
|
12
12
|
function Actions() {
|
|
13
13
|
return (
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
// @jsxRuntime classic
|
|
2
|
-
// @jsx createElement
|
|
3
1
|
import { attrs, createElement, createMixin, css } from '@remix-run/ui'
|
|
4
2
|
import type { CSSMixinDescriptor, ElementProps, Handle, Props, RemixNode } from '@remix-run/ui'
|
|
5
3
|
|
|
@@ -93,12 +91,61 @@ const buttonDefaultsMixin = createMixin<Element, [], ElementProps>(
|
|
|
93
91
|
|
|
94
92
|
const buttonDefaultsCss = buttonDefaultsMixin()
|
|
95
93
|
|
|
94
|
+
/**
|
|
95
|
+
* Base button styling with the default `type="button"` behavior for `<button>`
|
|
96
|
+
* hosts. Compose with a tone style (e.g. {@link primaryStyle}) when applying
|
|
97
|
+
* button styling without using the {@link Button} component.
|
|
98
|
+
*
|
|
99
|
+
* @category mixin
|
|
100
|
+
*/
|
|
96
101
|
export const baseStyle = [buttonDefaultsCss, buttonBaseStyleCss] as const
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Icon slot sizing and `aria-hidden` defaults for decorative icons rendered
|
|
105
|
+
* inside a button.
|
|
106
|
+
*
|
|
107
|
+
* @category mixin
|
|
108
|
+
*/
|
|
97
109
|
export const iconStyle = [buttonIconAttrsCss, buttonIconCss] as const
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Inline label slot with the standard button label spacing.
|
|
113
|
+
*
|
|
114
|
+
* @category mixin
|
|
115
|
+
*/
|
|
98
116
|
export const labelStyle = buttonLabelCss
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Primary visual treatment for buttons. Combine with {@link baseStyle} when
|
|
120
|
+
* styling a non-`Button` host element.
|
|
121
|
+
*
|
|
122
|
+
* @category mixin
|
|
123
|
+
*/
|
|
99
124
|
export const primaryStyle = createButtonCss(theme.colors.action.primary)
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Secondary visual treatment for buttons. Combine with {@link baseStyle} when
|
|
128
|
+
* styling a non-`Button` host element.
|
|
129
|
+
*
|
|
130
|
+
* @category mixin
|
|
131
|
+
*/
|
|
100
132
|
export const secondaryStyle = createButtonCss(theme.colors.action.secondary)
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Ghost visual treatment for buttons — transparent background with a hover
|
|
136
|
+
* surface. Combine with {@link baseStyle} when styling a non-`Button` host
|
|
137
|
+
* element.
|
|
138
|
+
*
|
|
139
|
+
* @category mixin
|
|
140
|
+
*/
|
|
101
141
|
export const ghostStyle = ghostButtonToneCss
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Danger visual treatment for destructive actions. Combine with
|
|
145
|
+
* {@link baseStyle} when styling a non-`Button` host element.
|
|
146
|
+
*
|
|
147
|
+
* @category mixin
|
|
148
|
+
*/
|
|
102
149
|
export const dangerStyle = createButtonCss(theme.colors.action.danger)
|
|
103
150
|
|
|
104
151
|
const toneStyleByTone = {
|
|
@@ -108,15 +155,54 @@ const toneStyleByTone = {
|
|
|
108
155
|
danger: dangerStyle,
|
|
109
156
|
} as const
|
|
110
157
|
|
|
158
|
+
/**
|
|
159
|
+
* Visual treatment supported by {@link Button} — `'primary'`, `'secondary'`,
|
|
160
|
+
* `'ghost'`, or `'danger'`.
|
|
161
|
+
*/
|
|
111
162
|
export type ButtonTone = keyof typeof toneStyleByTone
|
|
112
163
|
|
|
164
|
+
/**
|
|
165
|
+
* Props accepted by the {@link Button} component.
|
|
166
|
+
*
|
|
167
|
+
* Extends the native `<button>` element props with optional icon slots and a
|
|
168
|
+
* tone variant
|
|
169
|
+
*/
|
|
113
170
|
export type ButtonProps = Omit<Props<'button'>, 'children'> & {
|
|
171
|
+
/**
|
|
172
|
+
* Content rendered inside the button's label slot.
|
|
173
|
+
*/
|
|
114
174
|
readonly children?: RemixNode
|
|
175
|
+
/**
|
|
176
|
+
* Decorative icon rendered after the label, inside the icon slot.
|
|
177
|
+
*/
|
|
115
178
|
readonly endIcon?: RemixNode
|
|
179
|
+
/**
|
|
180
|
+
* Decorative icon rendered before the label, inside the icon slot.
|
|
181
|
+
*/
|
|
116
182
|
readonly startIcon?: RemixNode
|
|
183
|
+
/**
|
|
184
|
+
* Visual treatment to apply to the button (default `'secondary'`).
|
|
185
|
+
*/
|
|
117
186
|
readonly tone?: ButtonTone
|
|
118
187
|
}
|
|
119
188
|
|
|
189
|
+
/**
|
|
190
|
+
* Renders a `<button>` with `baseStyle` and the resolved tone style, along with
|
|
191
|
+
* optional start and end icons.
|
|
192
|
+
*
|
|
193
|
+
* @param handle Component handle providing the runtime API and the resolved {@link ButtonProps}.
|
|
194
|
+
* @returns A render function for the button element.
|
|
195
|
+
*
|
|
196
|
+
* @example
|
|
197
|
+
* ```tsx
|
|
198
|
+
* import { Button } from '@remix-run/ui/button'
|
|
199
|
+
* import { Glyph } from '@remix-run/ui/glyph'
|
|
200
|
+
*
|
|
201
|
+
* <Button startIcon={<Glyph name="add" />} tone="primary">
|
|
202
|
+
* Create project
|
|
203
|
+
* </Button>
|
|
204
|
+
* ```
|
|
205
|
+
*/
|
|
120
206
|
export function Button(handle: Handle<ButtonProps>) {
|
|
121
207
|
return () => {
|
|
122
208
|
let { children, endIcon, mix, startIcon, tone = 'secondary', ...buttonProps } = handle.props
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { Button } from '@remix-run/ui/button'
|
|
2
|
+
import { css } from '@remix-run/ui'
|
|
3
|
+
import { theme } from '@remix-run/ui/theme'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @name Button Component
|
|
7
|
+
* @description The Button component wraps the low-level style primitives and accepts a tone prop for quick theming.
|
|
8
|
+
* @order 2
|
|
9
|
+
*/
|
|
10
|
+
export default function Example() {
|
|
11
|
+
return () => (
|
|
12
|
+
<div mix={buttonRowCss}>
|
|
13
|
+
<Button tone="primary" type="submit">
|
|
14
|
+
Save
|
|
15
|
+
</Button>
|
|
16
|
+
<Button tone="secondary">Secondary</Button>
|
|
17
|
+
<Button tone="ghost">Ghost</Button>
|
|
18
|
+
<Button tone="danger">Delete</Button>
|
|
19
|
+
</div>
|
|
20
|
+
)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const buttonRowCss = css({
|
|
24
|
+
display: 'flex',
|
|
25
|
+
alignItems: 'center',
|
|
26
|
+
gap: theme.space.sm,
|
|
27
|
+
})
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { css } from '@remix-run/ui'
|
|
2
|
+
import * as button from '@remix-run/ui/button'
|
|
3
|
+
import { Glyph } from '@remix-run/ui/glyph'
|
|
4
|
+
import { theme } from '@remix-run/ui/theme'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @name Basic Button
|
|
8
|
+
* @description The default button contract supports both ordinary actions and link-shaped navigation.
|
|
9
|
+
* @order 1
|
|
10
|
+
*/
|
|
11
|
+
export default function Example() {
|
|
12
|
+
return () => (
|
|
13
|
+
<div mix={buttonRowCss}>
|
|
14
|
+
<button type="submit" mix={[button.baseStyle, button.primaryStyle]}>
|
|
15
|
+
<Glyph mix={button.iconStyle} name="add" />
|
|
16
|
+
<span mix={button.labelStyle}>Publish</span>
|
|
17
|
+
</button>
|
|
18
|
+
<a href="/api/remix/ui/button/overview/" mix={[button.baseStyle, button.secondaryStyle]}>
|
|
19
|
+
<span mix={button.labelStyle}>View button docs</span>
|
|
20
|
+
<Glyph mix={button.iconStyle} name="chevronRight" />
|
|
21
|
+
</a>
|
|
22
|
+
</div>
|
|
23
|
+
)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const buttonRowCss = css({
|
|
27
|
+
display: 'flex',
|
|
28
|
+
alignItems: 'center',
|
|
29
|
+
gap: theme.space.sm,
|
|
30
|
+
})
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import * as button from '@remix-run/ui/button'
|
|
2
|
+
import { css } from '@remix-run/ui'
|
|
3
|
+
import { Glyph } from '@remix-run/ui/glyph'
|
|
4
|
+
import { theme } from '@remix-run/ui/theme'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @name Button States
|
|
8
|
+
* @description Buttons support normal, disabled, and busy (loading) states using aria attributes.
|
|
9
|
+
* @order 3
|
|
10
|
+
*/
|
|
11
|
+
export default function Example() {
|
|
12
|
+
return () => (
|
|
13
|
+
<div mix={buttonRowCss}>
|
|
14
|
+
<button mix={[button.baseStyle, button.primaryStyle]}>
|
|
15
|
+
<Glyph mix={button.iconStyle} name="add" />
|
|
16
|
+
<span mix={button.labelStyle}>New issue</span>
|
|
17
|
+
</button>
|
|
18
|
+
|
|
19
|
+
<button mix={[button.baseStyle, button.ghostStyle]}>
|
|
20
|
+
<span mix={button.labelStyle}>Open</span>
|
|
21
|
+
<Glyph mix={button.iconStyle} name="chevronRight" />
|
|
22
|
+
</button>
|
|
23
|
+
|
|
24
|
+
<button disabled mix={[button.baseStyle, button.secondaryStyle]}>
|
|
25
|
+
<span mix={button.labelStyle}>Disabled</span>
|
|
26
|
+
</button>
|
|
27
|
+
|
|
28
|
+
<button aria-busy="true" mix={[button.baseStyle, button.secondaryStyle]}>
|
|
29
|
+
<Glyph mix={[button.iconStyle, spinnerGlyphCss, spinCss]} name="spinner" />
|
|
30
|
+
<span mix={button.labelStyle}>Saving</span>
|
|
31
|
+
</button>
|
|
32
|
+
</div>
|
|
33
|
+
)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const buttonRowCss = css({
|
|
37
|
+
display: 'flex',
|
|
38
|
+
alignItems: 'center',
|
|
39
|
+
gap: theme.space.sm,
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
const spinnerGlyphCss = css({
|
|
43
|
+
opacity: 0.72,
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
const spinCss = css({
|
|
47
|
+
'@keyframes demo-button-spin': {
|
|
48
|
+
from: { transform: 'rotate(0deg)' },
|
|
49
|
+
to: { transform: 'rotate(360deg)' },
|
|
50
|
+
},
|
|
51
|
+
animation: 'demo-button-spin 1s linear infinite',
|
|
52
|
+
})
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
// @jsxRuntime classic
|
|
2
|
-
// @jsx createElement
|
|
3
1
|
import {
|
|
4
2
|
attrs,
|
|
5
3
|
css,
|
|
@@ -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,
|
|
@@ -26,8 +25,8 @@ import { wait } from '../../utils/wait.ts'
|
|
|
26
25
|
const COMBOBOX_CHANGE_EVENT = 'rmx:combobox-change' as const
|
|
27
26
|
const INPUT_COMMIT_DELAY_MS = 50
|
|
28
27
|
|
|
29
|
-
type ComboboxChangeHandler = (
|
|
30
|
-
event: ComboboxChangeEvent,
|
|
28
|
+
type ComboboxChangeHandler<target extends HTMLElement> = (
|
|
29
|
+
event: Dispatched<ComboboxChangeEvent, target>,
|
|
31
30
|
signal: AbortSignal,
|
|
32
31
|
) => void | Promise<void>
|
|
33
32
|
|
|
@@ -966,12 +965,11 @@ const combobox = {
|
|
|
966
965
|
popover: popoverMixin,
|
|
967
966
|
} as const
|
|
968
967
|
|
|
969
|
-
export function onComboboxChange
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
)
|
|
968
|
+
export function onComboboxChange<target extends HTMLElement>(
|
|
969
|
+
handler: ComboboxChangeHandler<target>,
|
|
970
|
+
captureBoolean?: boolean,
|
|
971
|
+
) {
|
|
972
|
+
return on(COMBOBOX_CHANGE_EVENT, handler, captureBoolean)
|
|
975
973
|
}
|
|
976
974
|
|
|
977
975
|
export function Combobox(handle: Handle<ComboboxProps>) {
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import { css, type Handle } from '@remix-run/ui'
|
|
2
|
+
import { Combobox, ComboboxOption, onComboboxChange } from '@remix-run/ui/combobox'
|
|
3
|
+
import { theme } from '@remix-run/ui/theme'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @name Combobox Overview
|
|
7
|
+
* @description A searchable combobox with keyboard navigation. Try typing airport names or codes like ord, lax, or jfk.
|
|
8
|
+
*/
|
|
9
|
+
export default function Example(handle: Handle) {
|
|
10
|
+
let value: string | null = null
|
|
11
|
+
|
|
12
|
+
return () => (
|
|
13
|
+
<div mix={stackCss}>
|
|
14
|
+
<div mix={fieldCss}>
|
|
15
|
+
<label for="airport-combobox" mix={labelCss}>
|
|
16
|
+
Airport
|
|
17
|
+
</label>
|
|
18
|
+
|
|
19
|
+
<Combobox
|
|
20
|
+
inputId="airport-combobox"
|
|
21
|
+
mix={[
|
|
22
|
+
comboboxCss,
|
|
23
|
+
onComboboxChange((event) => {
|
|
24
|
+
value = event.value
|
|
25
|
+
void handle.update()
|
|
26
|
+
}),
|
|
27
|
+
]}
|
|
28
|
+
name="airport"
|
|
29
|
+
placeholder="Search airports or codes"
|
|
30
|
+
>
|
|
31
|
+
{airportOptions.map((airport) => (
|
|
32
|
+
<ComboboxOption
|
|
33
|
+
key={airport.value}
|
|
34
|
+
disabled={airport.disabled}
|
|
35
|
+
label={airport.label}
|
|
36
|
+
searchValue={airport.searchValue}
|
|
37
|
+
value={airport.value}
|
|
38
|
+
/>
|
|
39
|
+
))}
|
|
40
|
+
</Combobox>
|
|
41
|
+
|
|
42
|
+
<div mix={helpCss}>
|
|
43
|
+
Try typing `san`, `wash`, or airport codes like `ord`, `lax`, or `jfk`, then use ArrowDown
|
|
44
|
+
and Enter.
|
|
45
|
+
</div>
|
|
46
|
+
</div>
|
|
47
|
+
|
|
48
|
+
<p mix={valueCss}>{`value=${value ?? 'null'}`}</p>
|
|
49
|
+
</div>
|
|
50
|
+
)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
type AirportOption = {
|
|
54
|
+
disabled?: boolean
|
|
55
|
+
label: string
|
|
56
|
+
searchValue: string[]
|
|
57
|
+
value: string
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const airportOptions: AirportOption[] = [
|
|
61
|
+
{
|
|
62
|
+
label: 'Hartsfield-Jackson Atlanta International',
|
|
63
|
+
searchValue: ['atl', 'atlanta', 'hartsfield-jackson atlanta international'],
|
|
64
|
+
value: 'ATL',
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
label: 'Austin-Bergstrom International',
|
|
68
|
+
searchValue: ['aus', 'austin', 'austin-bergstrom international'],
|
|
69
|
+
value: 'AUS',
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
label: 'Boston Logan International',
|
|
73
|
+
searchValue: ['bos', 'boston', 'boston logan international'],
|
|
74
|
+
value: 'BOS',
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
label: 'Baltimore/Washington International',
|
|
78
|
+
searchValue: ['bwi', 'baltimore', 'washington', 'baltimore/washington international'],
|
|
79
|
+
value: 'BWI',
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
label: 'Ronald Reagan Washington National',
|
|
83
|
+
searchValue: ['dca', 'reagan', 'washington', 'ronald reagan washington national'],
|
|
84
|
+
value: 'DCA',
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
label: 'Denver International',
|
|
88
|
+
searchValue: ['den', 'denver', 'denver international'],
|
|
89
|
+
value: 'DEN',
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
label: 'Dallas/Fort Worth International',
|
|
93
|
+
searchValue: ['dfw', 'dallas', 'fort worth', 'dallas/fort worth international'],
|
|
94
|
+
value: 'DFW',
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
label: 'Newark Liberty International',
|
|
98
|
+
searchValue: ['ewr', 'newark', 'newark liberty international'],
|
|
99
|
+
value: 'EWR',
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
label: 'Daniel K. Inouye International',
|
|
103
|
+
searchValue: ['hnl', 'honolulu', 'daniel k. inouye international'],
|
|
104
|
+
value: 'HNL',
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
label: 'Washington Dulles International',
|
|
108
|
+
searchValue: ['iad', 'dulles', 'washington', 'washington dulles international'],
|
|
109
|
+
value: 'IAD',
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
label: 'John F. Kennedy International',
|
|
113
|
+
searchValue: ['jfk', 'kennedy', 'new york', 'john f. kennedy international'],
|
|
114
|
+
value: 'JFK',
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
label: 'Harry Reid International',
|
|
118
|
+
searchValue: ['las', 'las vegas', 'harry reid international'],
|
|
119
|
+
value: 'LAS',
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
label: 'Los Angeles International',
|
|
123
|
+
searchValue: ['lax', 'los angeles', 'los angeles international'],
|
|
124
|
+
value: 'LAX',
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
label: 'Orlando International',
|
|
128
|
+
searchValue: ['mco', 'orlando', 'orlando international'],
|
|
129
|
+
value: 'MCO',
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
label: 'Miami International',
|
|
133
|
+
searchValue: ['mia', 'miami', 'miami international'],
|
|
134
|
+
value: 'MIA',
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
label: "Chicago O'Hare International",
|
|
138
|
+
searchValue: ['ord', 'ohare', 'chicago', "chicago o'hare international"],
|
|
139
|
+
value: 'ORD',
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
label: 'Portland International',
|
|
143
|
+
searchValue: ['pdx', 'portland', 'portland international'],
|
|
144
|
+
value: 'PDX',
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
label: 'San Diego International',
|
|
148
|
+
searchValue: ['san', 'san diego', 'san diego international'],
|
|
149
|
+
value: 'SAN',
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
label: 'San Francisco International',
|
|
153
|
+
searchValue: ['sfo', 'san francisco', 'san francisco international'],
|
|
154
|
+
value: 'SFO',
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
disabled: true,
|
|
158
|
+
label: 'San Jose Mineta International',
|
|
159
|
+
searchValue: ['sjc', 'san jose', 'san jose mineta international'],
|
|
160
|
+
value: 'SJC',
|
|
161
|
+
},
|
|
162
|
+
]
|
|
163
|
+
|
|
164
|
+
const comboboxCss = css({
|
|
165
|
+
width: '16rem',
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
const stackCss = css({
|
|
169
|
+
display: 'flex',
|
|
170
|
+
flexDirection: 'column',
|
|
171
|
+
gap: theme.space.sm,
|
|
172
|
+
width: '100%',
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
const fieldCss = css({
|
|
176
|
+
display: 'flex',
|
|
177
|
+
flexDirection: 'column',
|
|
178
|
+
gap: theme.space.xs,
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
const labelCss = css({
|
|
182
|
+
margin: 0,
|
|
183
|
+
fontSize: theme.fontSize.xs,
|
|
184
|
+
fontWeight: theme.fontWeight.semibold,
|
|
185
|
+
color: theme.colors.text.primary,
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
const helpCss = css({
|
|
189
|
+
fontSize: theme.fontSize.xs,
|
|
190
|
+
lineHeight: theme.lineHeight.relaxed,
|
|
191
|
+
color: theme.colors.text.secondary,
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
const valueCss = css({
|
|
195
|
+
margin: 0,
|
|
196
|
+
fontFamily: theme.fontFamily.mono,
|
|
197
|
+
fontSize: theme.fontSize.xs,
|
|
198
|
+
color: theme.colors.text.secondary,
|
|
199
|
+
})
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# glyph
|
|
2
|
+
|
|
3
|
+
`Glyph` renders references into a shared SVG sprite sheet. Render a glyph sheet once, then render individual `Glyph` instances by name.
|
|
4
|
+
|
|
5
|
+
## Usage
|
|
6
|
+
|
|
7
|
+
```tsx
|
|
8
|
+
import { Glyph } from 'remix/ui/glyph'
|
|
9
|
+
import { RMX_01_GLYPHS } from 'remix/ui/theme'
|
|
10
|
+
|
|
11
|
+
function Layout() {
|
|
12
|
+
return (
|
|
13
|
+
<html>
|
|
14
|
+
<body>
|
|
15
|
+
<RMX_01_GLYPHS />
|
|
16
|
+
<button aria-label="Delete">
|
|
17
|
+
<Glyph name="trash" />
|
|
18
|
+
</button>
|
|
19
|
+
</body>
|
|
20
|
+
</html>
|
|
21
|
+
)
|
|
22
|
+
}
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Most glyphs are decorative because the surrounding control supplies the accessible name. `Glyph` sets `aria-hidden` by default in that case.
|
|
26
|
+
|
|
27
|
+
```tsx
|
|
28
|
+
<button aria-label="Search">
|
|
29
|
+
<Glyph name="search" />
|
|
30
|
+
</button>
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Give the glyph its own label only when the SVG itself is the accessible element.
|
|
34
|
+
|
|
35
|
+
```tsx
|
|
36
|
+
<Glyph aria-label="Search" name="search" viewBox="0 0 20 20" width="24" />
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Use `createGlyphSheet` when a theme or app provides its own complete glyph set. The generated sheet exposes the stable symbol ids and the original values for reuse.
|
|
40
|
+
|
|
41
|
+
```tsx
|
|
42
|
+
import { createGlyphSheet, type GlyphValues } from 'remix/ui/glyph'
|
|
43
|
+
|
|
44
|
+
declare const glyphValues: GlyphValues
|
|
45
|
+
|
|
46
|
+
export const AppGlyphs = createGlyphSheet(glyphValues)
|
|
47
|
+
|
|
48
|
+
AppGlyphs.ids.trash
|
|
49
|
+
AppGlyphs.values.trash
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## `glyph.*`
|
|
53
|
+
|
|
54
|
+
- `Glyph`: renders an `<svg>` with a `<use>` element that points at the package-owned symbol id for `name`.
|
|
55
|
+
- `createGlyphSheet(values)`: creates a hidden SVG sprite sheet component from a complete glyph value set.
|
|
56
|
+
- `GlyphName`: typed union of supported glyph names.
|
|
57
|
+
- `GlyphProps`: props accepted by `Glyph`.
|
|
58
|
+
- `GlyphSheetProps`: props accepted by generated glyph sheet components.
|
|
59
|
+
- `GlyphSymbol`: SVG symbol value accepted by glyph value maps.
|
|
60
|
+
- `GlyphValues`: object shape expected by `createGlyphSheet`.
|
|
61
|
+
- `GlyphSheetComponent`: generated sprite sheet component with `ids` and `values` attached.
|
|
62
|
+
|
|
63
|
+
The built-in glyph names are `add`, `alert`, `check`, `chevronDown`, `chevronVertical`, `chevronUp`, `chevronRight`, `close`, `copy`, `edit`, `expand`, `info`, `menu`, `open`, `search`, `spinner`, and `trash`.
|
|
64
|
+
|
|
65
|
+
## Behavior Notes
|
|
66
|
+
|
|
67
|
+
- Render the glyph sheet once before rendering glyph instances that reference it.
|
|
68
|
+
- `createGlyphSheet` renders a hidden zero-size SVG and clones each provided `<symbol>` with the stable package id.
|
|
69
|
+
- `Glyph` is `aria-hidden` by default when no accessible label or labelled-by relationship is provided.
|
|
70
|
+
- Labeled glyphs keep their accessible label and do not force `aria-hidden`.
|
|
71
|
+
- Host SVG props such as `viewBox`, `width`, `mix`, and `aria-label` are preserved.
|
|
72
|
+
- `createGlyphSheet` throws if a provided glyph value is not a `<symbol>` element.
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# listbox
|
|
2
|
+
|
|
3
|
+
`listbox` is a headless option-list primitive for controlled selection and highlighting. Use it under components like `select` and `combobox`, or directly when you need custom listbox markup.
|
|
4
|
+
|
|
5
|
+
## Usage
|
|
6
|
+
|
|
7
|
+
```tsx
|
|
8
|
+
import type { Handle } from 'remix/ui'
|
|
9
|
+
import { Glyph } from 'remix/ui/glyph'
|
|
10
|
+
import * as listbox from 'remix/ui/listbox'
|
|
11
|
+
import type { ListboxValue } from 'remix/ui/listbox'
|
|
12
|
+
|
|
13
|
+
function FrameworkListbox(handle: Handle) {
|
|
14
|
+
let value: ListboxValue = 'remix'
|
|
15
|
+
let activeValue: ListboxValue = 'remix'
|
|
16
|
+
|
|
17
|
+
return () => (
|
|
18
|
+
<listbox.Context
|
|
19
|
+
value={value}
|
|
20
|
+
activeValue={activeValue}
|
|
21
|
+
onSelect={(nextValue) => {
|
|
22
|
+
value = nextValue
|
|
23
|
+
void handle.update()
|
|
24
|
+
}}
|
|
25
|
+
onHighlight={(nextValue) => {
|
|
26
|
+
activeValue = nextValue
|
|
27
|
+
void handle.update()
|
|
28
|
+
}}
|
|
29
|
+
>
|
|
30
|
+
<div aria-label="Frameworks" tabIndex={0} mix={[listbox.listStyle, listbox.list()]}>
|
|
31
|
+
{frameworks.map((option) => (
|
|
32
|
+
<div key={option.value} mix={[listbox.optionStyle, listbox.option(option)]}>
|
|
33
|
+
<Glyph mix={listbox.glyphStyle} name="check" />
|
|
34
|
+
<span mix={listbox.labelStyle}>{option.label}</span>
|
|
35
|
+
</div>
|
|
36
|
+
))}
|
|
37
|
+
</div>
|
|
38
|
+
</listbox.Context>
|
|
39
|
+
)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
let frameworks = [
|
|
43
|
+
{ label: 'Remix', value: 'remix' },
|
|
44
|
+
{ disabled: true, label: 'React Router', value: 'react-router' },
|
|
45
|
+
{ label: 'React', value: 'react' },
|
|
46
|
+
{ label: 'Preact', value: 'preact' },
|
|
47
|
+
]
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Use `textValue` when the visible label is not the best string for typeahead search.
|
|
51
|
+
|
|
52
|
+
```tsx
|
|
53
|
+
<div
|
|
54
|
+
mix={[
|
|
55
|
+
listbox.option({
|
|
56
|
+
label: 'Staging',
|
|
57
|
+
textValue: 'beta',
|
|
58
|
+
value: 'staging',
|
|
59
|
+
}),
|
|
60
|
+
]}
|
|
61
|
+
>
|
|
62
|
+
Staging
|
|
63
|
+
</div>
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Use `ref` when a parent component needs imperative coordination with the current option registry.
|
|
67
|
+
|
|
68
|
+
```tsx
|
|
69
|
+
import type { ListboxRef } from 'remix/ui/listbox'
|
|
70
|
+
|
|
71
|
+
let listboxRef: ListboxRef | undefined
|
|
72
|
+
|
|
73
|
+
function selectLastOption() {
|
|
74
|
+
listboxRef?.navigateLast()
|
|
75
|
+
void listboxRef?.selectActive()
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
;<listbox.Context
|
|
79
|
+
value={value}
|
|
80
|
+
activeValue={activeValue}
|
|
81
|
+
ref={(ref) => {
|
|
82
|
+
listboxRef = ref
|
|
83
|
+
}}
|
|
84
|
+
onSelect={(nextValue) => {
|
|
85
|
+
value = nextValue
|
|
86
|
+
}}
|
|
87
|
+
onHighlight={(nextActiveValue) => {
|
|
88
|
+
activeValue = nextActiveValue
|
|
89
|
+
}}
|
|
90
|
+
>
|
|
91
|
+
{/* listbox markup */}
|
|
92
|
+
</listbox.Context>
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## `listbox.*`
|
|
96
|
+
|
|
97
|
+
- `listbox.Context`: provider for controlled `value` and `activeValue`, option registration, selection, highlighting, optional ref access, `flashSelection`, `selectionFlashAttribute`, and `onSelectSettled`.
|
|
98
|
+
- `listbox.list()`: mixin that wires `role="listbox"`, default `tabIndex={-1}`, keyboard navigation, focus scrolling, and typeahead highlighting.
|
|
99
|
+
- `listbox.option(options)`: mixin that registers an option with required `label` and `value`, optional `disabled` and `textValue`, and wires `role="option"`, id, selected, disabled, highlighted, mouse, and click behavior.
|
|
100
|
+
- `listStyle`, `optionStyle`, `glyphStyle`, and `labelStyle`: flat style mixins for standard listbox presentation.
|
|
101
|
+
- `ListboxValue`: selected or active value, represented as `string | null`.
|
|
102
|
+
- `ListboxOption`: option input shape with `label`, `value`, optional `disabled`, and optional `textValue`.
|
|
103
|
+
- `ListboxRegisteredOption`: registered option metadata passed to callbacks and refs.
|
|
104
|
+
- `ListboxRef`: live ref object exposing active/selected options, option navigation, search matching, scrolling, and selection helpers.
|
|
105
|
+
|
|
106
|
+
## Behavior Notes
|
|
107
|
+
|
|
108
|
+
- Selection and highlighting are controlled. `onSelect` and `onHighlight` notify the parent, but DOM state updates after the parent rerenders with new values.
|
|
109
|
+
- Disabled options are skipped by keyboard navigation, typeahead, mouse movement, and click selection.
|
|
110
|
+
- Arrow keys wrap through enabled options. `Home` and `End` move to enabled boundaries. `Enter` and Space select the active option.
|
|
111
|
+
- Mouse movement highlights enabled options. `mouseleave` clears the highlight when leaving the active option.
|
|
112
|
+
- `Tab` is prevented and highlights the first enabled option.
|
|
113
|
+
- Typeahead highlights the next matching enabled option without selecting it and supports `textValue`.
|
|
114
|
+
- Focus and keyboard navigation scroll the active option into view with nearest-edge alignment.
|
|
115
|
+
- `flashSelection` applies `selectionFlashAttribute` for 60ms, delays `onSelectSettled`, and ignores new highlight/select interactions until the flash completes.
|