@primitiv-ui/web 0.1.52 → 0.1.54

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.
@@ -19,8 +19,9 @@
19
19
  import type { HTMLAttributes } from "svelte/elements";
20
20
  import type { PrimitivAccordionContentElement, PrimitivAccordionElement, PrimitivAccordionItemElement, PrimitivAccordionTriggerElement, PrimitivAccordionTriggerIconElement } from "../accordion/elements.js";
21
21
  import type { PrimitivCollapsibleContentElement, PrimitivCollapsibleElement, PrimitivCollapsibleTriggerElement, PrimitivCollapsibleTriggerIconElement } from "../collapsible/elements.js";
22
+ import type { PrimitivModalCloseElement, PrimitivModalContentElement, PrimitivModalDescriptionElement, PrimitivModalElement, PrimitivModalTitleElement, PrimitivModalTriggerElement } from "../modal/elements.js";
22
23
  import type { PrimitivTabsContentElement, PrimitivTabsElement, PrimitivTabsListElement, PrimitivTabsTriggerElement } from "../tabs/elements.js";
23
- import type { PrimitivAccordionChangeEvent, PrimitivCollapsibleChangeEvent, PrimitivTabsChangeEvent } from "./events.js";
24
+ import type { PrimitivAccordionChangeEvent, PrimitivCollapsibleChangeEvent, PrimitivModalChangeEvent, PrimitivTabsChangeEvent } from "./events.js";
24
25
  /** Svelte's own attributes and events for an element. */
25
26
  type Attributes<E extends HTMLElement> = HTMLAttributes<E>;
