@lumiose/ui 0.1.1 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/dist/accordion/accordion.tsx +6 -4
- package/dist/autocomplete/autocomplete.tsx +6 -5
- package/dist/avatar/avatar.tsx +4 -3
- package/dist/batched.ts +11 -0
- package/dist/checkbox/checkbox.tsx +4 -3
- package/dist/checkbox-group/checkbox-group.tsx +3 -2
- package/dist/collapsible/collapsible.tsx +5 -3
- package/dist/collapsible/panel.ts +3 -1
- package/dist/combobox/combobox.tsx +8 -6
- package/dist/combobox/context.ts +2 -1
- package/dist/context-menu/context-menu.tsx +6 -4
- package/dist/dialog/dialog.tsx +4 -2
- package/dist/drawer/drawer.tsx +2 -1
- package/dist/field/field.tsx +13 -11
- package/dist/form/reset.ts +4 -2
- package/dist/input/input.tsx +4 -1
- package/dist/menu/menu.tsx +13 -11
- package/dist/meter/meter.tsx +2 -1
- package/dist/navigation-menu/navigation-menu.tsx +6 -4
- package/dist/not-inside.ts +13 -0
- package/dist/number-field/number-field.tsx +6 -4
- package/dist/otp-field/otp-field.tsx +11 -9
- package/dist/popover/hover.ts +2 -1
- package/dist/popover/popover-hover.script.ts +27 -0
- package/dist/popover/popover.script.ts +19 -25
- package/dist/popover/popover.tsx +18 -9
- package/dist/preview-card/preview-card.tsx +4 -2
- package/dist/progress/progress.tsx +2 -1
- package/dist/radio-group/radio-group.tsx +5 -3
- package/dist/select/select.script.ts +26 -0
- package/dist/select/select.tsx +43 -11
- package/dist/slider/slider.tsx +6 -4
- package/dist/switch/switch.tsx +4 -3
- package/dist/tabs/tabs.tsx +5 -3
- package/dist/toast/toast.tsx +5 -3
- package/dist/toggle/toggle.tsx +6 -5
- package/dist/toggle-group/toggle-group.script.ts +12 -4
- package/dist/toggle-group/toggle-group.tsx +16 -4
- package/dist/toolbar/toolbar.tsx +2 -1
- package/dist/tooltip/tooltip.tsx +6 -2
- package/docs/README.md +16 -7
- package/docs/accordion.md +3 -2
- package/docs/autocomplete.md +2 -2
- package/docs/avatar.md +2 -2
- package/docs/checkbox-group.md +1 -1
- package/docs/checkbox.md +1 -1
- package/docs/collapsible.md +4 -3
- package/docs/combobox.md +4 -3
- package/docs/context-menu.md +3 -3
- package/docs/dialog.md +1 -1
- package/docs/drawer.md +3 -2
- package/docs/field.md +2 -2
- package/docs/installation.md +3 -3
- package/docs/llms-full.txt +114 -73
- package/docs/llms.txt +14 -14
- package/docs/menu.md +4 -3
- package/docs/menubar.md +3 -3
- package/docs/navigation-menu.md +4 -3
- package/docs/otp-field.md +2 -2
- package/docs/popover.md +7 -4
- package/docs/preview-card.md +4 -3
- package/docs/radio-group.md +1 -1
- package/docs/select.md +25 -10
- package/docs/tabs.md +1 -1
- package/docs/toast.md +2 -2
- package/docs/toggle-group.md +10 -4
- package/docs/toolbar.md +2 -2
- package/docs/tooltip.md +4 -3
- package/package.json +5 -5
|
@@ -12,6 +12,8 @@ import { controlId, describedBy, useField } from "../field/context.ts";
|
|
|
12
12
|
import { onFormReset } from "../form/reset.ts";
|
|
13
13
|
import type { OTPFieldElement, OTPFieldValueDetail } from "./otp-field.script.ts";
|
|
14
14
|
import otpFieldScript from "./otp-field.script.ts?client";
|
|
15
|
+
import { batched } from "../batched.ts";
|
|
16
|
+
import { notInside } from "../not-inside.ts";
|
|
15
17
|
|
|
16
18
|
declare module "lumiose/jsx-runtime" {
|
|
17
19
|
namespace JSX {
|
|
@@ -144,7 +146,7 @@ const OTPFieldContext = createContext<OTPFieldContextValue>();
|
|
|
144
146
|
|
|
145
147
|
function useOTPField(part: string): OTPFieldContextValue {
|
|
146
148
|
const context = useContext(OTPFieldContext);
|
|
147
|
-
if (!context) throw
|
|
149
|
+
if (!context) throw notInside(`OTPField.${part}`, "<OTPField.Root>");
|
|
148
150
|
return context;
|
|
149
151
|
}
|
|
150
152
|
|
|
@@ -234,19 +236,19 @@ function Root({
|
|
|
234
236
|
validation-type={type()}
|
|
235
237
|
mask={mask}
|
|
236
238
|
auto-submit={autoSubmit}
|
|
237
|
-
on:input={(event: Event) => {
|
|
239
|
+
on:input={batched((event: Event) => {
|
|
238
240
|
const input = element()?.input;
|
|
239
241
|
// A composition reports once committed (the element fires `input` then, TASK-432).
|
|
240
242
|
if (input && event.target === input && !(event as InputEvent).isComposing) {
|
|
241
243
|
onValueChange?.(input.value);
|
|
242
244
|
}
|
|
243
|
-
}}
|
|
244
|
-
on:valuecomplete={(event: CustomEvent<OTPFieldValueDetail>) =>
|
|
245
|
-
onValueComplete?.(event.detail.value)
|
|
246
|
-
}
|
|
247
|
-
on:valueinvalid={(event: CustomEvent<OTPFieldValueDetail>) =>
|
|
248
|
-
onValueInvalid?.(event.detail.value)
|
|
249
|
-
}
|
|
245
|
+
})}
|
|
246
|
+
on:valuecomplete={batched((event: CustomEvent<OTPFieldValueDetail>) =>
|
|
247
|
+
onValueComplete?.(event.detail.value),
|
|
248
|
+
)}
|
|
249
|
+
on:valueinvalid={batched((event: CustomEvent<OTPFieldValueDetail>) =>
|
|
250
|
+
onValueInvalid?.(event.detail.value),
|
|
251
|
+
)}
|
|
250
252
|
>
|
|
251
253
|
{children}
|
|
252
254
|
</lu-otp-field>
|
package/dist/popover/hover.ts
CHANGED
|
@@ -157,7 +157,8 @@ export function hoverIntent(source: HoverSource): HoverIntent {
|
|
|
157
157
|
const { popup, x, y } = corridor;
|
|
158
158
|
// Arrived (the popup's `pointerover` cancelled the close), or closed already.
|
|
159
159
|
if (!closing.has(popup)) corridor = undefined;
|
|
160
|
-
else if (toward(popup.getBoundingClientRect(), x, y, event.clientX, event.clientY))
|
|
160
|
+
else if (toward(popup.getBoundingClientRect(), x, y, event.clientX, event.clientY))
|
|
161
|
+
close(popup);
|
|
161
162
|
else corridor = undefined;
|
|
162
163
|
});
|
|
163
164
|
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// Popover's hover script (TASK-394, TASK-541): only a Popover.Trigger with `openOnHover` loads it,
|
|
2
|
+
// so a page without one goes without the hover timing (hover.ts). A trigger with
|
|
3
|
+
// `data-open-on-hover` opens its popover after `data-delay` of hover by a mouse or a pen, and the
|
|
4
|
+
// popover closes `data-close-delay` after the pointer left both (popover.script.ts, which this
|
|
5
|
+
// script hands the timing to, follows what it opened: focus, a click on the trigger).
|
|
6
|
+
// It must not import `lumiose`: server-rendered pages load it without the runtime.
|
|
7
|
+
import { hoverIntent } from "./hover.ts";
|
|
8
|
+
import { isPopover, useHover } from "./popover.script.ts";
|
|
9
|
+
|
|
10
|
+
useHover(
|
|
11
|
+
hoverIntent({
|
|
12
|
+
triggerAt: (node) =>
|
|
13
|
+
node.closest?.<HTMLElement>("button[data-open-on-hover][popovertarget]") ?? undefined,
|
|
14
|
+
popupOf: (trigger) => {
|
|
15
|
+
const popup = document.getElementById(trigger.getAttribute("popovertarget")!);
|
|
16
|
+
return isPopover(popup) ? popup : undefined;
|
|
17
|
+
},
|
|
18
|
+
show: (trigger, popup) => {
|
|
19
|
+
// A hover-opened popover leaves focus where it is (popover.script.ts's `toggle`).
|
|
20
|
+
popup.popover = "manual";
|
|
21
|
+
(popup.showPopover as (options?: { source?: HTMLElement }) => void)({ source: trigger });
|
|
22
|
+
},
|
|
23
|
+
hide: (popup) => popup.hidePopover(),
|
|
24
|
+
delay: 300,
|
|
25
|
+
closeDelay: 0,
|
|
26
|
+
}),
|
|
27
|
+
);
|
|
@@ -21,6 +21,8 @@
|
|
|
21
21
|
// focus. It stays open while the pointer is on the trigger or the popup (WCAG 1.4.13: hoverable)
|
|
22
22
|
// and closes `data-close-delay` (0 ms, at least a 100 ms grace to cross the gap) after it left
|
|
23
23
|
// both. A click on the trigger keeps it open until it is dismissed (an outside press, Escape).
|
|
24
|
+
// The hover timing is popover-hover.script.ts's, which only a trigger with `openOnHover` loads
|
|
25
|
+
// (TASK-541); this script follows the popovers it opened.
|
|
24
26
|
// - A modal popover is a modal `<dialog data-side>` opened by `command="show-modal"`: placement,
|
|
25
27
|
// focus and `aria-expanded` as above; Dialog's script and the native dialog do the rest.
|
|
26
28
|
// - `data-trap-focus` (`modal="trap-focus"`, TASK-410): Tab and Shift+Tab go around the open
|
|
@@ -34,7 +36,7 @@
|
|
|
34
36
|
//
|
|
35
37
|
// It must not import `lumiose`: server-rendered pages load it without the runtime.
|
|
36
38
|
import { openLayer } from "../layer/layer.ts";
|
|
37
|
-
import {
|
|
39
|
+
import type { HoverIntent } from "./hover.ts";
|
|
38
40
|
import { anchoring, place } from "../placement/place.ts";
|
|
39
41
|
|
|
40
42
|
/** The last press, until a key is pressed: what an opening came from. */
|
|
@@ -75,7 +77,7 @@ function tabStops(popup: HTMLElement): HTMLElement[] {
|
|
|
75
77
|
});
|
|
76
78
|
}
|
|
77
79
|
|
|
78
|
-
const isPopover = (node: unknown): node is HTMLElement =>
|
|
80
|
+
export const isPopover = (node: unknown): node is HTMLElement =>
|
|
79
81
|
node instanceof HTMLElement &&
|
|
80
82
|
node.matches("[popover][data-side][role=dialog], dialog[data-side]");
|
|
81
83
|
|
|
@@ -135,9 +137,9 @@ on("click", (event) => {
|
|
|
135
137
|
const popup = trigger && document.getElementById(trigger.getAttribute("popovertarget")!);
|
|
136
138
|
if (!isPopover(popup) || trigger!.matches("[popovertargetaction=hide]")) return;
|
|
137
139
|
if (!popup.matches(":popover-open")) popup.popover = "manual";
|
|
138
|
-
else if (hovered
|
|
140
|
+
else if (hovered(popup)) {
|
|
139
141
|
event.preventDefault();
|
|
140
|
-
hover
|
|
142
|
+
hover!.forget(popup);
|
|
141
143
|
}
|
|
142
144
|
});
|
|
143
145
|
|
|
@@ -158,25 +160,17 @@ on("focusout", (event) => {
|
|
|
158
160
|
}
|
|
159
161
|
});
|
|
160
162
|
|
|
161
|
-
// Open on hover
|
|
163
|
+
// Open on hover: popover-hover.script.ts, which only hover triggers load, hands its timing here.
|
|
162
164
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
popup.popover = "manual";
|
|
173
|
-
(popup.showPopover as (options?: { source?: HTMLElement }) => void)({ source: trigger });
|
|
174
|
-
},
|
|
175
|
-
hide: (popup) => popup.hidePopover(),
|
|
176
|
-
delay: 300,
|
|
177
|
-
closeDelay: 0,
|
|
178
|
-
});
|
|
179
|
-
const hovered = hover.hovered;
|
|
165
|
+
let hover: HoverIntent | undefined;
|
|
166
|
+
|
|
167
|
+
/** Takes the hover timing of popover-hover.script.ts. */
|
|
168
|
+
export function useHover(intent: HoverIntent): void {
|
|
169
|
+
hover = intent;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** Whether a hover opened `popup`, and no click on its trigger has kept it open since. */
|
|
173
|
+
const hovered = (popup: HTMLElement) => hover?.hovered.has(popup) === true;
|
|
180
174
|
|
|
181
175
|
/** Stops dismissing `popup`: as it closes, so the next close request goes to the layer below. */
|
|
182
176
|
function release(popup: HTMLElement) {
|
|
@@ -226,7 +220,7 @@ on("toggle", (event) => {
|
|
|
226
220
|
|
|
227
221
|
if (open) {
|
|
228
222
|
// A hover opens it without taking focus from where it is.
|
|
229
|
-
if (hovered
|
|
223
|
+
if (hovered(popup) || popup.contains(document.activeElement)) return;
|
|
230
224
|
const touch = press?.pointerType === "touch" && event.timeStamp - press.timeStamp < 1000;
|
|
231
225
|
const first = touch ? undefined : tabStops(popup)[0];
|
|
232
226
|
if (first) first.focus();
|
|
@@ -238,10 +232,10 @@ on("toggle", (event) => {
|
|
|
238
232
|
}
|
|
239
233
|
|
|
240
234
|
placed.delete(popup);
|
|
241
|
-
const wasHovered = hovered
|
|
235
|
+
const wasHovered = hovered(popup);
|
|
242
236
|
const held = holding.has(popup);
|
|
243
237
|
holding.delete(popup);
|
|
244
|
-
hover
|
|
238
|
+
hover?.forget(popup);
|
|
245
239
|
// A hover opened it and focus was elsewhere: closing it moves no focus.
|
|
246
240
|
if (wasHovered && !held) return;
|
|
247
241
|
const active = document.activeElement;
|
package/dist/popover/popover.tsx
CHANGED
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
// `onOpenChange`. Ported from Timeless's approach (https://timeless.build).
|
|
7
7
|
//
|
|
8
8
|
// TASK-394: a trigger may open the popover on hover (`openOnHover`, after `delay`, closing
|
|
9
|
-
// `closeDelay` after the pointer leaves both), which popover.script.ts
|
|
9
|
+
// `closeDelay` after the pointer leaves both), which popover-hover.script.ts times (only hover
|
|
10
|
+
// triggers load it, TASK-541) and popover.script.ts follows. A `modal` popover is a
|
|
10
11
|
// modal `<dialog>` instead, opened by the trigger's `command="show-modal"`: the page behind it is
|
|
11
12
|
// inert and does not scroll, focus stays inside, and a press outside or Escape closes it.
|
|
12
13
|
import {
|
|
@@ -24,7 +25,10 @@ import {
|
|
|
24
25
|
import dialogScript from "../dialog/dialog.script.ts?client";
|
|
25
26
|
import { anchorName, placement, withStyle, type PlacementProps } from "../placement/placement.ts";
|
|
26
27
|
import popoverScript from "./popover.script.ts?client";
|
|
28
|
+
import popoverHoverScript from "./popover-hover.script.ts?client";
|
|
27
29
|
import { OUTSIDE_TOOLBAR, ToolbarContext } from "../toolbar/context.ts";
|
|
30
|
+
import { batched } from "../batched.ts";
|
|
31
|
+
import { notInside } from "../not-inside.ts";
|
|
28
32
|
|
|
29
33
|
/**
|
|
30
34
|
* `Popover.Root` renders no element: it holds the ids its parts share. `bind:open={signal}`
|
|
@@ -94,7 +98,7 @@ const PopoverContext = createContext<PopoverContextValue>();
|
|
|
94
98
|
|
|
95
99
|
function usePopover(): PopoverContextValue {
|
|
96
100
|
const context = useContext(PopoverContext);
|
|
97
|
-
if (!context) throw
|
|
101
|
+
if (!context) throw notInside("Popover parts", "<Popover.Root>");
|
|
98
102
|
return context;
|
|
99
103
|
}
|
|
100
104
|
|
|
@@ -171,11 +175,16 @@ function Trigger({
|
|
|
171
175
|
? { command: "show-modal", commandfor: popover.popupId, "aria-expanded": "false" as const }
|
|
172
176
|
: { popovertarget: popover.popupId };
|
|
173
177
|
// A function, so hover opening and its delays follow the props: the script reads them from
|
|
174
|
-
// the trigger at each hover (TASK-477).
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
178
|
+
// the trigger at each hover (TASK-477). Only a hover trigger loads the hover timing (TASK-541),
|
|
179
|
+
// on a server page or in an island, once `openOnHover` is on: the call is conditional, so an
|
|
180
|
+
// island loads the script when it is made (TASK-551).
|
|
181
|
+
const hover = () => {
|
|
182
|
+
if (openOnHover && !popover.modal && !off) {
|
|
183
|
+
popoverHoverScript();
|
|
184
|
+
return { "data-open-on-hover": "", "data-delay": delay, "data-close-delay": closeDelay };
|
|
185
|
+
}
|
|
186
|
+
return {};
|
|
187
|
+
};
|
|
179
188
|
return (
|
|
180
189
|
<button
|
|
181
190
|
type="button"
|
|
@@ -228,7 +237,7 @@ function Popup({
|
|
|
228
237
|
ref={attach}
|
|
229
238
|
id={popover.popupId}
|
|
230
239
|
closedby="any"
|
|
231
|
-
on:toggle={toggle}
|
|
240
|
+
on:toggle={batched(toggle)}
|
|
232
241
|
>
|
|
233
242
|
{isServer ? content : inside()}
|
|
234
243
|
</dialog>
|
|
@@ -247,7 +256,7 @@ function Popup({
|
|
|
247
256
|
id={popover.popupId}
|
|
248
257
|
popover="auto"
|
|
249
258
|
data-trap-focus={popover.trapFocus ? "" : undefined}
|
|
250
|
-
on:toggle={toggle}
|
|
259
|
+
on:toggle={batched(toggle)}
|
|
251
260
|
>
|
|
252
261
|
{isServer ? content : inside()}
|
|
253
262
|
</div>
|
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
import { createContext, effect, ref, useContext, useId } from "lumiose";
|
|
8
8
|
import { anchorName, placement, withStyle, type PlacementProps } from "../placement/placement.ts";
|
|
9
9
|
import previewCardScript from "./preview-card.script.ts?client";
|
|
10
|
+
import { batched } from "../batched.ts";
|
|
11
|
+
import { notInside } from "../not-inside.ts";
|
|
10
12
|
|
|
11
13
|
/**
|
|
12
14
|
* `PreviewCard.Root` renders no element: it holds the ids its parts share. `bind:open={signal}`
|
|
@@ -43,7 +45,7 @@ const PreviewCardContext = createContext<PreviewCardContextValue>();
|
|
|
43
45
|
|
|
44
46
|
function usePreviewCard(part: string): PreviewCardContextValue {
|
|
45
47
|
const context = useContext(PreviewCardContext);
|
|
46
|
-
if (!context) throw
|
|
48
|
+
if (!context) throw notInside(`PreviewCard.${part}`, "<PreviewCard.Root>");
|
|
47
49
|
return context;
|
|
48
50
|
}
|
|
49
51
|
|
|
@@ -106,7 +108,7 @@ function Popup({
|
|
|
106
108
|
id={card.popupId}
|
|
107
109
|
popover="manual"
|
|
108
110
|
data-preview-card-popup=""
|
|
109
|
-
on:toggle={(event: ToggleEvent) => card.onToggle(event.newState === "open")}
|
|
111
|
+
on:toggle={batched((event: ToggleEvent) => card.onToggle(event.newState === "open"))}
|
|
110
112
|
>
|
|
111
113
|
{children}
|
|
112
114
|
</div>
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
import { createContext, useContext, useId } from "lumiose";
|
|
7
7
|
import { withStyle } from "../placement/placement.ts";
|
|
8
8
|
import { formatValue, percent } from "./value.ts";
|
|
9
|
+
import { notInside } from "../not-inside.ts";
|
|
9
10
|
|
|
10
11
|
interface ProgressContextValue {
|
|
11
12
|
id: string;
|
|
@@ -52,7 +53,7 @@ export type ProgressValueProps = Omit<JSX.IntrinsicElements["span"], "children">
|
|
|
52
53
|
|
|
53
54
|
function useProgress(part: string): ProgressContextValue {
|
|
54
55
|
const context = useContext(ProgressContext);
|
|
55
|
-
if (!context) throw
|
|
56
|
+
if (!context) throw notInside(`Progress.${part}`, "<Progress.Root>");
|
|
56
57
|
return context;
|
|
57
58
|
}
|
|
58
59
|
|
|
@@ -7,6 +7,8 @@ import { createContext, effect, ref, signal, untrack, useContext, useId } from "
|
|
|
7
7
|
import checkboxScript from "../checkbox/checkbox.script.ts?client";
|
|
8
8
|
import { describedBy, labelledBy, useField, useFormValues } from "../field/context.ts";
|
|
9
9
|
import { onFormReset } from "../form/reset.ts";
|
|
10
|
+
import { batched } from "../batched.ts";
|
|
11
|
+
import { notInside } from "../not-inside.ts";
|
|
10
12
|
|
|
11
13
|
interface RadioGroupContextValue {
|
|
12
14
|
name: () => string;
|
|
@@ -121,12 +123,12 @@ export function RadioGroup({
|
|
|
121
123
|
role="radiogroup"
|
|
122
124
|
aria-required={required ? "true" : undefined}
|
|
123
125
|
aria-readonly={readOnly ? "true" : undefined}
|
|
124
|
-
on:change={(event: Event) => {
|
|
126
|
+
on:change={batched((event: Event) => {
|
|
125
127
|
const radio = event.target as HTMLInputElement;
|
|
126
128
|
if (radio.type !== "radio" || !radio.checked) return;
|
|
127
129
|
selected.set(radio.value);
|
|
128
130
|
onValueChange?.(radio.value);
|
|
129
|
-
}}
|
|
131
|
+
})}
|
|
130
132
|
>
|
|
131
133
|
{children}
|
|
132
134
|
</fieldset>
|
|
@@ -136,7 +138,7 @@ export function RadioGroup({
|
|
|
136
138
|
|
|
137
139
|
function Root({ value, ...rest }: RadioRootProps) {
|
|
138
140
|
const group = useContext(RadioGroupContext);
|
|
139
|
-
if (!group) throw
|
|
141
|
+
if (!group) throw notInside("Radio.Root", "<RadioGroup>");
|
|
140
142
|
return (
|
|
141
143
|
<input
|
|
142
144
|
required={group.required() || undefined}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// Select's client script (decision-63, TASK-537), loaded by a Select whose items have a `label`.
|
|
2
|
+
// A customizable select's picker (`appearance: base-select`) shows an option's `label` attribute
|
|
3
|
+
// instead of its content, through the browser's own style, which no CSS overrides. When such a
|
|
4
|
+
// select is pressed or takes focus, before its picker opens, this script removes the attribute from
|
|
5
|
+
// the options that Select.Item rendered with a label: the picker then shows their content,
|
|
6
|
+
// typeahead matches the label's hidden copy first in it, and their `aria-label` keeps their name.
|
|
7
|
+
// The classic select keeps the attribute, which it shows and typeahead matches; so does server HTML
|
|
8
|
+
// before the script runs, where the customizable picker shows the label.
|
|
9
|
+
// It must not import `lumiose`: server-rendered pages load it without the runtime.
|
|
10
|
+
|
|
11
|
+
/** The hidden copy of a label that Select.Item puts first in its option. */
|
|
12
|
+
const LABELLED = "option[label] > [data-label]:first-child";
|
|
13
|
+
|
|
14
|
+
/** Shows the content of the labelled items of the customizable select at or around `target`. */
|
|
15
|
+
export function showContent(target: EventTarget | null): void {
|
|
16
|
+
const select = (target as Element | null)?.closest?.("select");
|
|
17
|
+
if (!select || getComputedStyle(select).appearance !== "base-select") return;
|
|
18
|
+
for (const copy of select.querySelectorAll(LABELLED))
|
|
19
|
+
copy.parentElement!.removeAttribute("label");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
for (const type of ["pointerdown", "focusin"]) {
|
|
23
|
+
document.addEventListener(type, (event) => showContent(event.target), true);
|
|
24
|
+
}
|
|
25
|
+
// A select focused before the script ran (`autofocus`).
|
|
26
|
+
showContent(document.activeElement);
|
package/dist/select/select.tsx
CHANGED
|
@@ -3,14 +3,23 @@
|
|
|
3
3
|
// already does everything Base UI's Select does around the value: the combobox role, the keyboard
|
|
4
4
|
// and typeahead (on the closed control too), the top-layer picker anchored to the control, forms
|
|
5
5
|
// (`name`, `required`, autofill, reset, a disabled fieldset) and the mobile pickers. So Select
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
6
|
+
// works in server HTML, with no JavaScript, and in islands, where `value` drives the control when
|
|
7
|
+
// it changes and its `change` event reports back through `onValueChange`. Where customizable select
|
|
8
|
+
// is supported, `Select.Trigger` and `Select.Value` style the button and show the selected option's
|
|
9
|
+
// content, and options may hold more than text. TASK-358 records why it is native rather than the
|
|
10
|
+
// listbox pattern.
|
|
11
|
+
//
|
|
12
|
+
// An item's `label` (TASK-537) names it: the option's `label` attribute, which the classic select
|
|
13
|
+
// shows and typeahead matches, a hidden copy first in its content, which typeahead matches once
|
|
14
|
+
// the attribute is gone, and its `aria-label`. A customizable select's picker shows an option's
|
|
15
|
+
// `label` attribute instead of its content (the browser's style, which no CSS overrides), so
|
|
16
|
+
// select.script.ts removes the attribute from a customizable select's items as it takes focus.
|
|
11
17
|
import { createContext, effect, ref, untrack, useContext } from "lumiose";
|
|
18
|
+
import selectScript from "./select.script.ts?client";
|
|
12
19
|
import { fieldProps, useField, useFormValues } from "../field/context.ts";
|
|
13
20
|
import { onFormReset } from "../form/reset.ts";
|
|
21
|
+
import { batched } from "../batched.ts";
|
|
22
|
+
import { notInside } from "../not-inside.ts";
|
|
14
23
|
|
|
15
24
|
declare module "lumiose/jsx-runtime" {
|
|
16
25
|
namespace JSX {
|
|
@@ -67,6 +76,11 @@ export type SelectValueProps = Omit<JSX.IntrinsicElements["selectedcontent"], "c
|
|
|
67
76
|
export type SelectItemProps = Omit<JSX.IntrinsicElements["option"], "value" | "selected"> & {
|
|
68
77
|
/** The value the select takes, and submits, when the option is selected. */
|
|
69
78
|
value: string;
|
|
79
|
+
/**
|
|
80
|
+
* The option's name, for typeahead, assistive technology and the classic select, when its
|
|
81
|
+
* content starts with an icon or holds more than text. The picker shows the content.
|
|
82
|
+
*/
|
|
83
|
+
label?: string;
|
|
70
84
|
/** The option cannot be selected. */
|
|
71
85
|
disabled?: boolean;
|
|
72
86
|
};
|
|
@@ -112,9 +126,9 @@ function Root({ value, defaultValue, onValueChange, children, ...rest }: SelectR
|
|
|
112
126
|
select(element);
|
|
113
127
|
rest.ref?.(element);
|
|
114
128
|
}}
|
|
115
|
-
on:change={(event: Event) =>
|
|
116
|
-
onValueChange?.((event.currentTarget as HTMLSelectElement).value)
|
|
117
|
-
}
|
|
129
|
+
on:change={batched((event: Event) =>
|
|
130
|
+
onValueChange?.((event.currentTarget as HTMLSelectElement).value),
|
|
131
|
+
)}
|
|
118
132
|
>
|
|
119
133
|
{children}
|
|
120
134
|
</select>
|
|
@@ -134,11 +148,29 @@ function Value(props: SelectValueProps) {
|
|
|
134
148
|
return <selectedcontent {...props} />;
|
|
135
149
|
}
|
|
136
150
|
|
|
137
|
-
function Item({ children, value, ...rest }: SelectItemProps) {
|
|
151
|
+
function Item({ children, value, label, ...rest }: SelectItemProps) {
|
|
138
152
|
const select = useContext(SelectContext);
|
|
139
|
-
if (!select) throw
|
|
153
|
+
if (!select) throw notInside("Select parts", "<Select.Root>");
|
|
154
|
+
// Only items with a label need the script. Read where the attribute is, so a label that appears
|
|
155
|
+
// after an island's first render still loads it (TASK-552; the call is conditional, so an island
|
|
156
|
+
// loads the script when it is made, TASK-551).
|
|
157
|
+
const labelled = () => {
|
|
158
|
+
if (label !== undefined) selectScript();
|
|
159
|
+
return label;
|
|
160
|
+
};
|
|
140
161
|
return (
|
|
141
|
-
<option
|
|
162
|
+
<option
|
|
163
|
+
aria-label={label}
|
|
164
|
+
{...rest}
|
|
165
|
+
label={labelled()}
|
|
166
|
+
value={value}
|
|
167
|
+
selected={select.initial === value || undefined}
|
|
168
|
+
>
|
|
169
|
+
{label === undefined ? null : (
|
|
170
|
+
<span hidden data-label="">
|
|
171
|
+
{label}
|
|
172
|
+
</span>
|
|
173
|
+
)}
|
|
142
174
|
{children}
|
|
143
175
|
</option>
|
|
144
176
|
);
|
package/dist/slider/slider.tsx
CHANGED
|
@@ -13,6 +13,8 @@ import { percent } from "../progress/value.ts";
|
|
|
13
13
|
import { formatValues } from "./format.ts";
|
|
14
14
|
import type { SliderElement } from "./slider.script.ts";
|
|
15
15
|
import sliderScript from "./slider.script.ts?client";
|
|
16
|
+
import { batched } from "../batched.ts";
|
|
17
|
+
import { notInside } from "../not-inside.ts";
|
|
16
18
|
|
|
17
19
|
declare module "lumiose/jsx-runtime" {
|
|
18
20
|
namespace JSX {
|
|
@@ -155,7 +157,7 @@ const SliderContext = createContext<SliderContextValue>();
|
|
|
155
157
|
|
|
156
158
|
function useSlider(part: string): SliderContextValue {
|
|
157
159
|
const context = useContext(SliderContext);
|
|
158
|
-
if (!context) throw
|
|
160
|
+
if (!context) throw notInside(`Slider.${part}`, "<Slider.Root>");
|
|
159
161
|
return context;
|
|
160
162
|
}
|
|
161
163
|
|
|
@@ -259,12 +261,12 @@ function Root<V extends SliderValue = number>({
|
|
|
259
261
|
format={format ? JSON.stringify(format) : undefined}
|
|
260
262
|
locale={locale}
|
|
261
263
|
style={withStyle(style, untrack(range))}
|
|
262
|
-
on:input={() => {
|
|
264
|
+
on:input={batched(() => {
|
|
263
265
|
const next = current();
|
|
264
266
|
values.set(asList(next, low()));
|
|
265
267
|
onValueChange?.(next);
|
|
266
|
-
}}
|
|
267
|
-
on:change={() => onValueCommitted?.(current())}
|
|
268
|
+
})}
|
|
269
|
+
on:change={batched(() => onValueCommitted?.(current()))}
|
|
268
270
|
>
|
|
269
271
|
{children}
|
|
270
272
|
</lu-slider>
|
package/dist/switch/switch.tsx
CHANGED
|
@@ -7,6 +7,7 @@ import { ref, untrack } from "lumiose";
|
|
|
7
7
|
import checkboxScript from "../checkbox/checkbox.script.ts?client";
|
|
8
8
|
import { fieldProps, useField, useFormValues } from "../field/context.ts";
|
|
9
9
|
import { onFormReset } from "../form/reset.ts";
|
|
10
|
+
import { batched } from "../batched.ts";
|
|
10
11
|
|
|
11
12
|
/**
|
|
12
13
|
* The native checkbox props Root passes through, without the brand of intrinsic elements' props,
|
|
@@ -73,9 +74,9 @@ function Root({
|
|
|
73
74
|
checked={checked ?? initial}
|
|
74
75
|
aria-readonly={readOnly ? "true" : undefined}
|
|
75
76
|
data-unchecked-value={uncheckedValue}
|
|
76
|
-
on:change={(event: Event) =>
|
|
77
|
-
onCheckedChange?.((event.currentTarget as HTMLInputElement).checked)
|
|
78
|
-
}
|
|
77
|
+
on:change={batched((event: Event) =>
|
|
78
|
+
onCheckedChange?.((event.currentTarget as HTMLInputElement).checked),
|
|
79
|
+
)}
|
|
79
80
|
/>
|
|
80
81
|
);
|
|
81
82
|
}
|
package/dist/tabs/tabs.tsx
CHANGED
|
@@ -19,6 +19,8 @@ import {
|
|
|
19
19
|
import { withStyle } from "../placement/placement.ts";
|
|
20
20
|
import defineTabs from "./tabs.script.ts?client";
|
|
21
21
|
import type { LuTabs } from "./tabs.script.ts";
|
|
22
|
+
import { batched } from "../batched.ts";
|
|
23
|
+
import { notInside } from "../not-inside.ts";
|
|
22
24
|
|
|
23
25
|
declare module "lumiose/jsx-runtime" {
|
|
24
26
|
namespace JSX {
|
|
@@ -113,7 +115,7 @@ const TabsContext = createContext<TabsContextValue>();
|
|
|
113
115
|
|
|
114
116
|
function useTabs(): TabsContextValue {
|
|
115
117
|
const context = useContext(TabsContext);
|
|
116
|
-
if (!context) throw
|
|
118
|
+
if (!context) throw notInside("Tabs parts", "<Tabs.Root>");
|
|
117
119
|
return context;
|
|
118
120
|
}
|
|
119
121
|
|
|
@@ -166,12 +168,12 @@ function Root({
|
|
|
166
168
|
ref={element}
|
|
167
169
|
// The anchor the selected tab carries for a Tabs.Indicator (tabs.css).
|
|
168
170
|
style={withStyle(rest.style, { "--lu-tabs-anchor": `--lu-tabs-${id}` })}
|
|
169
|
-
on:valuechange={() => {
|
|
171
|
+
on:valuechange={batched(() => {
|
|
170
172
|
const next = element()?.value;
|
|
171
173
|
if (next == null) return;
|
|
172
174
|
selected.set(next);
|
|
173
175
|
onValueChange?.(next);
|
|
174
|
-
}}
|
|
176
|
+
})}
|
|
175
177
|
>
|
|
176
178
|
{children}
|
|
177
179
|
</lu-tabs>
|
package/dist/toast/toast.tsx
CHANGED
|
@@ -16,6 +16,8 @@ import { afterAnimations } from "../animation/animation.ts";
|
|
|
16
16
|
import toastScript from "./toast.script.ts?client";
|
|
17
17
|
import { CLOSE_COMMAND } from "./command.ts";
|
|
18
18
|
import type { LuToastViewport } from "./toast.script.ts";
|
|
19
|
+
import { batched } from "../batched.ts";
|
|
20
|
+
import { notInside } from "../not-inside.ts";
|
|
19
21
|
|
|
20
22
|
declare module "lumiose/jsx-runtime" {
|
|
21
23
|
namespace JSX {
|
|
@@ -254,7 +256,7 @@ const ToastItemContext = createContext<ToastItemContextValue>();
|
|
|
254
256
|
|
|
255
257
|
function useToastItem(): ToastItemContextValue {
|
|
256
258
|
const context = useContext(ToastItemContext);
|
|
257
|
-
if (!context) throw
|
|
259
|
+
if (!context) throw notInside("Toast parts", "<Toast.Root>");
|
|
258
260
|
return context;
|
|
259
261
|
}
|
|
260
262
|
|
|
@@ -326,12 +328,12 @@ function Root({ toast, swipeDirection, children, ref: forwarded, ...rest }: Toas
|
|
|
326
328
|
data-timeout={String(toast.timeout ?? provider?.timeout() ?? DEFAULT_TIMEOUT)}
|
|
327
329
|
data-swipe-directions={(swipeDirection ?? ["down", "right"]).join(" ") || undefined}
|
|
328
330
|
data-ending-style={ending() ? "" : undefined}
|
|
329
|
-
on:close={(event: Event) => {
|
|
331
|
+
on:close={batched((event: Event) => {
|
|
330
332
|
// The script closes it: a managed toast leaves through its manager.
|
|
331
333
|
if (!owner) return;
|
|
332
334
|
event.preventDefault();
|
|
333
335
|
owner.close(toast.id!);
|
|
334
|
-
}}
|
|
336
|
+
})}
|
|
335
337
|
>
|
|
336
338
|
{children}
|
|
337
339
|
</div>
|
package/dist/toggle/toggle.tsx
CHANGED
|
@@ -6,6 +6,7 @@ import { effect, onCleanup, signal, untrack, useContext } from "lumiose";
|
|
|
6
6
|
import type { ToggleChangeDetail } from "../toggle-group/toggle-group.script.ts";
|
|
7
7
|
import { ToolbarContext } from "../toolbar/context.ts";
|
|
8
8
|
import { ToggleGroupContext, type ToggleRecord } from "./context.ts";
|
|
9
|
+
import { batched } from "../batched.ts";
|
|
9
10
|
|
|
10
11
|
/**
|
|
11
12
|
* The button props Toggle passes through, without the brand of intrinsic elements' props, so that
|
|
@@ -82,16 +83,16 @@ export function Toggle({
|
|
|
82
83
|
aria-disabled={toolbar?.disabled() ? "true" : rest["aria-disabled"]}
|
|
83
84
|
value={value}
|
|
84
85
|
aria-pressed={on() ? "true" : "false"}
|
|
85
|
-
on:click={(event: MouseEvent) => {
|
|
86
|
+
on:click={batched((event: MouseEvent) => {
|
|
86
87
|
// In a group, the group's element presses it and reports `pressedchange`.
|
|
87
88
|
if (group || event.defaultPrevented) return;
|
|
88
89
|
const next = !state();
|
|
89
90
|
state.set(next);
|
|
90
91
|
onPressedChange?.(next);
|
|
91
|
-
}}
|
|
92
|
-
on:pressedchange={(event: CustomEvent<ToggleChangeDetail>) =>
|
|
93
|
-
onPressedChange?.(event.detail.pressed)
|
|
94
|
-
}
|
|
92
|
+
})}
|
|
93
|
+
on:pressedchange={batched((event: CustomEvent<ToggleChangeDetail>) =>
|
|
94
|
+
onPressedChange?.(event.detail.pressed),
|
|
95
|
+
)}
|
|
95
96
|
>
|
|
96
97
|
{children}
|
|
97
98
|
</button>
|
|
@@ -51,16 +51,24 @@ export class ToggleGroupElement extends HTMLElement {
|
|
|
51
51
|
);
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
-
/**
|
|
54
|
+
/**
|
|
55
|
+
* The pressed toggles' values. Setting it presses those toggles, without events; a value equal
|
|
56
|
+
* to the current one changes nothing and measures nothing (TASK-539).
|
|
57
|
+
*/
|
|
55
58
|
get value(): string[] {
|
|
56
59
|
return this.items.filter(pressed).map((item) => item.value);
|
|
57
60
|
}
|
|
58
61
|
|
|
59
62
|
set value(next: readonly string[] | null | undefined) {
|
|
60
63
|
const values = next ?? [];
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
+
let changed = false;
|
|
65
|
+
for (const item of this.items) {
|
|
66
|
+
const on = String(values.includes(item.value));
|
|
67
|
+
if (item.getAttribute("aria-pressed") === on) continue;
|
|
68
|
+
item.setAttribute("aria-pressed", on);
|
|
69
|
+
changed = true;
|
|
70
|
+
}
|
|
71
|
+
if (changed) this.#rove();
|
|
64
72
|
}
|
|
65
73
|
|
|
66
74
|
/** Whether several toggles can be pressed; reflects the `multiple` attribute. */
|