@lumiose/ui 0.0.0-stage → 0.1.1
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/LICENSE +21 -0
- package/README.md +53 -2
- package/dist/accordion/accordion.css +20 -0
- package/dist/accordion/accordion.script.ts +66 -0
- package/dist/accordion/accordion.tsx +288 -0
- package/dist/animation/animation.ts +34 -0
- package/dist/autocomplete/autocomplete.tsx +239 -0
- package/dist/avatar/avatar.css +54 -0
- package/dist/avatar/avatar.script.ts +96 -0
- package/dist/avatar/avatar.tsx +110 -0
- package/dist/button/button.script.ts +35 -0
- package/dist/button/button.tsx +40 -0
- package/dist/checkbox/checkbox.script.ts +225 -0
- package/dist/checkbox/checkbox.tsx +156 -0
- package/dist/checkbox/context.ts +17 -0
- package/dist/checkbox-group/checkbox-group.tsx +111 -0
- package/dist/collapsible/collapsible.css +12 -0
- package/dist/collapsible/collapsible.script.ts +7 -0
- package/dist/collapsible/collapsible.tsx +153 -0
- package/dist/collapsible/panel.ts +115 -0
- package/dist/combobox/combobox.css +37 -0
- package/dist/combobox/combobox.script.ts +974 -0
- package/dist/combobox/combobox.tsx +556 -0
- package/dist/combobox/context.ts +46 -0
- package/dist/context-menu/context-menu.css +16 -0
- package/dist/context-menu/context-menu.script.ts +172 -0
- package/dist/context-menu/context-menu.tsx +178 -0
- package/dist/dialog/dialog.css +35 -0
- package/dist/dialog/dialog.script.ts +269 -0
- package/dist/dialog/dialog.tsx +214 -0
- package/dist/drawer/drawer.css +38 -0
- package/dist/drawer/drawer.script.ts +207 -0
- package/dist/drawer/drawer.tsx +113 -0
- package/dist/field/context.ts +156 -0
- package/dist/field/field.css +16 -0
- package/dist/field/field.script.ts +422 -0
- package/dist/field/field.tsx +465 -0
- package/dist/fieldset/fieldset.tsx +41 -0
- package/dist/form/form.tsx +50 -0
- package/dist/form/native.ts +21 -0
- package/dist/form/reset.ts +53 -0
- package/dist/index.ts +261 -0
- package/dist/input/input.tsx +67 -0
- package/dist/layer/layer.ts +191 -0
- package/dist/locale/digits.ts +21 -0
- package/dist/menu/menu.css +22 -0
- package/dist/menu/menu.script.ts +591 -0
- package/dist/menu/menu.tsx +520 -0
- package/dist/menubar/menubar.css +16 -0
- package/dist/menubar/menubar.script.ts +195 -0
- package/dist/menubar/menubar.tsx +67 -0
- package/dist/meter/meter.tsx +134 -0
- package/dist/navigation-menu/navigation-menu.css +14 -0
- package/dist/navigation-menu/navigation-menu.script.ts +466 -0
- package/dist/navigation-menu/navigation-menu.tsx +266 -0
- package/dist/number-field/format.ts +70 -0
- package/dist/number-field/number-field.css +38 -0
- package/dist/number-field/number-field.script.ts +490 -0
- package/dist/number-field/number-field.tsx +403 -0
- package/dist/otp-field/otp-field.css +45 -0
- package/dist/otp-field/otp-field.script.ts +330 -0
- package/dist/otp-field/otp-field.tsx +320 -0
- package/dist/placement/place.ts +148 -0
- package/dist/placement/placement.css +155 -0
- package/dist/placement/placement.ts +60 -0
- package/dist/popover/hover.ts +178 -0
- package/dist/popover/popover.css +20 -0
- package/dist/popover/popover.script.ts +256 -0
- package/dist/popover/popover.tsx +317 -0
- package/dist/preview-card/preview-card.css +7 -0
- package/dist/preview-card/preview-card.script.ts +136 -0
- package/dist/preview-card/preview-card.tsx +128 -0
- package/dist/progress/progress.tsx +155 -0
- package/dist/progress/value.ts +23 -0
- package/dist/radio-group/radio-group.tsx +156 -0
- package/dist/scroll-area/scroll-area.css +72 -0
- package/dist/scroll-area/scroll-area.script.ts +281 -0
- package/dist/scroll-area/scroll-area.tsx +118 -0
- package/dist/select/select.css +25 -0
- package/dist/select/select.tsx +171 -0
- package/dist/separator/separator.css +12 -0
- package/dist/separator/separator.tsx +38 -0
- package/dist/slider/format.ts +33 -0
- package/dist/slider/slider.css +93 -0
- package/dist/slider/slider.script.ts +355 -0
- package/dist/slider/slider.tsx +374 -0
- package/dist/styles.css +30 -0
- package/dist/switch/switch.tsx +92 -0
- package/dist/tabs/tabs.css +54 -0
- package/dist/tabs/tabs.script.ts +225 -0
- package/dist/tabs/tabs.tsx +272 -0
- package/dist/themes/default.css +2111 -0
- package/dist/toast/command.ts +3 -0
- package/dist/toast/toast.css +68 -0
- package/dist/toast/toast.script.ts +516 -0
- package/dist/toast/toast.tsx +432 -0
- package/dist/toggle/context.ts +16 -0
- package/dist/toggle/toggle.tsx +99 -0
- package/dist/toggle-group/toggle-group.script.ts +166 -0
- package/dist/toggle-group/toggle-group.tsx +117 -0
- package/dist/toolbar/context.ts +18 -0
- package/dist/toolbar/toolbar.css +12 -0
- package/dist/toolbar/toolbar.script.ts +123 -0
- package/dist/toolbar/toolbar.tsx +180 -0
- package/dist/tooltip/tooltip.css +8 -0
- package/dist/tooltip/tooltip.script.ts +222 -0
- package/dist/tooltip/tooltip.tsx +149 -0
- package/docs/README.md +174 -0
- package/docs/accordion.md +208 -0
- package/docs/autocomplete.md +161 -0
- package/docs/avatar.md +198 -0
- package/docs/button.md +98 -0
- package/docs/checkbox-group.md +112 -0
- package/docs/checkbox.md +133 -0
- package/docs/collapsible.md +165 -0
- package/docs/combobox.md +369 -0
- package/docs/context-menu.md +197 -0
- package/docs/dialog.md +256 -0
- package/docs/drawer.md +204 -0
- package/docs/field.md +295 -0
- package/docs/fieldset.md +92 -0
- package/docs/form.md +131 -0
- package/docs/input.md +87 -0
- package/docs/installation.md +65 -0
- package/docs/llms-full.txt +6923 -0
- package/docs/llms.txt +53 -0
- package/docs/menu.md +365 -0
- package/docs/menubar.md +186 -0
- package/docs/meter.md +103 -0
- package/docs/navigation-menu.md +257 -0
- package/docs/number-field.md +181 -0
- package/docs/otp-field.md +160 -0
- package/docs/popover.md +271 -0
- package/docs/preview-card.md +130 -0
- package/docs/progress.md +115 -0
- package/docs/radio-group.md +127 -0
- package/docs/scroll-area.md +147 -0
- package/docs/select.md +236 -0
- package/docs/separator.md +116 -0
- package/docs/slider.md +184 -0
- package/docs/switch.md +98 -0
- package/docs/tabs.md +254 -0
- package/docs/theming.md +132 -0
- package/docs/toast.md +311 -0
- package/docs/toggle-group.md +119 -0
- package/docs/toggle.md +94 -0
- package/docs/toolbar.md +138 -0
- package/docs/tooltip.md +128 -0
- package/package.json +102 -3
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
# Accordion
|
|
2
|
+
|
|
3
|
+
> A stack of items whose triggers show and hide their panels, after the WAI-ARIA accordion pattern: each item is a native `<details>`, one open at a time through the browser's exclusive group (`name`) or several, so it works in server HTML with no JavaScript and find-in-page opens the item it lands in. `<lu-accordion>`, a 1 kB light-DOM element, adds the arrow keys and Base UI's animation hooks. `bind:value` (or `value` and `onValueChange`) keeps a signal in sync.
|
|
4
|
+
|
|
5
|
+
```ts
|
|
6
|
+
import { Accordion } from "@lumiose/ui"; // or "@lumiose/ui/accordion"
|
|
7
|
+
import "@lumiose/ui/accordion.css"; // the root is a block; disabled triggers ignore the pointer
|
|
8
|
+
import "@lumiose/ui/themes/default.css"; // optional: the default theme
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Anatomy
|
|
12
|
+
|
|
13
|
+
```tsx
|
|
14
|
+
<Accordion.Root>
|
|
15
|
+
<Accordion.Item>
|
|
16
|
+
<Accordion.Header>
|
|
17
|
+
<Accordion.Trigger />
|
|
18
|
+
</Accordion.Header>
|
|
19
|
+
<Accordion.Panel />
|
|
20
|
+
</Accordion.Item>
|
|
21
|
+
</Accordion.Root>
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
| Part | Renders |
|
|
25
|
+
| --------- | ------------------------------------------------------------------------------------------------------------------ |
|
|
26
|
+
| `Root` | `<lu-accordion>`, around the items. |
|
|
27
|
+
| `Item` | `<details name="…">`, right inside `Root`, `open` while its panel shows; no `name` when several may be open. |
|
|
28
|
+
| `Header` | `<h3>` around the trigger's label: inside the `<summary>`, where HTML allows a heading. |
|
|
29
|
+
| `Trigger` | `<summary aria-controls="…">`, the first child of `Item`: it opens and closes the panel. |
|
|
30
|
+
| `Panel` | `<div role="region" aria-labelledby="…">`, labelled by its trigger; the browser hides it while the item is closed. |
|
|
31
|
+
|
|
32
|
+
HTML puts a heading inside a `<summary>`, not a `<summary>` inside a heading, so `Header` renders
|
|
33
|
+
its `<h3>` inside the trigger's `<summary>`: the trigger names the heading, and the browser
|
|
34
|
+
exposes the `<summary>` as a button with its expanded state. Set `aria-level` on `Header` for
|
|
35
|
+
another heading level.
|
|
36
|
+
|
|
37
|
+
## API
|
|
38
|
+
|
|
39
|
+
### Accordion.Root
|
|
40
|
+
|
|
41
|
+
| Prop | Type | Default | |
|
|
42
|
+
| --------------- | ---------------------------- | ------------ | ----------------------------------------------------------------------------------------------------------------- |
|
|
43
|
+
| `value` | `string[]` | | The values of the open items: opens and closes them when it changes. Server HTML starts as it says. |
|
|
44
|
+
| `defaultValue` | `string[]` | `[]` | The items open at first, when there is no `value`. |
|
|
45
|
+
| `onValueChange` | `(value: string[]) => void` | | Called after an item opens or closes, with the values of the open items. |
|
|
46
|
+
| `bind:value` | `Signal<string[]>` | | Stands in for `value` and `onValueChange`. |
|
|
47
|
+
| `multiple` | `boolean` | `false` | Several items may be open at once. By default opening one closes the other, and only the first value starts open. |
|
|
48
|
+
| `disabled` | `boolean` | `false` | Every item is disabled. |
|
|
49
|
+
| `orientation` | `"vertical" \| "horizontal"` | `"vertical"` | The direction the arrow keys follow: Up and Down, or Left and Right. |
|
|
50
|
+
| `loopFocus` | `boolean` | `true` | The arrow keys go from the last trigger to the first and back. |
|
|
51
|
+
|
|
52
|
+
Without `value`, `onValueChange` or `bind:value` the items keep their own state.
|
|
53
|
+
|
|
54
|
+
### Accordion.Item
|
|
55
|
+
|
|
56
|
+
| Prop | Type | Default | |
|
|
57
|
+
| -------------- | ------------------------- | ---------------- | ---------------------------------------------------------------- |
|
|
58
|
+
| `value` | `string` | an id of its own | Identifies the item in the root's `value`. |
|
|
59
|
+
| `disabled` | `boolean` | `false` | Its trigger takes no focus and does not toggle the panel. |
|
|
60
|
+
| `onOpenChange` | `(open: boolean) => void` | | Called after the item opens or closes; not when it renders open. |
|
|
61
|
+
|
|
62
|
+
### The other parts
|
|
63
|
+
|
|
64
|
+
Each part takes the props of the element it renders (`class`, `style`, `aria-*`, `data-*`, event
|
|
65
|
+
handlers, `ref`) and passes them through. A `Trigger`'s `id` names it, and its `Panel` points at
|
|
66
|
+
it; a `Panel` keeps its own `id`, which its trigger points at (see [Ids](README.md#ids)). `name`,
|
|
67
|
+
`aria-controls` and `aria-labelledby` stay the parts' own, since they connect the parts.
|
|
68
|
+
|
|
69
|
+
- **`Trigger`**: a disabled trigger gets `aria-disabled="true"` and `tabindex="-1"`.
|
|
70
|
+
- **`Panel`**: the client script sets `--lu-accordion-panel-height` and `--lu-accordion-panel-width` on
|
|
71
|
+
it while it opens and closes, and `data-starting-style` and `data-ending-style`
|
|
72
|
+
([Styling](#styling)).
|
|
73
|
+
|
|
74
|
+
## Example
|
|
75
|
+
|
|
76
|
+
A FAQ in a server-rendered page, with no island, styled by the default theme:
|
|
77
|
+
|
|
78
|
+
```tsx check
|
|
79
|
+
import { Accordion } from "@lumiose/ui";
|
|
80
|
+
|
|
81
|
+
export function ShippingFaq() {
|
|
82
|
+
return (
|
|
83
|
+
<Accordion.Root class="lu-accordion" defaultValue={["shipping"]}>
|
|
84
|
+
<Accordion.Item value="shipping">
|
|
85
|
+
<Accordion.Header>
|
|
86
|
+
<Accordion.Trigger>When will my order ship?</Accordion.Trigger>
|
|
87
|
+
</Accordion.Header>
|
|
88
|
+
<Accordion.Panel>Within two working days.</Accordion.Panel>
|
|
89
|
+
</Accordion.Item>
|
|
90
|
+
<Accordion.Item value="returns">
|
|
91
|
+
<Accordion.Header>
|
|
92
|
+
<Accordion.Trigger>Can I return it?</Accordion.Trigger>
|
|
93
|
+
</Accordion.Header>
|
|
94
|
+
<Accordion.Panel>Yes, within 30 days, for free.</Accordion.Panel>
|
|
95
|
+
</Accordion.Item>
|
|
96
|
+
<Accordion.Item value="gifts" disabled>
|
|
97
|
+
<Accordion.Header>
|
|
98
|
+
<Accordion.Trigger>Gift cards</Accordion.Trigger>
|
|
99
|
+
</Accordion.Header>
|
|
100
|
+
<Accordion.Panel>Coming soon.</Accordion.Panel>
|
|
101
|
+
</Accordion.Item>
|
|
102
|
+
</Accordion.Root>
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
In an island, several items open, bound to a signal: `bind:value={open}` is short for
|
|
108
|
+
`value={open()}` plus `onValueChange={(next) => open.set(next)}`.
|
|
109
|
+
|
|
110
|
+
```tsx check
|
|
111
|
+
import { Accordion } from "@lumiose/ui";
|
|
112
|
+
import { signal } from "lumiose";
|
|
113
|
+
|
|
114
|
+
export function SettingsSections() {
|
|
115
|
+
const open = signal(["profile"]);
|
|
116
|
+
return (
|
|
117
|
+
<>
|
|
118
|
+
<Accordion.Root class="lu-accordion" multiple bind:value={open}>
|
|
119
|
+
<Accordion.Item value="profile">
|
|
120
|
+
<Accordion.Header>
|
|
121
|
+
<Accordion.Trigger>Profile</Accordion.Trigger>
|
|
122
|
+
</Accordion.Header>
|
|
123
|
+
<Accordion.Panel>Your name and photo.</Accordion.Panel>
|
|
124
|
+
</Accordion.Item>
|
|
125
|
+
<Accordion.Item value="security">
|
|
126
|
+
<Accordion.Header>
|
|
127
|
+
<Accordion.Trigger>Security</Accordion.Trigger>
|
|
128
|
+
</Accordion.Header>
|
|
129
|
+
<Accordion.Panel>Password and recovery keys.</Accordion.Panel>
|
|
130
|
+
</Accordion.Item>
|
|
131
|
+
</Accordion.Root>
|
|
132
|
+
<p>Open: {open().join(", ") || "none"}</p>
|
|
133
|
+
</>
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
## Styling
|
|
139
|
+
|
|
140
|
+
**With the default theme**, put `lu-accordion` on `Root`: items between borders, triggers as rows
|
|
141
|
+
with a chevron that turns while the item is open, and panels whose height animates.
|
|
142
|
+
|
|
143
|
+
**With your own CSS**, style the parts through `class`, `[open]` on the items and
|
|
144
|
+
`[aria-disabled]` on the triggers. The client script gives each panel what Base UI's does, for an
|
|
145
|
+
open and close animation: `--lu-accordion-panel-height` (the content's height in pixels while it
|
|
146
|
+
moves, `auto` once open), `data-starting-style` for the frame an opening starts from and
|
|
147
|
+
`data-ending-style` while it closes. The browser hides a panel as soon as its item closes, so keep
|
|
148
|
+
it visible while it closes with a transition on `::details-content`:
|
|
149
|
+
|
|
150
|
+
```css
|
|
151
|
+
.accordion > details > .panel {
|
|
152
|
+
overflow: hidden;
|
|
153
|
+
block-size: var(--lu-accordion-panel-height, auto);
|
|
154
|
+
transition: block-size 200ms ease-out;
|
|
155
|
+
}
|
|
156
|
+
.accordion > details > .panel:is([data-starting-style], [data-ending-style]) {
|
|
157
|
+
block-size: 0;
|
|
158
|
+
}
|
|
159
|
+
.accordion > details::details-content {
|
|
160
|
+
transition: content-visibility 200ms allow-discrete;
|
|
161
|
+
}
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
Opening an item closes the open one, and both animate. `@lumiose/ui/accordion.css`, in the
|
|
165
|
+
`lu.core` layer, makes `<lu-accordion>` a block, keeps a trigger's heading on the line of the
|
|
166
|
+
`<summary>`'s marker (`display: inline`), and makes a disabled trigger ignore the pointer.
|
|
167
|
+
|
|
168
|
+
## Keyboard and focus
|
|
169
|
+
|
|
170
|
+
The [WAI-ARIA accordion pattern](https://www.w3.org/WAI/ARIA/apg/patterns/accordion/), with its
|
|
171
|
+
optional keys, as in Base UI:
|
|
172
|
+
|
|
173
|
+
| Key | Does |
|
|
174
|
+
| -------------- | ------------------------------------------------------------------------------------------------------------------------- |
|
|
175
|
+
| Enter, Space | On a trigger: opens or closes its item. |
|
|
176
|
+
| Tab, Shift+Tab | Move through the triggers and the open panels' content. |
|
|
177
|
+
| Down, Up | Move focus to the next or the previous trigger; Right and Left when `orientation="horizontal"`, swapped in right-to-left. |
|
|
178
|
+
| Home, End | Move focus to the first or the last trigger. |
|
|
179
|
+
|
|
180
|
+
- **Disabled triggers** are out of the tab order, and the arrow keys skip them.
|
|
181
|
+
- **Looping**: from the last trigger, Down goes to the first, unless `loopFocus={false}`.
|
|
182
|
+
|
|
183
|
+
## Without JavaScript, and the element's script
|
|
184
|
+
|
|
185
|
+
Opening and closing, one open item at a time (the `name` group), find-in-page and links into a
|
|
186
|
+
closed item are HTML features: an accordion in a server-rendered page works before any script
|
|
187
|
+
loads, and with none. Disabled triggers ignore the pointer through CSS. `value`, `onValueChange`
|
|
188
|
+
and `bind:value` need the component to run in an island.
|
|
189
|
+
|
|
190
|
+
A page that renders an `Accordion` loads a 1 kB (brotli) module once, without the Lumiose runtime;
|
|
191
|
+
islands bundle it. It defines `<lu-accordion>`, which adds the arrow keys, Home and End, and sets
|
|
192
|
+
the panels' animation hooks (shared with [Collapsible](collapsible.md)); a disabled trigger does
|
|
193
|
+
not toggle, also from the keyboard.
|
|
194
|
+
|
|
195
|
+
## Browser support
|
|
196
|
+
|
|
197
|
+
- **`<details name>`** (one open item at a time): Chrome 120, Firefox 130, Safari 17.2. Older
|
|
198
|
+
browsers let several items open.
|
|
199
|
+
- **Find-in-page and links into a closed item** open it: Chrome 97, Firefox 139. Safari has not
|
|
200
|
+
shipped it yet (WebKit bug 304174): there, find-in-page skips a closed item.
|
|
201
|
+
- **`::details-content`**, which the closing transition needs: Chrome 131, Firefox 143, Safari
|
|
202
|
+
18.4. Elsewhere a panel disappears at once when its item closes.
|
|
203
|
+
|
|
204
|
+
## Related
|
|
205
|
+
|
|
206
|
+
- [Collapsible](collapsible.md): one panel and its trigger.
|
|
207
|
+
- [Tabs](tabs.md): one panel at a time, from a tab list.
|
|
208
|
+
- [UI components](README.md): the approach, styling and the other components.
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
# Autocomplete
|
|
2
|
+
|
|
3
|
+
> A text input with suggestions, for text that may be anything (a search, a tag, an address): the WAI-ARIA combobox pattern with list autocomplete, after Base UI's Autocomplete. Unlike a [Combobox](combobox.md), the text is the value: the input has the name and submits what is typed, and choosing a suggestion only fills it in. It runs on the Combobox's element (`<lu-combobox free>`, 5.0 kB) and parts, on server-rendered pages and in islands; `bind:value` keeps a signal in sync. Filtering, inline completion, groups, an empty state and a status.
|
|
4
|
+
|
|
5
|
+
```ts
|
|
6
|
+
import { Autocomplete } from "@lumiose/ui"; // or "@lumiose/ui/autocomplete"
|
|
7
|
+
import "@lumiose/ui/autocomplete.css"; // Combobox's CSS: places the popup, hides filtered options
|
|
8
|
+
import "@lumiose/ui/themes/default.css"; // optional: the default theme
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Anatomy
|
|
12
|
+
|
|
13
|
+
```tsx
|
|
14
|
+
<Autocomplete.Root>
|
|
15
|
+
<Autocomplete.Label />
|
|
16
|
+
<Autocomplete.Input />
|
|
17
|
+
<Autocomplete.Clear />
|
|
18
|
+
<Autocomplete.Trigger />
|
|
19
|
+
<Autocomplete.Popup>
|
|
20
|
+
<Autocomplete.List>
|
|
21
|
+
<Autocomplete.Item />
|
|
22
|
+
<Autocomplete.Group>
|
|
23
|
+
<Autocomplete.GroupLabel />
|
|
24
|
+
<Autocomplete.Item />
|
|
25
|
+
</Autocomplete.Group>
|
|
26
|
+
</Autocomplete.List>
|
|
27
|
+
<Autocomplete.Empty />
|
|
28
|
+
<Autocomplete.Status />
|
|
29
|
+
</Autocomplete.Popup>
|
|
30
|
+
</Autocomplete.Root>
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
| Part | Renders |
|
|
34
|
+
| --------------------- | ----------------------------------------------------------------------------------- |
|
|
35
|
+
| `Root` | `<lu-combobox free>`: the element, with no box; `inline` with inline completion |
|
|
36
|
+
| `Label` | `<label for>` the input |
|
|
37
|
+
| `Input` | `<input role="combobox" aria-autocomplete name value>`: the form control |
|
|
38
|
+
| `Clear`, `Trigger` | `<button tabindex="-1">`: empty the text, or open and close the suggestions |
|
|
39
|
+
| `Popup`, `List` | `<div popover>` around the `<div role="listbox">`, or the listbox as the popup |
|
|
40
|
+
| `Item` | `<div role="option" data-value>`: a suggestion; its `value` is the text it fills in |
|
|
41
|
+
| `Group`, `GroupLabel` | `<div role="group">` named by its `<div role="presentation">` |
|
|
42
|
+
| `Empty`, `Status` | `<div role="status">`: shown when nothing matches, or a message you update |
|
|
43
|
+
|
|
44
|
+
The parts are [Combobox](combobox.md)'s, under this Root: their props and styling hooks are the
|
|
45
|
+
same.
|
|
46
|
+
|
|
47
|
+
## API
|
|
48
|
+
|
|
49
|
+
### Autocomplete.Root
|
|
50
|
+
|
|
51
|
+
| Prop | Type | Default | |
|
|
52
|
+
| ----------------------------------- | ---------------------------------------- | ------------ | ------------------------------------------------------------------------------------------------------- |
|
|
53
|
+
| `value` | `string` | | Puts this text in the input when it changes. Pair it with `onValueChange`. |
|
|
54
|
+
| `defaultValue` | `string` | | The text at first; a form reset goes back to it. |
|
|
55
|
+
| `onValueChange` | `(value: string) => void` | | Called as the text changes: typing, choosing a suggestion, clearing. |
|
|
56
|
+
| `bind:value` | `Signal<string>` | | Stands in for `value` and `onValueChange`. |
|
|
57
|
+
| `open`, `onOpenChange`, `bind:open` | `boolean`, `(open: boolean) => void` | | Open or close the suggestions, and hear about it, as in Combobox. |
|
|
58
|
+
| `mode` | `"list" \| "both" \| "inline" \| "none"` | `"list"` | How the suggestions complete the text (below). |
|
|
59
|
+
| `filter` | `"contains" \| "startsWith"` | `"contains"` | How `"list"` and `"both"` filter the suggestions, ignoring case and accents. |
|
|
60
|
+
| `openOnInputClick` | `boolean` | `false` | A click on the input opens the suggestions; otherwise typing and Arrow Down do. |
|
|
61
|
+
| `autoHighlight` | `boolean` | `false` | The first suggestion is highlighted as you type, so Enter fills it in. |
|
|
62
|
+
| `keepHighlight` | `boolean` | `false` | A suggestion the pointer highlighted stays highlighted when it leaves the list. |
|
|
63
|
+
| `submitOnItemClick` | `boolean` | `false` | Choosing a suggestion (a click, or Enter on it) fills it in and submits the form, as a search box does. |
|
|
64
|
+
| `limit` | `number` | no limit | Shows at most this many suggestions; a narrower text shows the others. |
|
|
65
|
+
| `name` | `string` | | The name the text is submitted under (the input's). |
|
|
66
|
+
| `required`, `disabled` | `boolean` | `false` | As on a native input. |
|
|
67
|
+
|
|
68
|
+
The modes follow Base UI's:
|
|
69
|
+
|
|
70
|
+
- **`"list"`**: typing filters the suggestions; the input keeps what is typed while the arrows move
|
|
71
|
+
through them.
|
|
72
|
+
- **`"both"`**: filtered, and the arrows show the highlighted suggestion in the input, its
|
|
73
|
+
completed part selected; Escape takes it back, Tab or a click elsewhere keeps it.
|
|
74
|
+
- **`"inline"`**: the arrows show the highlighted suggestion in the input, unfiltered.
|
|
75
|
+
- **`"none"`**: neither: the page filters the suggestions itself (results from a server).
|
|
76
|
+
|
|
77
|
+
### The other parts
|
|
78
|
+
|
|
79
|
+
As in [Combobox](combobox.md#the-other-parts). An `Item`'s `value` is the text it fills in (and its
|
|
80
|
+
`label`, when the shown text differs, what typing matches). Inside a [Field](field.md), the input
|
|
81
|
+
takes the field's id, name, description and error, and the field shows its validation.
|
|
82
|
+
|
|
83
|
+
**Differences from Base UI's Autocomplete.** The suggestions are markup, not an `items` array, and
|
|
84
|
+
filtering compares their text (`mode="none"` hands it to you, as Base UI's `filter={null}`).
|
|
85
|
+
`limit` hides the matches past it, as filtering does, so it applies to the server's suggestions
|
|
86
|
+
too. No `Portal`, `Positioner`,
|
|
87
|
+
`Backdrop`, `Arrow`, `Value`, `Icon`, `Row`, `Collection` or `Separator`.
|
|
88
|
+
|
|
89
|
+
## Example
|
|
90
|
+
|
|
91
|
+
A search with suggestions, in an island:
|
|
92
|
+
|
|
93
|
+
```tsx check
|
|
94
|
+
import { Autocomplete } from "@lumiose/ui";
|
|
95
|
+
import { signal } from "lumiose";
|
|
96
|
+
|
|
97
|
+
const TOPICS = ["accessibility", "animation", "forms", "islands", "routing", "signals"];
|
|
98
|
+
|
|
99
|
+
export function TopicSearch() {
|
|
100
|
+
const query = signal("");
|
|
101
|
+
return (
|
|
102
|
+
<>
|
|
103
|
+
<Autocomplete.Root name="q" bind:value={query} class="lu-combobox">
|
|
104
|
+
<Autocomplete.Label>Search the docs</Autocomplete.Label>
|
|
105
|
+
<Autocomplete.Input placeholder="Any topic" />
|
|
106
|
+
<Autocomplete.Popup>
|
|
107
|
+
<Autocomplete.List>
|
|
108
|
+
{TOPICS.map((topic) => (
|
|
109
|
+
<Autocomplete.Item value={topic}>{topic}</Autocomplete.Item>
|
|
110
|
+
))}
|
|
111
|
+
</Autocomplete.List>
|
|
112
|
+
<Autocomplete.Empty>No suggestions: press Enter to search.</Autocomplete.Empty>
|
|
113
|
+
</Autocomplete.Popup>
|
|
114
|
+
</Autocomplete.Root>
|
|
115
|
+
<p>{query() ? `Searching for “${query()}”` : "Type to search."}</p>
|
|
116
|
+
</>
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## Styling
|
|
122
|
+
|
|
123
|
+
**With the default theme**, put `lu-combobox` on `Root`, as for a Combobox: the label, the input
|
|
124
|
+
with its buttons, and the popup with the highlighted suggestion marked.
|
|
125
|
+
|
|
126
|
+
**With your own CSS**, style the parts through `class` and their state: `[aria-expanded]` on the
|
|
127
|
+
input, `:popover-open` on the popup, `[data-highlighted]` and `[aria-disabled]` on the
|
|
128
|
+
suggestions, `[hidden]` on filtered ones.
|
|
129
|
+
|
|
130
|
+
`@lumiose/ui/autocomplete.css` is Combobox's stylesheet: it places the popup next to the input and
|
|
131
|
+
keeps filtered suggestions hidden.
|
|
132
|
+
|
|
133
|
+
## Keyboard and focus
|
|
134
|
+
|
|
135
|
+
| Key | Does |
|
|
136
|
+
| --------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
137
|
+
| Typing | Filters the suggestions (with `"list"` and `"both"`) and opens them; an empty input closes them. |
|
|
138
|
+
| Arrow Down / Arrow Up | Opens the suggestions, then moves the highlight (with `"both"` and `"inline"`, into the input). |
|
|
139
|
+
| Enter | Fills in the highlighted suggestion (the first, with `autoHighlight`); with none, submits. |
|
|
140
|
+
| Escape | Open: closes the suggestions and keeps the typed text (taking back an inline completion). Closed: clears the text, as Base UI's; closed and empty, it goes on to the page. |
|
|
141
|
+
| Tab | Closes the suggestions and moves on, keeping the text. |
|
|
142
|
+
|
|
143
|
+
Focus stays in the input: the highlight moves through `aria-activedescendant`.
|
|
144
|
+
|
|
145
|
+
## Without JavaScript, and the element's script
|
|
146
|
+
|
|
147
|
+
The input is a native text field with the name: typing, `required`, submission and reset work with
|
|
148
|
+
no script; the suggestions need it. `<lu-combobox>`'s script (5.0 kB brotli, shared with Combobox)
|
|
149
|
+
filters and opens them, moves the highlight, fills in a choice (firing `input` and `change` on the
|
|
150
|
+
input) and runs inline completion.
|
|
151
|
+
|
|
152
|
+
## Browser support
|
|
153
|
+
|
|
154
|
+
As [Combobox](combobox.md#browser-support): popover, and CSS anchor positioning where available
|
|
155
|
+
(the script places the popup elsewhere).
|
|
156
|
+
|
|
157
|
+
## Related
|
|
158
|
+
|
|
159
|
+
- [Combobox](combobox.md): a value picked from the options, when the text must match one.
|
|
160
|
+
- [Field](field.md): a label, a description and errors for an autocomplete.
|
|
161
|
+
- [Input](input.md): a text field without suggestions.
|
package/docs/avatar.md
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
# Avatar
|
|
2
|
+
|
|
3
|
+
> A person's picture with a fallback, such as their initials, after Base UI's Avatar: the image loads in the server HTML and shows once it has loaded, the fallback shows until then, after an optional delay, and when the image fails. Both sit in one cell, so neither moves the other. A 0.5 kB script follows the image's loading status; without JavaScript an avatar with an image shows the image alone.
|
|
4
|
+
|
|
5
|
+
```ts
|
|
6
|
+
import { Avatar } from "@lumiose/ui"; // or "@lumiose/ui/avatar"
|
|
7
|
+
import "@lumiose/ui/avatar.css"; // stacks the image over the fallback; hides each in turn
|
|
8
|
+
import "@lumiose/ui/themes/default.css"; // optional: the default theme
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Anatomy
|
|
12
|
+
|
|
13
|
+
```tsx
|
|
14
|
+
<Avatar.Root>
|
|
15
|
+
<Avatar.Image />
|
|
16
|
+
<Avatar.Fallback />
|
|
17
|
+
</Avatar.Root>
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
| Part | Renders |
|
|
21
|
+
| ---------- | ---------------------------------------------------------------------------------------------- |
|
|
22
|
+
| `Root` | `<lu-avatar>`, an inline grid with one cell, `data-loaded` once the image has loaded. |
|
|
23
|
+
| `Image` | `<img data-loading>`: `data-loading` while it loads, `data-error` when it failed, both hidden. |
|
|
24
|
+
| `Fallback` | `<span data-fallback>`, hidden once the image has loaded. |
|
|
25
|
+
|
|
26
|
+
The image is the root's `<img>` child; anything else inside, such as a status badge, is yours to
|
|
27
|
+
place.
|
|
28
|
+
|
|
29
|
+
## API
|
|
30
|
+
|
|
31
|
+
### Avatar.Image
|
|
32
|
+
|
|
33
|
+
| Prop | Type | Default | |
|
|
34
|
+
| ----------------------- | ---------------------------------------------------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
|
|
35
|
+
| `onLoadingStatusChange` | `(status: "loading" \| "loaded" \| "error") => void` | | Called when the loading status changes; `"error"` also when there is no `src`. An island hears the status the image had when it hydrated. |
|
|
36
|
+
|
|
37
|
+
Every native `<img>` prop passes through: `src`, `srcset`, `sizes`, `alt`, `width`, `height`,
|
|
38
|
+
`loading`, `decoding`, `fetchpriority`. A new `src` or `srcset` starts over at `"loading"`.
|
|
39
|
+
|
|
40
|
+
### Avatar.Fallback
|
|
41
|
+
|
|
42
|
+
| Prop | Type | Default | |
|
|
43
|
+
| ------- | -------- | ------- | --------------------------------------------------------------------------------------------------------- |
|
|
44
|
+
| `delay` | `number` | `0` | How long to wait before showing the fallback, in milliseconds: an image that loads sooner never shows it. |
|
|
45
|
+
|
|
46
|
+
### The other parts
|
|
47
|
+
|
|
48
|
+
`Root` and `Fallback` take the props of the element they render (`class`, `style`, `aria-*`,
|
|
49
|
+
`data-*`, event handlers) and pass them through. The root element has a `status` property
|
|
50
|
+
(`"loading"`, `"loaded"`, `"error"`, or `undefined` without an image), and the image dispatches
|
|
51
|
+
`loadingstatuschange` (`event.detail.status`) for `on:loadingstatuschange`.
|
|
52
|
+
|
|
53
|
+
**Differences from Base UI's Avatar.** The image is always in the page, as with Base UI's
|
|
54
|
+
`keepMounted`: it starts loading with the server HTML, supports `loading="lazy"`, and shows without
|
|
55
|
+
JavaScript. A missing `src` is `"error"`, not `"idle"`. No `render`.
|
|
56
|
+
|
|
57
|
+
## Example
|
|
58
|
+
|
|
59
|
+
Two people in a server-rendered page, with no island: an image that loads, and one that fails and
|
|
60
|
+
leaves the initials. The default theme draws the circles.
|
|
61
|
+
|
|
62
|
+
```tsx check
|
|
63
|
+
import { Avatar } from "@lumiose/ui";
|
|
64
|
+
|
|
65
|
+
// A portrait drawn inline, so the example runs anywhere.
|
|
66
|
+
const portrait = `data:image/svg+xml,${encodeURIComponent(
|
|
67
|
+
`<svg xmlns="http://www.w3.org/2000/svg" width="80" height="80" viewBox="0 0 80 80">
|
|
68
|
+
<rect width="80" height="80" fill="#c7d2fe"/>
|
|
69
|
+
<circle cx="40" cy="31" r="14" fill="#4338ca"/>
|
|
70
|
+
<path d="M12 80a28 28 0 0 1 56 0z" fill="#4338ca"/>
|
|
71
|
+
</svg>`,
|
|
72
|
+
)}`;
|
|
73
|
+
|
|
74
|
+
export function Reviewers() {
|
|
75
|
+
return (
|
|
76
|
+
<ul aria-label="Reviewers" style={{ display: "flex", gap: "0.5rem", "list-style": "none" }}>
|
|
77
|
+
<li>
|
|
78
|
+
<Avatar.Root class="lu-avatar">
|
|
79
|
+
<Avatar.Image src={portrait} alt="Ada Lovelace" />
|
|
80
|
+
<Avatar.Fallback role="img" aria-label="Ada Lovelace" delay={600}>
|
|
81
|
+
AL
|
|
82
|
+
</Avatar.Fallback>
|
|
83
|
+
</Avatar.Root>
|
|
84
|
+
</li>
|
|
85
|
+
<li>
|
|
86
|
+
<Avatar.Root class="lu-avatar">
|
|
87
|
+
<Avatar.Image src="/avatars/missing.jpg" alt="Grace Hopper" />
|
|
88
|
+
<Avatar.Fallback role="img" aria-label="Grace Hopper">
|
|
89
|
+
GH
|
|
90
|
+
</Avatar.Fallback>
|
|
91
|
+
</Avatar.Root>
|
|
92
|
+
</li>
|
|
93
|
+
</ul>
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
In an island, the loading status in a signal, as the picture breaks and comes back:
|
|
99
|
+
|
|
100
|
+
```tsx check
|
|
101
|
+
import { Avatar, type AvatarLoadingStatus } from "@lumiose/ui";
|
|
102
|
+
import { signal } from "lumiose";
|
|
103
|
+
|
|
104
|
+
const portrait = `data:image/svg+xml,${encodeURIComponent(
|
|
105
|
+
`<svg xmlns="http://www.w3.org/2000/svg" width="80" height="80" viewBox="0 0 80 80">
|
|
106
|
+
<rect width="80" height="80" fill="#fde68a"/>
|
|
107
|
+
<circle cx="40" cy="31" r="14" fill="#b45309"/>
|
|
108
|
+
<path d="M12 80a28 28 0 0 1 56 0z" fill="#b45309"/>
|
|
109
|
+
</svg>`,
|
|
110
|
+
)}`;
|
|
111
|
+
|
|
112
|
+
export function ProfilePicture() {
|
|
113
|
+
const broken = signal(false);
|
|
114
|
+
const status = signal<AvatarLoadingStatus>("loading");
|
|
115
|
+
return (
|
|
116
|
+
<>
|
|
117
|
+
<Avatar.Root class="lu-avatar">
|
|
118
|
+
<Avatar.Image
|
|
119
|
+
src={broken() ? "/avatars/missing.jpg" : portrait}
|
|
120
|
+
alt=""
|
|
121
|
+
onLoadingStatusChange={(next) => status.set(next)}
|
|
122
|
+
/>
|
|
123
|
+
<Avatar.Fallback aria-hidden="true">AL</Avatar.Fallback>
|
|
124
|
+
</Avatar.Root>
|
|
125
|
+
<p>Ada Lovelace (picture: {status()})</p>
|
|
126
|
+
<button type="button" class="lu-button" onClick={() => broken.set(!broken())}>
|
|
127
|
+
{broken() ? "Fix the picture" : "Break the picture"}
|
|
128
|
+
</button>
|
|
129
|
+
</>
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
## Styling
|
|
135
|
+
|
|
136
|
+
**With the default theme**, put `lu-avatar` on `Root`: a 40px circle on a muted surface, the image
|
|
137
|
+
covering it, the fallback's initials centred.
|
|
138
|
+
|
|
139
|
+
**With your own CSS**, give the root a size, so the image loading or failing never changes the
|
|
140
|
+
layout, and style the parts through `class`, `[data-loaded]`, `[data-loading]`, `[data-error]` and
|
|
141
|
+
`[data-fallback]`:
|
|
142
|
+
|
|
143
|
+
```css
|
|
144
|
+
.avatar {
|
|
145
|
+
place-items: center;
|
|
146
|
+
inline-size: 3rem;
|
|
147
|
+
block-size: 3rem;
|
|
148
|
+
overflow: hidden;
|
|
149
|
+
border-radius: 50%;
|
|
150
|
+
background: #f4f4f5;
|
|
151
|
+
}
|
|
152
|
+
.avatar > img {
|
|
153
|
+
inline-size: 100%;
|
|
154
|
+
block-size: 100%;
|
|
155
|
+
object-fit: cover;
|
|
156
|
+
}
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
`@lumiose/ui/avatar.css`, in the `lu.core` layer, puts the parts in one grid cell, the image above
|
|
160
|
+
the fallback, and hides them in turn with `visibility` (so a `loading="lazy"` image still loads):
|
|
161
|
+
the image while it loads, where scripting is enabled, and when it failed; the fallback once the
|
|
162
|
+
image has loaded. The fallback's `delay` is a zero-length `lu-avatar-delay` animation: an animation
|
|
163
|
+
of your own on the fallback replaces it, so list both (`animation: lu-avatar-delay 0s
|
|
164
|
+
var(--lu-avatar-delay, 0s) backwards, my-fade 200ms`).
|
|
165
|
+
|
|
166
|
+
## Keyboard and focus
|
|
167
|
+
|
|
168
|
+
An avatar takes no focus and has no keys. Name it once:
|
|
169
|
+
|
|
170
|
+
- **Next to the person's name**, it repeats it: `alt=""` on the image and `aria-hidden="true"` on
|
|
171
|
+
the fallback.
|
|
172
|
+
- **Alone**, name both parts, since only one shows at a time: the image's `alt`, and on the
|
|
173
|
+
fallback `role="img"` with an `aria-label`, so the initials are not read letter by letter.
|
|
174
|
+
|
|
175
|
+
## Without JavaScript, and the client script
|
|
176
|
+
|
|
177
|
+
The image and the fallback are in the server HTML. Without JavaScript nothing tells a failed image
|
|
178
|
+
from a loaded one, so an avatar with an image shows the image alone (the browser's broken-image box
|
|
179
|
+
when it fails) and hides its fallback, from sight and from assistive technology, which would name
|
|
180
|
+
the avatar twice; an avatar without an image shows its fallback.
|
|
181
|
+
`onLoadingStatusChange` needs the component to run in an island.
|
|
182
|
+
|
|
183
|
+
A page that renders an `Avatar` loads a 0.5 kB (brotli) script, without the Lumiose runtime;
|
|
184
|
+
islands bundle it. `<lu-avatar>` reads the image's state when it connects (a cached image shows
|
|
185
|
+
at once), then follows its `load` and `error` events and changes to its `src` and `srcset`. An
|
|
186
|
+
image that is complete without a size (an SVG without intrinsic dimensions, or a broken image) is
|
|
187
|
+
told apart with `decode()`.
|
|
188
|
+
|
|
189
|
+
## Browser support
|
|
190
|
+
|
|
191
|
+
- **The `scripting` media query**, which keeps a loading image hidden only where scripts run:
|
|
192
|
+
Chrome and Edge 120, Firefox 113, Safari 17. Elsewhere a loading image shows as it arrives.
|
|
193
|
+
- **CSS grid and `visibility`**: every browser.
|
|
194
|
+
|
|
195
|
+
## Related
|
|
196
|
+
|
|
197
|
+
- [UI components](README.md): the approach, styling and the other components.
|
|
198
|
+
- [Theming](theming.md): the default theme's classes and custom properties.
|
package/docs/button.md
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# Button
|
|
2
|
+
|
|
3
|
+
> A native `<button>`, after Base UI's Button. It is `type="button"` unless you say otherwise, so it submits a form only when asked to. Disabled it leaves the tab order; with `focusableWhenDisabled` it stays focusable as `aria-disabled="true"` (a loading state keeps focus where it was) and a tiny script cancels its clicks.
|
|
4
|
+
|
|
5
|
+
```ts
|
|
6
|
+
import { Button } from "@lumiose/ui"; // or "@lumiose/ui/button"
|
|
7
|
+
import "@lumiose/ui/themes/default.css"; // optional: the default theme
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
## Anatomy
|
|
11
|
+
|
|
12
|
+
```tsx
|
|
13
|
+
<Button>Save</Button>
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
| Part | Renders |
|
|
17
|
+
| -------- | ----------------------------------------------------------------------------- |
|
|
18
|
+
| `Button` | `<button type="button">`; `aria-disabled="true"` with `focusableWhenDisabled` |
|
|
19
|
+
|
|
20
|
+
## API
|
|
21
|
+
|
|
22
|
+
### Button
|
|
23
|
+
|
|
24
|
+
| Prop | Type | Default | |
|
|
25
|
+
| ----------------------- | --------- | ---------- | ------------------------------------------------------------------------------------------------------------------- |
|
|
26
|
+
| `type` | `string` | `"button"` | `"submit"` or `"reset"` to act on its form. |
|
|
27
|
+
| `disabled` | `boolean` | `false` | Natively disabled: out of the tab order, its clicks ignored. |
|
|
28
|
+
| `focusableWhenDisabled` | `boolean` | `false` | With `disabled`: it stays focusable as `aria-disabled="true"`, and its clicks, presses, Enter and Space do nothing. |
|
|
29
|
+
|
|
30
|
+
Every other native prop passes through: `name`, `value`, `form`, `command`, `commandfor`,
|
|
31
|
+
`popovertarget`, `id`, `class`, `aria-*`, `onClick`.
|
|
32
|
+
|
|
33
|
+
**Differences from Base UI's Button.** It is always a `<button>`: no `nativeButton` or `render`. For
|
|
34
|
+
a link that looks like a button, style an `<a>` with the same class.
|
|
35
|
+
|
|
36
|
+
## Example
|
|
37
|
+
|
|
38
|
+
A save button that keeps focus while it saves, in an island:
|
|
39
|
+
|
|
40
|
+
```tsx check
|
|
41
|
+
import { Button } from "@lumiose/ui";
|
|
42
|
+
import { signal } from "lumiose";
|
|
43
|
+
|
|
44
|
+
export function SavingButton() {
|
|
45
|
+
const saving = signal(false);
|
|
46
|
+
return (
|
|
47
|
+
<Button
|
|
48
|
+
class="lu-button"
|
|
49
|
+
data-variant="primary"
|
|
50
|
+
disabled={saving()}
|
|
51
|
+
focusableWhenDisabled
|
|
52
|
+
onClick={() => {
|
|
53
|
+
saving.set(true);
|
|
54
|
+
setTimeout(() => saving.set(false), 1500);
|
|
55
|
+
}}
|
|
56
|
+
>
|
|
57
|
+
{saving() ? "Saving…" : "Save"}
|
|
58
|
+
</Button>
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Styling
|
|
64
|
+
|
|
65
|
+
**With the default theme**, put `lu-button` on it (`data-variant="primary"` or
|
|
66
|
+
`data-variant="destructive"` for the others): a 36px tall button with a focus ring, faded when
|
|
67
|
+
`:disabled` or `[aria-disabled="true"]`.
|
|
68
|
+
|
|
69
|
+
**With your own CSS**, style the button and its states: `:hover`, `:focus-visible`, `:disabled`,
|
|
70
|
+
`[aria-disabled="true"]`.
|
|
71
|
+
|
|
72
|
+
Button ships no stylesheet: it needs none to work.
|
|
73
|
+
|
|
74
|
+
## Keyboard and focus
|
|
75
|
+
|
|
76
|
+
| Key | Does |
|
|
77
|
+
| ------------- | ----------------------------------------------------------------------------- |
|
|
78
|
+
| Enter / Space | Presses it, unless it is disabled. |
|
|
79
|
+
| Tab | Moves on. A disabled button is skipped, unless it is focusable when disabled. |
|
|
80
|
+
|
|
81
|
+
## Without JavaScript, and the client script
|
|
82
|
+
|
|
83
|
+
A native button: it works, submits and resets with no script. The client script (0.2 kB brotli)
|
|
84
|
+
only cancels the clicks, Enter and Space of an `aria-disabled` button (one without a `role` of its
|
|
85
|
+
own), and keeps its presses (`pointerdown`, `mousedown`) and Enter and Space keys from its
|
|
86
|
+
handlers and its ancestors' bubbling ones, as Base UI does; listeners that capture still see them,
|
|
87
|
+
and the other keys (Tab, a toolbar's arrows) go on. Without it such a button still acts, so a
|
|
88
|
+
server page that disables a button until something happens should use `disabled` alone.
|
|
89
|
+
|
|
90
|
+
## Browser support
|
|
91
|
+
|
|
92
|
+
Native `<button>`: every browser.
|
|
93
|
+
|
|
94
|
+
## Related
|
|
95
|
+
|
|
96
|
+
- [Toggle](toggle.md): a button that stays pressed.
|
|
97
|
+
- [Form](form.md): a form whose submit button is a Button.
|
|
98
|
+
- [Dialog](dialog.md): a Button with `command="show-modal"` opens a dialog without JavaScript.
|