@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
package/docs/meter.md
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# Meter
|
|
2
|
+
|
|
3
|
+
> A measurement within a known range (storage used, a password's strength, a rating), after Base UI's Meter: a native `<meter>` (with its value, range and `aria-valuetext`), labelled, with the value as text (a percentage of the range by default, or your `Intl.NumberFormat` options). No script.
|
|
4
|
+
|
|
5
|
+
```ts
|
|
6
|
+
import { Meter } from "@lumiose/ui"; // or "@lumiose/ui/meter"
|
|
7
|
+
import "@lumiose/ui/themes/default.css"; // optional: the default theme
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
## Anatomy
|
|
11
|
+
|
|
12
|
+
```tsx
|
|
13
|
+
<Meter.Root value={72}>
|
|
14
|
+
<Meter.Label />
|
|
15
|
+
<Meter.Value />
|
|
16
|
+
<Meter.Track />
|
|
17
|
+
</Meter.Root>
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
| Part | Renders |
|
|
21
|
+
| ------- | ----------------------------------------------- |
|
|
22
|
+
| `Root` | `<div>` with `--lu-value` (`0%` to `100%`) |
|
|
23
|
+
| `Label` | `<label for>` the track |
|
|
24
|
+
| `Value` | `<span>` with the formatted value |
|
|
25
|
+
| `Track` | `<meter value min max aria-valuetext>`: the bar |
|
|
26
|
+
|
|
27
|
+
## API
|
|
28
|
+
|
|
29
|
+
### Meter.Root
|
|
30
|
+
|
|
31
|
+
| Prop | Type | Default | |
|
|
32
|
+
| ------------------ | --------------------------------------------------- | ------------------- | ----------------------------------------------------------------------------------- |
|
|
33
|
+
| `value` | `number` | | The measurement, from `min` to `max`. Required. |
|
|
34
|
+
| `min` | `number` | `0` | The bottom of the range. |
|
|
35
|
+
| `max` | `number` | `100` | The top of the range. |
|
|
36
|
+
| `format` | `Intl.NumberFormatOptions` | a percentage | How the value is written. With it, the value itself is formatted, not a percentage. |
|
|
37
|
+
| `locale` | `Intl.LocalesArgument` | the browser's | The locale of the formatted value (the server's in server HTML). |
|
|
38
|
+
| `getAriaValueText` | `(formattedValue: string, value: number) => string` | the formatted value | The text assistive technology reads. |
|
|
39
|
+
|
|
40
|
+
Every other prop passes through to the `<div>`: `id`, `class`, `style`.
|
|
41
|
+
|
|
42
|
+
### Meter.Label, Meter.Track and Meter.Value
|
|
43
|
+
|
|
44
|
+
`Label` takes `<label>` props, its `id` among them; the `Track` keeps its own `id`, which the
|
|
45
|
+
`Label` before it points at (see [Ids](README.md#ids)). `Track` takes `<meter>` props, among them the native `low`, `high`
|
|
46
|
+
and `optimum`, which tell browsers which band a value is in (`aria-label` when there is no
|
|
47
|
+
`Label`). `Value` takes `<span>` props, and its `children` may be a function
|
|
48
|
+
`(formattedValue, value) => JSX.Element`.
|
|
49
|
+
|
|
50
|
+
**Differences from Base UI's Meter.** The `<meter>` element is the track and the indicator: there is
|
|
51
|
+
no `Meter.Indicator`. Root has no `role`; the `<meter>` has it (`meter`).
|
|
52
|
+
|
|
53
|
+
## Example
|
|
54
|
+
|
|
55
|
+
```tsx check
|
|
56
|
+
import { Meter } from "@lumiose/ui";
|
|
57
|
+
|
|
58
|
+
export function StorageMeter() {
|
|
59
|
+
return (
|
|
60
|
+
<Meter.Root
|
|
61
|
+
value={7.2}
|
|
62
|
+
max={10}
|
|
63
|
+
getAriaValueText={(_, gb) => `${gb} of 10 GB`}
|
|
64
|
+
class="lu-meter"
|
|
65
|
+
>
|
|
66
|
+
<Meter.Label>Storage used</Meter.Label>
|
|
67
|
+
<Meter.Value>{(_, gb) => `${gb} GB of 10`}</Meter.Value>
|
|
68
|
+
<Meter.Track low={6} high={9} optimum={0} />
|
|
69
|
+
</Meter.Root>
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Styling
|
|
75
|
+
|
|
76
|
+
**With the default theme**, put `lu-meter` on `Root`: the label and the value on a row above a 10px
|
|
77
|
+
bar, its edge in `--lu-input` (3:1, so the whole range shows) and its value in the primary colour
|
|
78
|
+
(a lighter blue in dark mode), whatever band it is in.
|
|
79
|
+
|
|
80
|
+
**With your own CSS**, style the bar with `appearance: none` and its pseudo-elements:
|
|
81
|
+
`::-webkit-meter-bar`, `::-webkit-meter-optimum-value`, `::-webkit-meter-suboptimum-value`,
|
|
82
|
+
`::-webkit-meter-even-less-good-value`, `::-moz-meter-bar` (and `:-moz-meter-optimum` and the
|
|
83
|
+
others). Or draw your own bar from `--lu-value` on `Root`.
|
|
84
|
+
|
|
85
|
+
Meter ships no stylesheet: it needs none to work.
|
|
86
|
+
|
|
87
|
+
## Keyboard and focus
|
|
88
|
+
|
|
89
|
+
A meter is not interactive: it takes no focus and no keys.
|
|
90
|
+
|
|
91
|
+
## Without JavaScript
|
|
92
|
+
|
|
93
|
+
Fully: the server renders the `<meter>` with its value, range, label and text.
|
|
94
|
+
|
|
95
|
+
## Browser support
|
|
96
|
+
|
|
97
|
+
Native `<meter>`: every browser. Its pseudo-elements differ between engines, hence the two sets
|
|
98
|
+
above. `role="meter"` reaches screen readers in recent versions; older ones read a progress bar.
|
|
99
|
+
|
|
100
|
+
## Related
|
|
101
|
+
|
|
102
|
+
- [Progress](progress.md): a task's progress, which can be indeterminate.
|
|
103
|
+
- [Slider](slider.md): a value the user picks within a range.
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
# Navigation Menu
|
|
2
|
+
|
|
3
|
+
> A site's navigation whose items open panels of links, after Base UI's NavigationMenu and the WAI-ARIA disclosure navigation pattern: a `<nav>` landmark around a list, each trigger opening its content through `popovertarget` (a native popover placed under it by CSS anchor positioning), so the links work and the panels open on a click, one at a time, in server HTML with no JavaScript. A 3.5 kB script adds hover intent, the arrow keys, closing when focus leaves and the direction a panel comes in from. `bind:value` (or `value` and `onValueChange`) keeps the open item in a signal.
|
|
4
|
+
|
|
5
|
+
```ts
|
|
6
|
+
import { NavigationMenu } from "@lumiose/ui"; // or "@lumiose/ui/navigation-menu"
|
|
7
|
+
import "@lumiose/ui/navigation-menu.css"; // places each content next to its trigger
|
|
8
|
+
import "@lumiose/ui/themes/default.css"; // optional: the default theme
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Anatomy
|
|
12
|
+
|
|
13
|
+
```tsx
|
|
14
|
+
<NavigationMenu.Root>
|
|
15
|
+
<NavigationMenu.List>
|
|
16
|
+
<NavigationMenu.Item>
|
|
17
|
+
<NavigationMenu.Trigger>
|
|
18
|
+
<NavigationMenu.Icon />
|
|
19
|
+
</NavigationMenu.Trigger>
|
|
20
|
+
<NavigationMenu.Content>
|
|
21
|
+
<NavigationMenu.Link />
|
|
22
|
+
</NavigationMenu.Content>
|
|
23
|
+
</NavigationMenu.Item>
|
|
24
|
+
<NavigationMenu.Item>
|
|
25
|
+
<NavigationMenu.Link />
|
|
26
|
+
</NavigationMenu.Item>
|
|
27
|
+
</NavigationMenu.List>
|
|
28
|
+
</NavigationMenu.Root>
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
| Part | Renders |
|
|
32
|
+
| --------- | ---------------------------------------------------------------------------------------------------------- |
|
|
33
|
+
| `Root` | `<nav data-orientation="…">` inside `<lu-navigation-menu>`, an element with no box; a `<div>` when nested. |
|
|
34
|
+
| `List` | `<ul>` of the items. |
|
|
35
|
+
| `Item` | `<li data-value="…">`: a trigger with its content, or a link. |
|
|
36
|
+
| `Trigger` | `<button type="button" popovertarget="…">`; `aria-expanded` follows its content where the script runs. |
|
|
37
|
+
| `Icon` | `<span aria-hidden="true">` inside the trigger, such as a chevron. |
|
|
38
|
+
| `Content` | `<div popover="auto" data-side="…" data-align="…">` of links, anchored to its trigger. |
|
|
39
|
+
| `Link` | `<a>`; `aria-current="page"` and `data-active` when `active`. |
|
|
40
|
+
|
|
41
|
+
Give `Root` an `aria-label` ("Main") when the page has more than one `<nav>`. A `Root` inside a
|
|
42
|
+
`Content` is a nested menu: a `<div>`, inside its parent's landmark. Focus, the pointer or a click
|
|
43
|
+
in it keep its parent's content open.
|
|
44
|
+
|
|
45
|
+
## API
|
|
46
|
+
|
|
47
|
+
### NavigationMenu.Root
|
|
48
|
+
|
|
49
|
+
| Prop | Type | Default | |
|
|
50
|
+
| --------------- | --------------------------------- | -------------- | ------------------------------------------------------------------------------------------------------------------- |
|
|
51
|
+
| `value` | `string \| null` | | The `value` of the item whose content is open, or `null`; opens or closes them when it changes, in the browser. |
|
|
52
|
+
| `defaultValue` | `string \| null` | `null` | The item open at first, in the browser, when `value` is left out. |
|
|
53
|
+
| `onValueChange` | `(value: string \| null) => void` | | Called after a content opens (its item's `value`) or the open one closes (`null`), whatever did it. |
|
|
54
|
+
| `bind:value` | `Signal<string \| null>` | | Stands in for `value` and `onValueChange`. |
|
|
55
|
+
| `delay` | `number` | `50` | How long the pointer rests on a trigger before its content opens, in milliseconds. |
|
|
56
|
+
| `closeDelay` | `number` | `50` | How long after the pointer leaves an item its content closes, in milliseconds (300 while it heads for the content). |
|
|
57
|
+
| `orientation` | `"horizontal" \| "vertical"` | `"horizontal"` | Which way the items run: vertical moves with Arrow Up and Down and places contents at the inline end. |
|
|
58
|
+
|
|
59
|
+
Server HTML always starts closed: a popover only opens from its trigger or script.
|
|
60
|
+
|
|
61
|
+
### NavigationMenu.Item
|
|
62
|
+
|
|
63
|
+
| Prop | Type | Default | |
|
|
64
|
+
| ------- | -------- | -------------- | --------------------------------------- |
|
|
65
|
+
| `value` | `string` | a generated id | The value `Root`'s `value` names it by. |
|
|
66
|
+
|
|
67
|
+
### NavigationMenu.Content
|
|
68
|
+
|
|
69
|
+
| Prop | Type | Default | |
|
|
70
|
+
| ------------- | -------------------------------------------------------------------------- | ----------------------------------------- | ----------------------------------------------- |
|
|
71
|
+
| `side` | `"top" \| "bottom" \| "left" \| "right" \| "inline-start" \| "inline-end"` | `"bottom"` (`"inline-end"` when vertical) | The trigger's side it goes on; it flips to fit. |
|
|
72
|
+
| `align` | `"start" \| "center" \| "end"` | `"center"` (`"start"` when vertical) | How it aligns with the trigger along that side. |
|
|
73
|
+
| `sideOffset` | `number` | | The gap to the trigger, in CSS pixels. |
|
|
74
|
+
| `alignOffset` | `number` | | A shift along the side, in CSS pixels. |
|
|
75
|
+
|
|
76
|
+
### NavigationMenu.Link
|
|
77
|
+
|
|
78
|
+
| Prop | Type | Default | |
|
|
79
|
+
| -------------- | --------- | ------- | ----------------------------------------------------------------------------- |
|
|
80
|
+
| `active` | `boolean` | `false` | The link to the current page: `aria-current="page"`. |
|
|
81
|
+
| `closeOnClick` | `boolean` | `false` | Whether following it closes the open content (every level, in a nested menu). |
|
|
82
|
+
|
|
83
|
+
### The other parts
|
|
84
|
+
|
|
85
|
+
Each part takes the props of the element it renders (`class`, `style`, `aria-*`, `data-*`, event
|
|
86
|
+
handlers) and passes them through: `href` and `target` on a `Link`, `disabled` and `id` on a
|
|
87
|
+
`Trigger`. A `Content` keeps its own `id`, which its trigger points at (see [Ids](README.md#ids)).
|
|
88
|
+
|
|
89
|
+
**Differences from Base UI's NavigationMenu.** Each `Content` is its own popover in the top layer,
|
|
90
|
+
anchored to its trigger: there is no shared `Viewport`, `Popup`, `Positioner`, `Portal`,
|
|
91
|
+
`Backdrop` or `Arrow`, and so no `--popup-width` and `--popup-height` to animate one box between
|
|
92
|
+
contents. `data-activation-direction` lets a content slide in from where the previous one was. No
|
|
93
|
+
`actionsRef`, `onOpenChangeComplete`, `keepMounted` or `render`.
|
|
94
|
+
|
|
95
|
+
## Example
|
|
96
|
+
|
|
97
|
+
A site's main navigation in a server-rendered page, with no island, styled by the default theme:
|
|
98
|
+
|
|
99
|
+
```tsx check
|
|
100
|
+
import { NavigationMenu } from "@lumiose/ui";
|
|
101
|
+
|
|
102
|
+
export function MainNavigation() {
|
|
103
|
+
return (
|
|
104
|
+
<NavigationMenu.Root aria-label="Main" class="lu-navigation-menu">
|
|
105
|
+
<NavigationMenu.List>
|
|
106
|
+
<NavigationMenu.Item>
|
|
107
|
+
<NavigationMenu.Trigger>
|
|
108
|
+
Products <NavigationMenu.Icon>▾</NavigationMenu.Icon>
|
|
109
|
+
</NavigationMenu.Trigger>
|
|
110
|
+
<NavigationMenu.Content>
|
|
111
|
+
<ul>
|
|
112
|
+
<li>
|
|
113
|
+
<NavigationMenu.Link href="#analytics">Analytics</NavigationMenu.Link>
|
|
114
|
+
</li>
|
|
115
|
+
<li>
|
|
116
|
+
<NavigationMenu.Link href="#automation">Automation</NavigationMenu.Link>
|
|
117
|
+
</li>
|
|
118
|
+
</ul>
|
|
119
|
+
</NavigationMenu.Content>
|
|
120
|
+
</NavigationMenu.Item>
|
|
121
|
+
<NavigationMenu.Item>
|
|
122
|
+
<NavigationMenu.Trigger>
|
|
123
|
+
Learn <NavigationMenu.Icon>▾</NavigationMenu.Icon>
|
|
124
|
+
</NavigationMenu.Trigger>
|
|
125
|
+
<NavigationMenu.Content>
|
|
126
|
+
<ul>
|
|
127
|
+
<li>
|
|
128
|
+
<NavigationMenu.Link href="#guides">Guides</NavigationMenu.Link>
|
|
129
|
+
</li>
|
|
130
|
+
<li>
|
|
131
|
+
<NavigationMenu.Link href="#blog">Blog</NavigationMenu.Link>
|
|
132
|
+
</li>
|
|
133
|
+
</ul>
|
|
134
|
+
</NavigationMenu.Content>
|
|
135
|
+
</NavigationMenu.Item>
|
|
136
|
+
<NavigationMenu.Item>
|
|
137
|
+
<NavigationMenu.Link href="#pricing" active>
|
|
138
|
+
Pricing
|
|
139
|
+
</NavigationMenu.Link>
|
|
140
|
+
</NavigationMenu.Item>
|
|
141
|
+
</NavigationMenu.List>
|
|
142
|
+
</NavigationMenu.Root>
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
In an island, the open item in a signal:
|
|
148
|
+
|
|
149
|
+
```tsx check
|
|
150
|
+
import { NavigationMenu } from "@lumiose/ui";
|
|
151
|
+
import { signal } from "lumiose";
|
|
152
|
+
|
|
153
|
+
export function SiteNavigation() {
|
|
154
|
+
const open = signal<string | null>(null);
|
|
155
|
+
return (
|
|
156
|
+
<>
|
|
157
|
+
<NavigationMenu.Root aria-label="Site" bind:value={open} class="lu-navigation-menu">
|
|
158
|
+
<NavigationMenu.List>
|
|
159
|
+
<NavigationMenu.Item value="docs">
|
|
160
|
+
<NavigationMenu.Trigger>Docs</NavigationMenu.Trigger>
|
|
161
|
+
<NavigationMenu.Content>
|
|
162
|
+
<NavigationMenu.Link href="#start">Getting started</NavigationMenu.Link>
|
|
163
|
+
</NavigationMenu.Content>
|
|
164
|
+
</NavigationMenu.Item>
|
|
165
|
+
<NavigationMenu.Item value="community">
|
|
166
|
+
<NavigationMenu.Trigger>Community</NavigationMenu.Trigger>
|
|
167
|
+
<NavigationMenu.Content>
|
|
168
|
+
<NavigationMenu.Link href="#forum">Forum</NavigationMenu.Link>
|
|
169
|
+
</NavigationMenu.Content>
|
|
170
|
+
</NavigationMenu.Item>
|
|
171
|
+
</NavigationMenu.List>
|
|
172
|
+
</NavigationMenu.Root>
|
|
173
|
+
<p>Open: {open() ?? "none"}</p>
|
|
174
|
+
</>
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
## Styling
|
|
180
|
+
|
|
181
|
+
**With the default theme**, put `lu-navigation-menu` on `Root`: the list in a row (a column when
|
|
182
|
+
vertical), the triggers and links as 36px targets, the current page underlined, an open trigger
|
|
183
|
+
filled with its icon turned, and each content a surface of links that fades and slides in.
|
|
184
|
+
|
|
185
|
+
**With your own CSS**, style the parts through `class`, the triggers' `[aria-expanded="true"]`,
|
|
186
|
+
links' `[aria-current="page"]`, and the contents' `:popover-open`, `[data-side]` and
|
|
187
|
+
`[data-activation-direction]` (`left`, `right`, `up` or `down`: where the content's trigger is from
|
|
188
|
+
the trigger of the content that was open before; absent when none was):
|
|
189
|
+
|
|
190
|
+
```css
|
|
191
|
+
.panel {
|
|
192
|
+
transition:
|
|
193
|
+
opacity 150ms,
|
|
194
|
+
translate 150ms,
|
|
195
|
+
overlay 150ms allow-discrete,
|
|
196
|
+
display 150ms allow-discrete;
|
|
197
|
+
}
|
|
198
|
+
.panel:not(:popover-open) {
|
|
199
|
+
opacity: 0;
|
|
200
|
+
}
|
|
201
|
+
@starting-style {
|
|
202
|
+
.panel[data-activation-direction="right"]:popover-open {
|
|
203
|
+
translate: 1rem 0;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
`@lumiose/ui/navigation-menu.css`, in the `lu.core` layer, holds the shared placement rules that put
|
|
209
|
+
each content next to its trigger, with the same flips as Popover and Menu, and gives
|
|
210
|
+
`<lu-navigation-menu>` no box.
|
|
211
|
+
|
|
212
|
+
## Keyboard and focus
|
|
213
|
+
|
|
214
|
+
| Key | Does |
|
|
215
|
+
| ----------------------- | ----------------------------------------------------------------------------------------------------------- |
|
|
216
|
+
| Tab, Shift+Tab | Every trigger and link is a tab stop; from an open trigger, Tab goes into its content. |
|
|
217
|
+
| Enter, Space | On a trigger: opens or closes its content. |
|
|
218
|
+
| Arrow Right, Arrow Left | Between the top-level triggers and links (Down and Up when vertical; swapped right to left). |
|
|
219
|
+
| Home, End | The first or last top-level trigger or link. |
|
|
220
|
+
| Arrow Down | On a trigger: opens its content on its first link (Arrow Right when vertical). In a content: the next link. |
|
|
221
|
+
| Arrow Up | In a content: the previous link. |
|
|
222
|
+
| Escape | Closes the open content and returns to its trigger. |
|
|
223
|
+
|
|
224
|
+
- **The pointer**: resting on a trigger opens its content after `delay`, at once while another one
|
|
225
|
+
is open; leaving the item closes it after `closeDelay`, unless a click kept it open. On the way
|
|
226
|
+
from a trigger to its open content, another trigger the pointer crosses waits 300ms before it
|
|
227
|
+
takes over, so a diagonal path into a wide panel keeps it. A click toggles a content; a click
|
|
228
|
+
outside closes it, but not a right-click or a press in a toast.
|
|
229
|
+
- **Focus** leaving an open item for elsewhere on the page closes its content. It is a navigation
|
|
230
|
+
landmark, not a menu: links keep their roles and the browser's link behaviour.
|
|
231
|
+
|
|
232
|
+
## Without JavaScript, and the client script
|
|
233
|
+
|
|
234
|
+
The `<nav>`, its links and the triggers are HTML: each trigger opens its content with
|
|
235
|
+
`popovertarget`, one at a time (an auto popover closes the others), and Escape or a click outside
|
|
236
|
+
closes it. `value`, `onValueChange` and `bind:value` need the component to run in an island.
|
|
237
|
+
|
|
238
|
+
A page that renders a `NavigationMenu` loads a 3.5 kB (brotli) script (its dismissal module is
|
|
239
|
+
Menu's and Popover's too), without the Lumiose runtime; islands bundle it. `<lu-navigation-menu>`
|
|
240
|
+
takes over the trigger's click (so a click keeps a content opened by hover open), opens contents on
|
|
241
|
+
hover, adds the arrow keys, closes a content when focus leaves its item, dismisses contents as Menu
|
|
242
|
+
and Popover do (a press of the main button outside, an Escape, another layer opening), keeps
|
|
243
|
+
`aria-expanded` and `data-activation-direction` current, and places the contents where CSS anchor
|
|
244
|
+
positioning is missing.
|
|
245
|
+
|
|
246
|
+
## Browser support
|
|
247
|
+
|
|
248
|
+
- **Popovers and `popovertarget`**: every current browser.
|
|
249
|
+
- **CSS anchor positioning**: Chrome and Edge 125, Firefox 147, Safari 26. Elsewhere the script
|
|
250
|
+
places the contents next to their triggers.
|
|
251
|
+
- **Exit animations** that keep a content in the top layer (`overlay`) are Chromium-only.
|
|
252
|
+
|
|
253
|
+
## Related
|
|
254
|
+
|
|
255
|
+
- [Menubar](menubar.md): a bar of application menus, with the menu roles.
|
|
256
|
+
- [Popover](popover.md): one popup anchored to its trigger.
|
|
257
|
+
- [UI components](README.md): the approach, styling and the other components.
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
# Number Field
|
|
2
|
+
|
|
3
|
+
> A number input with buttons to step it, after Base UI's NumberField and the APG spinbutton. Without `format` it is a native `<input type="number">`, which steps, validates and submits without JavaScript; with `format` (`Intl.NumberFormat` options: a currency, a percentage, grouping) it shows the formatted number, reads what is typed back in the same locale, and a hidden input submits the number. `<lu-number-field>`'s 2.6 kB script adds Page Up and Page Down, Home and End, press-and-hold buttons, a scrub area, the wheel when asked, and commit-time clamping. `bind:value` keeps a signal in sync.
|
|
4
|
+
|
|
5
|
+
```ts
|
|
6
|
+
import { NumberField } from "@lumiose/ui"; // or "@lumiose/ui/number-field"
|
|
7
|
+
import "@lumiose/ui/number-field.css"; // or "@lumiose/ui/styles.css" for every component
|
|
8
|
+
import "@lumiose/ui/themes/default.css"; // optional: the default theme
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Anatomy
|
|
12
|
+
|
|
13
|
+
```tsx
|
|
14
|
+
<NumberField.Root>
|
|
15
|
+
<NumberField.ScrubArea />
|
|
16
|
+
<NumberField.Group>
|
|
17
|
+
<NumberField.Decrement />
|
|
18
|
+
<NumberField.Input />
|
|
19
|
+
<NumberField.Increment />
|
|
20
|
+
</NumberField.Group>
|
|
21
|
+
</NumberField.Root>
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
| Part | Renders |
|
|
25
|
+
| ----------- | --------------------------------------------------------------------------------------------------------------------------------- |
|
|
26
|
+
| `Root` | `<lu-number-field>` with the range and format as attributes, and an `<input type="hidden">` that submits the number with `format` |
|
|
27
|
+
| `ScrubArea` | `<span data-scrub>`: dragging across it steps the number (usually around the label) |
|
|
28
|
+
| `Group` | `<div>` around the input and the buttons |
|
|
29
|
+
| `Input` | `<input type="number" inputmode min max step>`, or with `format` `<input type="text" role="spinbutton" aria-value…>` |
|
|
30
|
+
| `Increment` | `<button type="button" tabindex="-1" aria-label="Increase" aria-controls="…">`, disabled at `max` |
|
|
31
|
+
| `Decrement` | `<button type="button" tabindex="-1" aria-label="Decrease" aria-controls="…">`, disabled at `min` |
|
|
32
|
+
|
|
33
|
+
Name the field with a [Field](field.md) (`Field.Label`), a `<label for>` the Root's `id`, or an
|
|
34
|
+
`aria-label` on the `Input`.
|
|
35
|
+
|
|
36
|
+
## API
|
|
37
|
+
|
|
38
|
+
### NumberField.Root
|
|
39
|
+
|
|
40
|
+
| Prop | Type | Default | |
|
|
41
|
+
| ---------------------------------- | --------------------------------- | ------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
42
|
+
| `value` | `number \| null` | | Shows this number (`null`: empty) when it changes. Pair it with `onValueChange`. |
|
|
43
|
+
| `defaultValue` | `number \| null` | `null` | The number at first; a form reset goes back to it. |
|
|
44
|
+
| `onValueChange` | `(value: number \| null) => void` | | Called as the number changes: typing, a key, a button, the wheel, a drag. |
|
|
45
|
+
| `onValueCommitted` | `(value: number \| null) => void` | | Called when a change is done: the input left, a key pressed, a button or a drag released. |
|
|
46
|
+
| `bind:value` | `Signal<number \| null>` | | Stands in for `value` and `onValueChange`. |
|
|
47
|
+
| `min`, `max` | `number` | | The range. A typed number outside it becomes its end when committed. |
|
|
48
|
+
| `step` | `number` | `1` | The step of the arrow keys, the buttons, the wheel and a drag. |
|
|
49
|
+
| `largeStep` | `number` | `10` | The step of Page Up, Page Down, and Shift with an arrow key or a button. |
|
|
50
|
+
| `smallStep` | `number` | `0.1` | The step of Alt with an arrow key or a button. |
|
|
51
|
+
| `snapOnStep` | `boolean` | `false` | A step lands on the next multiple of its size (from `min`, or 0): 1.3 steps up to 2, not 2.3. |
|
|
52
|
+
| `format` | `Intl.NumberFormatOptions` | | How the number is shown; with it, the input is a text field and a hidden input submits the number. Options or a `locale` the browser refuses fall back to its defaults. |
|
|
53
|
+
| `locale` | `string` | the browser's | The locale of the formatted number (the server's in server HTML). |
|
|
54
|
+
| `allowWheelScrub` | `boolean` | `false` | The wheel steps the number while the input has focus and the pointer is over it. |
|
|
55
|
+
| `allowOutOfRange` | `boolean` | `false` | Typed numbers outside the range stay as typed, for the browser's validation to report. |
|
|
56
|
+
| `name`, `form` | `string` | | The name the number is submitted under, and the id of its form when it is outside it. |
|
|
57
|
+
| `id` | `string` | generated | The input's id, for a `<label for>`. |
|
|
58
|
+
| `required`, `disabled`, `readOnly` | `boolean` | `false` | As on a native input; read-only and disabled fields' buttons are disabled. |
|
|
59
|
+
|
|
60
|
+
### NumberField.ScrubArea
|
|
61
|
+
|
|
62
|
+
| Prop | Type | Default | |
|
|
63
|
+
| ------------------ | ---------------------------- | -------------- | ------------------------------------------------------------ |
|
|
64
|
+
| `direction` | `"horizontal" \| "vertical"` | `"horizontal"` | The drag that changes the number: right goes up, or up does. |
|
|
65
|
+
| `pixelSensitivity` | `number` | `2` | How many pixels of drag make one step. |
|
|
66
|
+
|
|
67
|
+
### The other parts
|
|
68
|
+
|
|
69
|
+
`Group` takes `<div>` props. `Input` takes `<input>` props except the range's own (`type`, `value`,
|
|
70
|
+
`min`, `max`, `step`, `name`, `required`, `readonly`, `id`: the Root's). `Increment` and
|
|
71
|
+
`Decrement` take `<button>` props; give them an `aria-label` in the page's language. Inside a
|
|
72
|
+
[Field](field.md), the input takes the field's id, name, description and error.
|
|
73
|
+
|
|
74
|
+
**Native or formatted.** A native number input cannot show grouping, a currency or a percentage,
|
|
75
|
+
and reads a typed number in the browser's own way, so a formatted field is a text field with the
|
|
76
|
+
spinbutton role, and the number it submits sits in a hidden input. The native input is the default
|
|
77
|
+
because it needs no JavaScript: typing, the arrow keys, `min`, `max` and `step` validation and the
|
|
78
|
+
form value are the browser's.
|
|
79
|
+
|
|
80
|
+
With `snapOnStep`, each kind of step snaps to its own size: Shift steps 37 up to 40, Alt steps
|
|
81
|
+
1.37 up to 1.4. A native field (no `format`) validates the value against `step` without
|
|
82
|
+
JavaScript; the script sets its `step` to `any`, so the values Alt's small step, Home, End and
|
|
83
|
+
clamping give always submit.
|
|
84
|
+
|
|
85
|
+
The keyboard: `inputmode="decimal"` when `min` is 0 or more, `"text"` otherwise, since iOS's
|
|
86
|
+
decimal keyboard has no minus sign. Digits of other scripts (full-width `1`, Arabic-Indic `٤`) are
|
|
87
|
+
read as numbers.
|
|
88
|
+
|
|
89
|
+
**Differences from Base UI's NumberField.** No `ScrubAreaCursor`: Base UI draws a cursor because
|
|
90
|
+
its scrub area locks the pointer (which hides the real one); this scrub area keeps the real cursor
|
|
91
|
+
(a resize cursor), so a drag stops at the screen's edge. No `inputRef`, no `render`. `locale` is a
|
|
92
|
+
string, written to the element for its script.
|
|
93
|
+
|
|
94
|
+
## Example
|
|
95
|
+
|
|
96
|
+
```tsx check
|
|
97
|
+
import { NumberField } from "@lumiose/ui";
|
|
98
|
+
import { signal } from "lumiose";
|
|
99
|
+
|
|
100
|
+
export function PriceField() {
|
|
101
|
+
const price = signal<number | null>(19.9);
|
|
102
|
+
return (
|
|
103
|
+
<NumberField.Root
|
|
104
|
+
id="price"
|
|
105
|
+
name="price"
|
|
106
|
+
bind:value={price}
|
|
107
|
+
min={0}
|
|
108
|
+
step={0.5}
|
|
109
|
+
format={{ style: "currency", currency: "EUR" }}
|
|
110
|
+
class="lu-number-field"
|
|
111
|
+
>
|
|
112
|
+
<NumberField.ScrubArea>
|
|
113
|
+
<label for="price">Price</label>
|
|
114
|
+
</NumberField.ScrubArea>
|
|
115
|
+
<NumberField.Group>
|
|
116
|
+
<NumberField.Decrement aria-label="Lower the price">−</NumberField.Decrement>
|
|
117
|
+
<NumberField.Input />
|
|
118
|
+
<NumberField.Increment aria-label="Raise the price">+</NumberField.Increment>
|
|
119
|
+
</NumberField.Group>
|
|
120
|
+
</NumberField.Root>
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
## Styling
|
|
126
|
+
|
|
127
|
+
**With the default theme**, put `lu-number-field` on `Root`: the label above one bordered group,
|
|
128
|
+
the input centred between two 36px buttons that dim at the ends of the range, and a focus ring
|
|
129
|
+
around the whole group.
|
|
130
|
+
|
|
131
|
+
**With your own CSS**, style the parts through `class`, the input's `:focus-visible`, the buttons'
|
|
132
|
+
`:disabled` and `[data-action="increment"]` / `[data-action="decrement"]`, the scrub area's
|
|
133
|
+
`[data-scrub]`, and `lu-number-field[data-scrubbing]` while a drag is on.
|
|
134
|
+
|
|
135
|
+
`@lumiose/ui/number-field.css` (0.2 kB brotli) hides the native spinners once the script runs
|
|
136
|
+
(`:defined`) and hides the buttons until then, keeps held buttons from selecting text, and gives a
|
|
137
|
+
scrub area its resize cursor and `touch-action: none`.
|
|
138
|
+
|
|
139
|
+
## Keyboard and focus
|
|
140
|
+
|
|
141
|
+
| Key | Does |
|
|
142
|
+
| ----------------------- | ------------------------------------------------------------------------- |
|
|
143
|
+
| Arrow Up / Arrow Down | One step up or down. |
|
|
144
|
+
| Shift + Arrow Up / Down | `largeStep` up or down. |
|
|
145
|
+
| Alt + Arrow Up / Down | `smallStep` up or down. |
|
|
146
|
+
| Page Up / Page Down | `largeStep` up or down. |
|
|
147
|
+
| Home / End | `min` or `max`, when set. |
|
|
148
|
+
| Enter | Commits the typed number (clamped, formatted); a form submits. |
|
|
149
|
+
| Tab | Moves on: one tab stop, the input (the buttons are not in the tab order). |
|
|
150
|
+
|
|
151
|
+
Right to left, dragging the scrub area toward the left steps up, as a slider runs. A press on a
|
|
152
|
+
button steps once (by `largeStep` with Shift, `smallStep` with Alt) and, with a mouse, puts focus
|
|
153
|
+
in the input, so the arrow keys go on stepping (a touch leaves focus where it is, so no virtual
|
|
154
|
+
keyboard opens); holding it steps again every 60 ms after 400 ms. A click from a keyboard or a screen reader on a button steps once. Every drag (the
|
|
155
|
+
scrub area) has the arrow keys as its alternative (WCAG 2.5.7). The wheel steps the number only
|
|
156
|
+
with `allowWheelScrub`, while the input has focus and the pointer is over it; otherwise the wheel
|
|
157
|
+
scrolls the page, as over any field (Chromium's own stepping of a focused number input is turned
|
|
158
|
+
off). Ctrl with the wheel zooms the page, and a sideways swipe never steps. Text that is no number
|
|
159
|
+
(Firefox lets letters into a number input) goes on commit, and the last number comes back.
|
|
160
|
+
|
|
161
|
+
## Without JavaScript, and the element's script
|
|
162
|
+
|
|
163
|
+
Without `format`, the native number input works and submits with no script; the buttons, which
|
|
164
|
+
need it, stay hidden and the native spinners show. With `format`, the text input submits its text
|
|
165
|
+
as typed until the script moves the name to the hidden input, so a form that must work without
|
|
166
|
+
JavaScript should leave `format` out.
|
|
167
|
+
|
|
168
|
+
`<lu-number-field>`'s script (2.6 kB brotli) adds the large step, Home and End, the buttons, the
|
|
169
|
+
scrub area and the wheel, clamps and formats on commit, keeps the hidden input, `aria-valuenow`
|
|
170
|
+
and `aria-valuetext` current, and follows a form reset.
|
|
171
|
+
|
|
172
|
+
## Browser support
|
|
173
|
+
|
|
174
|
+
Native number inputs and `Intl.NumberFormat` (with `formatToParts`): every supported browser.
|
|
175
|
+
Scrubbing uses pointer capture, without pointer lock.
|
|
176
|
+
|
|
177
|
+
## Related
|
|
178
|
+
|
|
179
|
+
- [Field](field.md): a label, a description and errors for a number field.
|
|
180
|
+
- [Slider](slider.md): a number picked by dragging along a range.
|
|
181
|
+
- [Input](input.md): any other text.
|