@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
package/README.md
CHANGED
|
@@ -5,8 +5,8 @@ elements first (`<dialog>`, popover, invoker commands, CSS anchor positioning),
|
|
|
5
5
|
elements (`lu-*`) only where the platform falls short, server-rendered without hydration. Parts
|
|
6
6
|
follow [Base UI](https://base-ui.com)'s anatomy; the approach is [Timeless](https://timeless.build)'s.
|
|
7
7
|
|
|
8
|
-
**In development
|
|
9
|
-
Lumiose packages share.
|
|
8
|
+
**In development**, on npm: `pnpm add @lumiose/ui` in a Lumiose app (`lumiose` is its peer
|
|
9
|
+
dependency). It keeps its own 0.x versions, apart from the version the other Lumiose packages share.
|
|
10
10
|
|
|
11
11
|
```tsx
|
|
12
12
|
import { Dialog } from "@lumiose/ui";
|
|
@@ -17,6 +17,8 @@ import {
|
|
|
17
17
|
} from "lumiose";
|
|
18
18
|
import defineAccordion from "./accordion.script.ts?client";
|
|
19
19
|
import type { LuAccordion } from "./accordion.script.ts";
|
|
20
|
+
import { batched } from "../batched.ts";
|
|
21
|
+
import { notInside } from "../not-inside.ts";
|
|
20
22
|
|
|
21
23
|
declare module "lumiose/jsx-runtime" {
|
|
22
24
|
namespace JSX {
|
|
@@ -118,13 +120,13 @@ const HeaderContext = createContext<AccordionHeaderProps>();
|
|
|
118
120
|
|
|
119
121
|
function useAccordion(): AccordionContextValue {
|
|
120
122
|
const context = useContext(AccordionContext);
|
|
121
|
-
if (!context) throw
|
|
123
|
+
if (!context) throw notInside("Accordion parts", "<Accordion.Root>");
|
|
122
124
|
return context;
|
|
123
125
|
}
|
|
124
126
|
|
|
125
127
|
function useItem(): ItemContextValue {
|
|
126
128
|
const context = useContext(ItemContext);
|
|
127
|
-
if (!context) throw
|
|
129
|
+
if (!context) throw notInside("Accordion parts", "an <Accordion.Item>");
|
|
128
130
|
return context;
|
|
129
131
|
}
|
|
130
132
|
|
|
@@ -223,13 +225,13 @@ function Item({
|
|
|
223
225
|
}}
|
|
224
226
|
name={accordion.multiple ? undefined : accordion.id}
|
|
225
227
|
open={shown}
|
|
226
|
-
on:toggle={(event: ToggleEvent) => {
|
|
228
|
+
on:toggle={batched((event: ToggleEvent) => {
|
|
227
229
|
const now = event.newState === "open";
|
|
228
230
|
accordion.onToggle(key, now);
|
|
229
231
|
if (now === shown) return;
|
|
230
232
|
shown = now;
|
|
231
233
|
onOpenChange?.(now);
|
|
232
|
-
}}
|
|
234
|
+
})}
|
|
233
235
|
>
|
|
234
236
|
{children}
|
|
235
237
|
</details>
|
|
@@ -24,6 +24,7 @@ import comboboxScript from "../combobox/combobox.script.ts?client";
|
|
|
24
24
|
import { ComboboxContext, type ComboboxContextValue } from "../combobox/context.ts";
|
|
25
25
|
import { controlId, useField, useFormValues } from "../field/context.ts";
|
|
26
26
|
import { onFormReset } from "../form/reset.ts";
|
|
27
|
+
import { batched } from "../batched.ts";
|
|
27
28
|
|
|
28
29
|
/**
|
|
29
30
|
* How the suggestions complete the text, as in Base UI: `"list"` filters them by the text,
|
|
@@ -193,13 +194,13 @@ function Root({
|
|
|
193
194
|
submit-on-item-click={submitOnItemClick}
|
|
194
195
|
limit={limit}
|
|
195
196
|
disabled={context.disabled()}
|
|
196
|
-
on:input={(event: Event) => {
|
|
197
|
+
on:input={batched((event: Event) => {
|
|
197
198
|
const input = host()?.input;
|
|
198
199
|
if (input && event.target === input) onValueChange?.(input.value);
|
|
199
|
-
}}
|
|
200
|
-
on:openchange={(event: CustomEvent<ComboboxOpenChangeDetail>) =>
|
|
201
|
-
onOpenChange?.(event.detail.open)
|
|
202
|
-
}
|
|
200
|
+
})}
|
|
201
|
+
on:openchange={batched((event: CustomEvent<ComboboxOpenChangeDetail>) =>
|
|
202
|
+
onOpenChange?.(event.detail.open),
|
|
203
|
+
)}
|
|
203
204
|
>
|
|
204
205
|
{children}
|
|
205
206
|
</lu-combobox>
|
package/dist/avatar/avatar.tsx
CHANGED
|
@@ -12,6 +12,7 @@ import type {
|
|
|
12
12
|
AvatarLoadingStatus,
|
|
13
13
|
AvatarLoadingStatusChangeDetail,
|
|
14
14
|
} from "./avatar.script.ts";
|
|
15
|
+
import { batched } from "../batched.ts";
|
|
15
16
|
|
|
16
17
|
export type { AvatarLoadingStatus } from "./avatar.script.ts";
|
|
17
18
|
|
|
@@ -75,9 +76,9 @@ function Image({ onLoadingStatusChange, ...rest }: AvatarImageProps) {
|
|
|
75
76
|
{...rest}
|
|
76
77
|
ref={image}
|
|
77
78
|
data-loading
|
|
78
|
-
on:loadingstatuschange={(event: CustomEvent<AvatarLoadingStatusChangeDetail>) =>
|
|
79
|
-
report(event.detail.status)
|
|
80
|
-
}
|
|
79
|
+
on:loadingstatuschange={batched((event: CustomEvent<AvatarLoadingStatusChangeDetail>) =>
|
|
80
|
+
report(event.detail.status),
|
|
81
|
+
)}
|
|
81
82
|
/>
|
|
82
83
|
);
|
|
83
84
|
}
|
package/dist/batched.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// The components' native listeners (`on:`) run inside `batch()`, as Lumiose's delegated event
|
|
2
|
+
// handlers do (TASK-540): the signal writes of a listener and of the callbacks it calls
|
|
3
|
+
// (`onValueChange`, `onOpenChange`, …) render together, before the event's dispatch returns, so
|
|
4
|
+
// the DOM is current right after it instead of a microtask later.
|
|
5
|
+
import { batch } from "lumiose";
|
|
6
|
+
|
|
7
|
+
/** `listener`, run inside `batch()`. */
|
|
8
|
+
export const batched =
|
|
9
|
+
<E>(listener: (event: E) => void) =>
|
|
10
|
+
(event: E): void =>
|
|
11
|
+
batch(() => listener(event));
|
|
@@ -9,6 +9,7 @@ import { fieldProps, useField, useFormValues } from "../field/context.ts";
|
|
|
9
9
|
import checkboxScript from "./checkbox.script.ts?client";
|
|
10
10
|
import { CheckboxGroupContext, optionId, type CheckboxGroupContextValue } from "./context.ts";
|
|
11
11
|
import { afterFormReset, onFormReset } from "../form/reset.ts";
|
|
12
|
+
import { batched } from "../batched.ts";
|
|
12
13
|
|
|
13
14
|
/**
|
|
14
15
|
* The native checkbox props Root passes through (`name`, `required`, `disabled`, `form`, `id`,
|
|
@@ -137,9 +138,9 @@ function Root({
|
|
|
137
138
|
data-parent={parent ? "" : undefined}
|
|
138
139
|
aria-readonly={readOnly ? "true" : undefined}
|
|
139
140
|
data-unchecked-value={uncheckedValue}
|
|
140
|
-
on:change={(event: Event) =>
|
|
141
|
-
onCheckedChange?.((event.currentTarget as HTMLInputElement).checked)
|
|
142
|
-
}
|
|
141
|
+
on:change={batched((event: Event) =>
|
|
142
|
+
onCheckedChange?.((event.currentTarget as HTMLInputElement).checked),
|
|
143
|
+
)}
|
|
143
144
|
/>
|
|
144
145
|
);
|
|
145
146
|
}
|
|
@@ -7,6 +7,7 @@ import checkboxScript from "../checkbox/checkbox.script.ts?client";
|
|
|
7
7
|
import { CheckboxGroupContext } from "../checkbox/context.ts";
|
|
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";
|
|
10
11
|
|
|
11
12
|
/** The fieldset props CheckboxGroup passes through, without the brand of intrinsic elements' props. */
|
|
12
13
|
type FieldsetProps = Omit<JSX.IntrinsicElements["fieldset"], symbol>;
|
|
@@ -98,11 +99,11 @@ export function CheckboxGroup({
|
|
|
98
99
|
}
|
|
99
100
|
aria-describedby={rest["aria-describedby"] ?? (field ? describedBy(field) : undefined)}
|
|
100
101
|
aria-invalid={rest["aria-invalid"] ?? (field?.invalid() ? "true" : undefined)}
|
|
101
|
-
on:change={(event: Event) => {
|
|
102
|
+
on:change={batched((event: Event) => {
|
|
102
103
|
if (!(event.target as Element).matches("input[type=checkbox]")) return;
|
|
103
104
|
// checkbox.script.ts ran first, for a parent's toggle.
|
|
104
105
|
report(checked(event.currentTarget as HTMLFieldSetElement));
|
|
105
|
-
}}
|
|
106
|
+
})}
|
|
106
107
|
>
|
|
107
108
|
{children}
|
|
108
109
|
</fieldset>
|
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
// trigger. Ported from Timeless's approach (https://timeless.build).
|
|
7
7
|
import { createContext, effect, ref, untrack, useContext, useId } from "lumiose";
|
|
8
8
|
import collapsibleScript from "./collapsible.script.ts?client";
|
|
9
|
+
import { batched } from "../batched.ts";
|
|
10
|
+
import { notInside } from "../not-inside.ts";
|
|
9
11
|
|
|
10
12
|
/**
|
|
11
13
|
* The `<details>` props Root passes on (`class`, `style`, `name`, `data-*`, `aria-*`, events),
|
|
@@ -53,7 +55,7 @@ const CollapsibleContext = createContext<CollapsibleContextValue>();
|
|
|
53
55
|
|
|
54
56
|
function useCollapsible(): CollapsibleContextValue {
|
|
55
57
|
const context = useContext(CollapsibleContext);
|
|
56
|
-
if (!context) throw
|
|
58
|
+
if (!context) throw notInside("Collapsible parts", "<Collapsible.Root>");
|
|
57
59
|
return context;
|
|
58
60
|
}
|
|
59
61
|
|
|
@@ -96,12 +98,12 @@ function Root({
|
|
|
96
98
|
forwarded?.(element);
|
|
97
99
|
}}
|
|
98
100
|
open={shown}
|
|
99
|
-
on:toggle={(event: ToggleEvent) => {
|
|
101
|
+
on:toggle={batched((event: ToggleEvent) => {
|
|
100
102
|
const now = event.newState === "open";
|
|
101
103
|
if (now === shown) return;
|
|
102
104
|
shown = now;
|
|
103
105
|
onOpenChange?.(now);
|
|
104
|
-
}}
|
|
106
|
+
})}
|
|
105
107
|
>
|
|
106
108
|
{children}
|
|
107
109
|
</details>
|
|
@@ -34,7 +34,9 @@ function panelOf(details: HTMLDetailsElement): HTMLElement | undefined {
|
|
|
34
34
|
|
|
35
35
|
/** The variables' prefix: an accordion item is a `<details>` right inside `<lu-accordion>`. */
|
|
36
36
|
const prefix = (details: HTMLDetailsElement) =>
|
|
37
|
-
details.parentElement?.localName === "lu-accordion"
|
|
37
|
+
details.parentElement?.localName === "lu-accordion"
|
|
38
|
+
? "--lu-accordion-panel"
|
|
39
|
+
: "--lu-collapsible-panel";
|
|
38
40
|
|
|
39
41
|
function measure(panel: HTMLElement, name: string) {
|
|
40
42
|
panel.style.setProperty(`${name}-height`, `${panel.scrollHeight}px`);
|
|
@@ -27,6 +27,8 @@ import { controlId, describedBy, labelledBy, useField, useFormValues } from "../
|
|
|
27
27
|
import { onFormReset } from "../form/reset.ts";
|
|
28
28
|
import { anchorName, placement, withStyle, type PlacementProps } from "../placement/placement.ts";
|
|
29
29
|
import { ComboboxContext, register, useCombobox, type ComboboxContextValue } from "./context.ts";
|
|
30
|
+
import { batched } from "../batched.ts";
|
|
31
|
+
import { notInside } from "../not-inside.ts";
|
|
30
32
|
|
|
31
33
|
declare module "lumiose/jsx-runtime" {
|
|
32
34
|
namespace JSX {
|
|
@@ -266,14 +268,14 @@ function Root<Multiple extends boolean | undefined = false>({
|
|
|
266
268
|
disabled={context.disabled()}
|
|
267
269
|
multiple={multiple}
|
|
268
270
|
filter={filter}
|
|
269
|
-
on:change={() => {
|
|
271
|
+
on:change={batched(() => {
|
|
270
272
|
const next = host()!.value;
|
|
271
273
|
selected.set(list(next));
|
|
272
274
|
onValueChange?.(next as ComboboxValue<Multiple>);
|
|
273
|
-
}}
|
|
274
|
-
on:openchange={(event: CustomEvent<ComboboxOpenChangeDetail>) =>
|
|
275
|
-
onOpenChange?.(event.detail.open)
|
|
276
|
-
}
|
|
275
|
+
})}
|
|
276
|
+
on:openchange={batched((event: CustomEvent<ComboboxOpenChangeDetail>) =>
|
|
277
|
+
onOpenChange?.(event.detail.open),
|
|
278
|
+
)}
|
|
277
279
|
>
|
|
278
280
|
{children}
|
|
279
281
|
</lu-combobox>
|
|
@@ -488,7 +490,7 @@ function GroupElement({
|
|
|
488
490
|
|
|
489
491
|
function GroupLabel({ children, ...rest }: ComboboxGroupLabelProps) {
|
|
490
492
|
const group = useContext(GroupContext);
|
|
491
|
-
if (!group) throw
|
|
493
|
+
if (!group) throw notInside("Combobox.GroupLabel", "<Combobox.Group>");
|
|
492
494
|
register(group.labels);
|
|
493
495
|
return (
|
|
494
496
|
<div role="presentation" {...rest} id={group.labelId}>
|
package/dist/combobox/context.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// The context the Combobox parts share, with Autocomplete's Root (TASK-377): the same parts work
|
|
2
2
|
// under either Root, which says how the input behaves.
|
|
3
3
|
import { createContext, onCleanup, useContext, type Signal } from "lumiose";
|
|
4
|
+
import { notInside } from "../not-inside.ts";
|
|
4
5
|
|
|
5
6
|
export interface ComboboxContextValue {
|
|
6
7
|
/** The element's id; the parts' ids and the anchor name derive from it. */
|
|
@@ -35,7 +36,7 @@ export const ComboboxContext = createContext<ComboboxContextValue>();
|
|
|
35
36
|
|
|
36
37
|
export function useCombobox(): ComboboxContextValue {
|
|
37
38
|
const context = useContext(ComboboxContext);
|
|
38
|
-
if (!context) throw
|
|
39
|
+
if (!context) throw notInside("Combobox parts", "<Combobox.Root>");
|
|
39
40
|
return context;
|
|
40
41
|
}
|
|
41
42
|
|
|
@@ -19,6 +19,8 @@ import type { MenuElement, MenuOpenChangeDetail } from "../menu/menu.script.ts";
|
|
|
19
19
|
import { anchorName } from "../placement/placement.ts";
|
|
20
20
|
import contextMenuScript from "./context-menu.script.ts?client";
|
|
21
21
|
import type { ContextMenuElement } from "./context-menu.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 {
|
|
@@ -71,7 +73,7 @@ const ContextMenuContext = createContext<ContextMenuContextValue>();
|
|
|
71
73
|
|
|
72
74
|
function useContextMenu(): ContextMenuContextValue {
|
|
73
75
|
const context = useContext(ContextMenuContext);
|
|
74
|
-
if (!context) throw
|
|
76
|
+
if (!context) throw notInside("ContextMenu parts", "<ContextMenu.Root>");
|
|
75
77
|
return context;
|
|
76
78
|
}
|
|
77
79
|
|
|
@@ -126,9 +128,9 @@ function Popup(props: ContextMenuPopupProps) {
|
|
|
126
128
|
return (
|
|
127
129
|
<lu-menu
|
|
128
130
|
ref={context.host}
|
|
129
|
-
on:openchange={(event: CustomEvent<MenuOpenChangeDetail>) =>
|
|
130
|
-
context.onOpenChange(event.detail.open)
|
|
131
|
-
}
|
|
131
|
+
on:openchange={batched((event: CustomEvent<MenuOpenChangeDetail>) =>
|
|
132
|
+
context.onOpenChange(event.detail.open),
|
|
133
|
+
)}
|
|
132
134
|
>
|
|
133
135
|
{/* The point the popup is anchored to: the script moves it to the pointer. */}
|
|
134
136
|
<span
|
package/dist/dialog/dialog.tsx
CHANGED
|
@@ -16,6 +16,8 @@ import {
|
|
|
16
16
|
type Signal,
|
|
17
17
|
} from "lumiose";
|
|
18
18
|
import dialogScript from "./dialog.script.ts?client";
|
|
19
|
+
import { batched } from "../batched.ts";
|
|
20
|
+
import { notInside } from "../not-inside.ts";
|
|
19
21
|
|
|
20
22
|
/**
|
|
21
23
|
* `Dialog.Root` renders no element: it holds the dialog's state and the ids its parts share.
|
|
@@ -68,7 +70,7 @@ const DialogContext = createContext<DialogContextValue>();
|
|
|
68
70
|
|
|
69
71
|
function useDialog(): DialogContextValue {
|
|
70
72
|
const context = useContext(DialogContext);
|
|
71
|
-
if (!context) throw
|
|
73
|
+
if (!context) throw notInside("Dialog parts", "<Dialog.Root>");
|
|
72
74
|
return context;
|
|
73
75
|
}
|
|
74
76
|
|
|
@@ -148,7 +150,7 @@ function Popup({ children, ref: forwarded, ...rest }: DialogPopupProps) {
|
|
|
148
150
|
forwarded?.(element);
|
|
149
151
|
}}
|
|
150
152
|
id={dialog.popupId}
|
|
151
|
-
on:toggle={(event: ToggleEvent) => dialog.onToggle(event.newState === "open")}
|
|
153
|
+
on:toggle={batched((event: ToggleEvent) => dialog.onToggle(event.newState === "open"))}
|
|
152
154
|
>
|
|
153
155
|
{isServer ? content : children}
|
|
154
156
|
</dialog>
|
package/dist/drawer/drawer.tsx
CHANGED
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
type DialogTriggerProps,
|
|
14
14
|
} from "../dialog/dialog.tsx";
|
|
15
15
|
import drawerScript from "./drawer.script.ts?client";
|
|
16
|
+
import { notInside } from "../not-inside.ts";
|
|
16
17
|
|
|
17
18
|
/** The direction a swipe closes the drawer in, and the edge of the window it sits against. */
|
|
18
19
|
export type DrawerSwipeDirection = "up" | "down" | "left" | "right";
|
|
@@ -68,7 +69,7 @@ function Root(props: DrawerRootProps) {
|
|
|
68
69
|
|
|
69
70
|
function Popup({ children, ...rest }: DrawerPopupProps) {
|
|
70
71
|
const drawer = useContext(DrawerContext);
|
|
71
|
-
if (!drawer) throw
|
|
72
|
+
if (!drawer) throw notInside("Drawer parts", "<Drawer.Root>");
|
|
72
73
|
return (
|
|
73
74
|
<Dialog.Popup {...rest} data-swipe-direction={drawer.direction()}>
|
|
74
75
|
{children}
|
package/dist/field/field.tsx
CHANGED
|
@@ -31,6 +31,8 @@ import {
|
|
|
31
31
|
import fieldScript from "./field.script.ts?client";
|
|
32
32
|
import { listenToForm, reportValidity } from "../form/native.ts";
|
|
33
33
|
import { Input, type InputProps } from "../input/input.tsx";
|
|
34
|
+
import { batched } from "../batched.ts";
|
|
35
|
+
import { notInside } from "../not-inside.ts";
|
|
34
36
|
|
|
35
37
|
declare module "lumiose/jsx-runtime" {
|
|
36
38
|
namespace JSX {
|
|
@@ -288,7 +290,7 @@ function Root({
|
|
|
288
290
|
* (cleared while it waits): its check runs now, and a synchronous one that fails stops the
|
|
289
291
|
* submission, which the form then reports. An async check still waits for the server.
|
|
290
292
|
*/
|
|
291
|
-
const onSubmit = (event: Event) => {
|
|
293
|
+
const onSubmit = batched((event: Event) => {
|
|
292
294
|
if (!due) return;
|
|
293
295
|
clearTimeout(timer);
|
|
294
296
|
due();
|
|
@@ -297,7 +299,7 @@ function Root({
|
|
|
297
299
|
event.preventDefault();
|
|
298
300
|
event.stopImmediatePropagation();
|
|
299
301
|
reportValidity(event.currentTarget as HTMLFormElement);
|
|
300
|
-
};
|
|
302
|
+
});
|
|
301
303
|
effect(() => {
|
|
302
304
|
if (!validate) return;
|
|
303
305
|
const element = document.getElementById(controlId(field));
|
|
@@ -314,13 +316,13 @@ function Root({
|
|
|
314
316
|
}
|
|
315
317
|
});
|
|
316
318
|
// A submit attempt checks every control (`invalid` does not bubble); a reset clears the checks.
|
|
317
|
-
const onInvalid = (event: Event) => snapshot(event.target);
|
|
318
|
-
const onReset = () => {
|
|
319
|
+
const onInvalid = batched((event: Event) => snapshot(event.target));
|
|
320
|
+
const onReset = batched(() => {
|
|
319
321
|
checked.set(undefined);
|
|
320
322
|
checking = undefined;
|
|
321
323
|
// The controls take their default values after the event.
|
|
322
324
|
setTimeout(() => run(document.getElementById(controlId(field)), 0, true));
|
|
323
|
-
};
|
|
325
|
+
});
|
|
324
326
|
onMount(() => {
|
|
325
327
|
const element = host();
|
|
326
328
|
if (!element) return;
|
|
@@ -349,15 +351,15 @@ function Root({
|
|
|
349
351
|
}}
|
|
350
352
|
id={id}
|
|
351
353
|
validation-mode={validationMode ?? form?.validationMode()}
|
|
352
|
-
on:input={(event: Event) => {
|
|
354
|
+
on:input={batched((event: Event) => {
|
|
353
355
|
run(event.target, validationDebounceTime);
|
|
354
356
|
snapshot(event.target);
|
|
355
|
-
}}
|
|
356
|
-
on:change={(event: Event) => {
|
|
357
|
+
})}
|
|
358
|
+
on:change={batched((event: Event) => {
|
|
357
359
|
run(event.target);
|
|
358
360
|
snapshot(event.target);
|
|
359
|
-
}}
|
|
360
|
-
on:focusout={(event: Event) => snapshot(event.target)}
|
|
361
|
+
})}
|
|
362
|
+
on:focusout={batched((event: Event) => snapshot(event.target))}
|
|
361
363
|
>
|
|
362
364
|
{children}
|
|
363
365
|
</lu-field>
|
|
@@ -372,7 +374,7 @@ function Validity({ children }: FieldValidityProps) {
|
|
|
372
374
|
|
|
373
375
|
function useFieldPart(part: string): FieldContextValue {
|
|
374
376
|
const field = useContext(FieldContext);
|
|
375
|
-
if (!field) throw
|
|
377
|
+
if (!field) throw notInside(`Field.${part}`, "<Field.Root>");
|
|
376
378
|
return field;
|
|
377
379
|
}
|
|
378
380
|
|
package/dist/form/reset.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// value when its form resets and again a task later, once the controls (and the elements'
|
|
5
5
|
// scripts, which follow the reset the same way) hold their defaults, and reports the new value
|
|
6
6
|
// when it differs.
|
|
7
|
-
import { onCleanup, onMount } from "lumiose";
|
|
7
|
+
import { batch, onCleanup, onMount } from "lumiose";
|
|
8
8
|
import { listenToForm } from "./native.ts";
|
|
9
9
|
|
|
10
10
|
const same = (a: unknown, b: unknown) =>
|
|
@@ -20,10 +20,12 @@ function listen(element: () => Element | undefined, atReset: () => () => void):
|
|
|
20
20
|
(host as { form?: HTMLFormElement | null } | undefined)?.form ?? host?.closest("form");
|
|
21
21
|
if (!form) return;
|
|
22
22
|
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
23
|
+
// In `batch()`, as the components' listeners (../batched.ts): the callbacks' writes render
|
|
24
|
+
// together.
|
|
23
25
|
const listener = () => {
|
|
24
26
|
const later = atReset();
|
|
25
27
|
clearTimeout(timer);
|
|
26
|
-
timer = setTimeout(later);
|
|
28
|
+
timer = setTimeout(() => batch(later));
|
|
27
29
|
};
|
|
28
30
|
const stop = listenToForm(form, "reset", listener);
|
|
29
31
|
onCleanup(() => {
|
package/dist/input/input.tsx
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import { ref, untrack } from "lumiose";
|
|
5
5
|
import { fieldProps, useField, useFormValues } from "../field/context.ts";
|
|
6
6
|
import { onFormReset } from "../form/reset.ts";
|
|
7
|
+
import { batched } from "../batched.ts";
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* An element's props without the brand of intrinsic elements' props, so that the component's
|
|
@@ -61,7 +62,9 @@ export function Input({ value, defaultValue, onValueChange, ...rest }: InputProp
|
|
|
61
62
|
rest.ref?.(element);
|
|
62
63
|
}}
|
|
63
64
|
value={value !== undefined ? value : untrack(() => sent?.[0] ?? defaultValue)}
|
|
64
|
-
on:input={(event: Event) =>
|
|
65
|
+
on:input={batched((event: Event) =>
|
|
66
|
+
onValueChange?.((event.currentTarget as HTMLInputElement).value),
|
|
67
|
+
)}
|
|
65
68
|
/>
|
|
66
69
|
);
|
|
67
70
|
}
|
package/dist/menu/menu.tsx
CHANGED
|
@@ -21,6 +21,8 @@ import type { MenuCheckedChangeDetail, MenuElement, MenuOpenChangeDetail } from
|
|
|
21
21
|
import menuScript from "./menu.script.ts?client";
|
|
22
22
|
import { anchorName, placement, withStyle, type Align, type Side } from "../placement/placement.ts";
|
|
23
23
|
import { OUTSIDE_TOOLBAR, ToolbarContext } from "../toolbar/context.ts";
|
|
24
|
+
import { batched } from "../batched.ts";
|
|
25
|
+
import { notInside } from "../not-inside.ts";
|
|
24
26
|
|
|
25
27
|
declare module "lumiose/jsx-runtime" {
|
|
26
28
|
namespace JSX {
|
|
@@ -189,7 +191,7 @@ const RadioContext = createContext<{ value: () => string | undefined }>();
|
|
|
189
191
|
|
|
190
192
|
function useMenu(): MenuContextValue {
|
|
191
193
|
const context = useContext(MenuContext);
|
|
192
|
-
if (!context) throw
|
|
194
|
+
if (!context) throw notInside("Menu parts", "<Menu.Root>");
|
|
193
195
|
return context;
|
|
194
196
|
}
|
|
195
197
|
|
|
@@ -244,9 +246,9 @@ function menuRoot(props: MenuRootProps, submenu: boolean) {
|
|
|
244
246
|
<lu-menu
|
|
245
247
|
ref={host}
|
|
246
248
|
role={context.menubar ? "none" : undefined}
|
|
247
|
-
on:openchange={(event: CustomEvent<MenuOpenChangeDetail>) =>
|
|
248
|
-
props.onOpenChange?.(event.detail.open)
|
|
249
|
-
}
|
|
249
|
+
on:openchange={batched((event: CustomEvent<MenuOpenChangeDetail>) =>
|
|
250
|
+
props.onOpenChange?.(event.detail.open),
|
|
251
|
+
)}
|
|
250
252
|
>
|
|
251
253
|
{props.children}
|
|
252
254
|
</lu-menu>
|
|
@@ -377,9 +379,9 @@ function CheckboxItem({
|
|
|
377
379
|
{...rest}
|
|
378
380
|
role="menuitemcheckbox"
|
|
379
381
|
aria-checked={checked ?? defaultChecked ?? false}
|
|
380
|
-
on:checkedchange={(event: CustomEvent<MenuCheckedChangeDetail>) =>
|
|
381
|
-
onCheckedChange?.(event.detail.checked)
|
|
382
|
-
}
|
|
382
|
+
on:checkedchange={batched((event: CustomEvent<MenuCheckedChangeDetail>) =>
|
|
383
|
+
onCheckedChange?.(event.detail.checked),
|
|
384
|
+
)}
|
|
383
385
|
>
|
|
384
386
|
{children}
|
|
385
387
|
</button>
|
|
@@ -416,7 +418,7 @@ function Group(props: MenuGroupProps) {
|
|
|
416
418
|
|
|
417
419
|
function GroupLabel({ children, ...rest }: MenuGroupLabelProps) {
|
|
418
420
|
const group = useContext(GroupContext);
|
|
419
|
-
if (!group) throw
|
|
421
|
+
if (!group) throw notInside("Menu.GroupLabel", "<Menu.Group> or <Menu.RadioGroup>");
|
|
420
422
|
register(group.labels);
|
|
421
423
|
return (
|
|
422
424
|
<div role="presentation" {...rest} id={group.labelId}>
|
|
@@ -442,12 +444,12 @@ function RadioGroup({
|
|
|
442
444
|
<GroupElement
|
|
443
445
|
{...rest}
|
|
444
446
|
group={group}
|
|
445
|
-
on:checkedchange={(event: CustomEvent<MenuCheckedChangeDetail>) => {
|
|
447
|
+
on:checkedchange={batched((event: CustomEvent<MenuCheckedChangeDetail>) => {
|
|
446
448
|
const item = event.target as HTMLButtonElement;
|
|
447
449
|
if (item.getAttribute("role") === "menuitemradio" && event.detail.checked) {
|
|
448
450
|
onValueChange?.(item.value);
|
|
449
451
|
}
|
|
450
|
-
}}
|
|
452
|
+
})}
|
|
451
453
|
>
|
|
452
454
|
{children}
|
|
453
455
|
</GroupElement>
|
|
@@ -466,7 +468,7 @@ function RadioItem({
|
|
|
466
468
|
}: MenuRadioItemProps) {
|
|
467
469
|
useMenu();
|
|
468
470
|
const radio = useContext(RadioContext);
|
|
469
|
-
if (!radio) throw
|
|
471
|
+
if (!radio) throw notInside("Menu.RadioItem", "<Menu.RadioGroup>");
|
|
470
472
|
return (
|
|
471
473
|
<button
|
|
472
474
|
type="button"
|
package/dist/meter/meter.tsx
CHANGED
|
@@ -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 "../progress/value.ts";
|
|
9
|
+
import { notInside } from "../not-inside.ts";
|
|
9
10
|
|
|
10
11
|
interface MeterContextValue {
|
|
11
12
|
id: string;
|
|
@@ -52,7 +53,7 @@ export type MeterValueProps = Omit<JSX.IntrinsicElements["span"], "children"> &
|
|
|
52
53
|
|
|
53
54
|
function useMeter(part: string): MeterContextValue {
|
|
54
55
|
const context = useContext(MeterContext);
|
|
55
|
-
if (!context) throw
|
|
56
|
+
if (!context) throw notInside(`Meter.${part}`, "<Meter.Root>");
|
|
56
57
|
return context;
|
|
57
58
|
}
|
|
58
59
|
|
|
@@ -15,6 +15,8 @@ import type {
|
|
|
15
15
|
NavigationMenuElement,
|
|
16
16
|
NavigationMenuValueChangeDetail,
|
|
17
17
|
} from "./navigation-menu.script.ts";
|
|
18
|
+
import { batched } from "../batched.ts";
|
|
19
|
+
import { notInside } from "../not-inside.ts";
|
|
18
20
|
|
|
19
21
|
declare module "lumiose/jsx-runtime" {
|
|
20
22
|
namespace JSX {
|
|
@@ -99,7 +101,7 @@ const ItemContext = createContext<ItemContextValue>();
|
|
|
99
101
|
|
|
100
102
|
function useItem(part: string): ItemContextValue {
|
|
101
103
|
const item = useContext(ItemContext);
|
|
102
|
-
if (!item) throw
|
|
104
|
+
if (!item) throw notInside(`NavigationMenu.${part}`, "<NavigationMenu.Item>");
|
|
103
105
|
return item;
|
|
104
106
|
}
|
|
105
107
|
|
|
@@ -150,9 +152,9 @@ function Root({
|
|
|
150
152
|
data-delay={delay}
|
|
151
153
|
data-close-delay={closeDelay}
|
|
152
154
|
data-orientation={vertical() ? "vertical" : "horizontal"}
|
|
153
|
-
on:valuechange={(event: CustomEvent<NavigationMenuValueChangeDetail>) =>
|
|
154
|
-
onValueChange?.(event.detail.value)
|
|
155
|
-
}
|
|
155
|
+
on:valuechange={batched((event: CustomEvent<NavigationMenuValueChangeDetail>) =>
|
|
156
|
+
onValueChange?.(event.detail.value),
|
|
157
|
+
)}
|
|
156
158
|
>
|
|
157
159
|
{/* A nested menu is a `<div>`, inside its parent's landmark. */}
|
|
158
160
|
<Dynamic component={nested ? "div" : "nav"} {...attributes()}>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// The error a compound component's part throws when the Root (or another part) it needs is not
|
|
2
|
+
// above it. The case that is hard to see is an island in between (TASK-550): a part passed to an
|
|
3
|
+
// island as a slot renders in the page, where the Root the island renders around the slot is not.
|
|
4
|
+
|
|
5
|
+
/** `subject must be inside container.`, with the island hint. */
|
|
6
|
+
export function notInside(subject: string, container: string): Error {
|
|
7
|
+
return new Error(
|
|
8
|
+
`${subject} must be inside ${container}. If this part looks inside, check for an island in between: ` +
|
|
9
|
+
"a part passed to an island as a slot (children or a JSX prop) renders in the page, not inside " +
|
|
10
|
+
"the components the island renders around it. Pass the data as props and render the parts " +
|
|
11
|
+
"inside the island.",
|
|
12
|
+
);
|
|
13
|
+
}
|
|
@@ -24,6 +24,8 @@ import { onFormReset } from "../form/reset.ts";
|
|
|
24
24
|
import { formatNumber, formatter } from "./format.ts";
|
|
25
25
|
import type { NumberFieldElement } from "./number-field.script.ts";
|
|
26
26
|
import numberFieldScript from "./number-field.script.ts?client";
|
|
27
|
+
import { batched } from "../batched.ts";
|
|
28
|
+
import { notInside } from "../not-inside.ts";
|
|
27
29
|
|
|
28
30
|
declare module "lumiose/jsx-runtime" {
|
|
29
31
|
namespace JSX {
|
|
@@ -164,7 +166,7 @@ const NumberFieldContext = createContext<NumberFieldContextValue>();
|
|
|
164
166
|
|
|
165
167
|
function useNumberField(part: string): NumberFieldContextValue {
|
|
166
168
|
const context = useContext(NumberFieldContext);
|
|
167
|
-
if (!context) throw
|
|
169
|
+
if (!context) throw notInside(`NumberField.${part}`, "<NumberField.Root>");
|
|
168
170
|
return context;
|
|
169
171
|
}
|
|
170
172
|
|
|
@@ -260,11 +262,11 @@ function Root({
|
|
|
260
262
|
locale={locale}
|
|
261
263
|
allow-wheel-scrub={allowWheelScrub}
|
|
262
264
|
allow-out-of-range={allowOutOfRange}
|
|
263
|
-
on:input={report}
|
|
264
|
-
on:change={() => {
|
|
265
|
+
on:input={batched(report)}
|
|
266
|
+
on:change={batched(() => {
|
|
265
267
|
report();
|
|
266
268
|
onValueCommitted?.(element()?.value ?? null);
|
|
267
|
-
}}
|
|
269
|
+
})}
|
|
268
270
|
>
|
|
269
271
|
<input
|
|
270
272
|
type="hidden"
|