@primitiv-ui/web 0.1.43 → 0.1.45

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.
@@ -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
+ }
@@ -1 +1 @@
1
- function g(i,t,e){return`${i}-${t}-${e}`}function K({orientation:i,dir:t,homeEnd:e=!1,activate:o=!1}){let r={};if(i==="horizontal"||i==="both"){let l=t==="rtl"?"ArrowLeft":"ArrowRight",d=t==="rtl"?"ArrowRight":"ArrowLeft";r[l]="next",r[d]="prev"}return(i==="vertical"||i==="both")&&(r.ArrowDown="next",r.ArrowUp="prev"),e&&(r.Home="first",r.End="last"),o&&(r.Enter="activate",r[" "]="activate"),r}function C({action:i,navigable:t,currentKey:e}){let o=t.length;if(o===0)return null;switch(i){case"activate":return{target:e};case"first":return{target:t[0]};case"last":return{target:t[o-1]};case void 0:return null;case"next":case"prev":{let r=t.indexOf(e);if(r===-1)return null;let l=i==="next"?1:o-1;return{target:t[(r+l)%o]}}}}function x(i){return i.active??i.values[0]}function p(i,t){return i.disabled.has(t)||i.active===t?null:t}function A(i,t,e){let o=K({orientation:i.orientation,dir:i.dir,homeEnd:!0,activate:!0})[e],r=C({action:o,navigable:i.values,currentKey:t});if(!r)return null;let l=r.target,d=i.activationMode==="automatic"||o==="activate";return{focus:l,activate:d?p(i,l):null}}var v=class{#r;#i;#t=new Map;#e=null;constructor(t,e){this.#r=t,this.#i=e}registerTrigger(t,e,o={}){this.#t.set(e,{value:t,disabled:o.disabled??!1})}setTriggers(t){this.#t.clear();for(let{value:e,element:o,disabled:r}of t)this.registerTrigger(e,o,{disabled:r})}unregisterTrigger(t){this.#t.delete(t)}connect(){if(this.#e)return;this.#e=new AbortController;let{signal:t}=this.#e;this.#r.addEventListener("click",e=>this.#l(e),{signal:t}),this.#r.addEventListener("keydown",e=>this.#s(e),{signal:t})}disconnect(){this.#e?.abort(),this.#e=null}#o(){return[...this.#t].sort(([t],[e])=>t.compareDocumentPosition(e)&Node.DOCUMENT_POSITION_FOLLOWING?-1:1)}#a(){let t=new Set;for(let e of this.#t.values())e.disabled&&t.add(e.value);return t}#l(t){let e=this.#n(t.target);if(!e)return;let{active:o}=this.#i.state(),r=p({disabled:this.#a(),active:o},e.value);if(r!==null){let l=this.#o().map(([,d])=>d.value);this.#i.activate(r,l.indexOf(r))}}#s(t){let e=this.#n(t.target);if(!e)return;let{active:o,orientation:r,dir:l,activationMode:d}=this.#i.state(),b=this.#o(),a=b.map(([,c])=>c.value),n=A({values:a,disabled:this.#a(),active:o,orientation:r,dir:l,activationMode:d},e.value,t.key);n&&(t.preventDefault(),n.activate!==null&&this.#i.activate(n.activate,a.indexOf(n.activate)),b[a.indexOf(n.focus)][0].focus())}#n(t){for(let[e,o]of this.#t)if(e.contains(t))return o}};var y=globalThis.HTMLElement??class{},E="primitiv-tabs",H="primitiv-tabs-list",M="primitiv-tabs-trigger",k="primitiv-tabs-content",S=0,L=new WeakSet;function I(i){let t=i.getAttribute("value");return i.childElementCount===1&&t!==null?{tab:i.firstElementChild,value:t}:(L.has(i)||(L.add(i),console.error(`<${M}> needs a value and exactly one element child (a <button>, an <a href> or a router link) to decorate.`,i)),null)}function s(i,t,e){i.getAttribute(t)!==e&&(e===null?i.removeAttribute(t):i.setAttribute(t,e))}var f=class extends y{#r;#i;#t=!1;#e=new v(this,this.#a());#o=new MutationObserver(()=>this.#c());connectedCallback(){this.#e.connect(),this.#o.observe(this,{subtree:!0,childList:!0,attributes:!0,attributeFilter:["value","disabled","label","orientation"]}),this.#c()}disconnectedCallback(){this.#e.disconnect(),this.#o.disconnect()}get value(){return this.getAttribute("value")??void 0}set value(t){this.setAttribute("value",t)}#a(){return{state:()=>({active:this.value,orientation:this.#s(),dir:this.matches(":dir(rtl)")?"rtl":"ltr",activationMode:this.#l()}),activate:(t,e)=>{let o=new CustomEvent("primitiv-tabs-change",{bubbles:!0,cancelable:!0,detail:{value:t,index:e}});this.dispatchEvent(o)&&(this.value=t)}}}#l(){let t=this.getAttribute("activation-mode");return t==="manual"?t:"automatic"}#s(){return this.getAttribute("orientation")==="vertical"?"vertical":"horizontal"}#n(t){return[...this.querySelectorAll(t)].filter(e=>e.closest(E)===this)}#d(){return this.id||(this.#r??=`${E}-${++S}`)}#c(){let t=this.#s(),e=this.value,o=this.#d();s(this,"data-orientation",t);for(let a of this.#n(H)){s(a,"role","tablist"),s(a,"aria-orientation",t),s(a,"data-orientation",t);let n=a.getAttribute("label");n!==null&&s(a,"aria-label",n)}let r=new Map;for(let a of this.#n(k)){let n=a.getAttribute("value");n!==null&&r.set(n,a)}let l=this.#n(M).flatMap(a=>{let n=I(a);return n?[{trigger:a,...n}]:[]}),d=x({values:l.map(a=>a.value),active:e});this.#e.setTriggers(l.map(({trigger:a,tab:n,value:c})=>({value:c,element:n,disabled:a.hasAttribute("disabled")})));for(let{trigger:a,tab:n,value:c}of l){let u=r.get(c),w=c===e,O=a.hasAttribute("disabled");n.id||(n.id=g(o,"trigger",c)),u&&!u.id&&(u.id=g(o,"panel",c)),s(n,"role","tab"),s(n,"aria-selected",String(w)),s(n,"aria-controls",u?u.id:null),s(n,"tabindex",c===d?"0":"-1"),s(n,"aria-disabled",O?"true":null),s(n,"data-disabled",O?"":null),s(n,"data-state",w?"active":"inactive"),s(n,"data-orientation",t),u&&s(u,"aria-labelledby",n.id)}for(let[a,n]of r){let c=a===e;s(n,"role","tabpanel"),s(n,"tabindex","0"),s(n,"hidden",c?null:""),s(n,"data-state",c?"active":"inactive"),s(n,"data-orientation",t)}let b=r.get(e);this.#t&&e!==this.#i&&b&&b.dispatchEvent(new CustomEvent("primitiv-tabs-show",{bubbles:!0})),this.#i=e,this.#t=!0}},h=class extends y{},m=class extends y{},T=class extends y{};function R(i=globalThis.customElements){if(!i)return;let t=[[E,f],[H,h],[M,m],[k,T]];for(let[e,o]of t)i.get(e)||i.define(e,o)}R();export{T as PrimitivTabsContentElement,f as PrimitivTabsElement,h as PrimitivTabsListElement,m as PrimitivTabsTriggerElement,v as TabsController,R as defineTabs};
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
- * Register `element` as the trigger for `value`. A disabled trigger is still
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
- this.#root = root;
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.#disabled(), active }, trigger.value);
18
+ const target = tabsClick({ disabled: this.disabled(), active }, trigger.value);
77
19
  if (target !== null) {
78
- const values = this.#ordered().map(([, t]) => t.value);
20
+ const values = this.ordered().map(([, t]) => t.value);
79
21
  this.#host.activate(target, values.indexOf(target));
80
22
  }
81
23
  }
82
- #onKeydown(event) {
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.#ordered();
26
+ const ordered = this.ordered();
88
27
  const values = ordered.map(([, t]) => t.value);
89
- const outcome = tabsKeydown({ values, disabled: this.#disabled(), active, orientation, dir, activationMode }, trigger.value, event.key);
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
  }
@@ -1,21 +1,15 @@
1
- /**
2
- * The base class every element extends. It falls back to an empty class where
3
- * the DOM does not exist, so a server renderer (Next.js, Nuxt, Angular SSR,
4
- * Astro) can import this module without `HTMLElement` throwing at load time
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, scoped with `closest()` so a tab set nested in a
12
- * panel keeps its own, and reconciles their ARIA from its reflected state. It
13
- * never caches the parts, so markup that arrives later is picked up on the next
14
- * reconcile. One `MutationObserver` is the only trigger after connect: it sees
15
- * parts arriving and leaving, and every attribute the root reads, including the
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 ElementBase {
21
+ export declare class PrimitivTabsElement extends PrimitivRootElement {
28
22
  #private;
29
- connectedCallback(): void;
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 {};