@primitiv-ui/web 0.1.43 → 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 +49 -6
- 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/cdn/accordion.js +1 -0
- package/dist/cdn/chunks/chunk-FKLOMAT4.js +1 -0
- package/dist/cdn/tabs.js +1 -1
- 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 +10 -2
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { ElementBase, owned } from "./element.js";
|
|
2
|
+
/** Ids generated so far, shared by every root so no two ever collide. */
|
|
3
|
+
let generatedIds = 0;
|
|
4
|
+
/**
|
|
5
|
+
* The lifecycle every component's root element shares (RFC 0033 §5.4, D11).
|
|
6
|
+
*
|
|
7
|
+
* The root never caches its parts, so markup that arrives later is picked up on
|
|
8
|
+
* the next reconcile. One `MutationObserver` is the only trigger after connect:
|
|
9
|
+
* it sees parts arriving and leaving and every attribute in
|
|
10
|
+
* {@link PrimitivRootElement.reconcileOn | `reconcileOn`}, and it already
|
|
11
|
+
* delivers a task's changes as one callback, so no further batching is needed.
|
|
12
|
+
* The root's own writes (role, aria-*, ids, data-*) stay out of that list, so
|
|
13
|
+
* reconciling never re-triggers itself. Leaving the document stops all
|
|
14
|
+
* listening; a move (an `hx-preserve`, a framework reparenting) is a disconnect
|
|
15
|
+
* followed by a connect, so handlers never stack.
|
|
16
|
+
*
|
|
17
|
+
* A subclass supplies its controller, the attributes it reads, and the
|
|
18
|
+
* reconcile that writes its parts' ARIA. Internal — not an entry point.
|
|
19
|
+
*/
|
|
20
|
+
export class PrimitivRootElement extends ElementBase {
|
|
21
|
+
#generatedId;
|
|
22
|
+
#observer = new MutationObserver(() => this.reconcile());
|
|
23
|
+
connectedCallback() {
|
|
24
|
+
this.controller.connect();
|
|
25
|
+
this.#observer.observe(this, {
|
|
26
|
+
subtree: true,
|
|
27
|
+
childList: true,
|
|
28
|
+
attributes: true,
|
|
29
|
+
attributeFilter: [...this.reconcileOn],
|
|
30
|
+
});
|
|
31
|
+
this.reconcile();
|
|
32
|
+
}
|
|
33
|
+
disconnectedCallback() {
|
|
34
|
+
this.controller.disconnect();
|
|
35
|
+
this.#observer.disconnect();
|
|
36
|
+
}
|
|
37
|
+
/** The root's id, or one generated once, which the parts' ids derive from. */
|
|
38
|
+
rootId() {
|
|
39
|
+
// Stryker disable next-line UpdateOperator: equivalent — counting down yields ids just as unique.
|
|
40
|
+
return this.id || (this.#generatedId ??= `${this.localName}-${++generatedIds}`);
|
|
41
|
+
}
|
|
42
|
+
/** The reading direction, from `:dir()`, so an inherited `dir` attribute applies (D8). */
|
|
43
|
+
direction() {
|
|
44
|
+
// Stryker disable next-line StringLiteral: equivalent — core tests only for "rtl", so any other string reads as left-to-right.
|
|
45
|
+
return this.matches(":dir(rtl)") ? "rtl" : "ltr";
|
|
46
|
+
}
|
|
47
|
+
/** This root's `selector` parts, never those of a nested root of the same kind. */
|
|
48
|
+
parts(selector) {
|
|
49
|
+
return owned(this, selector, this.localName);
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Announce a user-driven change as a bubbling, cancelable `type` event fired
|
|
53
|
+
* before it applies, so a listener can veto it — the elements' equivalent of
|
|
54
|
+
* React's controlled mode (D7). Returns `false` when it was vetoed.
|
|
55
|
+
*/
|
|
56
|
+
announce(type, detail) {
|
|
57
|
+
return this.dispatchEvent(new CustomEvent(type, { bubbles: true, cancelable: true, detail }));
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/** A registered trigger: its value, and whether it is disabled. */
|
|
2
|
+
export type Trigger = {
|
|
3
|
+
value: string;
|
|
4
|
+
disabled: boolean;
|
|
5
|
+
};
|
|
6
|
+
/**
|
|
7
|
+
* What every component controller shares (RFC 0033 §3.4, D28): an ordered
|
|
8
|
+
* collection of triggers, and click and keydown listeners on the root that
|
|
9
|
+
* find the trigger an event belongs to. A subclass supplies only what a click
|
|
10
|
+
* and a key press on a trigger mean for its component; it owns no state, which
|
|
11
|
+
* stays with its host. Internal — the public face is each subclass.
|
|
12
|
+
*/
|
|
13
|
+
export declare abstract class TriggerController {
|
|
14
|
+
#private;
|
|
15
|
+
constructor(root: HTMLElement);
|
|
16
|
+
/**
|
|
17
|
+
* Register `element` as the trigger for `value`. Registration order does not
|
|
18
|
+
* matter: the collection follows DOM order, so parts that arrive in any order
|
|
19
|
+
* (a fragment swap, a framework render) line up. Registering an element again
|
|
20
|
+
* updates it.
|
|
21
|
+
*/
|
|
22
|
+
registerTrigger(value: string, element: HTMLElement, options?: {
|
|
23
|
+
disabled?: boolean;
|
|
24
|
+
}): void;
|
|
25
|
+
/**
|
|
26
|
+
* Replace the whole collection in one call. A host that re-reads its parts on
|
|
27
|
+
* every change (the custom elements, the React adapter) hands over what it
|
|
28
|
+
* has now; a trigger missing from the list is simply no longer registered.
|
|
29
|
+
*/
|
|
30
|
+
setTriggers(triggers: ReadonlyArray<{
|
|
31
|
+
value: string;
|
|
32
|
+
element: HTMLElement;
|
|
33
|
+
disabled?: boolean;
|
|
34
|
+
}>): void;
|
|
35
|
+
/** Remove `element` from the collection. */
|
|
36
|
+
unregisterTrigger(element: HTMLElement): void;
|
|
37
|
+
/**
|
|
38
|
+
* Start listening on the root. Connecting again while connected does nothing,
|
|
39
|
+
* so a host whose lifecycle runs twice never stacks a second set of handlers.
|
|
40
|
+
*/
|
|
41
|
+
connect(): void;
|
|
42
|
+
/** Stop listening. The collection is kept, so a later `connect` resumes. */
|
|
43
|
+
disconnect(): void;
|
|
44
|
+
/** A click on `trigger`, or on something inside it. */
|
|
45
|
+
protected abstract onTriggerClick(trigger: Trigger, event: MouseEvent): void;
|
|
46
|
+
/** A key press on `trigger`, or on something inside it. */
|
|
47
|
+
protected abstract onTriggerKeydown(trigger: Trigger, event: KeyboardEvent): void;
|
|
48
|
+
/** Every registered trigger, in DOM order. */
|
|
49
|
+
protected ordered(): Array<[HTMLElement, Trigger]>;
|
|
50
|
+
/** The values of the disabled triggers. */
|
|
51
|
+
protected disabled(): Set<string>;
|
|
52
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What every component controller shares (RFC 0033 §3.4, D28): an ordered
|
|
3
|
+
* collection of triggers, and click and keydown listeners on the root that
|
|
4
|
+
* find the trigger an event belongs to. A subclass supplies only what a click
|
|
5
|
+
* and a key press on a trigger mean for its component; it owns no state, which
|
|
6
|
+
* stays with its host. Internal — the public face is each subclass.
|
|
7
|
+
*/
|
|
8
|
+
export class TriggerController {
|
|
9
|
+
#root;
|
|
10
|
+
#triggers = new Map();
|
|
11
|
+
#connection = null;
|
|
12
|
+
constructor(root) {
|
|
13
|
+
this.#root = root;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Register `element` as the trigger for `value`. Registration order does not
|
|
17
|
+
* matter: the collection follows DOM order, so parts that arrive in any order
|
|
18
|
+
* (a fragment swap, a framework render) line up. Registering an element again
|
|
19
|
+
* updates it.
|
|
20
|
+
*/
|
|
21
|
+
registerTrigger(value, element, options = {}) {
|
|
22
|
+
this.#triggers.set(element, { value, disabled: options.disabled ?? false });
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Replace the whole collection in one call. A host that re-reads its parts on
|
|
26
|
+
* every change (the custom elements, the React adapter) hands over what it
|
|
27
|
+
* has now; a trigger missing from the list is simply no longer registered.
|
|
28
|
+
*/
|
|
29
|
+
setTriggers(triggers) {
|
|
30
|
+
this.#triggers.clear();
|
|
31
|
+
for (const { value, element, disabled } of triggers) {
|
|
32
|
+
this.registerTrigger(value, element, { disabled });
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
/** Remove `element` from the collection. */
|
|
36
|
+
unregisterTrigger(element) {
|
|
37
|
+
this.#triggers.delete(element);
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Start listening on the root. Connecting again while connected does nothing,
|
|
41
|
+
* so a host whose lifecycle runs twice never stacks a second set of handlers.
|
|
42
|
+
*/
|
|
43
|
+
connect() {
|
|
44
|
+
if (this.#connection)
|
|
45
|
+
return;
|
|
46
|
+
this.#connection = new AbortController();
|
|
47
|
+
const { signal } = this.#connection;
|
|
48
|
+
this.#root.addEventListener("click", (event) => this.#dispatch(event, this.onTriggerClick), { signal });
|
|
49
|
+
this.#root.addEventListener("keydown", (event) => this.#dispatch(event, this.onTriggerKeydown), { signal });
|
|
50
|
+
}
|
|
51
|
+
/** Stop listening. The collection is kept, so a later `connect` resumes. */
|
|
52
|
+
disconnect() {
|
|
53
|
+
this.#connection?.abort();
|
|
54
|
+
this.#connection = null;
|
|
55
|
+
}
|
|
56
|
+
/** Every registered trigger, in DOM order. */
|
|
57
|
+
ordered() {
|
|
58
|
+
return [...this.#triggers].sort(([a], [b]) => a.compareDocumentPosition(b) & Node.DOCUMENT_POSITION_FOLLOWING ? -1 : 1);
|
|
59
|
+
}
|
|
60
|
+
/** The values of the disabled triggers. */
|
|
61
|
+
disabled() {
|
|
62
|
+
const disabled = new Set();
|
|
63
|
+
for (const trigger of this.#triggers.values()) {
|
|
64
|
+
if (trigger.disabled)
|
|
65
|
+
disabled.add(trigger.value);
|
|
66
|
+
}
|
|
67
|
+
return disabled;
|
|
68
|
+
}
|
|
69
|
+
/** Hand `event` to `handler` with the trigger it happened in, if any. */
|
|
70
|
+
#dispatch(event, handler) {
|
|
71
|
+
for (const [element, trigger] of this.#triggers) {
|
|
72
|
+
if (element.contains(event.target))
|
|
73
|
+
return handler.call(this, trigger, event);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What every element module shares: the SSR-safe base class, the write-only-on-
|
|
3
|
+
* change helper, report-once misuse logging, and the define loop. Internal —
|
|
4
|
+
* not an entry point.
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* The base class every element extends. It falls back to an empty class where
|
|
8
|
+
* the DOM does not exist, so a server renderer (Next.js, Nuxt, Angular SSR,
|
|
9
|
+
* Astro) can import an element module without `HTMLElement` throwing at load
|
|
10
|
+
* time (RFC 0033 D21).
|
|
11
|
+
*/
|
|
12
|
+
export declare const ElementBase: typeof HTMLElement;
|
|
13
|
+
/**
|
|
14
|
+
* Write an attribute only when it would change: `null` removes it. The elements
|
|
15
|
+
* write only the attributes in their contract (RFC 0033 §5.3), and comparing
|
|
16
|
+
* first means a value a framework or the server already rendered is never
|
|
17
|
+
* rewritten (D20).
|
|
18
|
+
*/
|
|
19
|
+
export declare function write(element: Element, name: string, value: string | null): void;
|
|
20
|
+
/**
|
|
21
|
+
* Report a misused part once, with the element attached so devtools can jump to
|
|
22
|
+
* it. Misuse is logged rather than thrown: a server-rendered page has no error
|
|
23
|
+
* boundary to catch a throw (RFC 0033 D24).
|
|
24
|
+
*/
|
|
25
|
+
export declare function reportOnce(element: Element, message: string): void;
|
|
26
|
+
/** The two calls a `define<Component>` makes on a custom-element registry. */
|
|
27
|
+
export type ElementRegistry = Pick<CustomElementRegistry, "get" | "define">;
|
|
28
|
+
/**
|
|
29
|
+
* Define `elements` on `registry`, skipping any name already defined, so a
|
|
30
|
+
* define is safe to call more than once. A no-op where there is no registry (a
|
|
31
|
+
* server render).
|
|
32
|
+
*/
|
|
33
|
+
export declare function defineElements(registry: ElementRegistry | undefined, elements: ReadonlyArray<[string, CustomElementConstructor]>): void;
|
|
34
|
+
/**
|
|
35
|
+
* The `selector` parts inside `scope` that belong to it, not to a nested
|
|
36
|
+
* `owner` — so a component nested in another's panel keeps its own parts.
|
|
37
|
+
*/
|
|
38
|
+
export declare function owned(scope: Element, selector: string, owner: string): HTMLElement[];
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What every element module shares: the SSR-safe base class, the write-only-on-
|
|
3
|
+
* change helper, report-once misuse logging, and the define loop. Internal —
|
|
4
|
+
* not an entry point.
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* The base class every element extends. It falls back to an empty class where
|
|
8
|
+
* the DOM does not exist, so a server renderer (Next.js, Nuxt, Angular SSR,
|
|
9
|
+
* Astro) can import an element module without `HTMLElement` throwing at load
|
|
10
|
+
* time (RFC 0033 D21).
|
|
11
|
+
*/
|
|
12
|
+
export const ElementBase = globalThis.HTMLElement ?? class {
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* Write an attribute only when it would change: `null` removes it. The elements
|
|
16
|
+
* write only the attributes in their contract (RFC 0033 §5.3), and comparing
|
|
17
|
+
* first means a value a framework or the server already rendered is never
|
|
18
|
+
* rewritten (D20).
|
|
19
|
+
*/
|
|
20
|
+
export function write(element, name, value) {
|
|
21
|
+
if (element.getAttribute(name) === value)
|
|
22
|
+
return;
|
|
23
|
+
if (value === null)
|
|
24
|
+
element.removeAttribute(name);
|
|
25
|
+
else
|
|
26
|
+
element.setAttribute(name, value);
|
|
27
|
+
}
|
|
28
|
+
/** Parts already reported as misused, so each is reported once, not per reconcile. */
|
|
29
|
+
const reported = new WeakSet();
|
|
30
|
+
/**
|
|
31
|
+
* Report a misused part once, with the element attached so devtools can jump to
|
|
32
|
+
* it. Misuse is logged rather than thrown: a server-rendered page has no error
|
|
33
|
+
* boundary to catch a throw (RFC 0033 D24).
|
|
34
|
+
*/
|
|
35
|
+
export function reportOnce(element, message) {
|
|
36
|
+
if (reported.has(element))
|
|
37
|
+
return;
|
|
38
|
+
reported.add(element);
|
|
39
|
+
console.error(message, element);
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Define `elements` on `registry`, skipping any name already defined, so a
|
|
43
|
+
* define is safe to call more than once. A no-op where there is no registry (a
|
|
44
|
+
* server render).
|
|
45
|
+
*/
|
|
46
|
+
export function defineElements(registry, elements) {
|
|
47
|
+
if (!registry)
|
|
48
|
+
return;
|
|
49
|
+
for (const [name, element] of elements) {
|
|
50
|
+
if (!registry.get(name))
|
|
51
|
+
registry.define(name, element);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* The `selector` parts inside `scope` that belong to it, not to a nested
|
|
56
|
+
* `owner` — so a component nested in another's panel keeps its own parts.
|
|
57
|
+
*/
|
|
58
|
+
export function owned(scope, selector, owner) {
|
|
59
|
+
return [...scope.querySelectorAll(selector)].filter((part) => part.closest(owner) === scope);
|
|
60
|
+
}
|
package/dist/primitiv-web.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
function
|
|
1
|
+
function b(e,t,i){return`${e}-${t}-${i}`}function A({orientation:e,dir:t,homeEnd:i=!1,activate:r=!1}){let n={};if(e==="horizontal"||e==="both"){let c=t==="rtl"?"ArrowLeft":"ArrowRight",d=t==="rtl"?"ArrowRight":"ArrowLeft";n[c]="next",n[d]="prev"}return(e==="vertical"||e==="both")&&(n.ArrowDown="next",n.ArrowUp="prev"),i&&(n.Home="first",n.End="last"),r&&(n.Enter="activate",n[" "]="activate"),n}function E({action:e,navigable:t,currentKey:i}){let r=t.length;if(r===0)return null;switch(e){case"activate":return{target:i};case"first":return{target:t[0]};case"last":return{target:t[r-1]};case void 0:return null;case"next":case"prev":{let n=t.indexOf(i);if(n===-1)return null;let c=e==="next"?1:r-1;return{target:t[(n+c)%r]}}}}function D(e){return e.active??e.values[0]}function R(e,t){return e.disabled.has(t)||e.active===t?null:t}function N(e,t,i){let r=A({orientation:e.orientation,dir:e.dir,homeEnd:!0,activate:!0})[i],n=E({action:r,navigable:e.values,currentKey:t});if(!n)return null;let c=n.target,d=e.activationMode==="automatic"||r==="activate";return{focus:c,activate:d?R(e,c):null}}function P(e,t){return e.disabled.has(t)?null:e.expanded.includes(t)?e.expanded.filter(i=>i!==t):e.multiple?[...e.expanded,t]:[t]}function $(e,t,i){let r=A({orientation:e.orientation,dir:e.dir,homeEnd:!0,activate:!0})[i];if(r==="activate")return e.disabled.has(t)?null:{focus:null,toggle:t};let n=e.values.filter(d=>!e.disabled.has(d)),c=E({action:r,navigable:n,currentKey:t});return c&&{focus:c.target,toggle:null}}var v=class{#t;#e=new Map;#i=null;constructor(t){this.#t=t}registerTrigger(t,i,r={}){this.#e.set(i,{value:t,disabled:r.disabled??!1})}setTriggers(t){this.#e.clear();for(let{value:i,element:r,disabled:n}of t)this.registerTrigger(i,r,{disabled:n})}unregisterTrigger(t){this.#e.delete(t)}connect(){if(this.#i)return;this.#i=new AbortController;let{signal:t}=this.#i;this.#t.addEventListener("click",i=>this.#o(i,this.onTriggerClick),{signal:t}),this.#t.addEventListener("keydown",i=>this.#o(i,this.onTriggerKeydown),{signal:t})}disconnect(){this.#i?.abort(),this.#i=null}ordered(){return[...this.#e].sort(([t],[i])=>t.compareDocumentPosition(i)&Node.DOCUMENT_POSITION_FOLLOWING?-1:1)}disabled(){let t=new Set;for(let i of this.#e.values())i.disabled&&t.add(i.value);return t}#o(t,i){for(let[r,n]of this.#e)if(r.contains(t.target))return i.call(this,n,t)}};var y=class extends v{#t;constructor(t,i){super(t),this.#t=i}#e(t){let{expanded:i,multiple:r}=this.#t.state(),n=P({disabled:this.disabled(),expanded:i,multiple:r},t);n!==null&&this.#t.change(n)}onTriggerClick(t){this.#e(t.value)}onTriggerKeydown(t,i){let{expanded:r,multiple:n,orientation:c,dir:d}=this.#t.state(),p=this.ordered(),s=p.map(([,l])=>l.value),o=$({values:s,disabled:this.disabled(),expanded:r,multiple:n,orientation:c,dir:d},t.value,i.key);o&&(i.preventDefault(),o.toggle!==null&&this.#e(o.toggle),o.focus!==null&&p[s.indexOf(o.focus)][0].focus())}};var g=globalThis.HTMLElement??class{};function a(e,t,i){e.getAttribute(t)!==i&&(i===null?e.removeAttribute(t):e.setAttribute(t,i))}var W=new WeakSet;function T(e,t){W.has(e)||(W.add(e),console.error(t,e))}function M(e,t){if(e)for(let[i,r]of t)e.get(i)||e.define(i,r)}function m(e,t,i){return[...e.querySelectorAll(t)].filter(r=>r.closest(i)===e)}var V=0,f=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}-${++V}`)}direction(){return this.matches(":dir(rtl)")?"rtl":"ltr"}parts(t){return m(this,t,this.localName)}announce(t,i){return this.dispatchEvent(new CustomEvent(t,{bubbles:!0,cancelable:!0,detail:i}))}};var X="primitiv-accordion",h="primitiv-accordion-item",z="primitiv-accordion-trigger",_="primitiv-accordion-content",j="primitiv-accordion-trigger-icon";function Y(e){return(e??"").split(/\s+/).filter(Boolean)}function Z(e){return e.childElementCount===1?e.firstElementChild:(T(e,`<${z}> needs exactly one element child (a <button>) to decorate.`),null)}var O=class extends f{#t=new Set;#e=!1;controller=new y(this,this.#i());reconcileOn=["value","disabled","orientation","force-mount"];get value(){return Y(this.getAttribute("value"))}set value(t){this.setAttribute("value",typeof t=="string"?t:t.join(" "))}#i(){return{state:()=>({expanded:this.value,multiple:this.hasAttribute("multiple"),orientation:this.#o(),dir:this.direction()}),change:t=>{this.announce("primitiv-accordion-change",{value:t})&&(this.value=t)}}}#o(){return this.getAttribute("orientation")==="horizontal"?"horizontal":"vertical"}reconcile(){let t=new Set(this.value),i=this.rootId();a(this,"data-orientation",this.#o());let r=[];for(let n of this.parts(h)){let c=n.getAttribute("value");if(c===null){T(n,`<${h}> needs a value, which pairs it with the root's value.`);continue}let d=t.has(c),p=d?"open":"closed",[s]=m(n,z,h),[o]=m(n,_,h),l=s?Z(s):null;if(l){let u=s.hasAttribute("disabled");r.push({value:c,element:l,disabled:u}),l.id||(l.id=b(i,"heading",c)),a(l,"aria-expanded",String(d)),a(l,"aria-disabled",String(u)),a(l,"data-disabled",String(u)),a(l,"data-state",p)}if(o){o.id||(o.id=b(i,"panel",c));let u=o.hasAttribute("force-mount");o.hasAttribute("role")||o.setAttribute("role","region"),a(o,"aria-labelledby",l?l.id:null),a(o,"hidden",d||u?null:""),a(o,"aria-hidden",d?null:"true"),a(o,"data-state",p),this.#e&&d&&!this.#t.has(c)&&o.dispatchEvent(new CustomEvent("primitiv-accordion-show",{bubbles:!0}))}l&&a(l,"aria-controls",o?o.id:null);for(let u of m(n,j,h))a(u,"aria-hidden","true"),a(u,"data-state",p)}this.controller.setTriggers(r),this.#t=t,this.#e=!0}},w=class extends g{},C=class extends g{},K=class extends g{},H=class extends g{};function G(e=globalThis.customElements){M(e,[[X,O],[h,w],[z,C],[_,K],[j,H]])}var x=class extends v{#t;constructor(t,i){super(t),this.#t=i}onTriggerClick(t){let{active:i}=this.#t.state(),r=R({disabled:this.disabled(),active:i},t.value);if(r!==null){let n=this.ordered().map(([,c])=>c.value);this.#t.activate(r,n.indexOf(r))}}onTriggerKeydown(t,i){let{active:r,orientation:n,dir:c,activationMode:d}=this.#t.state(),p=this.ordered(),s=p.map(([,l])=>l.value),o=N({values:s,disabled:this.disabled(),active:r,orientation:n,dir:c,activationMode:d},t.value,i.key);o&&(i.preventDefault(),o.activate!==null&&this.#t.activate(o.activate,s.indexOf(o.activate)),p[s.indexOf(o.focus)][0].focus())}};var tt="primitiv-tabs",J="primitiv-tabs-list",B="primitiv-tabs-trigger",Q="primitiv-tabs-content";function et(e){let t=e.getAttribute("value");return e.childElementCount===1&&t!==null?{tab:e.firstElementChild,value:t}:(T(e,`<${B}> needs a value and exactly one element child (a <button>, an <a href> or a router link) to decorate.`),null)}var L=class extends f{#t;#e=!1;controller=new x(this,this.#i());reconcileOn=["value","disabled","label","orientation"];get value(){return this.getAttribute("value")??void 0}set value(t){this.setAttribute("value",t)}#i(){return{state:()=>({active:this.value,orientation:this.#n(),dir:this.direction(),activationMode:this.#o()}),activate:(t,i)=>{this.announce("primitiv-tabs-change",{value:t,index:i})&&(this.value=t)}}}#o(){let t=this.getAttribute("activation-mode");return t==="manual"?t:"automatic"}#n(){return this.getAttribute("orientation")==="vertical"?"vertical":"horizontal"}reconcile(){let t=this.#n(),i=this.value,r=this.rootId();a(this,"data-orientation",t);for(let s of this.parts(J)){a(s,"role","tablist"),a(s,"aria-orientation",t),a(s,"data-orientation",t);let o=s.getAttribute("label");o!==null&&a(s,"aria-label",o)}let n=new Map;for(let s of this.parts(Q)){let o=s.getAttribute("value");o!==null&&n.set(o,s)}let c=this.parts(B).flatMap(s=>{let o=et(s);return o?[{trigger:s,...o}]:[]}),d=D({values:c.map(s=>s.value),active:i});this.controller.setTriggers(c.map(({trigger:s,tab:o,value:l})=>({value:l,element:o,disabled:s.hasAttribute("disabled")})));for(let{trigger:s,tab:o,value:l}of c){let u=n.get(l),F=l===i,U=s.hasAttribute("disabled");o.id||(o.id=b(r,"trigger",l)),u&&!u.id&&(u.id=b(r,"panel",l)),a(o,"role","tab"),a(o,"aria-selected",String(F)),a(o,"aria-controls",u?u.id:null),a(o,"tabindex",l===d?"0":"-1"),a(o,"aria-disabled",U?"true":null),a(o,"data-disabled",U?"":null),a(o,"data-state",F?"active":"inactive"),a(o,"data-orientation",t),u&&a(u,"aria-labelledby",o.id)}for(let[s,o]of n){let l=s===i;a(o,"role","tabpanel"),a(o,"tabindex","0"),a(o,"hidden",l?null:""),a(o,"data-state",l?"active":"inactive"),a(o,"data-orientation",t)}let p=n.get(i);this.#e&&i!==this.#t&&p&&p.dispatchEvent(new CustomEvent("primitiv-tabs-show",{bubbles:!0})),this.#t=i,this.#e=!0}},S=class extends g{},k=class extends g{},I=class extends g{};function q(e=globalThis.customElements){M(e,[[tt,L],[J,S],[B,k],[Q,I]])}G();q();export{y as AccordionController,K as PrimitivAccordionContentElement,O as PrimitivAccordionElement,w as PrimitivAccordionItemElement,C as PrimitivAccordionTriggerElement,H as PrimitivAccordionTriggerIconElement,I as PrimitivTabsContentElement,L as PrimitivTabsElement,S as PrimitivTabsListElement,k as PrimitivTabsTriggerElement,x as TabsController,G as defineAccordion,q as defineTabs};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type TabsActivationMode, type TabsOrientation, type TabsReadingDirection } from "@primitiv-ui/core";
|
|
2
|
+
import { TriggerController, type Trigger } from "../internal/TriggerController.js";
|
|
2
3
|
/**
|
|
3
4
|
* What a {@link TabsController} asks of whoever hosts it. The controller owns
|
|
4
5
|
* behaviour and never owns state (RFC 0033 D28): the custom elements keep state
|
|
@@ -20,38 +21,12 @@ export type TabsControllerHost = {
|
|
|
20
21
|
* The DOM behaviour of a tab set: listening, focus, and the ordered collection
|
|
21
22
|
* of triggers. It attaches to whatever elements register with it, so the same
|
|
22
23
|
* code runs under the custom elements and under the React adapter, which renders
|
|
23
|
-
* its own markup (RFC 0033 §3.4).
|
|
24
|
+
* its own markup (RFC 0033 §3.4). A disabled trigger stays in the collection —
|
|
25
|
+
* arrow keys land on it — but is never activated.
|
|
24
26
|
*/
|
|
25
|
-
export declare class TabsController {
|
|
27
|
+
export declare class TabsController extends TriggerController {
|
|
26
28
|
#private;
|
|
27
29
|
constructor(root: HTMLElement, host: TabsControllerHost);
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
* part of the collection — arrow keys land on it — but is never activated.
|
|
31
|
-
* Registration order does not matter: the collection follows DOM order, so
|
|
32
|
-
* parts that arrive in any order (a fragment swap, a framework render) line up.
|
|
33
|
-
* Registering an element again updates it.
|
|
34
|
-
*/
|
|
35
|
-
registerTrigger(value: string, element: HTMLElement, options?: {
|
|
36
|
-
disabled?: boolean;
|
|
37
|
-
}): void;
|
|
38
|
-
/**
|
|
39
|
-
* Replace the whole collection in one call. A host that re-reads its parts on
|
|
40
|
-
* every change (the custom elements, the React adapter) hands over what it
|
|
41
|
-
* has now; a trigger missing from the list is simply no longer registered.
|
|
42
|
-
*/
|
|
43
|
-
setTriggers(triggers: ReadonlyArray<{
|
|
44
|
-
value: string;
|
|
45
|
-
element: HTMLElement;
|
|
46
|
-
disabled?: boolean;
|
|
47
|
-
}>): void;
|
|
48
|
-
/** Remove `element` from the collection. */
|
|
49
|
-
unregisterTrigger(element: HTMLElement): void;
|
|
50
|
-
/**
|
|
51
|
-
* Start listening on the root. Connecting again while connected does nothing,
|
|
52
|
-
* so a host whose lifecycle runs twice never stacks a second set of handlers.
|
|
53
|
-
*/
|
|
54
|
-
connect(): void;
|
|
55
|
-
/** Stop listening. The collection is kept, so a later `connect` resumes. */
|
|
56
|
-
disconnect(): void;
|
|
30
|
+
protected onTriggerClick(trigger: Trigger): void;
|
|
31
|
+
protected onTriggerKeydown(trigger: Trigger, event: KeyboardEvent): void;
|
|
57
32
|
}
|
|
@@ -1,92 +1,31 @@
|
|
|
1
1
|
import { tabsClick, tabsKeydown, } from "@primitiv-ui/core";
|
|
2
|
+
import { TriggerController } from "../internal/TriggerController.js";
|
|
2
3
|
/**
|
|
3
4
|
* The DOM behaviour of a tab set: listening, focus, and the ordered collection
|
|
4
5
|
* of triggers. It attaches to whatever elements register with it, so the same
|
|
5
6
|
* code runs under the custom elements and under the React adapter, which renders
|
|
6
|
-
* its own markup (RFC 0033 §3.4).
|
|
7
|
+
* its own markup (RFC 0033 §3.4). A disabled trigger stays in the collection —
|
|
8
|
+
* arrow keys land on it — but is never activated.
|
|
7
9
|
*/
|
|
8
|
-
export class TabsController {
|
|
9
|
-
#root;
|
|
10
|
+
export class TabsController extends TriggerController {
|
|
10
11
|
#host;
|
|
11
|
-
#triggers = new Map();
|
|
12
|
-
#connection = null;
|
|
13
12
|
constructor(root, host) {
|
|
14
|
-
|
|
13
|
+
super(root);
|
|
15
14
|
this.#host = host;
|
|
16
15
|
}
|
|
17
|
-
|
|
18
|
-
* Register `element` as the trigger for `value`. A disabled trigger is still
|
|
19
|
-
* part of the collection — arrow keys land on it — but is never activated.
|
|
20
|
-
* Registration order does not matter: the collection follows DOM order, so
|
|
21
|
-
* parts that arrive in any order (a fragment swap, a framework render) line up.
|
|
22
|
-
* Registering an element again updates it.
|
|
23
|
-
*/
|
|
24
|
-
registerTrigger(value, element, options = {}) {
|
|
25
|
-
this.#triggers.set(element, { value, disabled: options.disabled ?? false });
|
|
26
|
-
}
|
|
27
|
-
/**
|
|
28
|
-
* Replace the whole collection in one call. A host that re-reads its parts on
|
|
29
|
-
* every change (the custom elements, the React adapter) hands over what it
|
|
30
|
-
* has now; a trigger missing from the list is simply no longer registered.
|
|
31
|
-
*/
|
|
32
|
-
setTriggers(triggers) {
|
|
33
|
-
this.#triggers.clear();
|
|
34
|
-
for (const { value, element, disabled } of triggers) {
|
|
35
|
-
this.registerTrigger(value, element, { disabled });
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
/** Remove `element` from the collection. */
|
|
39
|
-
unregisterTrigger(element) {
|
|
40
|
-
this.#triggers.delete(element);
|
|
41
|
-
}
|
|
42
|
-
/**
|
|
43
|
-
* Start listening on the root. Connecting again while connected does nothing,
|
|
44
|
-
* so a host whose lifecycle runs twice never stacks a second set of handlers.
|
|
45
|
-
*/
|
|
46
|
-
connect() {
|
|
47
|
-
if (this.#connection)
|
|
48
|
-
return;
|
|
49
|
-
this.#connection = new AbortController();
|
|
50
|
-
const { signal } = this.#connection;
|
|
51
|
-
this.#root.addEventListener("click", (event) => this.#onClick(event), { signal });
|
|
52
|
-
this.#root.addEventListener("keydown", (event) => this.#onKeydown(event), { signal });
|
|
53
|
-
}
|
|
54
|
-
/** Stop listening. The collection is kept, so a later `connect` resumes. */
|
|
55
|
-
disconnect() {
|
|
56
|
-
this.#connection?.abort();
|
|
57
|
-
this.#connection = null;
|
|
58
|
-
}
|
|
59
|
-
/** Every registered trigger, in DOM order. */
|
|
60
|
-
#ordered() {
|
|
61
|
-
return [...this.#triggers].sort(([a], [b]) => a.compareDocumentPosition(b) & Node.DOCUMENT_POSITION_FOLLOWING ? -1 : 1);
|
|
62
|
-
}
|
|
63
|
-
#disabled() {
|
|
64
|
-
const disabled = new Set();
|
|
65
|
-
for (const trigger of this.#triggers.values()) {
|
|
66
|
-
if (trigger.disabled)
|
|
67
|
-
disabled.add(trigger.value);
|
|
68
|
-
}
|
|
69
|
-
return disabled;
|
|
70
|
-
}
|
|
71
|
-
#onClick(event) {
|
|
72
|
-
const trigger = this.#triggerFor(event.target);
|
|
73
|
-
if (!trigger)
|
|
74
|
-
return;
|
|
16
|
+
onTriggerClick(trigger) {
|
|
75
17
|
const { active } = this.#host.state();
|
|
76
|
-
const target = tabsClick({ disabled: this
|
|
18
|
+
const target = tabsClick({ disabled: this.disabled(), active }, trigger.value);
|
|
77
19
|
if (target !== null) {
|
|
78
|
-
const values = this
|
|
20
|
+
const values = this.ordered().map(([, t]) => t.value);
|
|
79
21
|
this.#host.activate(target, values.indexOf(target));
|
|
80
22
|
}
|
|
81
23
|
}
|
|
82
|
-
|
|
83
|
-
const trigger = this.#triggerFor(event.target);
|
|
84
|
-
if (!trigger)
|
|
85
|
-
return;
|
|
24
|
+
onTriggerKeydown(trigger, event) {
|
|
86
25
|
const { active, orientation, dir, activationMode } = this.#host.state();
|
|
87
|
-
const ordered = this
|
|
26
|
+
const ordered = this.ordered();
|
|
88
27
|
const values = ordered.map(([, t]) => t.value);
|
|
89
|
-
const outcome = tabsKeydown({ values, disabled: this
|
|
28
|
+
const outcome = tabsKeydown({ values, disabled: this.disabled(), active, orientation, dir, activationMode }, trigger.value, event.key);
|
|
90
29
|
if (!outcome)
|
|
91
30
|
return;
|
|
92
31
|
event.preventDefault();
|
|
@@ -96,11 +35,4 @@ export class TabsController {
|
|
|
96
35
|
// The focus target is always one of `values`, so its index is always valid.
|
|
97
36
|
ordered[values.indexOf(outcome.focus)][0].focus();
|
|
98
37
|
}
|
|
99
|
-
#triggerFor(target) {
|
|
100
|
-
for (const [element, trigger] of this.#triggers) {
|
|
101
|
-
if (element.contains(target))
|
|
102
|
-
return trigger;
|
|
103
|
-
}
|
|
104
|
-
return undefined;
|
|
105
|
-
}
|
|
106
38
|
}
|
package/dist/tabs/elements.d.ts
CHANGED
|
@@ -1,21 +1,15 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
* (RFC 0033 D21).
|
|
6
|
-
*/
|
|
7
|
-
declare const ElementBase: typeof HTMLElement;
|
|
1
|
+
import { ElementBase, type ElementRegistry } from "../internal/element.js";
|
|
2
|
+
import { PrimitivRootElement } from "../internal/RootElement.js";
|
|
3
|
+
import { TabsController } from "./TabsController.js";
|
|
4
|
+
export type { ElementRegistry };
|
|
8
5
|
/**
|
|
9
6
|
* `<primitiv-tabs>` — the state owner of a WAI-ARIA tab set.
|
|
10
7
|
*
|
|
11
|
-
* The root finds its own parts,
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
* root's own `value` and `orientation`, and it already delivers a task's changes
|
|
17
|
-
* as one callback, so no further batching is needed. `activation-mode` is read
|
|
18
|
-
* live at each key press, so a change to it needs no reconcile.
|
|
8
|
+
* The root finds its own parts, so a tab set nested in a panel keeps its own,
|
|
9
|
+
* and reconciles their ARIA from its reflected state whenever a part arrives,
|
|
10
|
+
* leaves, or changes an attribute the root reads (the lifecycle is
|
|
11
|
+
* `PrimitivRootElement`'s). `activation-mode` is read live at each key press,
|
|
12
|
+
* so a change to it needs no reconcile.
|
|
19
13
|
*
|
|
20
14
|
* @customElement primitiv-tabs
|
|
21
15
|
* @summary The state owner of a WAI-ARIA tab set.
|
|
@@ -24,18 +18,14 @@ declare const ElementBase: typeof HTMLElement;
|
|
|
24
18
|
* @attr {"automatic" | "manual"} activation-mode - `automatic` activates a tab as focus arrives; `manual` waits for Enter or Space. Defaults to `automatic`.
|
|
25
19
|
* @fires {CustomEvent<{ value: string; index: number }>} primitiv-tabs-change - Bubbles, cancelable, fired before a user-driven change is applied. Call `preventDefault()` to veto it.
|
|
26
20
|
*/
|
|
27
|
-
export declare class PrimitivTabsElement extends
|
|
21
|
+
export declare class PrimitivTabsElement extends PrimitivRootElement {
|
|
28
22
|
#private;
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
* Leaving the document stops all listening. A move (an `hx-preserve`, a
|
|
32
|
-
* framework reparenting) is a disconnect followed by a connect, so handlers
|
|
33
|
-
* never stack.
|
|
34
|
-
*/
|
|
35
|
-
disconnectedCallback(): void;
|
|
23
|
+
protected readonly controller: TabsController;
|
|
24
|
+
protected readonly reconcileOn: readonly string[];
|
|
36
25
|
/** The active tab's value, reflected as the `value` attribute. */
|
|
37
26
|
get value(): string | undefined;
|
|
38
27
|
set value(next: string);
|
|
28
|
+
protected reconcile(): void;
|
|
39
29
|
}
|
|
40
30
|
/**
|
|
41
31
|
* `<primitiv-tabs-list>` — the tablist. Structural: the root writes its ARIA.
|
|
@@ -66,8 +56,6 @@ export declare class PrimitivTabsTriggerElement extends ElementBase {
|
|
|
66
56
|
*/
|
|
67
57
|
export declare class PrimitivTabsContentElement extends ElementBase {
|
|
68
58
|
}
|
|
69
|
-
/** The two calls `defineTabs` makes on a custom-element registry. */
|
|
70
|
-
export type ElementRegistry = Pick<CustomElementRegistry, "get" | "define">;
|
|
71
59
|
/**
|
|
72
60
|
* Define the four Tabs elements on `registry` (the global `customElements` by
|
|
73
61
|
* default, so a scoped registry can be passed when browsers support one). Safe
|
|
@@ -75,4 +63,3 @@ export type ElementRegistry = Pick<CustomElementRegistry, "get" | "define">;
|
|
|
75
63
|
* render).
|
|
76
64
|
*/
|
|
77
65
|
export declare function defineTabs(registry?: ElementRegistry | undefined): void;
|
|
78
|
-
export {};
|