@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,374 @@
|
|
|
1
|
+
// Slider (decision-62, TASK-372): one or more native `<input type="range">` thumbs over a shared
|
|
2
|
+
// track, after Base UI's Slider. The platform drags each thumb, moves it with the arrow keys,
|
|
3
|
+
// Home and End, follows the direction and submits it, in server HTML and without JavaScript.
|
|
4
|
+
// Root renders `<lu-slider>`, whose script (slider.script.ts) keeps two thumbs in order, moves the
|
|
5
|
+
// nearest thumb on a press on the track, adds Page Up and Page Down, and keeps `--lu-slider-start`,
|
|
6
|
+
// `--lu-slider-end` and the value text current. The `value` prop drives the thumbs when it changes;
|
|
7
|
+
// their `input` and `change` events report back through `onValueChange` and `onValueCommitted`.
|
|
8
|
+
import { createContext, effect, onCleanup, ref, signal, untrack, useContext, useId } from "lumiose";
|
|
9
|
+
import { controlId, describedBy, useField, useFormValues } from "../field/context.ts";
|
|
10
|
+
import { onFormReset } from "../form/reset.ts";
|
|
11
|
+
import { withStyle } from "../placement/placement.ts";
|
|
12
|
+
import { percent } from "../progress/value.ts";
|
|
13
|
+
import { formatValues } from "./format.ts";
|
|
14
|
+
import type { SliderElement } from "./slider.script.ts";
|
|
15
|
+
import sliderScript from "./slider.script.ts?client";
|
|
16
|
+
|
|
17
|
+
declare module "lumiose/jsx-runtime" {
|
|
18
|
+
namespace JSX {
|
|
19
|
+
interface IntrinsicElements {
|
|
20
|
+
/**
|
|
21
|
+
* Slider's element. Attributes `large-step`, `min-steps-between-values`,
|
|
22
|
+
* `thumb-collision-behavior` (`push`, `swap`, `none`), `format` (`Intl.NumberFormat` options
|
|
23
|
+
* as JSON) and `locale`; property `value`.
|
|
24
|
+
*/
|
|
25
|
+
"lu-slider": JSX.HTMLProps<SliderElement> & {
|
|
26
|
+
"large-step"?: number;
|
|
27
|
+
"min-steps-between-values"?: number;
|
|
28
|
+
"thumb-collision-behavior"?: "push" | "swap" | "none";
|
|
29
|
+
format?: string;
|
|
30
|
+
locale?: string;
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** A slider's value: a number, or one per thumb. */
|
|
37
|
+
export type SliderValue = number | number[];
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* The element props Root passes to `<lu-slider>`, without the brand of intrinsic elements' props,
|
|
41
|
+
* so that `bind:value` applies to Root.
|
|
42
|
+
*/
|
|
43
|
+
type ElementProps = {
|
|
44
|
+
[
|
|
45
|
+
K in keyof JSX.HTMLProps<SliderElement> as K extends string ? K : never
|
|
46
|
+
]: JSX.HTMLProps<SliderElement>[K];
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* `Slider.Root` renders `<lu-slider>`, a block around the other parts. `bind:value={signal}`
|
|
51
|
+
* stands in for `value` and `onValueChange`; an array (one value per thumb) makes a range slider.
|
|
52
|
+
*/
|
|
53
|
+
export interface SliderRootProps<V extends SliderValue = number> extends Omit<
|
|
54
|
+
ElementProps,
|
|
55
|
+
"children" | "onChange" | "onInput"
|
|
56
|
+
> {
|
|
57
|
+
/** Moves the thumbs to this value (an array: one per thumb) when it changes. */
|
|
58
|
+
value?: V;
|
|
59
|
+
/** The value at first, when `value` is left out; a form reset goes back to it. Default `min`. */
|
|
60
|
+
defaultValue?: V;
|
|
61
|
+
/** Called with the new value as a thumb moves. */
|
|
62
|
+
onValueChange?: (value: V) => void;
|
|
63
|
+
/** Called with the value when a move ends: a release, a key press. */
|
|
64
|
+
onValueCommitted?: (value: V) => void;
|
|
65
|
+
/** The smallest value. Default `0`. */
|
|
66
|
+
min?: number;
|
|
67
|
+
/** The largest value. Default `100`. */
|
|
68
|
+
max?: number;
|
|
69
|
+
/** The step between values. Default `1`. */
|
|
70
|
+
step?: number;
|
|
71
|
+
/** The step of Page Up, Page Down and Shift with an arrow key. Default `10`. */
|
|
72
|
+
largeStep?: number;
|
|
73
|
+
/** With several thumbs: how many steps apart they stay. Default `0`. */
|
|
74
|
+
minStepsBetweenValues?: number;
|
|
75
|
+
/**
|
|
76
|
+
* With several thumbs: a thumb moved into another pushes it along (`"push"`, default), stops at
|
|
77
|
+
* it (`"none"`), or, dragged past it, swaps places with it (`"swap"`: the drag goes on with the
|
|
78
|
+
* other thumb; the keys stop at it).
|
|
79
|
+
*/
|
|
80
|
+
thumbCollisionBehavior?: "push" | "swap" | "none";
|
|
81
|
+
/**
|
|
82
|
+
* Where the thumbs stop at the ends: inside the control (`"edge"`, default, as native range
|
|
83
|
+
* inputs do) or with their centre on its ends (`"center"`, overhanging it by half a thumb).
|
|
84
|
+
* Both need `--lu-slider-thumb-size`, the thumbs' size, in your CSS (the default theme sets it).
|
|
85
|
+
*/
|
|
86
|
+
thumbAlignment?: "edge" | "center";
|
|
87
|
+
/** Left to right (and the other way right to left), or bottom to top. Default `"horizontal"`. */
|
|
88
|
+
orientation?: "horizontal" | "vertical";
|
|
89
|
+
/** Disables every thumb. */
|
|
90
|
+
disabled?: boolean;
|
|
91
|
+
/** The name the thumbs submit their values under (one entry per thumb). */
|
|
92
|
+
name?: string;
|
|
93
|
+
/** The id of a form the thumbs belong to, when they are outside it. */
|
|
94
|
+
form?: string;
|
|
95
|
+
/**
|
|
96
|
+
* How the values are written: `Intl.NumberFormat` options. With it, each thumb's
|
|
97
|
+
* `aria-valuetext` is its formatted value. Default: plain numbers.
|
|
98
|
+
*/
|
|
99
|
+
format?: Intl.NumberFormatOptions;
|
|
100
|
+
/** The locale of the formatted values. Default: the browser's (the server's in server HTML). */
|
|
101
|
+
locale?: string;
|
|
102
|
+
/** The other parts: `Label`, `Value` and the `Control` with the track and the thumbs. */
|
|
103
|
+
children?: JSX.Element;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** The `<label>` of the slider, for its first thumb. */
|
|
107
|
+
export type SliderLabelProps = JSX.IntrinsicElements["label"];
|
|
108
|
+
|
|
109
|
+
/** An `<output>` with the values as text. */
|
|
110
|
+
export type SliderValueProps = Omit<JSX.IntrinsicElements["output"], "children"> & {
|
|
111
|
+
/** Renders the values your way, from the formatted values and the numbers. */
|
|
112
|
+
children?: (formattedValues: string[], values: number[]) => JSX.Element;
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* The `<div>` that holds the track and the thumbs, stacked; presses on it move a thumb. With two
|
|
117
|
+
* thumbs or more it is a `role="group"` the Label names.
|
|
118
|
+
*/
|
|
119
|
+
export type SliderControlProps = JSX.IntrinsicElements["div"];
|
|
120
|
+
|
|
121
|
+
/** The `<div>` rail of the track. */
|
|
122
|
+
export type SliderTrackProps = JSX.IntrinsicElements["div"];
|
|
123
|
+
|
|
124
|
+
/** The `<div>` that fills the selected part of the track, from `--lu-slider-start` to `--lu-slider-end`. */
|
|
125
|
+
export type SliderIndicatorProps = JSX.IntrinsicElements["div"];
|
|
126
|
+
|
|
127
|
+
/** A thumb: a native `<input type="range">`, the next value of the slider in order. */
|
|
128
|
+
export type SliderThumbProps = Omit<
|
|
129
|
+
JSX.IntrinsicElements["input"],
|
|
130
|
+
symbol | "type" | "value" | "min" | "max" | "step" | "name" | "form"
|
|
131
|
+
>;
|
|
132
|
+
|
|
133
|
+
interface SliderContextValue {
|
|
134
|
+
id: string;
|
|
135
|
+
/** The Label's id: its own `id`, which the Control and thumbs after it point at (M39). */
|
|
136
|
+
labelId: string;
|
|
137
|
+
/** A thumb's id: in a Field the first one is the field's control (TASK-449 L22). */
|
|
138
|
+
thumbId: (index: number) => string;
|
|
139
|
+
values: () => number[];
|
|
140
|
+
min: () => number;
|
|
141
|
+
max: () => number;
|
|
142
|
+
step: () => number;
|
|
143
|
+
vertical: () => boolean;
|
|
144
|
+
disabled: () => boolean;
|
|
145
|
+
name: () => string | undefined;
|
|
146
|
+
form: () => string | undefined;
|
|
147
|
+
texts: () => string[];
|
|
148
|
+
formatted: () => boolean;
|
|
149
|
+
/** How many thumbs have rendered: the next thumb's index. */
|
|
150
|
+
thumbs: { count: number };
|
|
151
|
+
labels: { count: number };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const SliderContext = createContext<SliderContextValue>();
|
|
155
|
+
|
|
156
|
+
function useSlider(part: string): SliderContextValue {
|
|
157
|
+
const context = useContext(SliderContext);
|
|
158
|
+
if (!context) throw new Error(`Slider.${part} must be inside <Slider.Root>.`);
|
|
159
|
+
return context;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const asList = (value: SliderValue | undefined, fallback: number): number[] =>
|
|
163
|
+
value === undefined ? [fallback] : typeof value === "number" ? [value] : [...value];
|
|
164
|
+
|
|
165
|
+
function Root<V extends SliderValue = number>({
|
|
166
|
+
value,
|
|
167
|
+
defaultValue,
|
|
168
|
+
onValueChange,
|
|
169
|
+
onValueCommitted,
|
|
170
|
+
min,
|
|
171
|
+
max,
|
|
172
|
+
step,
|
|
173
|
+
largeStep,
|
|
174
|
+
minStepsBetweenValues,
|
|
175
|
+
thumbCollisionBehavior,
|
|
176
|
+
thumbAlignment,
|
|
177
|
+
orientation,
|
|
178
|
+
disabled,
|
|
179
|
+
name,
|
|
180
|
+
form,
|
|
181
|
+
format,
|
|
182
|
+
locale,
|
|
183
|
+
children,
|
|
184
|
+
style,
|
|
185
|
+
...rest
|
|
186
|
+
}: SliderRootProps<V>) {
|
|
187
|
+
// Thumb order, Page Up and Page Down, track presses and the value text (decision-63).
|
|
188
|
+
sliderScript();
|
|
189
|
+
const id = useId();
|
|
190
|
+
const field = useField();
|
|
191
|
+
const element = ref<SliderElement>();
|
|
192
|
+
const low = () => min ?? 0;
|
|
193
|
+
const high = () => max ?? 100;
|
|
194
|
+
// A failed submission's values (a Form's `values`), one per thumb, replace the default (TASK-430).
|
|
195
|
+
const sent = useFormValues(
|
|
196
|
+
untrack(() => name ?? field?.name()),
|
|
197
|
+
untrack(() => disabled),
|
|
198
|
+
)
|
|
199
|
+
?.map(Number)
|
|
200
|
+
.filter(Number.isFinite);
|
|
201
|
+
const values = signal<number[]>(
|
|
202
|
+
untrack(() => asList(value ?? (sent?.length ? sent : defaultValue), min ?? 0)),
|
|
203
|
+
);
|
|
204
|
+
// The `value` prop drives the thumbs when it changes.
|
|
205
|
+
effect(() => {
|
|
206
|
+
const next = value;
|
|
207
|
+
if (next === undefined) return;
|
|
208
|
+
values.set(asList(next, low()));
|
|
209
|
+
const host = element();
|
|
210
|
+
if (host?.isConnected) host.value = next;
|
|
211
|
+
});
|
|
212
|
+
const context: SliderContextValue = {
|
|
213
|
+
id,
|
|
214
|
+
labelId: `${id}-label`,
|
|
215
|
+
thumbId: (index) => (index === 0 && field ? controlId(field) : `${id}-thumb-${index}`),
|
|
216
|
+
values,
|
|
217
|
+
min: low,
|
|
218
|
+
max: high,
|
|
219
|
+
step: () => step ?? 1,
|
|
220
|
+
vertical: () => orientation === "vertical",
|
|
221
|
+
disabled: () => disabled === true || field?.disabled() === true,
|
|
222
|
+
name: () => name ?? field?.name(),
|
|
223
|
+
form: () => form,
|
|
224
|
+
texts: () => formatValues(values(), format ? JSON.stringify(format) : null, locale ?? null),
|
|
225
|
+
formatted: () => format !== undefined,
|
|
226
|
+
thumbs: { count: 0 },
|
|
227
|
+
labels: { count: 0 },
|
|
228
|
+
};
|
|
229
|
+
const current = () => (element()?.value ?? values()) as V;
|
|
230
|
+
// A form reset puts the thumbs back without an `input` event: it is reported (TASK-435).
|
|
231
|
+
onFormReset(
|
|
232
|
+
() => element()?.thumbs[0],
|
|
233
|
+
current,
|
|
234
|
+
(next) => {
|
|
235
|
+
values.set(asList(next, low()));
|
|
236
|
+
onValueChange?.(next);
|
|
237
|
+
},
|
|
238
|
+
);
|
|
239
|
+
// The selected part of the track, in the server HTML too; the script keeps it from then on.
|
|
240
|
+
const range = () => {
|
|
241
|
+
const list = values();
|
|
242
|
+
return {
|
|
243
|
+
"--lu-slider-start": `${list.length > 1 ? percent(list[0]!, low(), high()) : 0}%`,
|
|
244
|
+
"--lu-slider-end": `${percent(list.at(-1)!, low(), high())}%`,
|
|
245
|
+
};
|
|
246
|
+
};
|
|
247
|
+
// Nothing points at the element: a caller's `id` names it (TASK-441 M39).
|
|
248
|
+
return (
|
|
249
|
+
<SliderContext value={context}>
|
|
250
|
+
<lu-slider
|
|
251
|
+
{...rest}
|
|
252
|
+
ref={element}
|
|
253
|
+
id={rest.id ?? id}
|
|
254
|
+
data-orientation={orientation ?? "horizontal"}
|
|
255
|
+
data-thumb-alignment={thumbAlignment}
|
|
256
|
+
large-step={largeStep}
|
|
257
|
+
min-steps-between-values={minStepsBetweenValues}
|
|
258
|
+
thumb-collision-behavior={thumbCollisionBehavior}
|
|
259
|
+
format={format ? JSON.stringify(format) : undefined}
|
|
260
|
+
locale={locale}
|
|
261
|
+
style={withStyle(style, untrack(range))}
|
|
262
|
+
on:input={() => {
|
|
263
|
+
const next = current();
|
|
264
|
+
values.set(asList(next, low()));
|
|
265
|
+
onValueChange?.(next);
|
|
266
|
+
}}
|
|
267
|
+
on:change={() => onValueCommitted?.(current())}
|
|
268
|
+
>
|
|
269
|
+
{children}
|
|
270
|
+
</lu-slider>
|
|
271
|
+
</SliderContext>
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function Label({ children, ...rest }: SliderLabelProps) {
|
|
276
|
+
const slider = useSlider("Label");
|
|
277
|
+
// A caller's `id` names it; the Control and the thumbs after it point at that (TASK-441 M39).
|
|
278
|
+
if (rest.id) slider.labelId = rest.id;
|
|
279
|
+
slider.labels.count++;
|
|
280
|
+
return (
|
|
281
|
+
<label {...rest} id={slider.labelId} for={slider.thumbId(0)}>
|
|
282
|
+
{children}
|
|
283
|
+
</label>
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function Value({ children, ...rest }: SliderValueProps) {
|
|
288
|
+
const slider = useSlider("Value");
|
|
289
|
+
// Showing the values, it is `for` the thumbs and the script keeps it current.
|
|
290
|
+
const thumbs = () =>
|
|
291
|
+
slider
|
|
292
|
+
.values()
|
|
293
|
+
.map((_, i) => slider.thumbId(i))
|
|
294
|
+
.join(" ");
|
|
295
|
+
return (
|
|
296
|
+
// Not a live region: the thumbs announce their values (TASK-439, M31).
|
|
297
|
+
<output aria-live="off" {...rest} for={children ? undefined : thumbs()}>
|
|
298
|
+
{children ? children(slider.texts(), slider.values()) : slider.texts().join(" – ")}
|
|
299
|
+
</output>
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function Control({ children, ...rest }: SliderControlProps) {
|
|
304
|
+
const slider = useSlider("Control");
|
|
305
|
+
// A range's thumbs, as one group the Label names; each thumb takes a name of its own.
|
|
306
|
+
const range = untrack(() => slider.values().length > 1);
|
|
307
|
+
const labelled = untrack(() => slider.labels.count > 0);
|
|
308
|
+
return (
|
|
309
|
+
<div
|
|
310
|
+
role={range ? "group" : undefined}
|
|
311
|
+
aria-labelledby={
|
|
312
|
+
range && labelled && rest["aria-label"] === undefined ? slider.labelId : undefined
|
|
313
|
+
}
|
|
314
|
+
{...rest}
|
|
315
|
+
>
|
|
316
|
+
{children}
|
|
317
|
+
</div>
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function Track({ children, ...rest }: SliderTrackProps) {
|
|
322
|
+
useSlider("Track");
|
|
323
|
+
return <div {...rest}>{children}</div>;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function Indicator(props: SliderIndicatorProps) {
|
|
327
|
+
useSlider("Indicator");
|
|
328
|
+
return <div {...props} />;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function Thumb(props: SliderThumbProps) {
|
|
332
|
+
const slider = useSlider("Thumb");
|
|
333
|
+
const field = useField();
|
|
334
|
+
const index = slider.thumbs.count++;
|
|
335
|
+
onCleanup(() => slider.thumbs.count--);
|
|
336
|
+
const labelled = untrack(() => slider.labels.count > 0);
|
|
337
|
+
return (
|
|
338
|
+
<input
|
|
339
|
+
aria-labelledby={labelled && props["aria-label"] === undefined ? slider.labelId : undefined}
|
|
340
|
+
aria-describedby={field ? describedBy(field) : undefined}
|
|
341
|
+
aria-invalid={field?.invalid() ? "true" : undefined}
|
|
342
|
+
{...props}
|
|
343
|
+
type="range"
|
|
344
|
+
id={slider.thumbId(index)}
|
|
345
|
+
min={slider.min()}
|
|
346
|
+
max={slider.max()}
|
|
347
|
+
step={slider.step()}
|
|
348
|
+
value={slider.values()[index] ?? slider.min()}
|
|
349
|
+
name={slider.name()}
|
|
350
|
+
form={slider.form()}
|
|
351
|
+
disabled={props.disabled || slider.disabled() || undefined}
|
|
352
|
+
aria-orientation={slider.vertical() ? "vertical" : undefined}
|
|
353
|
+
aria-valuetext={slider.formatted() ? slider.texts()[index] : props["aria-valuetext"]}
|
|
354
|
+
/>
|
|
355
|
+
);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* A slider: one value (one thumb) or a range (two thumbs), picked by dragging or with the keys.
|
|
360
|
+
* Native range inputs, so it works and submits without JavaScript. Unstyled: style the parts
|
|
361
|
+
* through `class`, the thumbs' pseudo-elements, `--lu-slider-start` and `--lu-slider-end`.
|
|
362
|
+
*
|
|
363
|
+
* @example
|
|
364
|
+
* <Slider.Root name="volume" defaultValue={40}>
|
|
365
|
+
* <Slider.Label>Volume</Slider.Label>
|
|
366
|
+
* <Slider.Control>
|
|
367
|
+
* <Slider.Track>
|
|
368
|
+
* <Slider.Indicator />
|
|
369
|
+
* </Slider.Track>
|
|
370
|
+
* <Slider.Thumb />
|
|
371
|
+
* </Slider.Control>
|
|
372
|
+
* </Slider.Root>
|
|
373
|
+
*/
|
|
374
|
+
export const Slider = { Root, Label, Value, Control, Track, Indicator, Thumb };
|
package/dist/styles.css
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* The CSS every @lumiose/ui component needs to work, and nothing else: the package is unstyled
|
|
3
|
+
* (decision-62). It sits in the `lu.core` layer, below the optional theme's `lu.theme`
|
|
4
|
+
* (`@lumiose/ui/themes/default.css`), so any app CSS outside layers wins over both.
|
|
5
|
+
*/
|
|
6
|
+
@layer lu.core, lu.theme;
|
|
7
|
+
|
|
8
|
+
@import "./dialog/dialog.css";
|
|
9
|
+
@import "./popover/popover.css";
|
|
10
|
+
@import "./tooltip/tooltip.css";
|
|
11
|
+
@import "./tabs/tabs.css";
|
|
12
|
+
@import "./menu/menu.css";
|
|
13
|
+
@import "./combobox/combobox.css";
|
|
14
|
+
@import "./select/select.css";
|
|
15
|
+
@import "./field/field.css";
|
|
16
|
+
@import "./collapsible/collapsible.css";
|
|
17
|
+
@import "./accordion/accordion.css";
|
|
18
|
+
@import "./toast/toast.css";
|
|
19
|
+
@import "./slider/slider.css";
|
|
20
|
+
@import "./context-menu/context-menu.css";
|
|
21
|
+
@import "./drawer/drawer.css";
|
|
22
|
+
@import "./number-field/number-field.css";
|
|
23
|
+
@import "./separator/separator.css";
|
|
24
|
+
@import "./avatar/avatar.css";
|
|
25
|
+
@import "./otp-field/otp-field.css";
|
|
26
|
+
@import "./preview-card/preview-card.css";
|
|
27
|
+
@import "./toolbar/toolbar.css";
|
|
28
|
+
@import "./scroll-area/scroll-area.css";
|
|
29
|
+
@import "./menubar/menubar.css";
|
|
30
|
+
@import "./navigation-menu/navigation-menu.css";
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
// Switch (decision-62, TASK-368): a native `<input type="checkbox" role="switch">`, after Base UI's
|
|
2
|
+
// Switch: on or off, announced as a switch. Space and a label click toggle it, the form submits its
|
|
3
|
+
// value when on, reset and `required` work, in server HTML and without JavaScript (`<input switch>`
|
|
4
|
+
// exists only in Safari). Inside a Field it takes the field's id, name and ARIA references;
|
|
5
|
+
// checkbox.script.ts adds read-only.
|
|
6
|
+
import { ref, untrack } from "lumiose";
|
|
7
|
+
import checkboxScript from "../checkbox/checkbox.script.ts?client";
|
|
8
|
+
import { fieldProps, useField, useFormValues } from "../field/context.ts";
|
|
9
|
+
import { onFormReset } from "../form/reset.ts";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* The native checkbox props Root passes through, without the brand of intrinsic elements' props,
|
|
13
|
+
* so that `bind:checked` applies to Root.
|
|
14
|
+
*/
|
|
15
|
+
type InputProps = Omit<
|
|
16
|
+
JSX.IntrinsicElements["input"],
|
|
17
|
+
symbol | "type" | "role" | "checked" | "value" | "bind:checked" | "bind:value" | "readonly"
|
|
18
|
+
>;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* `Switch.Root` renders an `<input type="checkbox" role="switch">`. `bind:checked={signal}`
|
|
22
|
+
* stands in for `checked` and `onCheckedChange`.
|
|
23
|
+
*/
|
|
24
|
+
export type SwitchRootProps = InputProps & {
|
|
25
|
+
/** Turns it on (`true`) or off (`false`) when it changes. Pair it with `onCheckedChange`. */
|
|
26
|
+
checked?: boolean;
|
|
27
|
+
/** Whether it starts on when `checked` is left out; a form reset goes back to it. */
|
|
28
|
+
defaultChecked?: boolean;
|
|
29
|
+
/** Called after the user turns it on or off. */
|
|
30
|
+
onCheckedChange?: (checked: boolean) => void;
|
|
31
|
+
/** The value it submits when on. Default `"on"`. */
|
|
32
|
+
value?: string;
|
|
33
|
+
/** It cannot be toggled, but stays focusable and is submitted (`aria-readonly`). */
|
|
34
|
+
readOnly?: boolean;
|
|
35
|
+
/**
|
|
36
|
+
* The value it submits when off; by default a switch that is off submits nothing. Needs the
|
|
37
|
+
* client script (`data-unchecked-value`).
|
|
38
|
+
*/
|
|
39
|
+
uncheckedValue?: string;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
function Root({
|
|
43
|
+
checked,
|
|
44
|
+
defaultChecked,
|
|
45
|
+
onCheckedChange,
|
|
46
|
+
value,
|
|
47
|
+
readOnly,
|
|
48
|
+
uncheckedValue,
|
|
49
|
+
...rest
|
|
50
|
+
}: SwitchRootProps) {
|
|
51
|
+
checkboxScript();
|
|
52
|
+
const field = useField();
|
|
53
|
+
// A failed submission's values (a Form's `values`): on when they hold its value (TASK-430).
|
|
54
|
+
const sent = useFormValues(
|
|
55
|
+
untrack(() => rest.name ?? field?.name()),
|
|
56
|
+
untrack(() => rest.disabled),
|
|
57
|
+
);
|
|
58
|
+
const initial = untrack(() => (sent ? sent.includes(value ?? "on") : defaultChecked));
|
|
59
|
+
// A form reset fires no `change`: the reset state goes to `onCheckedChange` (TASK-435).
|
|
60
|
+
const input = ref<HTMLInputElement>();
|
|
61
|
+
if (onCheckedChange) onFormReset(input, () => input()!.checked, onCheckedChange);
|
|
62
|
+
return (
|
|
63
|
+
<input
|
|
64
|
+
{...rest}
|
|
65
|
+
{...fieldProps(field, rest)}
|
|
66
|
+
ref={(element: HTMLInputElement) => {
|
|
67
|
+
input(element);
|
|
68
|
+
rest.ref?.(element);
|
|
69
|
+
}}
|
|
70
|
+
type="checkbox"
|
|
71
|
+
role="switch"
|
|
72
|
+
value={value}
|
|
73
|
+
checked={checked ?? initial}
|
|
74
|
+
aria-readonly={readOnly ? "true" : undefined}
|
|
75
|
+
data-unchecked-value={uncheckedValue}
|
|
76
|
+
on:change={(event: Event) =>
|
|
77
|
+
onCheckedChange?.((event.currentTarget as HTMLInputElement).checked)
|
|
78
|
+
}
|
|
79
|
+
/>
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* A native switch: a checkbox announced as a switch. Unstyled: style it through `class` and
|
|
85
|
+
* `:checked`, `:disabled` and `:focus-visible`, with `appearance: none` to draw the track and thumb.
|
|
86
|
+
*
|
|
87
|
+
* @example
|
|
88
|
+
* <label>
|
|
89
|
+
* <Switch.Root name="notifications" defaultChecked /> Notifications
|
|
90
|
+
* </label>
|
|
91
|
+
*/
|
|
92
|
+
export const Switch = { Root };
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Tabs need almost no CSS: `hidden` hides the other panels. `<lu-tabs>`, a custom element, is
|
|
3
|
+
* inline until styled: it is a block, like the `<div>` it stands for.
|
|
4
|
+
*
|
|
5
|
+
* Tabs.Indicator (TASK-395) covers the selected tab's box. CSS anchor positioning places it from
|
|
6
|
+
* the server HTML on (the selected tab is the anchor `--lu-tabs-anchor` names), with no script and
|
|
7
|
+
* no shift on load; once the script runs, its `--lu-tabs-indicator-*` variables take over (a theme
|
|
8
|
+
* may transition them), and they place it where anchor positioning is missing. It lies behind the
|
|
9
|
+
* tabs' text (the list isolates it), so a highlight does not cover the text, and contrast checkers
|
|
10
|
+
* see the tab on top.
|
|
11
|
+
*/
|
|
12
|
+
@layer lu.core, lu.theme;
|
|
13
|
+
|
|
14
|
+
@layer lu.core {
|
|
15
|
+
lu-tabs {
|
|
16
|
+
display: block;
|
|
17
|
+
}
|
|
18
|
+
lu-tabs [role="tablist"]:has(> [data-indicator]) {
|
|
19
|
+
position: relative;
|
|
20
|
+
isolation: isolate;
|
|
21
|
+
}
|
|
22
|
+
/* Positioned, so axe's stacking order puts the tabs over the indicator too, as browsers do. */
|
|
23
|
+
lu-tabs [role="tablist"]:has(> [data-indicator]) > [role="tab"] {
|
|
24
|
+
position: relative;
|
|
25
|
+
}
|
|
26
|
+
lu-tabs [data-indicator] {
|
|
27
|
+
position: absolute;
|
|
28
|
+
box-sizing: border-box;
|
|
29
|
+
z-index: -1;
|
|
30
|
+
pointer-events: none;
|
|
31
|
+
left: var(--lu-tabs-indicator-left);
|
|
32
|
+
top: var(--lu-tabs-indicator-top);
|
|
33
|
+
width: var(--lu-tabs-indicator-width);
|
|
34
|
+
height: var(--lu-tabs-indicator-height);
|
|
35
|
+
}
|
|
36
|
+
/* No anchor positioning and no script yet: nothing places it. */
|
|
37
|
+
@supports not (anchor-name: --a) {
|
|
38
|
+
lu-tabs:not(:defined) [data-indicator] {
|
|
39
|
+
visibility: hidden;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
@supports (anchor-name: --a) {
|
|
43
|
+
lu-tabs [role="tab"][aria-selected="true"] {
|
|
44
|
+
anchor-name: var(--lu-tabs-anchor);
|
|
45
|
+
}
|
|
46
|
+
lu-tabs [data-indicator] {
|
|
47
|
+
position-anchor: var(--lu-tabs-anchor);
|
|
48
|
+
left: var(--lu-tabs-indicator-left, anchor(left));
|
|
49
|
+
top: var(--lu-tabs-indicator-top, anchor(top));
|
|
50
|
+
width: var(--lu-tabs-indicator-width, anchor-size(width));
|
|
51
|
+
height: var(--lu-tabs-indicator-height, anchor-size(height));
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|