@primitiv-ui/web 0.1.42 → 0.1.44
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 +59 -10
- package/dist/accordion/AccordionController.d.ts +33 -0
- package/dist/accordion/AccordionController.js +41 -0
- package/dist/accordion/elements.d.ts +79 -0
- package/dist/accordion/elements.js +194 -0
- package/dist/accordion/index.d.ts +2 -0
- package/dist/accordion/index.js +2 -0
- package/dist/bundle.d.ts +2 -1
- package/dist/bundle.js +2 -1
- package/dist/cdn/accordion.js +1 -0
- package/dist/cdn/chunks/chunk-FKLOMAT4.js +1 -0
- package/dist/cdn/tabs.js +1 -0
- package/dist/custom-elements.json +308 -48
- package/dist/define/accordion.d.ts +1 -0
- package/dist/define/accordion.js +11 -0
- package/dist/define/index.d.ts +1 -0
- package/dist/define/index.js +1 -0
- package/dist/frameworks/events.d.ts +4 -0
- package/dist/frameworks/react.d.ts +20 -2
- package/dist/frameworks/svelte.d.ts +19 -1
- package/dist/frameworks/vue.d.ts +20 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/internal/RootElement.d.ts +46 -0
- package/dist/internal/RootElement.js +59 -0
- package/dist/internal/TriggerController.d.ts +52 -0
- package/dist/internal/TriggerController.js +76 -0
- package/dist/internal/element.d.ts +38 -0
- package/dist/internal/element.js +60 -0
- package/dist/primitiv-web.js +1 -1
- package/dist/tabs/TabsController.d.ts +6 -31
- package/dist/tabs/TabsController.js +11 -79
- package/dist/tabs/elements.d.ts +13 -26
- package/dist/tabs/elements.js +22 -100
- package/dist/tags.d.ts +6 -0
- package/package.json +13 -4
package/README.md
CHANGED
|
@@ -6,16 +6,23 @@ framework and no runtime dependencies beyond
|
|
|
6
6
|
server-rendered stack (HTMX, Rails, Django, Laravel, Spring …), and inside
|
|
7
7
|
React, Vue, Angular or any other framework.
|
|
8
8
|
|
|
9
|
-
> **Status:
|
|
10
|
-
>
|
|
11
|
-
|
|
9
|
+
> **Status: published.** Tabs and Accordion so far. See
|
|
10
|
+
> [RFC 0033](../../docs/rfcs/0033-web-components.md) for the design.
|
|
11
|
+
|
|
12
|
+
## Components
|
|
13
|
+
|
|
14
|
+
| Component | Elements | Change event | First release |
|
|
15
|
+
|---|---|---|---|
|
|
16
|
+
| Tabs | `primitiv-tabs`, `-list`, `-trigger`, `-content` | `primitiv-tabs-change` | 0.1.42 |
|
|
17
|
+
| Accordion | `primitiv-accordion`, `-item`, `-trigger`, `-content`, `-trigger-icon` | `primitiv-accordion-change` | 0.1.44 |
|
|
12
18
|
|
|
13
19
|
## Use it
|
|
14
20
|
|
|
15
|
-
**No build step** — one script tag defines
|
|
21
|
+
**No build step** — one script tag per component defines that component's
|
|
22
|
+
elements and nothing else:
|
|
16
23
|
|
|
17
24
|
```html
|
|
18
|
-
<script type="module" src="https://cdn.jsdelivr.net/npm/@primitiv-ui/web@<version>/dist/
|
|
25
|
+
<script type="module" src="https://cdn.jsdelivr.net/npm/@primitiv-ui/web@<version>/dist/cdn/tabs.js"></script>
|
|
19
26
|
|
|
20
27
|
<primitiv-tabs value="overview">
|
|
21
28
|
<primitiv-tabs-list label="Account sections">
|
|
@@ -30,12 +37,18 @@ React, Vue, Angular or any other framework.
|
|
|
30
37
|
**With a bundler** — import a `define` entry once, anywhere:
|
|
31
38
|
|
|
32
39
|
```ts
|
|
33
|
-
import "@primitiv-ui/web/define";
|
|
34
|
-
import "@primitiv-ui/web/define
|
|
40
|
+
import "@primitiv-ui/web/define/tabs"; // one component's elements
|
|
41
|
+
import "@primitiv-ui/web/define"; // or every element
|
|
35
42
|
```
|
|
36
43
|
|
|
37
|
-
|
|
38
|
-
`
|
|
44
|
+
Each `dist/cdn/<component>.js` imports the code components share from
|
|
45
|
+
`dist/cdn/chunks/` by a relative URL, so a page that loads two components
|
|
46
|
+
downloads it once. `dist/primitiv-web.js` is the alternative: every element in
|
|
47
|
+
one file. Per-component files ship from 0.1.43.
|
|
48
|
+
|
|
49
|
+
The main entry (`@primitiv-ui/web`, `/tabs`, `/accordion`) exports the classes
|
|
50
|
+
and `defineTabs(registry?)` / `defineAccordion(registry?)` with **no side
|
|
51
|
+
effects**.
|
|
39
52
|
|
|
40
53
|
**Render `hidden` on inactive panels yourself.** Until the module loads, CSS
|
|
41
54
|
cannot tell which panel is active, so markup that omits it shows every panel.
|
|
@@ -44,6 +57,42 @@ cannot tell which panel is active, so markup that omits it shows every panel.
|
|
|
44
57
|
index }`) — call `preventDefault()` to veto a change — and `primitiv-tabs-show`
|
|
45
58
|
on a panel as it becomes active, for lazy loading.
|
|
46
59
|
|
|
60
|
+
### Accordion
|
|
61
|
+
|
|
62
|
+
```html
|
|
63
|
+
<script type="module" src="https://cdn.jsdelivr.net/npm/@primitiv-ui/web@<version>/dist/cdn/accordion.js"></script>
|
|
64
|
+
|
|
65
|
+
<primitiv-accordion value="shipping">
|
|
66
|
+
<primitiv-accordion-item value="shipping">
|
|
67
|
+
<h3>
|
|
68
|
+
<primitiv-accordion-trigger><button type="button">Shipping</button></primitiv-accordion-trigger>
|
|
69
|
+
</h3>
|
|
70
|
+
<primitiv-accordion-content>Free on orders over £50.</primitiv-accordion-content>
|
|
71
|
+
</primitiv-accordion-item>
|
|
72
|
+
<primitiv-accordion-item value="returns">
|
|
73
|
+
<h3>
|
|
74
|
+
<primitiv-accordion-trigger><button type="button">Returns</button></primitiv-accordion-trigger>
|
|
75
|
+
</h3>
|
|
76
|
+
<primitiv-accordion-content hidden>30 days, no questions asked.</primitiv-accordion-content>
|
|
77
|
+
</primitiv-accordion-item>
|
|
78
|
+
</primitiv-accordion>
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
- **`value` lists the open items, separated by spaces** (so item values cannot
|
|
82
|
+
contain whitespace), and is reflected as items toggle. `multiple` lets more
|
|
83
|
+
than one be open; `orientation="horizontal"` switches the arrow pair. The
|
|
84
|
+
`value` property reads as an array and accepts an array or a string.
|
|
85
|
+
- **The heading is yours.** Wrap each trigger in a real `<h2>`–`<h6>` at the
|
|
86
|
+
level your outline needs; there is no header element.
|
|
87
|
+
- **`force-mount` on a panel** keeps it unhidden so styles can animate it,
|
|
88
|
+
hiding it with `aria-hidden` while closed instead — the registry stylesheet
|
|
89
|
+
needs it.
|
|
90
|
+
- **`role="none"` on a panel** opts it out of being a region landmark, for an
|
|
91
|
+
accordion with many items.
|
|
92
|
+
- **Listen with `primitiv-accordion-change`** (bubbles, cancelable, `detail: {
|
|
93
|
+
value }`, every item open afterwards) and `primitiv-accordion-show` on a panel
|
|
94
|
+
as its item opens. Render `hidden` on closed panels yourself, as for Tabs.
|
|
95
|
+
|
|
47
96
|
**TypeScript** — opt into the tag map so DOM queries return the element classes:
|
|
48
97
|
`import type {} from "@primitiv-ui/web/tags"` (npm only).
|
|
49
98
|
|
|
@@ -78,7 +127,7 @@ The elements write no `class` and no `style`. For the registry look, apply the
|
|
|
78
127
|
|
|
79
128
|
## Layers
|
|
80
129
|
|
|
81
|
-
- **Controllers** (`TabsController`) hold a component's DOM behaviour —
|
|
130
|
+
- **Controllers** (`TabsController`, `AccordionController`) hold a component's DOM behaviour —
|
|
82
131
|
listeners, focus, the ordered collection of parts — and attach to whatever
|
|
83
132
|
elements register with them. They never own state; their host does.
|
|
84
133
|
- **Element hosts** (`<primitiv-tabs>` …) are the public face: they keep state
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { type AccordionOrientation, type AccordionReadingDirection } from "@primitiv-ui/core";
|
|
2
|
+
import { TriggerController, type Trigger } from "../internal/TriggerController.js";
|
|
3
|
+
/**
|
|
4
|
+
* What an {@link AccordionController} asks of whoever hosts it. The controller
|
|
5
|
+
* owns behaviour and never owns state (RFC 0033 D28): the custom elements keep
|
|
6
|
+
* the open items in a reflected `value` attribute, the React adapter keeps them
|
|
7
|
+
* in React, and each answers these two calls from wherever it keeps them.
|
|
8
|
+
*/
|
|
9
|
+
export type AccordionControllerHost = {
|
|
10
|
+
/** The state the controller cannot see for itself. */
|
|
11
|
+
state(): {
|
|
12
|
+
/** The values of the open items. */
|
|
13
|
+
expanded: readonly string[];
|
|
14
|
+
multiple: boolean;
|
|
15
|
+
orientation: AccordionOrientation;
|
|
16
|
+
dir: AccordionReadingDirection;
|
|
17
|
+
};
|
|
18
|
+
/** Apply a change the user asked for: `value` is every item open afterwards. */
|
|
19
|
+
change(value: string[]): void;
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* The DOM behaviour of an accordion: listening, focus, and the ordered
|
|
23
|
+
* collection of triggers. It attaches to whatever elements register with it, so
|
|
24
|
+
* the same code runs under the custom elements and under the React adapter,
|
|
25
|
+
* which renders its own markup (RFC 0033 §3.4). A disabled trigger is skipped by
|
|
26
|
+
* the arrow keys and never toggles.
|
|
27
|
+
*/
|
|
28
|
+
export declare class AccordionController extends TriggerController {
|
|
29
|
+
#private;
|
|
30
|
+
constructor(root: HTMLElement, host: AccordionControllerHost);
|
|
31
|
+
protected onTriggerClick(trigger: Trigger): void;
|
|
32
|
+
protected onTriggerKeydown(trigger: Trigger, event: KeyboardEvent): void;
|
|
33
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { accordionKeydown, accordionToggle, } from "@primitiv-ui/core";
|
|
2
|
+
import { TriggerController } from "../internal/TriggerController.js";
|
|
3
|
+
/**
|
|
4
|
+
* The DOM behaviour of an accordion: listening, focus, and the ordered
|
|
5
|
+
* collection of triggers. It attaches to whatever elements register with it, so
|
|
6
|
+
* the same code runs under the custom elements and under the React adapter,
|
|
7
|
+
* which renders its own markup (RFC 0033 §3.4). A disabled trigger is skipped by
|
|
8
|
+
* the arrow keys and never toggles.
|
|
9
|
+
*/
|
|
10
|
+
export class AccordionController extends TriggerController {
|
|
11
|
+
#host;
|
|
12
|
+
constructor(root, host) {
|
|
13
|
+
super(root);
|
|
14
|
+
this.#host = host;
|
|
15
|
+
}
|
|
16
|
+
/** Ask the host to toggle `value`, unless the toggle is a no-op. */
|
|
17
|
+
#toggle(value) {
|
|
18
|
+
const { expanded, multiple } = this.#host.state();
|
|
19
|
+
const next = accordionToggle({ disabled: this.disabled(), expanded, multiple }, value);
|
|
20
|
+
if (next !== null)
|
|
21
|
+
this.#host.change(next);
|
|
22
|
+
}
|
|
23
|
+
onTriggerClick(trigger) {
|
|
24
|
+
this.#toggle(trigger.value);
|
|
25
|
+
}
|
|
26
|
+
onTriggerKeydown(trigger, event) {
|
|
27
|
+
const { expanded, multiple, orientation, dir } = this.#host.state();
|
|
28
|
+
const ordered = this.ordered();
|
|
29
|
+
const values = ordered.map(([, t]) => t.value);
|
|
30
|
+
const outcome = accordionKeydown({ values, disabled: this.disabled(), expanded, multiple, orientation, dir }, trigger.value, event.key);
|
|
31
|
+
if (!outcome)
|
|
32
|
+
return;
|
|
33
|
+
// Enter and Space would also click the button; the toggle happens here once.
|
|
34
|
+
event.preventDefault();
|
|
35
|
+
if (outcome.toggle !== null)
|
|
36
|
+
this.#toggle(outcome.toggle);
|
|
37
|
+
// A focus target is always one of `values`, so its index is always valid.
|
|
38
|
+
if (outcome.focus !== null)
|
|
39
|
+
ordered[values.indexOf(outcome.focus)][0].focus();
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { ElementBase, type ElementRegistry } from "../internal/element.js";
|
|
2
|
+
import { PrimitivRootElement } from "../internal/RootElement.js";
|
|
3
|
+
import { AccordionController } from "./AccordionController.js";
|
|
4
|
+
export type { ElementRegistry };
|
|
5
|
+
/**
|
|
6
|
+
* `<primitiv-accordion>` — the state owner of a WAI-ARIA accordion.
|
|
7
|
+
*
|
|
8
|
+
* The root finds its own items, so an accordion nested in a panel keeps its
|
|
9
|
+
* own, and reconciles every part's ARIA from its reflected state whenever a part
|
|
10
|
+
* arrives, leaves, or changes an attribute the root reads (the lifecycle is
|
|
11
|
+
* `PrimitivRootElement`'s). `multiple` is read live at each toggle, so a change
|
|
12
|
+
* to it needs no reconcile.
|
|
13
|
+
*
|
|
14
|
+
* Each trigger sits in a heading you write (`<h3>`, at the level the page's
|
|
15
|
+
* outline needs): a real heading element is the header, so there is no element
|
|
16
|
+
* for it.
|
|
17
|
+
*
|
|
18
|
+
* @customElement primitiv-accordion
|
|
19
|
+
* @summary The state owner of a WAI-ARIA accordion.
|
|
20
|
+
* @attr {string} value - The open items' values, separated by spaces. Reflected: it changes as the user toggles items, so a server re-render or a history snapshot sees what is open. Item values therefore cannot contain whitespace.
|
|
21
|
+
* @attr {boolean} multiple - Lets more than one item be open at once. Without it, opening an item closes the open one.
|
|
22
|
+
* @attr {"vertical" | "horizontal"} orientation - Layout axis, which picks the arrow-key pair. Defaults to `vertical`.
|
|
23
|
+
* @fires {CustomEvent<{ value: string[] }>} primitiv-accordion-change - Bubbles, cancelable, fired before a user-driven toggle is applied, with every item open afterwards. Call `preventDefault()` to veto it.
|
|
24
|
+
*/
|
|
25
|
+
export declare class PrimitivAccordionElement extends PrimitivRootElement {
|
|
26
|
+
#private;
|
|
27
|
+
protected readonly controller: AccordionController;
|
|
28
|
+
protected readonly reconcileOn: readonly string[];
|
|
29
|
+
/** The open items' values, reflected as the space-separated `value` attribute. */
|
|
30
|
+
get value(): string[];
|
|
31
|
+
/** Set the open items, as an array or a space-separated string. */
|
|
32
|
+
set value(next: string | readonly string[]);
|
|
33
|
+
protected reconcile(): void;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* `<primitiv-accordion-item>` — one collapsible section. Structural: it pairs a
|
|
37
|
+
* trigger and a panel and carries the value the root's `value` lists.
|
|
38
|
+
*
|
|
39
|
+
* @customElement primitiv-accordion-item
|
|
40
|
+
* @attr {string} value - The item's value, listed in the root's `value` while it is open. No whitespace.
|
|
41
|
+
*/
|
|
42
|
+
export declare class PrimitivAccordionItemElement extends ElementBase {
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* `<primitiv-accordion-trigger>` — an interactive part. It decorates exactly one
|
|
46
|
+
* child the consumer writes, a `<button>`, which receives `aria-expanded`,
|
|
47
|
+
* `aria-controls` and the state attributes, so the rendered DOM matches React's
|
|
48
|
+
* `<button aria-expanded>` (RFC 0033 D24). Put it inside a heading.
|
|
49
|
+
*
|
|
50
|
+
* @customElement primitiv-accordion-trigger
|
|
51
|
+
* @attr {boolean} disabled - Marks the item disabled: the button stays focusable but never toggles, and the arrow keys skip it.
|
|
52
|
+
*/
|
|
53
|
+
export declare class PrimitivAccordionTriggerElement extends ElementBase {
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* `<primitiv-accordion-content>` — an item's panel. Structural: the root writes
|
|
57
|
+
* its ARIA and hides it while the item is closed.
|
|
58
|
+
*
|
|
59
|
+
* @customElement primitiv-accordion-content
|
|
60
|
+
* @attr {boolean} force-mount - Never sets `hidden`, so styles can animate the panel open and closed. A closed panel always carries `aria-hidden`, so it stays out of the accessibility tree either way.
|
|
61
|
+
* @fires {CustomEvent} primitiv-accordion-show - Bubbles from the panel as its item opens (not on first render), so a lazy panel can fetch its content.
|
|
62
|
+
*/
|
|
63
|
+
export declare class PrimitivAccordionContentElement extends ElementBase {
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* `<primitiv-accordion-trigger-icon>` — a decorative indicator inside the
|
|
67
|
+
* trigger's button. The root hides it from assistive technology and mirrors its
|
|
68
|
+
* item's `data-state`, for a chevron that flips.
|
|
69
|
+
*
|
|
70
|
+
* @customElement primitiv-accordion-trigger-icon
|
|
71
|
+
*/
|
|
72
|
+
export declare class PrimitivAccordionTriggerIconElement extends ElementBase {
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Define the five Accordion elements on `registry` (the global `customElements`
|
|
76
|
+
* by default). Safe to call more than once, and a no-op where there is no
|
|
77
|
+
* registry (a server render).
|
|
78
|
+
*/
|
|
79
|
+
export declare function defineAccordion(registry?: ElementRegistry | undefined): void;
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import { deriveId } from "@primitiv-ui/core";
|
|
2
|
+
import { defineElements, ElementBase, owned, reportOnce, write } from "../internal/element.js";
|
|
3
|
+
import { PrimitivRootElement } from "../internal/RootElement.js";
|
|
4
|
+
import { AccordionController } from "./AccordionController.js";
|
|
5
|
+
// Stryker disable StringLiteral: static — evaluated once at import, so runtime mutant switching never reaches them; the names are pinned by the defineAccordion registry test instead.
|
|
6
|
+
const ROOT = "primitiv-accordion";
|
|
7
|
+
const ITEM = "primitiv-accordion-item";
|
|
8
|
+
const TRIGGER = "primitiv-accordion-trigger";
|
|
9
|
+
const CONTENT = "primitiv-accordion-content";
|
|
10
|
+
const ICON = "primitiv-accordion-trigger-icon";
|
|
11
|
+
// Stryker restore StringLiteral
|
|
12
|
+
/** Split a space-separated `value` into item values. */
|
|
13
|
+
function tokens(value) {
|
|
14
|
+
// Stryker disable next-line Regex: equivalent — splitting on single whitespace leaves empty tokens, which the filter drops.
|
|
15
|
+
return (value ?? "").split(/\s+/).filter(Boolean);
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* A trigger decorates exactly one element child — the `<button>` the consumer
|
|
19
|
+
* writes. Anything else is reported once and left undecorated rather than
|
|
20
|
+
* thrown (RFC 0033 D24).
|
|
21
|
+
*/
|
|
22
|
+
function decoratable(trigger) {
|
|
23
|
+
if (trigger.childElementCount === 1)
|
|
24
|
+
return trigger.firstElementChild;
|
|
25
|
+
reportOnce(trigger, `<${TRIGGER}> needs exactly one element child (a <button>) to decorate.`);
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* `<primitiv-accordion>` — the state owner of a WAI-ARIA accordion.
|
|
30
|
+
*
|
|
31
|
+
* The root finds its own items, so an accordion nested in a panel keeps its
|
|
32
|
+
* own, and reconciles every part's ARIA from its reflected state whenever a part
|
|
33
|
+
* arrives, leaves, or changes an attribute the root reads (the lifecycle is
|
|
34
|
+
* `PrimitivRootElement`'s). `multiple` is read live at each toggle, so a change
|
|
35
|
+
* to it needs no reconcile.
|
|
36
|
+
*
|
|
37
|
+
* Each trigger sits in a heading you write (`<h3>`, at the level the page's
|
|
38
|
+
* outline needs): a real heading element is the header, so there is no element
|
|
39
|
+
* for it.
|
|
40
|
+
*
|
|
41
|
+
* @customElement primitiv-accordion
|
|
42
|
+
* @summary The state owner of a WAI-ARIA accordion.
|
|
43
|
+
* @attr {string} value - The open items' values, separated by spaces. Reflected: it changes as the user toggles items, so a server re-render or a history snapshot sees what is open. Item values therefore cannot contain whitespace.
|
|
44
|
+
* @attr {boolean} multiple - Lets more than one item be open at once. Without it, opening an item closes the open one.
|
|
45
|
+
* @attr {"vertical" | "horizontal"} orientation - Layout axis, which picks the arrow-key pair. Defaults to `vertical`.
|
|
46
|
+
* @fires {CustomEvent<{ value: string[] }>} primitiv-accordion-change - Bubbles, cancelable, fired before a user-driven toggle is applied, with every item open afterwards. Call `preventDefault()` to veto it.
|
|
47
|
+
*/
|
|
48
|
+
export class PrimitivAccordionElement extends PrimitivRootElement {
|
|
49
|
+
/** The items open at the last reconcile, so an item opening fires primitiv-accordion-show. */
|
|
50
|
+
#shown = new Set();
|
|
51
|
+
#rendered = false;
|
|
52
|
+
controller = new AccordionController(this, this.#host());
|
|
53
|
+
reconcileOn = ["value", "disabled", "orientation", "force-mount"];
|
|
54
|
+
/** The open items' values, reflected as the space-separated `value` attribute. */
|
|
55
|
+
get value() {
|
|
56
|
+
return tokens(this.getAttribute("value"));
|
|
57
|
+
}
|
|
58
|
+
/** Set the open items, as an array or a space-separated string. */
|
|
59
|
+
set value(next) {
|
|
60
|
+
this.setAttribute("value", typeof next === "string" ? next : next.join(" "));
|
|
61
|
+
}
|
|
62
|
+
/** The controller's view of this element: state lives in reflected attributes. */
|
|
63
|
+
#host() {
|
|
64
|
+
return {
|
|
65
|
+
state: () => ({
|
|
66
|
+
expanded: this.value,
|
|
67
|
+
multiple: this.hasAttribute("multiple"),
|
|
68
|
+
orientation: this.#orientation(),
|
|
69
|
+
dir: this.direction(),
|
|
70
|
+
}),
|
|
71
|
+
change: (value) => {
|
|
72
|
+
if (this.announce("primitiv-accordion-change", { value }))
|
|
73
|
+
this.value = value;
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
#orientation() {
|
|
78
|
+
return this.getAttribute("orientation") === "horizontal" ? "horizontal" : "vertical";
|
|
79
|
+
}
|
|
80
|
+
reconcile() {
|
|
81
|
+
const expanded = new Set(this.value);
|
|
82
|
+
const rootId = this.rootId();
|
|
83
|
+
write(this, "data-orientation", this.#orientation());
|
|
84
|
+
const triggers = [];
|
|
85
|
+
for (const item of this.parts(ITEM)) {
|
|
86
|
+
const value = item.getAttribute("value");
|
|
87
|
+
if (value === null) {
|
|
88
|
+
reportOnce(item, `<${ITEM}> needs a value, which pairs it with the root's value.`);
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
const open = expanded.has(value);
|
|
92
|
+
const state = open ? "open" : "closed";
|
|
93
|
+
const [trigger] = owned(item, TRIGGER, ITEM);
|
|
94
|
+
const [panel] = owned(item, CONTENT, ITEM);
|
|
95
|
+
const button = trigger ? decoratable(trigger) : null;
|
|
96
|
+
if (button) {
|
|
97
|
+
const disabled = trigger.hasAttribute("disabled");
|
|
98
|
+
triggers.push({ value, element: button, disabled });
|
|
99
|
+
if (!button.id)
|
|
100
|
+
button.id = deriveId(rootId, "heading", value);
|
|
101
|
+
write(button, "aria-expanded", String(open));
|
|
102
|
+
write(button, "aria-disabled", String(disabled));
|
|
103
|
+
write(button, "data-disabled", String(disabled));
|
|
104
|
+
write(button, "data-state", state);
|
|
105
|
+
}
|
|
106
|
+
if (panel) {
|
|
107
|
+
if (!panel.id)
|
|
108
|
+
panel.id = deriveId(rootId, "panel", value);
|
|
109
|
+
const forced = panel.hasAttribute("force-mount");
|
|
110
|
+
// A role the markup already has is kept: `role="none"` opts a panel
|
|
111
|
+
// out of being a region landmark.
|
|
112
|
+
if (!panel.hasAttribute("role"))
|
|
113
|
+
panel.setAttribute("role", "region");
|
|
114
|
+
write(panel, "aria-labelledby", button ? button.id : null);
|
|
115
|
+
write(panel, "hidden", open || forced ? null : "");
|
|
116
|
+
// Every closed panel, not only a force-mount one: a stylesheet that
|
|
117
|
+
// animates the panel gives it a display that wins over `hidden` (the
|
|
118
|
+
// registry's does), which would leave an empty region exposed.
|
|
119
|
+
write(panel, "aria-hidden", open ? null : "true");
|
|
120
|
+
write(panel, "data-state", state);
|
|
121
|
+
// An item that opens announces it, so a lazy panel can fetch its
|
|
122
|
+
// content (`hx-trigger="primitiv-accordion-show once"`). The first
|
|
123
|
+
// render shows what the server already rendered, so it announces nothing.
|
|
124
|
+
if (this.#rendered && open && !this.#shown.has(value)) {
|
|
125
|
+
panel.dispatchEvent(new CustomEvent("primitiv-accordion-show", { bubbles: true }));
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
if (button)
|
|
129
|
+
write(button, "aria-controls", panel ? panel.id : null);
|
|
130
|
+
for (const icon of owned(item, ICON, ITEM)) {
|
|
131
|
+
write(icon, "aria-hidden", "true");
|
|
132
|
+
write(icon, "data-state", state);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
// Replace the registration wholesale: a removed item, or a trigger whose
|
|
136
|
+
// child a framework re-rendered, simply is not registered again.
|
|
137
|
+
this.controller.setTriggers(triggers);
|
|
138
|
+
this.#shown = expanded;
|
|
139
|
+
this.#rendered = true;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* `<primitiv-accordion-item>` — one collapsible section. Structural: it pairs a
|
|
144
|
+
* trigger and a panel and carries the value the root's `value` lists.
|
|
145
|
+
*
|
|
146
|
+
* @customElement primitiv-accordion-item
|
|
147
|
+
* @attr {string} value - The item's value, listed in the root's `value` while it is open. No whitespace.
|
|
148
|
+
*/
|
|
149
|
+
export class PrimitivAccordionItemElement extends ElementBase {
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* `<primitiv-accordion-trigger>` — an interactive part. It decorates exactly one
|
|
153
|
+
* child the consumer writes, a `<button>`, which receives `aria-expanded`,
|
|
154
|
+
* `aria-controls` and the state attributes, so the rendered DOM matches React's
|
|
155
|
+
* `<button aria-expanded>` (RFC 0033 D24). Put it inside a heading.
|
|
156
|
+
*
|
|
157
|
+
* @customElement primitiv-accordion-trigger
|
|
158
|
+
* @attr {boolean} disabled - Marks the item disabled: the button stays focusable but never toggles, and the arrow keys skip it.
|
|
159
|
+
*/
|
|
160
|
+
export class PrimitivAccordionTriggerElement extends ElementBase {
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* `<primitiv-accordion-content>` — an item's panel. Structural: the root writes
|
|
164
|
+
* its ARIA and hides it while the item is closed.
|
|
165
|
+
*
|
|
166
|
+
* @customElement primitiv-accordion-content
|
|
167
|
+
* @attr {boolean} force-mount - Never sets `hidden`, so styles can animate the panel open and closed. A closed panel always carries `aria-hidden`, so it stays out of the accessibility tree either way.
|
|
168
|
+
* @fires {CustomEvent} primitiv-accordion-show - Bubbles from the panel as its item opens (not on first render), so a lazy panel can fetch its content.
|
|
169
|
+
*/
|
|
170
|
+
export class PrimitivAccordionContentElement extends ElementBase {
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* `<primitiv-accordion-trigger-icon>` — a decorative indicator inside the
|
|
174
|
+
* trigger's button. The root hides it from assistive technology and mirrors its
|
|
175
|
+
* item's `data-state`, for a chevron that flips.
|
|
176
|
+
*
|
|
177
|
+
* @customElement primitiv-accordion-trigger-icon
|
|
178
|
+
*/
|
|
179
|
+
export class PrimitivAccordionTriggerIconElement extends ElementBase {
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Define the five Accordion elements on `registry` (the global `customElements`
|
|
183
|
+
* by default). Safe to call more than once, and a no-op where there is no
|
|
184
|
+
* registry (a server render).
|
|
185
|
+
*/
|
|
186
|
+
export function defineAccordion(registry = globalThis.customElements) {
|
|
187
|
+
defineElements(registry, [
|
|
188
|
+
[ROOT, PrimitivAccordionElement],
|
|
189
|
+
[ITEM, PrimitivAccordionItemElement],
|
|
190
|
+
[TRIGGER, PrimitivAccordionTriggerElement],
|
|
191
|
+
[CONTENT, PrimitivAccordionContentElement],
|
|
192
|
+
[ICON, PrimitivAccordionTriggerIconElement],
|
|
193
|
+
]);
|
|
194
|
+
}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
export { AccordionController, type AccordionControllerHost } from "./AccordionController.js";
|
|
2
|
+
export { defineAccordion, PrimitivAccordionContentElement, PrimitivAccordionElement, PrimitivAccordionItemElement, PrimitivAccordionTriggerElement, PrimitivAccordionTriggerIconElement, } from "./elements.js";
|
package/dist/bundle.d.ts
CHANGED
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
* <script type="module" src="https://cdn.jsdelivr.net/npm/@primitiv-ui/web@<version>/dist/primitiv-web.js"></script>
|
|
7
7
|
* ```
|
|
8
8
|
*
|
|
9
|
-
* Loading it defines every element and exports every class.
|
|
9
|
+
* Loading it defines every element and exports every class. To load one
|
|
10
|
+
* component only, use its `dist/cdn/<component>.js` (scripts/build-cdn.mjs).
|
|
10
11
|
*/
|
|
11
12
|
export * from "./index.js";
|
|
12
13
|
import "./define/index.js";
|
package/dist/bundle.js
CHANGED
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
* <script type="module" src="https://cdn.jsdelivr.net/npm/@primitiv-ui/web@<version>/dist/primitiv-web.js"></script>
|
|
7
7
|
* ```
|
|
8
8
|
*
|
|
9
|
-
* Loading it defines every element and exports every class.
|
|
9
|
+
* Loading it defines every element and exports every class. To load one
|
|
10
|
+
* component only, use its `dist/cdn/<component>.js` (scripts/build-cdn.mjs).
|
|
10
11
|
*/
|
|
11
12
|
export * from "./index.js";
|
|
12
13
|
import "./define/index.js";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{a as b,e as C,f as O,g,h as n,i as v,j as H,k as f,l as R,m as M}from"./chunks/chunk-FKLOMAT4.js";var m=class extends M{#t;constructor(e,s){super(e),this.#t=s}#e(e){let{expanded:s,multiple:c}=this.#t.state(),r=C({disabled:this.disabled(),expanded:s,multiple:c},e);r!==null&&this.#t.change(r)}onTriggerClick(e){this.#e(e.value)}onTriggerKeydown(e,s){let{expanded:c,multiple:r,orientation:l,dir:d}=this.#t.state(),u=this.ordered(),p=u.map(([,o])=>o.value),t=O({values:p,disabled:this.disabled(),expanded:c,multiple:r,orientation:l,dir:d},e.value,s.key);t&&(s.preventDefault(),t.toggle!==null&&this.#e(t.toggle),t.focus!==null&&u[p.indexOf(t.focus)][0].focus())}};var k="primitiv-accordion",h="primitiv-accordion-item",w="primitiv-accordion-trigger",L="primitiv-accordion-content",S="primitiv-accordion-trigger-icon";function D(i){return(i??"").split(/\s+/).filter(Boolean)}function K(i){return i.childElementCount===1?i.firstElementChild:(v(i,`<${w}> needs exactly one element child (a <button>) to decorate.`),null)}var y=class extends R{#t=new Set;#e=!1;controller=new m(this,this.#i());reconcileOn=["value","disabled","orientation","force-mount"];get value(){return D(this.getAttribute("value"))}set value(e){this.setAttribute("value",typeof e=="string"?e:e.join(" "))}#i(){return{state:()=>({expanded:this.value,multiple:this.hasAttribute("multiple"),orientation:this.#o(),dir:this.direction()}),change:e=>{this.announce("primitiv-accordion-change",{value:e})&&(this.value=e)}}}#o(){return this.getAttribute("orientation")==="horizontal"?"horizontal":"vertical"}reconcile(){let e=new Set(this.value),s=this.rootId();n(this,"data-orientation",this.#o());let c=[];for(let r of this.parts(h)){let l=r.getAttribute("value");if(l===null){v(r,`<${h}> needs a value, which pairs it with the root's value.`);continue}let d=e.has(l),u=d?"open":"closed",[p]=f(r,w,h),[t]=f(r,L,h),o=p?K(p):null;if(o){let a=p.hasAttribute("disabled");c.push({value:l,element:o,disabled:a}),o.id||(o.id=b(s,"heading",l)),n(o,"aria-expanded",String(d)),n(o,"aria-disabled",String(a)),n(o,"data-disabled",String(a)),n(o,"data-state",u)}if(t){t.id||(t.id=b(s,"panel",l));let a=t.hasAttribute("force-mount");t.hasAttribute("role")||t.setAttribute("role","region"),n(t,"aria-labelledby",o?o.id:null),n(t,"hidden",d||a?null:""),n(t,"aria-hidden",d?null:"true"),n(t,"data-state",u),this.#e&&d&&!this.#t.has(l)&&t.dispatchEvent(new CustomEvent("primitiv-accordion-show",{bubbles:!0}))}o&&n(o,"aria-controls",t?t.id:null);for(let a of f(r,S,h))n(a,"aria-hidden","true"),n(a,"data-state",u)}this.controller.setTriggers(c),this.#t=e,this.#e=!0}},x=class extends g{},A=class extends g{},T=class extends g{},E=class extends g{};function I(i=globalThis.customElements){H(i,[[k,y],[h,x],[w,A],[L,T],[S,E]])}I();
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
function b(e,t,r){return`${e}-${t}-${r}`}function s({orientation:e,dir:t,homeEnd:r=!1,activate:n=!1}){let o={};if(e==="horizontal"||e==="both"){let i=t==="rtl"?"ArrowLeft":"ArrowRight",a=t==="rtl"?"ArrowRight":"ArrowLeft";o[i]="next",o[a]="prev"}return(e==="vertical"||e==="both")&&(o.ArrowDown="next",o.ArrowUp="prev"),r&&(o.Home="first",o.End="last"),n&&(o.Enter="activate",o[" "]="activate"),o}function c({action:e,navigable:t,currentKey:r}){let n=t.length;if(n===0)return null;switch(e){case"activate":return{target:r};case"first":return{target:t[0]};case"last":return{target:t[n-1]};case void 0:return null;case"next":case"prev":{let o=t.indexOf(r);if(o===-1)return null;let i=e==="next"?1:n-1;return{target:t[(o+i)%n]}}}}function y(e){return e.active??e.values[0]}function l(e,t){return e.disabled.has(t)||e.active===t?null:t}function f(e,t,r){let n=s({orientation:e.orientation,dir:e.dir,homeEnd:!0,activate:!0})[r],o=c({action:n,navigable:e.values,currentKey:t});if(!o)return null;let i=o.target,a=e.activationMode==="automatic"||n==="activate";return{focus:i,activate:a?l(e,i):null}}function m(e,t){return e.disabled.has(t)?null:e.expanded.includes(t)?e.expanded.filter(r=>r!==t):e.multiple?[...e.expanded,t]:[t]}function T(e,t,r){let n=s({orientation:e.orientation,dir:e.dir,homeEnd:!0,activate:!0})[r];if(n==="activate")return e.disabled.has(t)?null:{focus:null,toggle:t};let o=e.values.filter(a=>!e.disabled.has(a)),i=c({action:n,navigable:o,currentKey:t});return i&&{focus:i.target,toggle:null}}var g=globalThis.HTMLElement??class{};function D(e,t,r){e.getAttribute(t)!==r&&(r===null?e.removeAttribute(t):e.setAttribute(t,r))}var d=new WeakSet;function I(e,t){d.has(e)||(d.add(e),console.error(t,e))}function N(e,t){if(e)for(let[r,n]of t)e.get(r)||e.define(r,n)}function u(e,t,r){return[...e.querySelectorAll(t)].filter(n=>n.closest(r)===e)}var h=0,p=class extends g{#e;#t=new MutationObserver(()=>this.reconcile());connectedCallback(){this.controller.connect(),this.#t.observe(this,{subtree:!0,childList:!0,attributes:!0,attributeFilter:[...this.reconcileOn]}),this.reconcile()}disconnectedCallback(){this.controller.disconnect(),this.#t.disconnect()}rootId(){return this.id||(this.#e??=`${this.localName}-${++h}`)}direction(){return this.matches(":dir(rtl)")?"rtl":"ltr"}parts(t){return u(this,t,this.localName)}announce(t,r){return this.dispatchEvent(new CustomEvent(t,{bubbles:!0,cancelable:!0,detail:r}))}};var v=class{#e;#t=new Map;#r=null;constructor(t){this.#e=t}registerTrigger(t,r,n={}){this.#t.set(r,{value:t,disabled:n.disabled??!1})}setTriggers(t){this.#t.clear();for(let{value:r,element:n,disabled:o}of t)this.registerTrigger(r,n,{disabled:o})}unregisterTrigger(t){this.#t.delete(t)}connect(){if(this.#r)return;this.#r=new AbortController;let{signal:t}=this.#r;this.#e.addEventListener("click",r=>this.#n(r,this.onTriggerClick),{signal:t}),this.#e.addEventListener("keydown",r=>this.#n(r,this.onTriggerKeydown),{signal:t})}disconnect(){this.#r?.abort(),this.#r=null}ordered(){return[...this.#t].sort(([t],[r])=>t.compareDocumentPosition(r)&Node.DOCUMENT_POSITION_FOLLOWING?-1:1)}disabled(){let t=new Set;for(let r of this.#t.values())r.disabled&&t.add(r.value);return t}#n(t,r){for(let[n,o]of this.#t)if(n.contains(t.target))return r.call(this,o,t)}};export{b as a,y as b,l as c,f as d,m as e,T as f,g,D as h,I as i,N as j,u as k,p as l,v as m};
|
package/dist/cdn/tabs.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{a as h,b as M,c as A,d as C,g as b,h as a,i as O,j as H,l as w,m as R}from"./chunks/chunk-FKLOMAT4.js";var v=class extends R{#t;constructor(i,n){super(i),this.#t=n}onTriggerClick(i){let{active:n}=this.#t.state(),r=A({disabled:this.disabled(),active:n},i.value);if(r!==null){let l=this.ordered().map(([,d])=>d.value);this.#t.activate(r,l.indexOf(r))}}onTriggerKeydown(i,n){let{active:r,orientation:l,dir:d,activationMode:p}=this.#t.state(),u=this.ordered(),e=u.map(([,o])=>o.value),t=C({values:e,disabled:this.disabled(),active:r,orientation:l,dir:d,activationMode:p},i.value,n.key);t&&(n.preventDefault(),t.activate!==null&&this.#t.activate(t.activate,e.indexOf(t.activate)),u[e.indexOf(t.focus)][0].focus())}};var D="primitiv-tabs",I="primitiv-tabs-list",y="primitiv-tabs-trigger",L="primitiv-tabs-content";function K(s){let i=s.getAttribute("value");return s.childElementCount===1&&i!==null?{tab:s.firstElementChild,value:i}:(O(s,`<${y}> needs a value and exactly one element child (a <button>, an <a href> or a router link) to decorate.`),null)}var f=class extends w{#t;#e=!1;controller=new v(this,this.#a());reconcileOn=["value","disabled","label","orientation"];get value(){return this.getAttribute("value")??void 0}set value(i){this.setAttribute("value",i)}#a(){return{state:()=>({active:this.value,orientation:this.#i(),dir:this.direction(),activationMode:this.#n()}),activate:(i,n)=>{this.announce("primitiv-tabs-change",{value:i,index:n})&&(this.value=i)}}}#n(){let i=this.getAttribute("activation-mode");return i==="manual"?i:"automatic"}#i(){return this.getAttribute("orientation")==="vertical"?"vertical":"horizontal"}reconcile(){let i=this.#i(),n=this.value,r=this.rootId();a(this,"data-orientation",i);for(let e of this.parts(I)){a(e,"role","tablist"),a(e,"aria-orientation",i),a(e,"data-orientation",i);let t=e.getAttribute("label");t!==null&&a(e,"aria-label",t)}let l=new Map;for(let e of this.parts(L)){let t=e.getAttribute("value");t!==null&&l.set(t,e)}let d=this.parts(y).flatMap(e=>{let t=K(e);return t?[{trigger:e,...t}]:[]}),p=M({values:d.map(e=>e.value),active:n});this.controller.setTriggers(d.map(({trigger:e,tab:t,value:o})=>({value:o,element:t,disabled:e.hasAttribute("disabled")})));for(let{trigger:e,tab:t,value:o}of d){let c=l.get(o),x=o===n,E=e.hasAttribute("disabled");t.id||(t.id=h(r,"trigger",o)),c&&!c.id&&(c.id=h(r,"panel",o)),a(t,"role","tab"),a(t,"aria-selected",String(x)),a(t,"aria-controls",c?c.id:null),a(t,"tabindex",o===p?"0":"-1"),a(t,"aria-disabled",E?"true":null),a(t,"data-disabled",E?"":null),a(t,"data-state",x?"active":"inactive"),a(t,"data-orientation",i),c&&a(c,"aria-labelledby",t.id)}for(let[e,t]of l){let o=e===n;a(t,"role","tabpanel"),a(t,"tabindex","0"),a(t,"hidden",o?null:""),a(t,"data-state",o?"active":"inactive"),a(t,"data-orientation",i)}let u=l.get(n);this.#e&&n!==this.#t&&u&&u.dispatchEvent(new CustomEvent("primitiv-tabs-show",{bubbles:!0})),this.#t=n,this.#e=!0}},m=class extends b{},g=class extends b{},T=class extends b{};function k(s=globalThis.customElements){H(s,[[D,f],[I,m],[y,g],[L,T]])}k();
|