26
27
  declare module "svelte/elements" {
@@ -54,6 +55,15 @@ declare module "svelte/elements" {
54
55
  "onprimitiv-collapsible-show"?: (event: CustomEvent) => void;
55
56
  };
56
57
  "primitiv-collapsible-trigger-icon": Attributes<PrimitivCollapsibleTriggerIconElement>;
58
+ "primitiv-modal": Attributes<PrimitivModalElement> & {
59
+ open?: boolean;
60
+ "onprimitiv-modal-change"?: (event: PrimitivModalChangeEvent) => void;
61
+ };
62
+ "primitiv-modal-trigger": Attributes<PrimitivModalTriggerElement>;
63
+ "primitiv-modal-content": Attributes<PrimitivModalContentElement>;
64
+ "primitiv-modal-title": Attributes<PrimitivModalTitleElement>;
65
+ "primitiv-modal-description": Attributes<PrimitivModalDescriptionElement>;
66
+ "primitiv-modal-close": Attributes<PrimitivModalCloseElement>;
57
67
  "primitiv-tabs": Attributes<PrimitivTabsElement> & {
58
68
  value?: string;
59
69
  orientation?: "horizontal" | "vertical";
@@ -19,7 +19,7 @@
19
19
  * JSR does not allow a published module to do.
20
20
  */
21
21
  import type { DefineComponent, EmitsOptions } from "vue";
22
- import type { PrimitivAccordionChangeEvent, PrimitivCollapsibleChangeEvent, PrimitivTabsChangeEvent } from "./events.js";
22
+ import type { PrimitivAccordionChangeEvent, PrimitivCollapsibleChangeEvent, PrimitivModalChangeEvent, PrimitivTabsChangeEvent } from "./events.js";
23
23
  /** A tag as Vue's template checker sees it: its props, and the events it emits. */
24
24
  type Tag<Props, Events extends EmitsOptions = Record<never, never>> = DefineComponent<Props, Record<never, never>, Record<never, never>, Record<never, never>, Record<never, never>, Record<never, never>, Record<never, never>, Events>;
25
25
  declare module "vue" {
@@ -57,6 +57,16 @@ declare module "vue" {
57
57
  "primitiv-collapsible-show": (event: CustomEvent) => void;
58
58
  }>;
59
59
  "primitiv-collapsible-trigger-icon": Tag<Record<never, never>>;
60
+ "primitiv-modal": Tag<{
61
+ open?: boolean | "";
62
+ }, {
63
+ "primitiv-modal-change": (event: PrimitivModalChangeEvent) => void;
64
+ }>;
65
+ "primitiv-modal-trigger": Tag<Record<never, never>>;
66
+ "primitiv-modal-content": Tag<Record<never, never>>;
67
+ "primitiv-modal-title": Tag<Record<never, never>>;
68
+ "primitiv-modal-description": Tag<Record<never, never>>;
69
+ "primitiv-modal-close": Tag<Record<never, never>>;
60
70
  "primitiv-tabs": Tag<{
61
71
  value?: string;
62
72
  orientation?: "horizontal" | "vertical";
package/dist/index.d.ts CHANGED
@@ -5,4 +5,5 @@
5
5
  */
6
6
  export * from "./accordion/index.js";
7
7
  export * from "./collapsible/index.js";
8
+ export * from "./modal/index.js";
8
9
  export * from "./tabs/index.js";
package/dist/index.js CHANGED
@@ -5,4 +5,5 @@
5
5
  */
6
6
  export * from "./accordion/index.js";
7
7
  export * from "./collapsible/index.js";
8
+ export * from "./modal/index.js";
8
9
  export * from "./tabs/index.js";
@@ -0,0 +1,58 @@
1
+ /**
2
+ * What a {@link ModalController} asks of whoever hosts it. The controller owns
3
+ * behaviour and never owns state (RFC 0033 D28): the custom elements keep the
4
+ * open state in a reflected `open` attribute, the React adapter keeps it in
5
+ * React, and each answers these calls from wherever it keeps it.
6
+ */
7
+ export type ModalControllerHost = {
8
+ /** The state the controller cannot see for itself. */
9
+ state(): {
10
+ /** Whether the modal is open. */
11
+ open: boolean;
12
+ };
13
+ /** Apply a change the user asked for: `open` is the state afterwards. */
14
+ change(open: boolean): void;
15
+ /**
16
+ * Escape was pressed (the dialog's native `cancel` event). Call
17
+ * `preventDefault()` on `event` to keep the modal open. Optional.
18
+ */
19
+ escapeKeyDown?(event: Event): void;
20
+ /**
21
+ * A pointer went down outside the dialog's box — on the `::backdrop`. Call
22
+ * `preventDefault()` on `event` to keep the modal open. Optional.
23
+ */
24
+ pointerDownOutside?(event: PointerEvent): void;
25
+ };
26
+ /**
27
+ * The DOM behaviour of a modal dialog, over a native `<dialog>` and its
28
+ * `showModal()`: the browser owns the inert background, the top layer and
29
+ * Escape, and this adds what it leaves out (WAI-ARIA APG Modal Dialog):
30
+ *
31
+ * - **Sync** — {@link ModalController.sync | `sync()`} shows the dialog
32
+ * modally or closes it to match the host's state. A host calls it whenever
33
+ * that state may have changed.
34
+ * - **Dismissal** — Escape and a press on the backdrop ask the host to close,
35
+ * each vetoable through its optional hook, and Escape never lets the browser
36
+ * close the dialog itself, so the host's state stays the only source of
37
+ * truth. A dialog that closes natively anyway (a `<form method="dialog">`)
38
+ * reports the close.
39
+ * - **Focus trap** — Tab wraps from the last focusable to the first and
40
+ * Shift+Tab back, since a native modal dialog lets focus escape to the
41
+ * browser chrome.
42
+ *
43
+ * Opening and closing from a trigger or a close button are the host's, as
44
+ * plain clicks: the React adapter renders those buttons apart from the dialog.
45
+ */
46
+ export declare class ModalController {
47
+ #private;
48
+ constructor(dialog: HTMLDialogElement, host: ModalControllerHost);
49
+ /**
50
+ * Start listening on the dialog. Connecting again while connected does
51
+ * nothing, so a host whose lifecycle runs twice never stacks handlers.
52
+ */
53
+ connect(): void;
54
+ /** Stop listening. A later `connect` resumes. */
55
+ disconnect(): void;
56
+ /** Show the dialog modally, or close it, to match the host's state. */
57
+ sync(): void;
58
+ }
@@ -0,0 +1,91 @@
1
+ import { FOCUSABLE_SELECTOR, modalPointerOutside, modalTabKeydown } from "@primitiv-ui/core";
2
+ /**
3
+ * The DOM behaviour of a modal dialog, over a native `<dialog>` and its
4
+ * `showModal()`: the browser owns the inert background, the top layer and
5
+ * Escape, and this adds what it leaves out (WAI-ARIA APG Modal Dialog):
6
+ *
7
+ * - **Sync** — {@link ModalController.sync | `sync()`} shows the dialog
8
+ * modally or closes it to match the host's state. A host calls it whenever
9
+ * that state may have changed.
10
+ * - **Dismissal** — Escape and a press on the backdrop ask the host to close,
11
+ * each vetoable through its optional hook, and Escape never lets the browser
12
+ * close the dialog itself, so the host's state stays the only source of
13
+ * truth. A dialog that closes natively anyway (a `<form method="dialog">`)
14
+ * reports the close.
15
+ * - **Focus trap** — Tab wraps from the last focusable to the first and
16
+ * Shift+Tab back, since a native modal dialog lets focus escape to the
17
+ * browser chrome.
18
+ *
19
+ * Opening and closing from a trigger or a close button are the host's, as
20
+ * plain clicks: the React adapter renders those buttons apart from the dialog.
21
+ */
22
+ export class ModalController {
23
+ #dialog;
24
+ #host;
25
+ #connection = null;
26
+ constructor(dialog, host) {
27
+ this.#dialog = dialog;
28
+ this.#host = host;
29
+ }
30
+ /**
31
+ * Start listening on the dialog. Connecting again while connected does
32
+ * nothing, so a host whose lifecycle runs twice never stacks handlers.
33
+ */
34
+ connect() {
35
+ if (this.#connection)
36
+ return;
37
+ this.#connection = new AbortController();
38
+ const { signal } = this.#connection;
39
+ const dialog = this.#dialog;
40
+ dialog.addEventListener("close", () => this.#host.change(false), { signal });
41
+ dialog.addEventListener("cancel", (event) => this.#cancel(event), { signal });
42
+ dialog.addEventListener("pointerdown", (event) => this.#pointerdown(event), { signal });
43
+ dialog.addEventListener("keydown", (event) => this.#keydown(event), { signal });
44
+ }
45
+ /** Stop listening. A later `connect` resumes. */
46
+ disconnect() {
47
+ this.#connection?.abort();
48
+ this.#connection = null;
49
+ }
50
+ /** Show the dialog modally, or close it, to match the host's state. */
51
+ sync() {
52
+ const { open } = this.#host.state();
53
+ if (open && !this.#dialog.open)
54
+ this.#dialog.showModal();
55
+ else if (!open && this.#dialog.open)
56
+ this.#dialog.close();
57
+ }
58
+ #cancel(event) {
59
+ this.#host.escapeKeyDown?.(event);
60
+ const vetoed = event.defaultPrevented;
61
+ // Never let the browser close the dialog itself: the host's state decides,
62
+ // and `sync()` closes it.
63
+ event.preventDefault();
64
+ if (!vetoed)
65
+ this.#host.change(false);
66
+ }
67
+ #pointerdown(event) {
68
+ const point = { x: event.clientX, y: event.clientY };
69
+ if (!modalPointerOutside(this.#dialog.getBoundingClientRect(), point))
70
+ return;
71
+ this.#host.pointerDownOutside?.(event);
72
+ if (!event.defaultPrevented)
73
+ this.#host.change(false);
74
+ }
75
+ #keydown(event) {
76
+ if (!this.#dialog.open)
77
+ return;
78
+ const focusables = [...this.#dialog.querySelectorAll(FOCUSABLE_SELECTOR)];
79
+ const outcome = modalTabKeydown({
80
+ key: event.key,
81
+ shiftKey: event.shiftKey,
82
+ active: focusables.indexOf(document.activeElement),
83
+ count: focusables.length,
84
+ });
85
+ if (!outcome)
86
+ return;
87
+ event.preventDefault();
88
+ if (outcome.focus !== null)
89
+ focusables[outcome.focus].focus();
90
+ }
91
+ }
@@ -0,0 +1,86 @@
1
+ import { ElementBase, type ElementRegistry } from "../internal/element.js";
2
+ import { PrimitivRootElement } from "../internal/RootElement.js";
3
+ export type { ElementRegistry };
4
+ /**
5
+ * `<primitiv-modal>` — the state owner of a modal dialog, built on the native
6
+ * `<dialog>` and its `showModal()`: the browser makes the page behind it inert,
7
+ * paints the `::backdrop` and owns the top layer, and the element adds the
8
+ * rest of the WAI-ARIA Modal Dialog pattern — Tab wraps inside the dialog,
9
+ * Escape and a press on the backdrop close it, and closing hands focus back to
10
+ * the button that opened it.
11
+ *
12
+ * The root finds its own parts, so a modal nested in a dialog keeps its own,
13
+ * and reconciles their ARIA from its reflected state whenever a part arrives,
14
+ * leaves, or changes an attribute the root reads (the lifecycle is
15
+ * `PrimitivRootElement`'s). Leave `open` off the `<dialog>` itself: the root
16
+ * opens it, modally.
17
+ *
18
+ * @customElement primitiv-modal
19
+ * @summary The state owner of a modal dialog on the native `<dialog>`.
20
+ * @attr {boolean} open - Whether the dialog is open. Reflected: it changes as the user opens and dismisses the dialog, so a server re-render or a history snapshot sees the live state. Set it (or the `open` property) to open or close the dialog from script.
21
+ * @fires {CustomEvent<{ open: boolean }>} primitiv-modal-change - Bubbles, cancelable, fired before a user-driven change is applied — a trigger or close click, Escape, a press on the backdrop, or the dialog closing natively — with the open state afterwards. Call `preventDefault()` to veto it, e.g. to keep a form with unsaved changes open.
22
+ */
23
+ export declare class PrimitivModalElement extends PrimitivRootElement {
24
+ #private;
25
+ protected readonly controller: {
26
+ connect(): void;
27
+ disconnect(): void;
28
+ };
29
+ protected readonly reconcileOn: readonly string[];
30
+ /** Whether the dialog is open, reflected as the boolean `open` attribute. */
31
+ get open(): boolean;
32
+ set open(next: boolean);
33
+ protected reconcile(): void;
34
+ }
35
+ /**
36
+ * `<primitiv-modal-trigger>` — an interactive part. It decorates exactly one
37
+ * child the consumer writes, a `<button>`, which opens the dialog and receives
38
+ * `aria-haspopup="dialog"`, `aria-expanded` and `aria-controls`, so the
39
+ * rendered DOM matches React's (RFC 0033 D24). A modal may have several.
40
+ *
41
+ * @customElement primitiv-modal-trigger
42
+ */
43
+ export declare class PrimitivModalTriggerElement extends ElementBase {
44
+ }
45
+ /**
46
+ * `<primitiv-modal-content>` — decorates exactly one native `<dialog>` the
47
+ * consumer writes, which the root opens with `showModal()`, closes, and gives
48
+ * an id, `data-state`, and `aria-labelledby` / `aria-describedby` pointing at
49
+ * the title and description. The dialog keeps its implicit `dialog` role.
50
+ *
51
+ * @customElement primitiv-modal-content
52
+ */
53
+ export declare class PrimitivModalContentElement extends ElementBase {
54
+ }
55
+ /**
56
+ * `<primitiv-modal-title>` — decorates exactly one heading the consumer writes
57
+ * (an `<h2>`, usually), whose id becomes the dialog's `aria-labelledby`, so it
58
+ * is the name a screen reader announces as the dialog opens.
59
+ *
60
+ * @customElement primitiv-modal-title
61
+ */
62
+ export declare class PrimitivModalTitleElement extends ElementBase {
63
+ }
64
+ /**
65
+ * `<primitiv-modal-description>` — decorates exactly one element the consumer
66
+ * writes (a `<p>`, usually), whose id becomes the dialog's `aria-describedby`.
67
+ *
68
+ * @customElement primitiv-modal-description
69
+ */
70
+ export declare class PrimitivModalDescriptionElement extends ElementBase {
71
+ }
72
+ /**
73
+ * `<primitiv-modal-close>` — an interactive part. It decorates exactly one
74
+ * `<button>` the consumer writes, which closes the dialog. A modal may have
75
+ * several: the header's icon button and the footer's Cancel.
76
+ *
77
+ * @customElement primitiv-modal-close
78
+ */
79
+ export declare class PrimitivModalCloseElement extends ElementBase {
80
+ }
81
+ /**
82
+ * Define the six Modal elements on `registry` (the global `customElements` by
83
+ * default). Safe to call more than once, and a no-op where there is no
84
+ * registry (a server render).
85
+ */
86
+ export declare function defineModal(registry?: ElementRegistry | undefined): void;
@@ -0,0 +1,235 @@
1
+ import { defineElements, ElementBase, reportOnce, write } from "../internal/element.js";
2
+ import { PrimitivRootElement } from "../internal/RootElement.js";
3
+ import { ModalController } from "./ModalController.js";
4
+ // Stryker disable StringLiteral: static — evaluated once at import, so runtime mutant switching never reaches them; the names are pinned by the defineModal registry test instead.
5
+ const ROOT = "primitiv-modal";
6
+ const TRIGGER = "primitiv-modal-trigger";
7
+ const CONTENT = "primitiv-modal-content";
8
+ const TITLE = "primitiv-modal-title";
9
+ const DESCRIPTION = "primitiv-modal-description";
10
+ const CLOSE = "primitiv-modal-close";
11
+ // Stryker restore StringLiteral
12
+ /**
13
+ * The one element child `part` decorates — the `<button>`, `<dialog>` or
14
+ * heading the consumer writes — or `null`, reported once, when it has anything
15
+ * but exactly one (RFC 0033 D24). `what` names the child in the report.
16
+ */
17
+ function decoratable(part, what) {
18
+ if (part.childElementCount === 1)
19
+ return part.firstElementChild;
20
+ reportOnce(part, `<${part.localName}> needs exactly one element child (${what}) to decorate.`);
21
+ return null;
22
+ }
23
+ /** The `<dialog>` a content part decorates, or `null`, reported once, without exactly one. */
24
+ function decoratableDialog(content) {
25
+ const child = content.firstElementChild;
26
+ if (content.childElementCount === 1 && child instanceof HTMLDialogElement)
27
+ return child;
28
+ reportOnce(content, `<${CONTENT}> needs exactly one <dialog> child to decorate.`);
29
+ return null;
30
+ }
31
+ /**
32
+ * `<primitiv-modal>` — the state owner of a modal dialog, built on the native
33
+ * `<dialog>` and its `showModal()`: the browser makes the page behind it inert,
34
+ * paints the `::backdrop` and owns the top layer, and the element adds the
35
+ * rest of the WAI-ARIA Modal Dialog pattern — Tab wraps inside the dialog,
36
+ * Escape and a press on the backdrop close it, and closing hands focus back to
37
+ * the button that opened it.
38
+ *
39
+ * The root finds its own parts, so a modal nested in a dialog keeps its own,
40
+ * and reconciles their ARIA from its reflected state whenever a part arrives,
41
+ * leaves, or changes an attribute the root reads (the lifecycle is
42
+ * `PrimitivRootElement`'s). Leave `open` off the `<dialog>` itself: the root
43
+ * opens it, modally.
44
+ *
45
+ * @customElement primitiv-modal
46
+ * @summary The state owner of a modal dialog on the native `<dialog>`.
47
+ * @attr {boolean} open - Whether the dialog is open. Reflected: it changes as the user opens and dismisses the dialog, so a server re-render or a history snapshot sees the live state. Set it (or the `open` property) to open or close the dialog from script.
48
+ * @fires {CustomEvent<{ open: boolean }>} primitiv-modal-change - Bubbles, cancelable, fired before a user-driven change is applied — a trigger or close click, Escape, a press on the backdrop, or the dialog closing natively — with the open state afterwards. Call `preventDefault()` to veto it, e.g. to keep a form with unsaved changes open.
49
+ */
50
+ export class PrimitivModalElement extends PrimitivRootElement {
51
+ /** The dialog the root currently decorates, and the controller over it. */
52
+ #dialog = null;
53
+ #modal = null;
54
+ /** The decorated trigger and close buttons, as of the last reconcile. */
55
+ // Stryker disable next-line ArrayDeclaration: equivalent — the first reconcile runs as the root connects, before any click can reach it, and replaces both.
56
+ #openers = [];
57
+ // Stryker disable next-line ArrayDeclaration: equivalent — as above.
58
+ #closers = [];
59
+ /** The root's own click listener, while connected. */
60
+ #listening = null;
61
+ controller = {
62
+ connect: () => this.#connect(),
63
+ disconnect: () => this.#disconnect(),
64
+ };
65
+ reconcileOn = ["open"];
66
+ /** Whether the dialog is open, reflected as the boolean `open` attribute. */
67
+ get open() {
68
+ return this.hasAttribute("open");
69
+ }
70
+ set open(next) {
71
+ this.toggleAttribute("open", next);
72
+ }
73
+ /** The controller's view of this element: state lives in the reflected attribute. */
74
+ #host() {
75
+ return {
76
+ state: () => ({ open: this.open }),
77
+ change: (open) => this.#change(open),
78
+ };
79
+ }
80
+ /** A change the user asked for: announced first, so a listener can veto it. */
81
+ #change(open) {
82
+ if (open === this.open)
83
+ return;
84
+ if (this.announce("primitiv-modal-change", { open }))
85
+ this.open = open;
86
+ // A veto can leave the dialog out of step — it already closed natively —
87
+ // so put it back where the state says.
88
+ else
89
+ this.#modal?.sync();
90
+ }
91
+ #connect() {
92
+ this.#listening = new AbortController();
93
+ this.addEventListener("click", (event) => this.#click(event), {
94
+ signal: this.#listening.signal,
95
+ });
96
+ this.#modal?.connect();
97
+ }
98
+ #disconnect() {
99
+ // A root disconnects only after it connected, so it is listening.
100
+ this.#listening.abort();
101
+ this.#listening = null;
102
+ this.#modal?.disconnect();
103
+ // Leaving the document drops the dialog from the top layer but leaves it
104
+ // `open`, so a move would bring it back non-modally. Drop the attribute
105
+ // rather than calling close(): close() queues a close event that a move's
106
+ // reconnect would hear as the user dismissing it. The state is kept, and
107
+ // the next connect reopens the dialog modally.
108
+ if (this.#dialog)
109
+ write(this.#dialog, "open", null);
110
+ }
111
+ #click(event) {
112
+ const target = event.target;
113
+ if (this.#openers.some((button) => button.contains(target)))
114
+ this.#change(true);
115
+ else if (this.#closers.some((button) => button.contains(target)))
116
+ this.#change(false);
117
+ }
118
+ /**
119
+ * Decorate `dialog` from now on: the controller follows it. Only a reconcile
120
+ * calls this, and a root reconciles only while connected, so the new
121
+ * controller listens at once.
122
+ */
123
+ #use(dialog) {
124
+ // Stryker disable next-line ConditionalExpression: equivalent — rebuilding the controller over the same dialog swaps one listener set for an identical one.
125
+ if (dialog === this.#dialog)
126
+ return;
127
+ this.#modal?.disconnect();
128
+ // A dialog that arrives already `open` — a history snapshot, taken while it
129
+ // was showing — is open non-modally, and `showModal()` would leave it so.
130
+ // Drop the attribute (that fires no close event), and the sync below
131
+ // reopens it modally if the root says it is open.
132
+ if (dialog)
133
+ write(dialog, "open", null);
134
+ this.#dialog = dialog;
135
+ this.#modal = dialog ? new ModalController(dialog, this.#host()) : null;
136
+ this.#modal?.connect();
137
+ }
138
+ reconcile() {
139
+ const open = this.open;
140
+ const state = open ? "open" : "closed";
141
+ const rootId = this.rootId();
142
+ write(this, "data-state", state);
143
+ const [content] = this.parts(CONTENT);
144
+ const dialog = content ? decoratableDialog(content) : null;
145
+ const [titlePart] = this.parts(TITLE);
146
+ const title = titlePart ? decoratable(titlePart, "a heading") : null;
147
+ const [descriptionPart] = this.parts(DESCRIPTION);
148
+ const description = descriptionPart ? decoratable(descriptionPart, "a <p>") : null;
149
+ if (title && !title.id)
150
+ title.id = `${rootId}-title`;
151
+ if (description && !description.id)
152
+ description.id = `${rootId}-description`;
153
+ if (dialog) {
154
+ if (!dialog.id)
155
+ dialog.id = `${rootId}-content`;
156
+ write(dialog, "data-state", state);
157
+ write(dialog, "aria-labelledby", title ? title.id : null);
158
+ write(dialog, "aria-describedby", description ? description.id : null);
159
+ }
160
+ this.#openers = this.#buttons(TRIGGER);
161
+ for (const button of this.#openers) {
162
+ write(button, "aria-haspopup", "dialog");
163
+ write(button, "aria-expanded", String(open));
164
+ write(button, "aria-controls", dialog ? dialog.id : null);
165
+ }
166
+ this.#closers = this.#buttons(CLOSE);
167
+ this.#use(dialog);
168
+ this.#modal?.sync();
169
+ }
170
+ /** The buttons this root's `selector` parts decorate. */
171
+ #buttons(selector) {
172
+ return this.parts(selector).flatMap((part) => decoratable(part, "a <button>") ?? []);
173
+ }
174
+ }
175
+ /**
176
+ * `<primitiv-modal-trigger>` — an interactive part. It decorates exactly one
177
+ * child the consumer writes, a `<button>`, which opens the dialog and receives
178
+ * `aria-haspopup="dialog"`, `aria-expanded` and `aria-controls`, so the
179
+ * rendered DOM matches React's (RFC 0033 D24). A modal may have several.
180
+ *
181
+ * @customElement primitiv-modal-trigger
182
+ */
183
+ export class PrimitivModalTriggerElement extends ElementBase {
184
+ }
185
+ /**
186
+ * `<primitiv-modal-content>` — decorates exactly one native `<dialog>` the
187
+ * consumer writes, which the root opens with `showModal()`, closes, and gives
188
+ * an id, `data-state`, and `aria-labelledby` / `aria-describedby` pointing at
189
+ * the title and description. The dialog keeps its implicit `dialog` role.
190
+ *
191
+ * @customElement primitiv-modal-content
192
+ */
193
+ export class PrimitivModalContentElement extends ElementBase {
194
+ }
195
+ /**
196
+ * `<primitiv-modal-title>` — decorates exactly one heading the consumer writes
197
+ * (an `<h2>`, usually), whose id becomes the dialog's `aria-labelledby`, so it
198
+ * is the name a screen reader announces as the dialog opens.
199
+ *
200
+ * @customElement primitiv-modal-title
201
+ */
202
+ export class PrimitivModalTitleElement extends ElementBase {
203
+ }
204
+ /**
205
+ * `<primitiv-modal-description>` — decorates exactly one element the consumer
206
+ * writes (a `<p>`, usually), whose id becomes the dialog's `aria-describedby`.
207
+ *
208
+ * @customElement primitiv-modal-description
209
+ */
210
+ export class PrimitivModalDescriptionElement extends ElementBase {
211
+ }
212
+ /**
213
+ * `<primitiv-modal-close>` — an interactive part. It decorates exactly one
214
+ * `<button>` the consumer writes, which closes the dialog. A modal may have
215
+ * several: the header's icon button and the footer's Cancel.
216
+ *
217
+ * @customElement primitiv-modal-close
218
+ */
219
+ export class PrimitivModalCloseElement extends ElementBase {
220
+ }
221
+ /**
222
+ * Define the six Modal elements on `registry` (the global `customElements` by
223
+ * default). Safe to call more than once, and a no-op where there is no
224
+ * registry (a server render).
225
+ */
226
+ export function defineModal(registry = globalThis.customElements) {
227
+ defineElements(registry, [
228
+ [ROOT, PrimitivModalElement],
229
+ [TRIGGER, PrimitivModalTriggerElement],
230
+ [CONTENT, PrimitivModalContentElement],
231
+ [TITLE, PrimitivModalTitleElement],
232
+ [DESCRIPTION, PrimitivModalDescriptionElement],
233
+ [CLOSE, PrimitivModalCloseElement],
234
+ ]);
235
+ }
@@ -0,0 +1,2 @@
1
+ export { ModalController, type ModalControllerHost } from "./ModalController.js";
2
+ export { defineModal, PrimitivModalCloseElement, PrimitivModalContentElement, PrimitivModalDescriptionElement, PrimitivModalElement, PrimitivModalTitleElement, PrimitivModalTriggerElement, } from "./elements.js";
@@ -0,0 +1,2 @@
1
+ export { ModalController } from "./ModalController.js";
2
+ export { defineModal, PrimitivModalCloseElement, PrimitivModalContentElement, PrimitivModalDescriptionElement, PrimitivModalElement, PrimitivModalTitleElement, PrimitivModalTriggerElement, } from "./elements.js";
@@ -1 +1 @@
1
- function h(e,t,o){return`${e}-${t}-${o}`}function C({orientation:e,dir:t,homeEnd:o=!1,activate:r=!1}){let s={};if(e==="horizontal"||e==="both"){let c=t==="rtl"?"ArrowLeft":"ArrowRight",a=t==="rtl"?"ArrowRight":"ArrowLeft";s[c]="next",s[a]="prev"}return(e==="vertical"||e==="both")&&(s.ArrowDown="next",s.ArrowUp="prev"),o&&(s.Home="first",s.End="last"),r&&(s.Enter="activate",s[" "]="activate"),s}function w({action:e,navigable:t,currentKey:o}){let r=t.length;if(r===0)return null;switch(e){case"activate":return{target:o};case"first":return{target:t[0]};case"last":return{target:t[r-1]};case void 0:return null;case"next":case"prev":{let s=t.indexOf(o);if(s===-1)return null;let c=e==="next"?1:r-1;return{target:t[(s+c)%r]}}}}function B(e){return e.active??e.values[0]}function O(e,t){return e.disabled.has(t)||e.active===t?null:t}function q(e,t,o){let r=C({orientation:e.orientation,dir:e.dir,homeEnd:!0,activate:!0})[o],s=w({action:r,navigable:e.values,currentKey:t});if(!s)return null;let c=s.target,a=e.activationMode==="automatic"||r==="activate";return{focus:c,activate:a?O(e,c):null}}function F(e,t){return e.disabled.has(t)?null:e.expanded.includes(t)?e.expanded.filter(o=>o!==t):e.multiple?[...e.expanded,t]:[t]}function U(e,t,o){let r=C({orientation:e.orientation,dir:e.dir,homeEnd:!0,activate:!0})[o];if(r==="activate")return e.disabled.has(t)?null:{focus:null,toggle:t};let s=e.values.filter(a=>!e.disabled.has(a)),c=w({action:r,navigable:s,currentKey:t});return c&&{focus:c.target,toggle:null}}function R(e){return e.disabled?null:!e.open}function W(e,t){if(t!=="Enter"&&t!==" ")return null;let o=R(e);return o===null?null:{open:o}}var b=class{#t;#e=new Map;#o=null;constructor(t){this.#t=t}registerTrigger(t,o,r={}){this.#e.set(o,{value:t,disabled:r.disabled??!1})}setTriggers(t){this.#e.clear();for(let{value:o,element:r,disabled:s}of t)this.registerTrigger(o,r,{disabled:s})}unregisterTrigger(t){this.#e.delete(t)}connect(){if(this.#o)return;this.#o=new AbortController;let{signal:t}=this.#o;this.#t.addEventListener("click",o=>this.#i(o,this.onTriggerClick),{signal:t}),this.#t.addEventListener("keydown",o=>this.#i(o,this.onTriggerKeydown),{signal:t})}disconnect(){this.#o?.abort(),this.#o=null}ordered(){return[...this.#e].sort(([t],[o])=>t.compareDocumentPosition(o)&Node.DOCUMENT_POSITION_FOLLOWING?-1:1)}disabled(){let t=new Set;for(let o of this.#e.values())o.disabled&&t.add(o.value);return t}#i(t,o){for(let[r,s]of this.#e)if(r.contains(t.target))return o.call(this,s,t)}};var x=class extends b{#t;constructor(t,o){super(t),this.#t=o}#e(t){let{expanded:o,multiple:r}=this.#t.state(),s=F({disabled:this.disabled(),expanded:o,multiple:r},t);s!==null&&this.#t.change(s)}onTriggerClick(t){this.#e(t.value)}onTriggerKeydown(t,o){let{expanded:r,multiple:s,orientation:c,dir:a}=this.#t.state(),p=this.ordered(),l=p.map(([,d])=>d.value),i=U({values:l,disabled:this.disabled(),expanded:r,multiple:s,orientation:c,dir:a},t.value,o.key);i&&(o.preventDefault(),i.toggle!==null&&this.#e(i.toggle),i.focus!==null&&p[l.indexOf(i.focus)][0].focus())}};var g=globalThis.HTMLElement??class{};function n(e,t,o){e.getAttribute(t)!==o&&(o===null?e.removeAttribute(t):e.setAttribute(t,o))}var tt=new WeakSet;function f(e,t){tt.has(e)||(tt.add(e),console.error(t,e))}function v(e,t){if(e)for(let[o,r]of t)e.get(o)||e.define(o,r)}function y(e,t,o){return[...e.querySelectorAll(t)].filter(r=>r.closest(o)===e)}var lt=0,m=class extends g{#t;#e=new MutationObserver(()=>this.reconcile());connectedCallback(){this.controller.connect(),this.#e.observe(this,{subtree:!0,childList:!0,attributes:!0,attributeFilter:[...this.reconcileOn]}),this.reconcile()}disconnectedCallback(){this.controller.disconnect(),this.#e.disconnect()}rootId(){return this.id||(this.#t??=`${this.localName}-${++lt}`)}direction(){return this.matches(":dir(rtl)")?"rtl":"ltr"}parts(t){return y(this,t,this.localName)}announce(t,o){return this.dispatchEvent(new CustomEvent(t,{bubbles:!0,cancelable:!0,detail:o}))}};var at="primitiv-accordion",T="primitiv-accordion-item",_="primitiv-accordion-trigger",et="primitiv-accordion-content",ot="primitiv-accordion-trigger-icon";function ct(e){return(e??"").split(/\s+/).filter(Boolean)}function dt(e){return e.childElementCount===1?e.firstElementChild:(f(e,`<${_}> needs exactly one element child (a <button>) to decorate.`),null)}var M=class extends m{#t=new Set;#e=!1;controller=new x(this,this.#o());reconcileOn=["value","disabled","orientation","force-mount"];get value(){return ct(this.getAttribute("value"))}set value(t){this.setAttribute("value",typeof t=="string"?t:t.join(" "))}#o(){return{state:()=>({expanded:this.value,multiple:this.hasAttribute("multiple"),orientation:this.#i(),dir:this.direction()}),change:t=>{this.announce("primitiv-accordion-change",{value:t})&&(this.value=t)}}}#i(){return this.getAttribute("orientation")==="horizontal"?"horizontal":"vertical"}reconcile(){let t=new Set(this.value),o=this.rootId();n(this,"data-orientation",this.#i());let r=[];for(let s of this.parts(T)){let c=s.getAttribute("value");if(c===null){f(s,`<${T}> needs a value, which pairs it with the root's value.`);continue}let a=t.has(c),p=a?"open":"closed",[l]=y(s,_,T),[i]=y(s,et,T),d=l?dt(l):null;if(d){let u=l.hasAttribute("disabled");r.push({value:c,element:d,disabled:u}),d.id||(d.id=h(o,"heading",c)),n(d,"aria-expanded",String(a)),n(d,"aria-disabled",String(u)),n(d,"data-disabled",String(u)),n(d,"data-state",p)}if(i){i.id||(i.id=h(o,"panel",c));let u=i.hasAttribute("force-mount");i.hasAttribute("role")||i.setAttribute("role","region"),n(i,"aria-labelledby",d?d.id:null),n(i,"hidden",a||u?null:""),n(i,"aria-hidden",a?null:"true"),n(i,"data-state",p),this.#e&&a&&!this.#t.has(c)&&i.dispatchEvent(new CustomEvent("primitiv-accordion-show",{bubbles:!0}))}d&&n(d,"aria-controls",i?i.id:null);for(let u of y(s,ot,T))n(u,"aria-hidden","true"),n(u,"data-state",p)}this.controller.setTriggers(r),this.#t=t,this.#e=!0}},H=class extends g{},K=class extends g{},L=class extends g{},S=class extends g{};function j(e=globalThis.customElements){v(e,[[at,M],[T,H],[_,K],[et,L],[ot,S]])}var E=class extends b{#t;constructor(t,o){super(t),this.#t=o}onTriggerClick(t){let o=R({open:this.#t.state().open,disabled:t.disabled});o!==null&&this.#t.change(o)}onTriggerKeydown(t,o){let r=W({open:this.#t.state().open,disabled:t.disabled},o.key);r&&(o.preventDefault(),this.#t.change(r.open))}};var pt="primitiv-collapsible",V="primitiv-collapsible-trigger",it="primitiv-collapsible-content",nt="primitiv-collapsible-trigger-icon",ut="--primitiv-collapsible-collapsed-height";function gt(e){return e.childElementCount===1?e.firstElementChild:(f(e,`<${V}> needs exactly one element child (a <button>) to decorate.`),null)}function bt(e){return Number.isNaN(Number(e))?e:`${e}px`}function mt(e,t,o){e.style.getPropertyValue(t)!==o&&(e.style.setProperty(t,o),e.getAttribute("style")===""&&e.removeAttribute("style"))}var I=class extends m{#t;controller=new E(this,this.#e());reconcileOn=["open","disabled","force-mount","collapsed-height"];get open(){return this.hasAttribute("open")}set open(t){this.toggleAttribute("open",t)}#e(){return{state:()=>({open:this.open}),change:t=>{this.announce("primitiv-collapsible-change",{open:t})&&(this.open=t)}}}reconcile(){let t=this.open,o=t?"open":"closed",r=this.hasAttribute("disabled"),s=this.rootId();n(this,"data-state",o),n(this,"data-disabled",String(r));let[c]=this.parts(V),[a]=this.parts(it),p=c?gt(c):null;if(p&&(p.id||(p.id=`${s}-trigger`),n(p,"aria-expanded",String(t)),n(p,"aria-disabled",r?"true":null),n(p,"data-disabled",String(r)),n(p,"data-state",o)),a){a.id||(a.id=`${s}-content`);let l=a.getAttribute("collapsed-height"),i=l!==null,d=i||a.hasAttribute("force-mount");n(a,"hidden",t||d?null:""),n(a,"aria-hidden",t||i?null:"true"),n(a,"data-state",o),n(a,"data-disabled",String(r)),n(a,"data-clamped",i?"true":null),mt(a,ut,i?bt(l):""),t&&this.#t===!1&&a.dispatchEvent(new CustomEvent("primitiv-collapsible-show",{bubbles:!0}))}p&&n(p,"aria-controls",a?a.id:null);for(let l of this.parts(nt))n(l,"aria-hidden","true"),n(l,"data-state",o);this.controller.setTriggers(p?[{value:"trigger",element:p,disabled:r}]:[]),this.#t=t}},k=class extends g{},N=class extends g{},D=class extends g{};function J(e=globalThis.customElements){v(e,[[pt,I],[V,k],[it,N],[nt,D]])}var A=class extends b{#t;constructor(t,o){super(t),this.#t=o}onTriggerClick(t){let{active:o}=this.#t.state(),r=O({disabled:this.disabled(),active:o},t.value);if(r!==null){let s=this.ordered().map(([,c])=>c.value);this.#t.activate(r,s.indexOf(r))}}onTriggerKeydown(t,o){let{active:r,orientation:s,dir:c,activationMode:a}=this.#t.state(),p=this.ordered(),l=p.map(([,d])=>d.value),i=q({values:l,disabled:this.disabled(),active:r,orientation:s,dir:c,activationMode:a},t.value,o.key);i&&(o.preventDefault(),i.activate!==null&&this.#t.activate(i.activate,l.indexOf(i.activate)),p[l.indexOf(i.focus)][0].focus())}};var ht="primitiv-tabs",rt="primitiv-tabs-list",Q="primitiv-tabs-trigger",st="primitiv-tabs-content";function ft(e){let t=e.getAttribute("value");return e.childElementCount===1&&t!==null?{tab:e.firstElementChild,value:t}:(f(e,`<${Q}> needs a value and exactly one element child (a <button>, an <a href> or a router link) to decorate.`),null)}var P=class extends m{#t;#e=!1;controller=new A(this,this.#o());reconcileOn=["value","disabled","label","orientation"];get value(){return this.getAttribute("value")??void 0}set value(t){this.setAttribute("value",t)}#o(){return{state:()=>({active:this.value,orientation:this.#n(),dir:this.direction(),activationMode:this.#i()}),activate:(t,o)=>{this.announce("primitiv-tabs-change",{value:t,index:o})&&(this.value=t)}}}#i(){let t=this.getAttribute("activation-mode");return t==="manual"?t:"automatic"}#n(){return this.getAttribute("orientation")==="vertical"?"vertical":"horizontal"}reconcile(){let t=this.#n(),o=this.value,r=this.rootId();n(this,"data-orientation",t);for(let l of this.parts(rt)){n(l,"role","tablist"),n(l,"aria-orientation",t),n(l,"data-orientation",t);let i=l.getAttribute("label");i!==null&&n(l,"aria-label",i)}let s=new Map;for(let l of this.parts(st)){let i=l.getAttribute("value");i!==null&&s.set(i,l)}let c=this.parts(Q).flatMap(l=>{let i=ft(l);return i?[{trigger:l,...i}]:[]}),a=B({values:c.map(l=>l.value),active:o});this.controller.setTriggers(c.map(({trigger:l,tab:i,value:d})=>({value:d,element:i,disabled:l.hasAttribute("disabled")})));for(let{trigger:l,tab:i,value:d}of c){let u=s.get(d),Y=d===o,Z=l.hasAttribute("disabled");i.id||(i.id=h(r,"trigger",d)),u&&!u.id&&(u.id=h(r,"panel",d)),n(i,"role","tab"),n(i,"aria-selected",String(Y)),n(i,"aria-controls",u?u.id:null),n(i,"tabindex",d===a?"0":"-1"),n(i,"aria-disabled",Z?"true":null),n(i,"data-disabled",Z?"":null),n(i,"data-state",Y?"active":"inactive"),n(i,"data-orientation",t),u&&n(u,"aria-labelledby",i.id)}for(let[l,i]of s){let d=l===o;n(i,"role","tabpanel"),n(i,"tabindex","0"),n(i,"hidden",d?null:""),n(i,"data-state",d?"active":"inactive"),n(i,"data-orientation",t)}let p=s.get(o);this.#e&&o!==this.#t&&p&&p.dispatchEvent(new CustomEvent("primitiv-tabs-show",{bubbles:!0})),this.#t=o,this.#e=!0}},$=class extends g{},G=class extends g{},z=class extends g{};function X(e=globalThis.customElements){v(e,[[ht,P],[rt,$],[Q,G],[st,z]])}j();J();X();export{x as AccordionController,E as CollapsibleController,L as PrimitivAccordionContentElement,M as PrimitivAccordionElement,H as PrimitivAccordionItemElement,K as PrimitivAccordionTriggerElement,S as PrimitivAccordionTriggerIconElement,N as PrimitivCollapsibleContentElement,I as PrimitivCollapsibleElement,k as PrimitivCollapsibleTriggerElement,D as PrimitivCollapsibleTriggerIconElement,z as PrimitivTabsContentElement,P as PrimitivTabsElement,$ as PrimitivTabsListElement,G as PrimitivTabsTriggerElement,A as TabsController,j as defineAccordion,J as defineCollapsible,X as defineTabs};
1
+ function v(e,t,o){return`${e}-${t}-${o}`}function M({orientation:e,dir:t,homeEnd:o=!1,activate:n=!1}){let l={};if(e==="horizontal"||e==="both"){let s=t==="rtl"?"ArrowLeft":"ArrowRight",d=t==="rtl"?"ArrowRight":"ArrowLeft";l[s]="next",l[d]="prev"}return(e==="vertical"||e==="both")&&(l.ArrowDown="next",l.ArrowUp="prev"),o&&(l.Home="first",l.End="last"),n&&(l.Enter="activate",l[" "]="activate"),l}function O({action:e,navigable:t,currentKey:o}){let n=t.length;if(n===0)return null;switch(e){case"activate":return{target:o};case"first":return{target:t[0]};case"last":return{target:t[n-1]};case void 0:return null;case"next":case"prev":{let l=t.indexOf(o);if(l===-1)return null;let s=e==="next"?1:n-1;return{target:t[(l+s)%n]}}}}function V(e){return e.active??e.values[0]}function w(e,t){return e.disabled.has(t)||e.active===t?null:t}function X(e,t,o){let n=M({orientation:e.orientation,dir:e.dir,homeEnd:!0,activate:!0})[o],l=O({action:n,navigable:e.values,currentKey:t});if(!l)return null;let s=l.target,d=e.activationMode==="automatic"||n==="activate";return{focus:s,activate:d?w(e,s):null}}function Y(e,t){return e.disabled.has(t)?null:e.expanded.includes(t)?e.expanded.filter(o=>o!==t):e.multiple?[...e.expanded,t]:[t]}function J(e,t,o){let n=M({orientation:e.orientation,dir:e.dir,homeEnd:!0,activate:!0})[o];if(n==="activate")return e.disabled.has(t)?null:{focus:null,toggle:t};let l=e.values.filter(d=>!e.disabled.has(d)),s=O({action:n,navigable:l,currentKey:t});return s&&{focus:s.target,toggle:null}}function H(e){return e.disabled?null:!e.open}function Q(e,t){if(t!=="Enter"&&t!==" ")return null;let o=H(e);return o===null?null:{open:o}}var Z=["a[href]","button:not([disabled])","input:not([disabled])","select:not([disabled])","textarea:not([disabled])",'[tabindex]:not([tabindex="-1"])'].join(",");function tt({key:e,shiftKey:t,active:o,count:n}){if(e!=="Tab")return null;if(n===0)return{focus:null};let l=n-1;return!t&&o===l?{focus:0}:t&&o===0?{focus:l}:null}function et(e,t){return t.x<e.left||t.x>e.right||t.y<e.top||t.y>e.bottom}var b=class{#t;#e=new Map;#o=null;constructor(t){this.#t=t}registerTrigger(t,o,n={}){this.#e.set(o,{value:t,disabled:n.disabled??!1})}setTriggers(t){this.#e.clear();for(let{value:o,element:n,disabled:l}of t)this.registerTrigger(o,n,{disabled:l})}unregisterTrigger(t){this.#e.delete(t)}connect(){if(this.#o)return;this.#o=new AbortController;let{signal:t}=this.#o;this.#t.addEventListener("click",o=>this.#n(o,this.onTriggerClick),{signal:t}),this.#t.addEventListener("keydown",o=>this.#n(o,this.onTriggerKeydown),{signal:t})}disconnect(){this.#o?.abort(),this.#o=null}ordered(){return[...this.#e].sort(([t],[o])=>t.compareDocumentPosition(o)&Node.DOCUMENT_POSITION_FOLLOWING?-1:1)}disabled(){let t=new Set;for(let o of this.#e.values())o.disabled&&t.add(o.value);return t}#n(t,o){for(let[n,l]of this.#e)if(n.contains(t.target))return o.call(this,l,t)}};var x=class extends b{#t;constructor(t,o){super(t),this.#t=o}#e(t){let{expanded:o,multiple:n}=this.#t.state(),l=Y({disabled:this.disabled(),expanded:o,multiple:n},t);l!==null&&this.#t.change(l)}onTriggerClick(t){this.#e(t.value)}onTriggerKeydown(t,o){let{expanded:n,multiple:l,orientation:s,dir:d}=this.#t.state(),u=this.ordered(),a=u.map(([,c])=>c.value),i=J({values:a,disabled:this.disabled(),expanded:n,multiple:l,orientation:s,dir:d},t.value,o.key);i&&(o.preventDefault(),i.toggle!==null&&this.#e(i.toggle),i.focus!==null&&u[a.indexOf(i.focus)][0].focus())}};var p=globalThis.HTMLElement??class{};function r(e,t,o){e.getAttribute(t)!==o&&(o===null?e.removeAttribute(t):e.setAttribute(t,o))}var gt=new WeakSet;function h(e,t){gt.has(e)||(gt.add(e),console.error(t,e))}function f(e,t){if(e)for(let[o,n]of t)e.get(o)||e.define(o,n)}function y(e,t,o){return[...e.querySelectorAll(t)].filter(n=>n.closest(o)===e)}var At=0,m=class extends p{#t;#e=new MutationObserver(()=>this.reconcile());connectedCallback(){this.controller.connect(),this.#e.observe(this,{subtree:!0,childList:!0,attributes:!0,attributeFilter:[...this.reconcileOn]}),this.reconcile()}disconnectedCallback(){this.controller.disconnect(),this.#e.disconnect()}rootId(){return this.id||(this.#t??=`${this.localName}-${++At}`)}direction(){return this.matches(":dir(rtl)")?"rtl":"ltr"}parts(t){return y(this,t,this.localName)}announce(t,o){return this.dispatchEvent(new CustomEvent(t,{bubbles:!0,cancelable:!0,detail:o}))}};var Mt="primitiv-accordion",T="primitiv-accordion-item",ot="primitiv-accordion-trigger",ht="primitiv-accordion-content",mt="primitiv-accordion-trigger-icon";function Ot(e){return(e??"").split(/\s+/).filter(Boolean)}function wt(e){return e.childElementCount===1?e.firstElementChild:(h(e,`<${ot}> needs exactly one element child (a <button>) to decorate.`),null)}var R=class extends m{#t=new Set;#e=!1;controller=new x(this,this.#o());reconcileOn=["value","disabled","orientation","force-mount"];get value(){return Ot(this.getAttribute("value"))}set value(t){this.setAttribute("value",typeof t=="string"?t:t.join(" "))}#o(){return{state:()=>({expanded:this.value,multiple:this.hasAttribute("multiple"),orientation:this.#n(),dir:this.direction()}),change:t=>{this.announce("primitiv-accordion-change",{value:t})&&(this.value=t)}}}#n(){return this.getAttribute("orientation")==="horizontal"?"horizontal":"vertical"}reconcile(){let t=new Set(this.value),o=this.rootId();r(this,"data-orientation",this.#n());let n=[];for(let l of this.parts(T)){let s=l.getAttribute("value");if(s===null){h(l,`<${T}> needs a value, which pairs it with the root's value.`);continue}let d=t.has(s),u=d?"open":"closed",[a]=y(l,ot,T),[i]=y(l,ht,T),c=a?wt(a):null;if(c){let g=a.hasAttribute("disabled");n.push({value:s,element:c,disabled:g}),c.id||(c.id=v(o,"heading",s)),r(c,"aria-expanded",String(d)),r(c,"aria-disabled",String(g)),r(c,"data-disabled",String(g)),r(c,"data-state",u)}if(i){i.id||(i.id=v(o,"panel",s));let g=i.hasAttribute("force-mount");i.hasAttribute("role")||i.setAttribute("role","region"),r(i,"aria-labelledby",c?c.id:null),r(i,"hidden",d||g?null:""),r(i,"aria-hidden",d?null:"true"),r(i,"data-state",u),this.#e&&d&&!this.#t.has(s)&&i.dispatchEvent(new CustomEvent("primitiv-accordion-show",{bubbles:!0}))}c&&r(c,"aria-controls",i?i.id:null);for(let g of y(l,mt,T))r(g,"aria-hidden","true"),r(g,"data-state",u)}this.controller.setTriggers(n),this.#t=t,this.#e=!0}},L=class extends p{},K=class extends p{},S=class extends p{},k=class extends p{};function nt(e=globalThis.customElements){f(e,[[Mt,R],[T,L],[ot,K],[ht,S],[mt,k]])}var E=class extends b{#t;constructor(t,o){super(t),this.#t=o}onTriggerClick(t){let o=H({open:this.#t.state().open,disabled:t.disabled});o!==null&&this.#t.change(o)}onTriggerKeydown(t,o){let n=Q({open:this.#t.state().open,disabled:t.disabled},o.key);n&&(o.preventDefault(),this.#t.change(n.open))}};var Ht="primitiv-collapsible",it="primitiv-collapsible-trigger",bt="primitiv-collapsible-content",ft="primitiv-collapsible-trigger-icon",Rt="--primitiv-collapsible-collapsed-height";function Lt(e){return e.childElementCount===1?e.firstElementChild:(h(e,`<${it}> needs exactly one element child (a <button>) to decorate.`),null)}function Kt(e){return Number.isNaN(Number(e))?e:`${e}px`}function St(e,t,o){e.style.getPropertyValue(t)!==o&&(e.style.setProperty(t,o),e.getAttribute("style")===""&&e.removeAttribute("style"))}var I=class extends m{#t;controller=new E(this,this.#e());reconcileOn=["open","disabled","force-mount","collapsed-height"];get open(){return this.hasAttribute("open")}set open(t){this.toggleAttribute("open",t)}#e(){return{state:()=>({open:this.open}),change:t=>{this.announce("primitiv-collapsible-change",{open:t})&&(this.open=t)}}}reconcile(){let t=this.open,o=t?"open":"closed",n=this.hasAttribute("disabled"),l=this.rootId();r(this,"data-state",o),r(this,"data-disabled",String(n));let[s]=this.parts(it),[d]=this.parts(bt),u=s?Lt(s):null;if(u&&(u.id||(u.id=`${l}-trigger`),r(u,"aria-expanded",String(t)),r(u,"aria-disabled",n?"true":null),r(u,"data-disabled",String(n)),r(u,"data-state",o)),d){d.id||(d.id=`${l}-content`);let a=d.getAttribute("collapsed-height"),i=a!==null,c=i||d.hasAttribute("force-mount");r(d,"hidden",t||c?null:""),r(d,"aria-hidden",t||i?null:"true"),r(d,"data-state",o),r(d,"data-disabled",String(n)),r(d,"data-clamped",i?"true":null),St(d,Rt,i?Kt(a):""),t&&this.#t===!1&&d.dispatchEvent(new CustomEvent("primitiv-collapsible-show",{bubbles:!0}))}u&&r(u,"aria-controls",d?d.id:null);for(let a of this.parts(ft))r(a,"aria-hidden","true"),r(a,"data-state",o);this.controller.setTriggers(u?[{value:"trigger",element:u,disabled:n}]:[]),this.#t=t}},D=class extends p{},N=class extends p{},P=class extends p{};function rt(e=globalThis.customElements){f(e,[[Ht,I],[it,D],[bt,N],[ft,P]])}var C=class{#t;#e;#o=null;constructor(t,o){this.#t=t,this.#e=o}connect(){if(this.#o)return;this.#o=new AbortController;let{signal:t}=this.#o,o=this.#t;o.addEventListener("close",()=>this.#e.change(!1),{signal:t}),o.addEventListener("cancel",n=>this.#n(n),{signal:t}),o.addEventListener("pointerdown",n=>this.#i(n),{signal:t}),o.addEventListener("keydown",n=>this.#r(n),{signal:t})}disconnect(){this.#o?.abort(),this.#o=null}sync(){let{open:t}=this.#e.state();t&&!this.#t.open?this.#t.showModal():!t&&this.#t.open&&this.#t.close()}#n(t){this.#e.escapeKeyDown?.(t);let o=t.defaultPrevented;t.preventDefault(),o||this.#e.change(!1)}#i(t){let o={x:t.clientX,y:t.clientY};et(this.#t.getBoundingClientRect(),o)&&(this.#e.pointerDownOutside?.(t),t.defaultPrevented||this.#e.change(!1))}#r(t){if(!this.#t.open)return;let o=[...this.#t.querySelectorAll(Z)],n=tt({key:t.key,shiftKey:t.shiftKey,active:o.indexOf(document.activeElement),count:o.length});n&&(t.preventDefault(),n.focus!==null&&o[n.focus].focus())}};var kt="primitiv-modal",vt="primitiv-modal-trigger",st="primitiv-modal-content",yt="primitiv-modal-title",Tt="primitiv-modal-description",xt="primitiv-modal-close";function lt(e,t){return e.childElementCount===1?e.firstElementChild:(h(e,`<${e.localName}> needs exactly one element child (${t}) to decorate.`),null)}function It(e){let t=e.firstElementChild;return e.childElementCount===1&&t instanceof HTMLDialogElement?t:(h(e,`<${st}> needs exactly one <dialog> child to decorate.`),null)}var $=class extends m{#t=null;#e=null;#o=[];#n=[];#i=null;controller={connect:()=>this.#a(),disconnect:()=>this.#d()};reconcileOn=["open"];get open(){return this.hasAttribute("open")}set open(t){this.toggleAttribute("open",t)}#r(){return{state:()=>({open:this.open}),change:t=>this.#l(t)}}#l(t){t!==this.open&&(this.announce("primitiv-modal-change",{open:t})?this.open=t:this.#e?.sync())}#a(){this.#i=new AbortController,this.addEventListener("click",t=>this.#c(t),{signal:this.#i.signal}),this.#e?.connect()}#d(){this.#i.abort(),this.#i=null,this.#e?.disconnect(),this.#t&&r(this.#t,"open",null)}#c(t){let o=t.target;this.#o.some(n=>n.contains(o))?this.#l(!0):this.#n.some(n=>n.contains(o))&&this.#l(!1)}#u(t){t!==this.#t&&(this.#e?.disconnect(),t&&r(t,"open",null),this.#t=t,this.#e=t?new C(t,this.#r()):null,this.#e?.connect())}reconcile(){let t=this.open,o=t?"open":"closed",n=this.rootId();r(this,"data-state",o);let[l]=this.parts(st),s=l?It(l):null,[d]=this.parts(yt),u=d?lt(d,"a heading"):null,[a]=this.parts(Tt),i=a?lt(a,"a <p>"):null;u&&!u.id&&(u.id=`${n}-title`),i&&!i.id&&(i.id=`${n}-description`),s&&(s.id||(s.id=`${n}-content`),r(s,"data-state",o),r(s,"aria-labelledby",u?u.id:null),r(s,"aria-describedby",i?i.id:null)),this.#o=this.#s(vt);for(let c of this.#o)r(c,"aria-haspopup","dialog"),r(c,"aria-expanded",String(t)),r(c,"aria-controls",s?s.id:null);this.#n=this.#s(xt),this.#u(s),this.#e?.sync()}#s(t){return this.parts(t).flatMap(o=>lt(o,"a <button>")??[])}},B=class extends p{},G=class extends p{},z=class extends p{},F=class extends p{},U=class extends p{};function at(e=globalThis.customElements){f(e,[[kt,$],[vt,B],[st,G],[yt,z],[Tt,F],[xt,U]])}var A=class extends b{#t;constructor(t,o){super(t),this.#t=o}onTriggerClick(t){let{active:o}=this.#t.state(),n=w({disabled:this.disabled(),active:o},t.value);if(n!==null){let l=this.ordered().map(([,s])=>s.value);this.#t.activate(n,l.indexOf(n))}}onTriggerKeydown(t,o){let{active:n,orientation:l,dir:s,activationMode:d}=this.#t.state(),u=this.ordered(),a=u.map(([,c])=>c.value),i=X({values:a,disabled:this.disabled(),active:n,orientation:l,dir:s,activationMode:d},t.value,o.key);i&&(o.preventDefault(),i.activate!==null&&this.#t.activate(i.activate,a.indexOf(i.activate)),u[a.indexOf(i.focus)][0].focus())}};var Dt="primitiv-tabs",Et="primitiv-tabs-list",dt="primitiv-tabs-trigger",Ct="primitiv-tabs-content";function Nt(e){let t=e.getAttribute("value");return e.childElementCount===1&&t!==null?{tab:e.firstElementChild,value:t}:(h(e,`<${dt}> needs a value and exactly one element child (a <button>, an <a href> or a router link) to decorate.`),null)}var _=class extends m{#t;#e=!1;controller=new A(this,this.#o());reconcileOn=["value","disabled","label","orientation"];get value(){return this.getAttribute("value")??void 0}set value(t){this.setAttribute("value",t)}#o(){return{state:()=>({active:this.value,orientation:this.#i(),dir:this.direction(),activationMode:this.#n()}),activate:(t,o)=>{this.announce("primitiv-tabs-change",{value:t,index:o})&&(this.value=t)}}}#n(){let t=this.getAttribute("activation-mode");return t==="manual"?t:"automatic"}#i(){return this.getAttribute("orientation")==="vertical"?"vertical":"horizontal"}reconcile(){let t=this.#i(),o=this.value,n=this.rootId();r(this,"data-orientation",t);for(let a of this.parts(Et)){r(a,"role","tablist"),r(a,"aria-orientation",t),r(a,"data-orientation",t);let i=a.getAttribute("label");i!==null&&r(a,"aria-label",i)}let l=new Map;for(let a of this.parts(Ct)){let i=a.getAttribute("value");i!==null&&l.set(i,a)}let s=this.parts(dt).flatMap(a=>{let i=Nt(a);return i?[{trigger:a,...i}]:[]}),d=V({values:s.map(a=>a.value),active:o});this.controller.setTriggers(s.map(({trigger:a,tab:i,value:c})=>({value:c,element:i,disabled:a.hasAttribute("disabled")})));for(let{trigger:a,tab:i,value:c}of s){let g=l.get(c),ut=c===o,pt=a.hasAttribute("disabled");i.id||(i.id=v(n,"trigger",c)),g&&!g.id&&(g.id=v(n,"panel",c)),r(i,"role","tab"),r(i,"aria-selected",String(ut)),r(i,"aria-controls",g?g.id:null),r(i,"tabindex",c===d?"0":"-1"),r(i,"aria-disabled",pt?"true":null),r(i,"data-disabled",pt?"":null),r(i,"data-state",ut?"active":"inactive"),r(i,"data-orientation",t),g&&r(g,"aria-labelledby",i.id)}for(let[a,i]of l){let c=a===o;r(i,"role","tabpanel"),r(i,"tabindex","0"),r(i,"hidden",c?null:""),r(i,"data-state",c?"active":"inactive"),r(i,"data-orientation",t)}let u=l.get(o);this.#e&&o!==this.#t&&u&&u.dispatchEvent(new CustomEvent("primitiv-tabs-show",{bubbles:!0})),this.#t=o,this.#e=!0}},q=class extends p{},j=class extends p{},W=class extends p{};function ct(e=globalThis.customElements){f(e,[[Dt,_],[Et,q],[dt,j],[Ct,W]])}nt();rt();at();ct();export{x as AccordionController,E as CollapsibleController,C as ModalController,S as PrimitivAccordionContentElement,R as PrimitivAccordionElement,L as PrimitivAccordionItemElement,K as PrimitivAccordionTriggerElement,k as PrimitivAccordionTriggerIconElement,N as PrimitivCollapsibleContentElement,I as PrimitivCollapsibleElement,D as PrimitivCollapsibleTriggerElement,P as PrimitivCollapsibleTriggerIconElement,U as PrimitivModalCloseElement,G as PrimitivModalContentElement,F as PrimitivModalDescriptionElement,$ as PrimitivModalElement,z as PrimitivModalTitleElement,B as PrimitivModalTriggerElement,W as PrimitivTabsContentElement,_ as PrimitivTabsElement,q as PrimitivTabsListElement,j as PrimitivTabsTriggerElement,A as TabsController,nt as defineAccordion,rt as defineCollapsible,at as defineModal,ct as defineTabs};
package/dist/tags.d.ts CHANGED
@@ -14,6 +14,7 @@
14
14
  */
15
15
  import type { PrimitivAccordionContentElement, PrimitivAccordionElement, PrimitivAccordionItemElement, PrimitivAccordionTriggerElement, PrimitivAccordionTriggerIconElement } from "./accordion/elements.js";
16
16
  import type { PrimitivCollapsibleContentElement, PrimitivCollapsibleElement, PrimitivCollapsibleTriggerElement, PrimitivCollapsibleTriggerIconElement } from "./collapsible/elements.js";
17
+ import type { PrimitivModalCloseElement, PrimitivModalContentElement, PrimitivModalDescriptionElement, PrimitivModalElement, PrimitivModalTitleElement, PrimitivModalTriggerElement } from "./modal/elements.js";
17
18
  import type { PrimitivTabsContentElement, PrimitivTabsElement, PrimitivTabsListElement, PrimitivTabsTriggerElement } from "./tabs/elements.js";
18
19
  declare global {
19
20
  interface HTMLElementTagNameMap {
@@ -26,6 +27,12 @@ declare global {
26
27
  "primitiv-collapsible-trigger": PrimitivCollapsibleTriggerElement;
27
28
  "primitiv-collapsible-content": PrimitivCollapsibleContentElement;
28
29
  "primitiv-collapsible-trigger-icon": PrimitivCollapsibleTriggerIconElement;
30
+ "primitiv-modal": PrimitivModalElement;
31
+ "primitiv-modal-trigger": PrimitivModalTriggerElement;
32
+ "primitiv-modal-content": PrimitivModalContentElement;
33
+ "primitiv-modal-title": PrimitivModalTitleElement;
34
+ "primitiv-modal-description": PrimitivModalDescriptionElement;
35
+ "primitiv-modal-close": PrimitivModalCloseElement;
29
36
  "primitiv-tabs": PrimitivTabsElement;
30
37
  "primitiv-tabs-list": PrimitivTabsListElement;
31
38
  "primitiv-tabs-trigger": PrimitivTabsTriggerElement;