remix 3.0.0-beta.0 → 3.0.0-beta.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/dist/fetch-router.d.ts +7 -0
- package/dist/fetch-router.d.ts.map +1 -1
- package/dist/node-tsx/load-module.d.ts +2 -0
- package/dist/node-tsx/load-module.d.ts.map +1 -0
- package/dist/node-tsx/load-module.js +2 -0
- package/dist/node-tsx.d.ts +3 -0
- package/dist/node-tsx.d.ts.map +1 -0
- package/{src/node-serve.ts → dist/node-tsx.js} +2 -1
- package/dist/render-middleware.d.ts +2 -0
- package/dist/render-middleware.d.ts.map +1 -0
- package/dist/render-middleware.js +2 -0
- package/dist/route-pattern/href.d.ts +2 -0
- package/dist/route-pattern/href.d.ts.map +1 -0
- package/dist/route-pattern/href.js +2 -0
- package/dist/route-pattern/join.d.ts +2 -0
- package/dist/route-pattern/join.d.ts.map +1 -0
- package/dist/route-pattern/join.js +2 -0
- package/dist/route-pattern/match.d.ts +2 -0
- package/dist/route-pattern/match.d.ts.map +1 -0
- package/dist/route-pattern/match.js +2 -0
- package/package.json +158 -44
- package/src/assert/README.md +109 -0
- package/src/assets/README.md +539 -0
- package/src/async-context-middleware/README.md +100 -0
- package/src/auth/README.md +445 -0
- package/src/auth-middleware/README.md +246 -0
- package/src/cli/README.md +78 -0
- package/src/compression-middleware/README.md +176 -0
- package/src/cookie/README.md +106 -0
- package/src/cop-middleware/README.md +117 -0
- package/src/cors-middleware/README.md +174 -0
- package/src/csrf-middleware/README.md +99 -0
- package/src/data-schema/README.md +422 -0
- package/src/data-table/README.md +552 -0
- package/src/data-table-mysql/README.md +97 -0
- package/src/data-table-postgres/README.md +74 -0
- package/src/data-table-sqlite/README.md +84 -0
- package/src/fetch-proxy/README.md +46 -0
- package/src/fetch-router/README.md +902 -0
- package/src/fetch-router.ts +7 -0
- package/src/file-storage/README.md +57 -0
- package/src/file-storage-s3/README.md +47 -0
- package/src/form-data-middleware/README.md +109 -0
- package/src/form-data-parser/README.md +160 -0
- package/src/fs/README.md +60 -0
- package/src/headers/README.md +629 -0
- package/src/html-template/README.md +101 -0
- package/src/lazy-file/README.md +109 -0
- package/src/logger-middleware/README.md +132 -0
- package/src/method-override-middleware/README.md +71 -0
- package/src/mime/README.md +110 -0
- package/src/multipart-parser/README.md +241 -0
- package/src/node-fetch-server/README.md +352 -0
- package/src/node-tsx/README.md +79 -0
- package/src/node-tsx/load-module.ts +2 -0
- package/{dist/node-serve.js → src/node-tsx.ts} +2 -1
- package/src/render-middleware/README.md +99 -0
- package/src/render-middleware.ts +2 -0
- package/src/route-pattern/README.md +291 -0
- package/src/route-pattern/href.ts +2 -0
- package/src/route-pattern/join.ts +2 -0
- package/src/route-pattern/match.ts +2 -0
- package/src/session/README.md +171 -0
- package/src/session-middleware/README.md +109 -0
- package/src/session-storage-memcache/README.md +37 -0
- package/src/session-storage-redis/README.md +37 -0
- package/src/static-middleware/README.md +89 -0
- package/src/tar-parser/README.md +74 -0
- package/src/terminal/README.md +92 -0
- package/src/test/README.md +430 -0
- package/src/ui/README.md +219 -0
- package/src/ui/accordion/README.md +166 -0
- package/src/ui/anchor/README.md +153 -0
- package/src/ui/animation/README.md +316 -0
- package/src/ui/breadcrumbs/README.md +55 -0
- package/src/ui/button/README.md +44 -0
- package/src/ui/combobox/README.md +145 -0
- package/src/ui/glyph/README.md +72 -0
- package/src/ui/listbox/README.md +115 -0
- package/src/ui/menu/README.md +96 -0
- package/src/ui/popover/README.md +122 -0
- package/src/ui/scroll-lock/README.md +33 -0
- package/src/ui/select/README.md +107 -0
- package/src/ui/server/README.md +90 -0
- package/src/ui/test/README.md +107 -0
- package/src/ui/theme/README.md +103 -0
- package/dist/node-serve.d.ts +0 -2
- package/dist/node-serve.d.ts.map +0 -1
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# button
|
|
2
|
+
|
|
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
|
+
|
|
5
|
+
## Usage
|
|
6
|
+
|
|
7
|
+
```tsx
|
|
8
|
+
import { Button } from 'remix/ui/button'
|
|
9
|
+
import * as button from 'remix/ui/button'
|
|
10
|
+
import { Glyph } from 'remix/ui/glyph'
|
|
11
|
+
|
|
12
|
+
function Actions() {
|
|
13
|
+
return (
|
|
14
|
+
<div>
|
|
15
|
+
<Button startIcon={<Glyph name="add" />} tone="primary">
|
|
16
|
+
Create project
|
|
17
|
+
</Button>
|
|
18
|
+
|
|
19
|
+
<a href="/projects" mix={[button.baseStyle, button.secondaryStyle]}>
|
|
20
|
+
<span mix={button.labelStyle}>View projects</span>
|
|
21
|
+
<Glyph mix={button.iconStyle} name="chevronRight" />
|
|
22
|
+
</a>
|
|
23
|
+
</div>
|
|
24
|
+
)
|
|
25
|
+
}
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## `button.*`
|
|
29
|
+
|
|
30
|
+
- `Button`: thin button wrapper for the common `base + tone + label/icon slots` case. Pass `tone`, `startIcon`, and `endIcon` when the default authored structure is enough.
|
|
31
|
+
- `button.baseStyle`: base host styling plus the default `type="button"` behavior for button elements.
|
|
32
|
+
- `button.primaryStyle`, `button.secondaryStyle`, `button.ghostStyle`, and `button.dangerStyle`: visual button treatments.
|
|
33
|
+
- `button.labelStyle`: inline label slot with the standard button spacing.
|
|
34
|
+
- `button.iconStyle`: icon slot sizing and `aria-hidden` defaults for decorative icons.
|
|
35
|
+
|
|
36
|
+
## Behavior Notes
|
|
37
|
+
|
|
38
|
+
- `button.baseStyle` only adds `type="button"` when the host element is a `<button>` and no explicit `type` was provided.
|
|
39
|
+
- `Button` renders `children` inside `button.labelStyle` and renders `startIcon` and `endIcon` inside `button.iconStyle`.
|
|
40
|
+
- Use an explicit accessible name when you render an icon-only button.
|
|
41
|
+
|
|
42
|
+
## When To Use Something Else
|
|
43
|
+
|
|
44
|
+
Use `button.*Style` exports directly when a control needs button structure plus extra behavior or layout, like `select`, `menu`, or `tabs`. Those controls own their own interaction mixins and should not hide that behavior behind `Button`.
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
# Combobox
|
|
2
|
+
|
|
3
|
+
`Combobox` is the input-first popup value picker for `remix/ui`.
|
|
4
|
+
|
|
5
|
+
Use it when the user should type draft text, filter a popup list, and still commit one stable form value. If you just need a button-triggered picker, use `Select` instead.
|
|
6
|
+
|
|
7
|
+
## Usage
|
|
8
|
+
|
|
9
|
+
```tsx
|
|
10
|
+
import { css, type Handle } from 'remix/ui'
|
|
11
|
+
import { Combobox, ComboboxOption, onComboboxChange } from 'remix/ui/combobox'
|
|
12
|
+
|
|
13
|
+
let airports = [
|
|
14
|
+
{
|
|
15
|
+
label: 'Los Angeles International',
|
|
16
|
+
searchValue: ['lax', 'los angeles', 'los angeles international'],
|
|
17
|
+
value: 'LAX',
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
label: 'John F. Kennedy International',
|
|
21
|
+
searchValue: ['jfk', 'new york', 'john f. kennedy international'],
|
|
22
|
+
value: 'JFK',
|
|
23
|
+
},
|
|
24
|
+
] as const
|
|
25
|
+
|
|
26
|
+
export default function AirportField(handle: Handle) {
|
|
27
|
+
let value: string | null = null
|
|
28
|
+
|
|
29
|
+
return () => (
|
|
30
|
+
<div mix={root}>
|
|
31
|
+
<Combobox
|
|
32
|
+
inputId="airport"
|
|
33
|
+
mix={onComboboxChange((event) => {
|
|
34
|
+
value = event.value
|
|
35
|
+
void handle.update()
|
|
36
|
+
})}
|
|
37
|
+
name="airport"
|
|
38
|
+
placeholder="Search airports or codes"
|
|
39
|
+
>
|
|
40
|
+
{airports.map((airport) => (
|
|
41
|
+
<ComboboxOption
|
|
42
|
+
key={airport.value}
|
|
43
|
+
label={airport.label}
|
|
44
|
+
searchValue={airport.searchValue}
|
|
45
|
+
value={airport.value}
|
|
46
|
+
/>
|
|
47
|
+
))}
|
|
48
|
+
</Combobox>
|
|
49
|
+
|
|
50
|
+
<p>{`value=${value ?? 'null'}`}</p>
|
|
51
|
+
</div>
|
|
52
|
+
)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
let root = css({
|
|
56
|
+
display: 'grid',
|
|
57
|
+
gap: '8px',
|
|
58
|
+
})
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Public API
|
|
62
|
+
|
|
63
|
+
### `Combobox`
|
|
64
|
+
|
|
65
|
+
The convenience component.
|
|
66
|
+
|
|
67
|
+
- Renders the text input, popover surface, listbox root, and hidden form input.
|
|
68
|
+
- Dispatches a bubbled custom event that `onComboboxChange(...)` listens for when the committed value changes.
|
|
69
|
+
- Accepts `defaultValue`, `disabled`, `inputId`, `name`, and `placeholder`.
|
|
70
|
+
|
|
71
|
+
### `ComboboxOption`
|
|
72
|
+
|
|
73
|
+
The default option row for `Combobox`.
|
|
74
|
+
|
|
75
|
+
- Uses the shared listbox option visuals.
|
|
76
|
+
- Accepts `label`, `value`, optional `searchValue`, and optional `disabled`.
|
|
77
|
+
- `searchValue` can be a string or string array for aliases like airport codes, abbreviations, or alternate labels.
|
|
78
|
+
|
|
79
|
+
### `onComboboxChange(...)`
|
|
80
|
+
|
|
81
|
+
The listener mixin for bubbled committed-value changes.
|
|
82
|
+
|
|
83
|
+
The event object includes:
|
|
84
|
+
|
|
85
|
+
- `event.value`: the committed value or `null`
|
|
86
|
+
- `event.label`: the committed option label or `null`
|
|
87
|
+
- `event.optionId`: the generated option id or `null`
|
|
88
|
+
|
|
89
|
+
### `combobox.Context`
|
|
90
|
+
|
|
91
|
+
The lower-level coordinator for custom combobox composition.
|
|
92
|
+
|
|
93
|
+
It wraps the shared `popover` and `listbox` contexts and owns the draft text, committed value, popup state, and selection timing.
|
|
94
|
+
|
|
95
|
+
### `combobox.input()`
|
|
96
|
+
|
|
97
|
+
Turns the host input into the combobox input.
|
|
98
|
+
|
|
99
|
+
- Keeps focus on the input during list navigation and pointer selection.
|
|
100
|
+
- Wires `role="combobox"`, `aria-expanded`, `aria-controls`, and `aria-activedescendant`.
|
|
101
|
+
- Opens from typing, click, and arrow-key navigation.
|
|
102
|
+
|
|
103
|
+
### `combobox.popover()`
|
|
104
|
+
|
|
105
|
+
Turns the host into the combobox popover surface.
|
|
106
|
+
|
|
107
|
+
- Uses the shared popover primitive.
|
|
108
|
+
- Keeps anchor clicks inside the session so the input stays interactive while open.
|
|
109
|
+
- Applies the combobox open/close reason contract used by `combobox.popoverStyle`.
|
|
110
|
+
|
|
111
|
+
### `combobox.list()`
|
|
112
|
+
|
|
113
|
+
Turns the host into the popup listbox root and applies the generated list id.
|
|
114
|
+
|
|
115
|
+
### `combobox.option(options)`
|
|
116
|
+
|
|
117
|
+
Registers one option with the combobox and listbox layers.
|
|
118
|
+
|
|
119
|
+
- Accepts `label`, `value`, optional `searchValue`, and optional `disabled`.
|
|
120
|
+
- Hides non-matching options from the current draft filter.
|
|
121
|
+
- Prevents pointer selection from blurring the input before the click commits.
|
|
122
|
+
|
|
123
|
+
### `combobox.hiddenInput()`
|
|
124
|
+
|
|
125
|
+
Mirrors the committed value into a hidden input for forms.
|
|
126
|
+
|
|
127
|
+
Apply it to an `<input type="hidden" />` inside the same `combobox.Context`.
|
|
128
|
+
|
|
129
|
+
## Behavior Notes
|
|
130
|
+
|
|
131
|
+
- Typing opens the popup in hint mode when there are matches.
|
|
132
|
+
- If typing leaves no matches, the popup closes immediately without the navigation fade-out.
|
|
133
|
+
- Selecting from the list flashes the option, then closes the popup and finally commits the visible input label.
|
|
134
|
+
- Typing clears the committed value immediately; the hidden form value becomes empty until the user commits again.
|
|
135
|
+
- Blur commits an exact `label` or `searchValue` match. A non-matching blur clears the draft text and committed value.
|
|
136
|
+
- `Escape` keeps exact-match draft text but clears non-matching draft text and selection.
|
|
137
|
+
- Disabled options can stay visible in filtered results, but they are skipped by keyboard navigation and selection.
|
|
138
|
+
|
|
139
|
+
## When To Use Something Else
|
|
140
|
+
|
|
141
|
+
Use `Select` when you want the ordinary button-triggered single-select control.
|
|
142
|
+
|
|
143
|
+
Use `listbox` when you need listbox semantics without an editable text input.
|
|
144
|
+
|
|
145
|
+
Use `popover` directly for custom floating panels that are not value-picking controls.
|
|
@@ -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.
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# menu
|
|
2
|
+
|
|
3
|
+
`Menu` renders a button-triggered menu with keyboard navigation, checked items, selection events, and nested submenus. Use it for action menus and command groups.
|
|
4
|
+
|
|
5
|
+
## Usage
|
|
6
|
+
|
|
7
|
+
```tsx
|
|
8
|
+
import type { Handle } from 'remix/ui'
|
|
9
|
+
import { Menu, MenuItem, Submenu, onMenuSelect } from 'remix/ui/menu'
|
|
10
|
+
import { separatorStyle } from 'remix/ui/separator'
|
|
11
|
+
|
|
12
|
+
export function ViewMenu(handle: Handle) {
|
|
13
|
+
let wordWrap = false
|
|
14
|
+
let density = 'comfortable'
|
|
15
|
+
|
|
16
|
+
return () => (
|
|
17
|
+
<Menu
|
|
18
|
+
label="View"
|
|
19
|
+
mix={onMenuSelect((event) => {
|
|
20
|
+
if (event.item.name === 'wordWrap') {
|
|
21
|
+
wordWrap = event.item.checked ?? false
|
|
22
|
+
} else if (event.item.name === 'density' && event.item.value) {
|
|
23
|
+
density = event.item.value
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
void handle.update()
|
|
27
|
+
})}
|
|
28
|
+
>
|
|
29
|
+
<MenuItem checked={wordWrap} name="wordWrap" type="checkbox">
|
|
30
|
+
Word wrap
|
|
31
|
+
</MenuItem>
|
|
32
|
+
<MenuItem disabled name="minimap">
|
|
33
|
+
Minimap
|
|
34
|
+
</MenuItem>
|
|
35
|
+
<hr mix={separatorStyle} />
|
|
36
|
+
<MenuItem checked={density === 'compact'} name="density" type="radio" value="compact">
|
|
37
|
+
Compact
|
|
38
|
+
</MenuItem>
|
|
39
|
+
<MenuItem checked={density === 'comfortable'} name="density" type="radio" value="comfortable">
|
|
40
|
+
Comfortable
|
|
41
|
+
</MenuItem>
|
|
42
|
+
<Submenu label="Zoom">
|
|
43
|
+
<MenuItem name="zoomIn" value="zoom-in">
|
|
44
|
+
Zoom in
|
|
45
|
+
</MenuItem>
|
|
46
|
+
<MenuItem name="zoomOut" value="zoom-out">
|
|
47
|
+
Zoom out
|
|
48
|
+
</MenuItem>
|
|
49
|
+
</Submenu>
|
|
50
|
+
</Menu>
|
|
51
|
+
)
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Use `label` or `searchValue` when the rendered item content is not the text that should be used for event labels or typeahead.
|
|
56
|
+
|
|
57
|
+
```tsx
|
|
58
|
+
<MenuItem label="Open command palette" name="commandPalette" searchValue="palette">
|
|
59
|
+
Command palette
|
|
60
|
+
</MenuItem>
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Use `menuLabel` when the menu surface needs a different accessible label from the visible trigger.
|
|
64
|
+
|
|
65
|
+
```tsx
|
|
66
|
+
<Menu label="..." menuLabel="Project actions">
|
|
67
|
+
<MenuItem name="rename">Rename project</MenuItem>
|
|
68
|
+
</Menu>
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## `menu.*`
|
|
72
|
+
|
|
73
|
+
- `Menu`: composed trigger, popover, and list component for the common menu case.
|
|
74
|
+
- `MenuItem`: menu item wrapper. Supports regular, checkbox, and radio item roles through `type`, `checked`, `name`, `value`, `label`, `disabled`, and `searchValue`.
|
|
75
|
+
- `Submenu`: nested menu wrapper with its own trigger and child menu surface.
|
|
76
|
+
- `MenuList`: lower-level list wrapper for custom composition.
|
|
77
|
+
- `onMenuSelect(...)`: event mixin for the bubbling `MenuSelectEvent`.
|
|
78
|
+
- `MenuSelectEvent`: bubbling event class whose `item` describes the selected item.
|
|
79
|
+
- `MenuSelectItem`: selected item shape with `checked`, `id`, `label`, `name`, `type`, and `value`.
|
|
80
|
+
- `menu.Context`, `menu.trigger()`, `menu.popover()`, `menu.list()`, `menu.item(...)`, and `menu.submenuTrigger(...)`: lower-level composition primitives.
|
|
81
|
+
- `buttonStyle`, `popoverStyle`, `listStyle`, `itemStyle`, `itemSlotStyle`, `itemLabelStyle`, `itemGlyphStyle`, and `triggerGlyphStyle`: flat style mixins used by the wrappers.
|
|
82
|
+
- `MenuProps`, `MenuItemProps`, `MenuListProps`, `MenuProviderProps`, `MenuTriggerOptions`, `MenuItemOptions`, and `SubmenuProps`: public TypeScript props and option types.
|
|
83
|
+
|
|
84
|
+
## Behavior Notes
|
|
85
|
+
|
|
86
|
+
- Click opens the root menu and focuses the list; clicking the trigger again closes it and restores focus.
|
|
87
|
+
- `ArrowDown` opens from the trigger at the first enabled item. `ArrowUp` opens at the last enabled item. Enter and Space open the menu with focus on the list.
|
|
88
|
+
- Keyboard navigation skips disabled items and does not wrap past the first or last enabled item.
|
|
89
|
+
- `Home` and `End` move to the first and last enabled item. Enter and Space activate the highlighted item.
|
|
90
|
+
- Printable keys use typeahead. Typeahead matches `searchValue` when provided and otherwise uses the item label.
|
|
91
|
+
- Submenus open with `ArrowRight`, close with `ArrowLeft`, and are anchored to the submenu trigger with `right-start` placement.
|
|
92
|
+
- Pointer movement highlights enabled items. Submenus open after a short focus/pointer delay and use hover aim so they stay open while moving toward the child surface.
|
|
93
|
+
- Selecting a regular item flashes that item. Selecting a checkbox or radio item flashes the committed checked state.
|
|
94
|
+
- Selection dispatches one bubbled `MenuSelectEvent`, closes the full menu tree, and restores focus to the root trigger.
|
|
95
|
+
- The composed `Menu` re-dispatches selection from its trigger so handlers on the `Menu` button and shared ancestors see the event once.
|
|
96
|
+
- `menuLabel`, `Submenu.menuLabel`, and `Submenu.listProps` let composed menus label and customize menu surfaces separately from trigger content.
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# Popover
|
|
2
|
+
|
|
3
|
+
`popover` is a low-level primitive for anchored, dismissible floating panels.
|
|
4
|
+
|
|
5
|
+
Use it for custom surfaces like filters, inspectors, and view options. Higher-level widgets like `menu`, `select`, and `combobox` should build on top of it instead of exposing raw `popover.*` mixins directly.
|
|
6
|
+
|
|
7
|
+
## Usage
|
|
8
|
+
|
|
9
|
+
```tsx
|
|
10
|
+
import { css, on, type Handle } from 'remix/ui'
|
|
11
|
+
import { Button } from 'remix/ui/button'
|
|
12
|
+
import { Glyph } from 'remix/ui/glyph'
|
|
13
|
+
import { popover } from 'remix/ui/popover'
|
|
14
|
+
|
|
15
|
+
export function ViewOptions(handle: Handle) {
|
|
16
|
+
let open = false
|
|
17
|
+
|
|
18
|
+
function openPopover() {
|
|
19
|
+
open = true
|
|
20
|
+
void handle.update()
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function closePopover() {
|
|
24
|
+
open = false
|
|
25
|
+
void handle.update()
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return () => (
|
|
29
|
+
<popover.Context>
|
|
30
|
+
<Button
|
|
31
|
+
endIcon={<Glyph name="chevronDown" />}
|
|
32
|
+
mix={[
|
|
33
|
+
popover.anchor({ placement: 'bottom-end' }),
|
|
34
|
+
popover.focusOnHide(),
|
|
35
|
+
on('click', openPopover),
|
|
36
|
+
]}
|
|
37
|
+
tone="secondary"
|
|
38
|
+
>
|
|
39
|
+
View options
|
|
40
|
+
</Button>
|
|
41
|
+
|
|
42
|
+
<div
|
|
43
|
+
mix={[
|
|
44
|
+
popover.surfaceStyle,
|
|
45
|
+
popover.surface({
|
|
46
|
+
open,
|
|
47
|
+
onHide() {
|
|
48
|
+
closePopover()
|
|
49
|
+
},
|
|
50
|
+
}),
|
|
51
|
+
]}
|
|
52
|
+
>
|
|
53
|
+
<div mix={popover.contentStyle}>
|
|
54
|
+
<Button mix={[popover.focusOnShow(), on('click', closePopover)]} tone="ghost">
|
|
55
|
+
Close
|
|
56
|
+
</Button>
|
|
57
|
+
<div mix={panelBody}>Panel content</div>
|
|
58
|
+
</div>
|
|
59
|
+
</div>
|
|
60
|
+
</popover.Context>
|
|
61
|
+
)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
let panelBody = css({
|
|
65
|
+
padding: '12px',
|
|
66
|
+
})
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## `popover.*`
|
|
70
|
+
|
|
71
|
+
### `popover.Context`
|
|
72
|
+
|
|
73
|
+
Provides shared coordination for one popover instance. Render the anchor, any focus targets, and the surface inside the same context.
|
|
74
|
+
|
|
75
|
+
### `popover.anchor(options)`
|
|
76
|
+
|
|
77
|
+
Registers the host element as the anchor for the current popover surface.
|
|
78
|
+
|
|
79
|
+
- Accepts standard `AnchorOptions`.
|
|
80
|
+
- The stored anchor controls where the surface is positioned when it opens.
|
|
81
|
+
- Apply it to the button or other element the surface should attach to.
|
|
82
|
+
|
|
83
|
+
### `popover.surface({ open, onHide, ... })`
|
|
84
|
+
|
|
85
|
+
Turns the host into the controlled popover surface.
|
|
86
|
+
|
|
87
|
+
- Wires `popover="manual"` and native `showPopover()` / `hidePopover()` behavior.
|
|
88
|
+
- Calls `onHide` for `Escape` and outside clicks with a `PopoverHideRequest`.
|
|
89
|
+
- Restores focus to the registered hide target unless `restoreFocusOnHide: false`.
|
|
90
|
+
- Accepts `closeOnAnchorClick: false` when the anchor must stay interactive while open.
|
|
91
|
+
|
|
92
|
+
Apply this to the actual floating root, not a nested child.
|
|
93
|
+
|
|
94
|
+
### `popover.focusOnShow()`
|
|
95
|
+
|
|
96
|
+
Registers the element that should receive focus when the popover opens.
|
|
97
|
+
|
|
98
|
+
### `popover.focusOnHide()`
|
|
99
|
+
|
|
100
|
+
Registers the element that should receive focus again when the popover closes.
|
|
101
|
+
|
|
102
|
+
### `popover.surfaceStyle`
|
|
103
|
+
|
|
104
|
+
Default floating-surface presentation for popovers.
|
|
105
|
+
|
|
106
|
+
### `popover.contentStyle`
|
|
107
|
+
|
|
108
|
+
Default inner scroll container for popover content.
|
|
109
|
+
|
|
110
|
+
## Behavior Notes
|
|
111
|
+
|
|
112
|
+
- Opening anchors the surface to the registered anchor and locks page scrolling until close.
|
|
113
|
+
- `onHide` receives `{ reason: 'escape-key' | 'outside-click', target? }`.
|
|
114
|
+
- `popover.focusOnShow()` wins on open when present.
|
|
115
|
+
- `popover.focusOnHide()` is used on close by default when focus restoration is enabled.
|
|
116
|
+
- `closeOnAnchorClick: false` keeps anchor clicks inside the current session, which is useful for input-driven popovers like comboboxes.
|
|
117
|
+
|
|
118
|
+
## When To Use Something Else
|
|
119
|
+
|
|
120
|
+
- Use `menu` for command surfaces.
|
|
121
|
+
- Use `listbox` or `select` for committed value picking.
|
|
122
|
+
- Use `combobox` for input-first popup selection.
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# scroll-lock
|
|
2
|
+
|
|
3
|
+
`scroll-lock` locks document scrolling while a floating or modal surface is open. Use `lockScroll` directly for imperative flows or `lockScrollOnToggle` for popover-style elements that emit `beforetoggle`.
|
|
4
|
+
|
|
5
|
+
## Usage
|
|
6
|
+
|
|
7
|
+
```tsx
|
|
8
|
+
import { lockScroll, lockScrollOnToggle } from 'remix/ui/scroll-lock'
|
|
9
|
+
|
|
10
|
+
function DialogSurface() {
|
|
11
|
+
return <div popover="auto" mix={lockScrollOnToggle()} />
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function openModal() {
|
|
15
|
+
let unlock = lockScroll()
|
|
16
|
+
|
|
17
|
+
// Later, when the modal closes:
|
|
18
|
+
unlock()
|
|
19
|
+
}
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## API
|
|
23
|
+
|
|
24
|
+
- `lockScroll(document?)`: locks the target document and returns an idempotent unlock function.
|
|
25
|
+
- `lockScrollOnToggle()`: mixin that locks on `beforetoggle` open, unlocks on close, and releases the lock when the host unmounts.
|
|
26
|
+
|
|
27
|
+
## Behavior Notes
|
|
28
|
+
|
|
29
|
+
- The lock stores and restores the document element's inline `overflow` and `scrollbarGutter`.
|
|
30
|
+
- Scroll position is restored when the last active lock is released.
|
|
31
|
+
- Multiple locks are reference-counted, so the document stays locked until every unlock function has run.
|
|
32
|
+
- When a scrollbar is present and computed `scrollbar-gutter` is `auto`, the document reserves a stable gutter while locked.
|
|
33
|
+
- `lockScrollOnToggle` uses the host element's owner document.
|