@primitiv-ui/web 0.1.46 → 0.1.47

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 CHANGED
@@ -6,7 +6,7 @@ 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: published.** Tabs and Accordion so far. See
9
+ > **Status: published.** Tabs, Accordion and Collapsible so far. See
10
10
  > [RFC 0033](../../docs/rfcs/0033-web-components.md) for the design.
11
11
 
12
12
  ## Components
@@ -15,6 +15,7 @@ React, Vue, Angular or any other framework.
15
15
  |---|---|---|---|
16
16
  | Tabs | `primitiv-tabs`, `-list`, `-trigger`, `-content` | `primitiv-tabs-change` | 0.1.42 |
17
17
  | Accordion | `primitiv-accordion`, `-item`, `-trigger`, `-content`, `-trigger-icon` | `primitiv-accordion-change` | 0.1.44 |
18
+ | Collapsible | `primitiv-collapsible`, `-trigger`, `-content`, `-trigger-icon` | `primitiv-collapsible-change` | 0.1.47 |
18
19
 
19
20
  ## Use it
20
21
 
@@ -46,9 +47,9 @@ Each `dist/cdn/<component>.js` imports the code components share from
46
47
  downloads it once. `dist/primitiv-web.js` is the alternative: every element in
47
48
  one file. Per-component files ship from 0.1.43.
48
49
 
49
- The main entry (`@primitiv-ui/web`, `/tabs`, `/accordion`) exports the classes
50
- and `defineTabs(registry?)` / `defineAccordion(registry?)` with **no side
51
- effects**.
50
+ The main entry (`@primitiv-ui/web`, `/tabs`, `/accordion`, `/collapsible`)
51
+ exports the classes and `defineTabs(registry?)` / `defineAccordion(registry?)` /
52
+ `defineCollapsible(registry?)` with **no side effects**.
52
53
 
53
54
  **Render `hidden` on inactive panels yourself.** Until the module loads, CSS
54
55
  cannot tell which panel is active, so markup that omits it shows every panel.
@@ -93,6 +94,30 @@ on a panel as it becomes active, for lazy loading.
93
94
  value }`, every item open afterwards) and `primitiv-accordion-show` on a panel
94
95
  as its item opens. Render `hidden` on closed panels yourself, as for Tabs.
95
96
 
97
+ ### Collapsible
98
+
99
+ ```html
100
+ <script type="module" src="https://cdn.jsdelivr.net/npm/@primitiv-ui/web@<version>/dist/cdn/collapsible.js"></script>
101
+
102
+ <primitiv-collapsible>
103
+ <primitiv-collapsible-trigger><button type="button">Shipping details</button></primitiv-collapsible-trigger>
104
+ <primitiv-collapsible-content hidden>Orders ship within two business days.</primitiv-collapsible-content>
105
+ </primitiv-collapsible>
106
+ ```
107
+
108
+ - **`open` is the state**, a boolean attribute reflected as the user toggles,
109
+ and the `open` property reads and sets it. `disabled` on the root keeps the
110
+ button focusable but stops it toggling.
111
+ - **`collapsed-height` on the panel** shows a clamped preview while closed (the
112
+ read-more pattern): a bare number is pixels, anything else a CSS length. It is
113
+ published as `--primitiv-collapsible-collapsed-height` with
114
+ `data-clamped="true"`, and the panel is never hidden, since the preview is
115
+ real content. `force-mount` keeps a closed panel unhidden for your own
116
+ animation, with `aria-hidden` instead.
117
+ - **Listen with `primitiv-collapsible-change`** (bubbles, cancelable, `detail: {
118
+ open }`) and `primitiv-collapsible-show` on the panel as it opens. Render
119
+ `hidden` on a closed panel yourself, as for Tabs.
120
+
96
121
  **TypeScript** — opt into the tag map so DOM queries return the element classes:
97
122
  `import type {} from "@primitiv-ui/web/tags"` (npm only).
98
123
 
@@ -127,7 +152,7 @@ The elements write no `class` and no `style`. For the registry look, apply the
127
152
 
128
153
  ## Layers
129
154
 
130
- - **Controllers** (`TabsController`, `AccordionController`) hold a component's DOM behaviour —
155
+ - **Controllers** (`TabsController`, `AccordionController`, `CollapsibleController`) hold a component's DOM behaviour —
131
156
  listeners, focus, the ordered collection of parts — and attach to whatever
132
157
  elements register with them. They never own state; their host does.
133
158
  - **Element hosts** (`<primitiv-tabs>` …) are the public face: they keep state
@@ -1 +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();
1
+ import{a as b,e as C,f as O,i as g,j as n,k as v,l as H,m as f,n as R,o as M}from"./chunks/chunk-WVI5UEAW.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 v(t,e,r){return`${t}-${e}-${r}`}function a({orientation:t,dir:e,homeEnd:r=!1,activate:n=!1}){let o={};if(t==="horizontal"||t==="both"){let i=e==="rtl"?"ArrowLeft":"ArrowRight",l=e==="rtl"?"ArrowRight":"ArrowLeft";o[i]="next",o[l]="prev"}return(t==="vertical"||t==="both")&&(o.ArrowDown="next",o.ArrowUp="prev"),r&&(o.Home="first",o.End="last"),n&&(o.Enter="activate",o[" "]="activate"),o}function s({action:t,navigable:e,currentKey:r}){let n=e.length;if(n===0)return null;switch(t){case"activate":return{target:r};case"first":return{target:e[0]};case"last":return{target:e[n-1]};case void 0:return null;case"next":case"prev":{let o=e.indexOf(r);if(o===-1)return null;let i=t==="next"?1:n-1;return{target:e[(o+i)%n]}}}}function f(t){return t.active??t.values[0]}function c(t,e){return t.disabled.has(e)||t.active===e?null:e}function m(t,e,r){let n=a({orientation:t.orientation,dir:t.dir,homeEnd:!0,activate:!0})[r],o=s({action:n,navigable:t.values,currentKey:e});if(!o)return null;let i=o.target,l=t.activationMode==="automatic"||n==="activate";return{focus:i,activate:l?c(t,i):null}}function T(t,e){return t.disabled.has(e)?null:t.expanded.includes(e)?t.expanded.filter(r=>r!==e):t.multiple?[...t.expanded,e]:[e]}function h(t,e,r){let n=a({orientation:t.orientation,dir:t.dir,homeEnd:!0,activate:!0})[r];if(n==="activate")return t.disabled.has(e)?null:{focus:null,toggle:e};let o=t.values.filter(l=>!t.disabled.has(l)),i=s({action:n,navigable:o,currentKey:e});return i&&{focus:i.target,toggle:null}}function d(t){return t.disabled?null:!t.open}function x(t,e){if(e!=="Enter"&&e!==" ")return null;let r=d(t);return r===null?null:{open:r}}var u=globalThis.HTMLElement??class{};function $(t,e,r){t.getAttribute(e)!==r&&(r===null?t.removeAttribute(e):t.setAttribute(e,r))}var g=new WeakSet;function z(t,e){g.has(t)||(g.add(t),console.error(e,t))}function G(t,e){if(t)for(let[r,n]of e)t.get(r)||t.define(r,n)}function p(t,e,r){return[...t.querySelectorAll(e)].filter(n=>n.closest(r)===t)}var A=0,b=class extends u{#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}-${++A}`)}direction(){return this.matches(":dir(rtl)")?"rtl":"ltr"}parts(e){return p(this,e,this.localName)}announce(e,r){return this.dispatchEvent(new CustomEvent(e,{bubbles:!0,cancelable:!0,detail:r}))}};var y=class{#t;#e=new Map;#r=null;constructor(e){this.#t=e}registerTrigger(e,r,n={}){this.#e.set(r,{value:e,disabled:n.disabled??!1})}setTriggers(e){this.#e.clear();for(let{value:r,element:n,disabled:o}of e)this.registerTrigger(r,n,{disabled:o})}unregisterTrigger(e){this.#e.delete(e)}connect(){if(this.#r)return;this.#r=new AbortController;let{signal:e}=this.#r;this.#t.addEventListener("click",r=>this.#n(r,this.onTriggerClick),{signal:e}),this.#t.addEventListener("keydown",r=>this.#n(r,this.onTriggerKeydown),{signal:e})}disconnect(){this.#r?.abort(),this.#r=null}ordered(){return[...this.#e].sort(([e],[r])=>e.compareDocumentPosition(r)&Node.DOCUMENT_POSITION_FOLLOWING?-1:1)}disabled(){let e=new Set;for(let r of this.#e.values())r.disabled&&e.add(r.value);return e}#n(e,r){for(let[n,o]of this.#e)if(n.contains(e.target))return r.call(this,o,e)}};export{v as a,f as b,c,m as d,T as e,h as f,d as g,x as h,u as i,$ as j,z as k,G as l,p as m,b as n,y as o};
@@ -0,0 +1 @@
1
+ import{g as y,h as T,i as d,j as i,k as x,l as C,n as E,o as v}from"./chunks/chunk-WVI5UEAW.js";var p=class extends v{#t;constructor(t,o){super(t),this.#t=o}onTriggerClick(t){let o=y({open:this.#t.state().open,disabled:t.disabled});o!==null&&this.#t.change(o)}onTriggerKeydown(t,o){let s=T({open:this.#t.state().open,disabled:t.disabled},o.key);s&&(o.preventDefault(),this.#t.change(s.open))}};var R="primitiv-collapsible",b="primitiv-collapsible-trigger",H="primitiv-collapsible-content",w="primitiv-collapsible-trigger-icon",L="--primitiv-collapsible-collapsed-height";function O(e){return e.childElementCount===1?e.firstElementChild:(x(e,`<${b}> needs exactly one element child (a <button>) to decorate.`),null)}function M(e){return Number.isNaN(Number(e))?e:`${e}px`}function I(e,t,o){e.style.getPropertyValue(t)!==o&&(e.style.setProperty(t,o),e.getAttribute("style")===""&&e.removeAttribute("style"))}var c=class extends E{#t;controller=new p(this,this.#e());reconcileOn=["open","disabled","force-mount","collapsed-height"];get open(){return this.hasAttribute("open")}set open(t){this.toggleAttribute("open",t)}#e(){return{state:()=>({open:this.open}),change:t=>{this.announce("primitiv-collapsible-change",{open:t})&&(this.open=t)}}}reconcile(){let t=this.open,o=t?"open":"closed",s=this.hasAttribute("disabled"),m=this.rootId();i(this,"data-state",o),i(this,"data-disabled",String(s));let[f]=this.parts(b),[n]=this.parts(H),r=f?O(f):null;if(r&&(r.id||(r.id=`${m}-trigger`),i(r,"aria-expanded",String(t)),i(r,"aria-disabled",s?"true":null),i(r,"data-disabled",String(s)),i(r,"data-state",o)),n){n.id||(n.id=`${m}-content`);let l=n.getAttribute("collapsed-height"),a=l!==null,N=a||n.hasAttribute("force-mount");i(n,"hidden",t||N?null:""),i(n,"aria-hidden",t||a?null:"true"),i(n,"data-state",o),i(n,"data-disabled",String(s)),i(n,"data-clamped",a?"true":null),I(n,L,a?M(l):""),t&&this.#t===!1&&n.dispatchEvent(new CustomEvent("primitiv-collapsible-show",{bubbles:!0}))}r&&i(r,"aria-controls",n?n.id:null);for(let l of this.parts(w))i(l,"aria-hidden","true"),i(l,"data-state",o);this.controller.setTriggers(r?[{value:"trigger",element:r,disabled:s}]:[]),this.#t=t}},h=class extends d{},u=class extends d{},g=class extends d{};function A(e=globalThis.customElements){C(e,[[R,c],[b,h],[H,u],[w,g]])}A();
package/dist/cdn/tabs.js CHANGED
@@ -1 +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();
1
+ import{a as h,b as M,c as A,d as C,i as b,j as a,k as O,l as H,n as w,o as R}from"./chunks/chunk-WVI5UEAW.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();
@@ -0,0 +1,32 @@
1
+ import { TriggerController, type Trigger } from "../internal/TriggerController.js";
2
+ /**
3
+ * What a {@link CollapsibleController} asks of whoever hosts it. The controller
4
+ * owns behaviour and never owns state (RFC 0033 D28): the custom elements keep
5
+ * the open state in a reflected `open` attribute, the React adapter keeps it in
6
+ * React, and each answers these two calls from wherever it keeps it.
7
+ */
8
+ export type CollapsibleControllerHost = {
9
+ /** The state the controller cannot see for itself. */
10
+ state(): {
11
+ /** Whether the panel is open. */
12
+ open: boolean;
13
+ };
14
+ /** Apply a change the user asked for: `open` is the state afterwards. */
15
+ change(open: boolean): void;
16
+ };
17
+ /**
18
+ * The DOM behaviour of a collapsible: a click, Enter or Space on its trigger
19
+ * toggles it. It attaches to whatever trigger registers with it, so the same
20
+ * code runs under the custom elements and under the React adapter, which renders
21
+ * its own markup (RFC 0033 §3.4). There is one trigger; registering it
22
+ * `disabled` disables the collapsible — it stays focusable and never toggles.
23
+ * Enter and Space are handled here, not left to native activation, so a
24
+ * trigger that is not a `<button>` (an `<a>`, a `<div role="button">`)
25
+ * toggles too.
26
+ */
27
+ export declare class CollapsibleController extends TriggerController {
28
+ #private;
29
+ constructor(root: HTMLElement, host: CollapsibleControllerHost);
30
+ protected onTriggerClick(trigger: Trigger): void;
31
+ protected onTriggerKeydown(trigger: Trigger, event: KeyboardEvent): void;
32
+ }
@@ -0,0 +1,32 @@
1
+ import { collapsibleKeydown, collapsibleToggle } from "@primitiv-ui/core";
2
+ import { TriggerController } from "../internal/TriggerController.js";
3
+ /**
4
+ * The DOM behaviour of a collapsible: a click, Enter or Space on its trigger
5
+ * toggles it. It attaches to whatever trigger registers with it, so the same
6
+ * code runs under the custom elements and under the React adapter, which renders
7
+ * its own markup (RFC 0033 §3.4). There is one trigger; registering it
8
+ * `disabled` disables the collapsible — it stays focusable and never toggles.
9
+ * Enter and Space are handled here, not left to native activation, so a
10
+ * trigger that is not a `<button>` (an `<a>`, a `<div role="button">`)
11
+ * toggles too.
12
+ */
13
+ export class CollapsibleController extends TriggerController {
14
+ #host;
15
+ constructor(root, host) {
16
+ super(root);
17
+ this.#host = host;
18
+ }
19
+ onTriggerClick(trigger) {
20
+ const open = collapsibleToggle({ open: this.#host.state().open, disabled: trigger.disabled });
21
+ if (open !== null)
22
+ this.#host.change(open);
23
+ }
24
+ onTriggerKeydown(trigger, event) {
25
+ const outcome = collapsibleKeydown({ open: this.#host.state().open, disabled: trigger.disabled }, event.key);
26
+ if (!outcome)
27
+ return;
28
+ // Enter and Space would also click a button; the toggle happens here once.
29
+ event.preventDefault();
30
+ this.#host.change(outcome.open);
31
+ }
32
+ }
@@ -0,0 +1,64 @@
1
+ import { ElementBase, type ElementRegistry } from "../internal/element.js";
2
+ import { PrimitivRootElement } from "../internal/RootElement.js";
3
+ import { CollapsibleController } from "./CollapsibleController.js";
4
+ export type { ElementRegistry };
5
+ /**
6
+ * `<primitiv-collapsible>` — the state owner of a single disclosure: a trigger
7
+ * that shows and hides one panel.
8
+ *
9
+ * The root finds its own parts, so a collapsible nested in a panel keeps its
10
+ * own, and reconciles their ARIA from its reflected state whenever a part
11
+ * arrives, leaves, or changes an attribute the root reads (the lifecycle is
12
+ * `PrimitivRootElement`'s).
13
+ *
14
+ * @customElement primitiv-collapsible
15
+ * @summary The state owner of a single disclosure: a trigger that shows and hides one panel.
16
+ * @attr {boolean} open - Whether the panel is open. Reflected: it changes as the user toggles, so a server re-render or a history snapshot sees the live state.
17
+ * @attr {boolean} disabled - Disables the collapsible: the trigger's button stays focusable, is marked `aria-disabled`, and never toggles.
18
+ * @fires {CustomEvent<{ open: boolean }>} primitiv-collapsible-change - Bubbles, cancelable, fired before a user-driven toggle is applied, with the open state afterwards. Call `preventDefault()` to veto it.
19
+ */
20
+ export declare class PrimitivCollapsibleElement extends PrimitivRootElement {
21
+ #private;
22
+ protected readonly controller: CollapsibleController;
23
+ protected readonly reconcileOn: readonly string[];
24
+ /** Whether the panel is open, reflected as the boolean `open` attribute. */
25
+ get open(): boolean;
26
+ set open(next: boolean);
27
+ protected reconcile(): void;
28
+ }
29
+ /**
30
+ * `<primitiv-collapsible-trigger>` — an interactive part. It decorates exactly
31
+ * one child the consumer writes, a `<button>`, which receives `aria-expanded`,
32
+ * `aria-controls` and the state attributes, so the rendered DOM matches React's
33
+ * `<button aria-expanded>` (RFC 0033 D24).
34
+ *
35
+ * @customElement primitiv-collapsible-trigger
36
+ */
37
+ export declare class PrimitivCollapsibleTriggerElement extends ElementBase {
38
+ }
39
+ /**
40
+ * `<primitiv-collapsible-content>` — the panel. Structural: the root writes its
41
+ * id and state and hides it while closed.
42
+ *
43
+ * @customElement primitiv-collapsible-content
44
+ * @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.
45
+ * @attr {string} collapsed-height - Shows a clamped preview while closed instead of hiding the panel (the read-more pattern). A bare number is pixels; anything else is a CSS length, used verbatim. Published as the `--primitiv-collapsible-collapsed-height` custom property, with `data-clamped="true"`; the panel is never hidden, since the preview is real content.
46
+ * @fires {CustomEvent} primitiv-collapsible-show - Bubbles from the panel as it opens (not on first render), so a lazy panel can fetch its content.
47
+ */
48
+ export declare class PrimitivCollapsibleContentElement extends ElementBase {
49
+ }
50
+ /**
51
+ * `<primitiv-collapsible-trigger-icon>` — a decorative indicator, usually inside
52
+ * the trigger's button. The root hides it from assistive technology and mirrors
53
+ * the `data-state`, for a chevron that flips.
54
+ *
55
+ * @customElement primitiv-collapsible-trigger-icon
56
+ */
57
+ export declare class PrimitivCollapsibleTriggerIconElement extends ElementBase {
58
+ }
59
+ /**
60
+ * Define the four Collapsible elements on `registry` (the global
61
+ * `customElements` by default). Safe to call more than once, and a no-op where
62
+ * there is no registry (a server render).
63
+ */
64
+ export declare function defineCollapsible(registry?: ElementRegistry | undefined): void;
@@ -0,0 +1,173 @@
1
+ import { defineElements, ElementBase, reportOnce, write } from "../internal/element.js";
2
+ import { PrimitivRootElement } from "../internal/RootElement.js";
3
+ import { CollapsibleController } from "./CollapsibleController.js";
4
+ // Stryker disable StringLiteral: static — evaluated once at import, so runtime mutant switching never reaches them; the names are pinned by the defineCollapsible registry test instead.
5
+ const ROOT = "primitiv-collapsible";
6
+ const TRIGGER = "primitiv-collapsible-trigger";
7
+ const CONTENT = "primitiv-collapsible-content";
8
+ const ICON = "primitiv-collapsible-trigger-icon";
9
+ const HEIGHT = "--primitiv-collapsible-collapsed-height";
10
+ // Stryker restore StringLiteral
11
+ /**
12
+ * A trigger decorates exactly one element child — the `<button>` the consumer
13
+ * writes. Anything else is reported once and left undecorated rather than
14
+ * thrown (RFC 0033 D24).
15
+ */
16
+ function decoratable(trigger) {
17
+ if (trigger.childElementCount === 1)
18
+ return trigger.firstElementChild;
19
+ reportOnce(trigger, `<${TRIGGER}> needs exactly one element child (a <button>) to decorate.`);
20
+ return null;
21
+ }
22
+ /** A `collapsed-height` as a CSS length: a bare number is pixels, anything else is used verbatim. */
23
+ function cssLength(value) {
24
+ return Number.isNaN(Number(value)) ? value : `${value}px`;
25
+ }
26
+ /** Set or remove (`""`) a custom property, only when it would change (D20). */
27
+ function writeProperty(element, name, value) {
28
+ if (element.style.getPropertyValue(name) === value)
29
+ return;
30
+ // Setting the empty string removes the property (CSSOM).
31
+ element.style.setProperty(name, value);
32
+ // Removing the last property leaves `style=""` behind; drop it.
33
+ if (element.getAttribute("style") === "")
34
+ element.removeAttribute("style");
35
+ }
36
+ /**
37
+ * `<primitiv-collapsible>` — the state owner of a single disclosure: a trigger
38
+ * that shows and hides one panel.
39
+ *
40
+ * The root finds its own parts, so a collapsible nested in a panel keeps its
41
+ * own, and reconciles their ARIA from its reflected state whenever a part
42
+ * arrives, leaves, or changes an attribute the root reads (the lifecycle is
43
+ * `PrimitivRootElement`'s).
44
+ *
45
+ * @customElement primitiv-collapsible
46
+ * @summary The state owner of a single disclosure: a trigger that shows and hides one panel.
47
+ * @attr {boolean} open - Whether the panel is open. Reflected: it changes as the user toggles, so a server re-render or a history snapshot sees the live state.
48
+ * @attr {boolean} disabled - Disables the collapsible: the trigger's button stays focusable, is marked `aria-disabled`, and never toggles.
49
+ * @fires {CustomEvent<{ open: boolean }>} primitiv-collapsible-change - Bubbles, cancelable, fired before a user-driven toggle is applied, with the open state afterwards. Call `preventDefault()` to veto it.
50
+ */
51
+ export class PrimitivCollapsibleElement extends PrimitivRootElement {
52
+ /**
53
+ * Whether the panel was open at the last reconcile, so opening fires
54
+ * primitiv-collapsible-show; `undefined` until the first render.
55
+ */
56
+ #shown;
57
+ controller = new CollapsibleController(this, this.#host());
58
+ reconcileOn = ["open", "disabled", "force-mount", "collapsed-height"];
59
+ /** Whether the panel is open, reflected as the boolean `open` attribute. */
60
+ get open() {
61
+ return this.hasAttribute("open");
62
+ }
63
+ set open(next) {
64
+ this.toggleAttribute("open", next);
65
+ }
66
+ /** The controller's view of this element: state lives in reflected attributes. */
67
+ #host() {
68
+ return {
69
+ state: () => ({ open: this.open }),
70
+ change: (open) => {
71
+ if (this.announce("primitiv-collapsible-change", { open }))
72
+ this.open = open;
73
+ },
74
+ };
75
+ }
76
+ reconcile() {
77
+ const open = this.open;
78
+ const state = open ? "open" : "closed";
79
+ const disabled = this.hasAttribute("disabled");
80
+ const rootId = this.rootId();
81
+ write(this, "data-state", state);
82
+ write(this, "data-disabled", String(disabled));
83
+ const [trigger] = this.parts(TRIGGER);
84
+ const [panel] = this.parts(CONTENT);
85
+ const button = trigger ? decoratable(trigger) : null;
86
+ if (button) {
87
+ if (!button.id)
88
+ button.id = `${rootId}-trigger`;
89
+ write(button, "aria-expanded", String(open));
90
+ write(button, "aria-disabled", disabled ? "true" : null);
91
+ write(button, "data-disabled", String(disabled));
92
+ write(button, "data-state", state);
93
+ }
94
+ if (panel) {
95
+ if (!panel.id)
96
+ panel.id = `${rootId}-content`;
97
+ const height = panel.getAttribute("collapsed-height");
98
+ // A clamped preview is real, readable content while closed, so it is
99
+ // never hidden; a force-mount panel stays rendered so styles can animate it.
100
+ const clamped = height !== null;
101
+ const mounted = clamped || panel.hasAttribute("force-mount");
102
+ write(panel, "hidden", open || mounted ? null : "");
103
+ // Every closed panel but a preview, not only a force-mount one: a
104
+ // stylesheet that animates the panel gives it a display that wins over
105
+ // `hidden` (the registry's does), which would leave it exposed.
106
+ write(panel, "aria-hidden", open || clamped ? null : "true");
107
+ write(panel, "data-state", state);
108
+ write(panel, "data-disabled", String(disabled));
109
+ write(panel, "data-clamped", clamped ? "true" : null);
110
+ writeProperty(panel, HEIGHT, clamped ? cssLength(height) : "");
111
+ // Opening announces it, so a lazy panel can fetch its content
112
+ // (`hx-trigger="primitiv-collapsible-show once"`). The first render shows
113
+ // what the server already rendered, so it announces nothing.
114
+ if (open && this.#shown === false) {
115
+ panel.dispatchEvent(new CustomEvent("primitiv-collapsible-show", { bubbles: true }));
116
+ }
117
+ }
118
+ if (button)
119
+ write(button, "aria-controls", panel ? panel.id : null);
120
+ for (const icon of this.parts(ICON)) {
121
+ write(icon, "aria-hidden", "true");
122
+ write(icon, "data-state", state);
123
+ }
124
+ // Replace the registration wholesale: a trigger whose child was re-rendered
125
+ // simply is not registered again.
126
+ // Stryker disable next-line StringLiteral: equivalent — a collapsible has one trigger, so its value is never read.
127
+ this.controller.setTriggers(button ? [{ value: "trigger", element: button, disabled }] : []);
128
+ this.#shown = open;
129
+ }
130
+ }
131
+ /**
132
+ * `<primitiv-collapsible-trigger>` — an interactive part. It decorates exactly
133
+ * one child the consumer writes, a `<button>`, which receives `aria-expanded`,
134
+ * `aria-controls` and the state attributes, so the rendered DOM matches React's
135
+ * `<button aria-expanded>` (RFC 0033 D24).
136
+ *
137
+ * @customElement primitiv-collapsible-trigger
138
+ */
139
+ export class PrimitivCollapsibleTriggerElement extends ElementBase {
140
+ }
141
+ /**
142
+ * `<primitiv-collapsible-content>` — the panel. Structural: the root writes its
143
+ * id and state and hides it while closed.
144
+ *
145
+ * @customElement primitiv-collapsible-content
146
+ * @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.
147
+ * @attr {string} collapsed-height - Shows a clamped preview while closed instead of hiding the panel (the read-more pattern). A bare number is pixels; anything else is a CSS length, used verbatim. Published as the `--primitiv-collapsible-collapsed-height` custom property, with `data-clamped="true"`; the panel is never hidden, since the preview is real content.
148
+ * @fires {CustomEvent} primitiv-collapsible-show - Bubbles from the panel as it opens (not on first render), so a lazy panel can fetch its content.
149
+ */
150
+ export class PrimitivCollapsibleContentElement extends ElementBase {
151
+ }
152
+ /**
153
+ * `<primitiv-collapsible-trigger-icon>` — a decorative indicator, usually inside
154
+ * the trigger's button. The root hides it from assistive technology and mirrors
155
+ * the `data-state`, for a chevron that flips.
156
+ *
157
+ * @customElement primitiv-collapsible-trigger-icon
158
+ */
159
+ export class PrimitivCollapsibleTriggerIconElement extends ElementBase {
160
+ }
161
+ /**
162
+ * Define the four Collapsible elements on `registry` (the global
163
+ * `customElements` by default). Safe to call more than once, and a no-op where
164
+ * there is no registry (a server render).
165
+ */
166
+ export function defineCollapsible(registry = globalThis.customElements) {
167
+ defineElements(registry, [
168
+ [ROOT, PrimitivCollapsibleElement],
169
+ [TRIGGER, PrimitivCollapsibleTriggerElement],
170
+ [CONTENT, PrimitivCollapsibleContentElement],
171
+ [ICON, PrimitivCollapsibleTriggerIconElement],
172
+ ]);
173
+ }
@@ -0,0 +1,2 @@
1
+ export { CollapsibleController, type CollapsibleControllerHost } from "./CollapsibleController.js";
2
+ export { defineCollapsible, PrimitivCollapsibleContentElement, PrimitivCollapsibleElement, PrimitivCollapsibleTriggerElement, PrimitivCollapsibleTriggerIconElement, } from "./elements.js";
@@ -0,0 +1,2 @@
1
+ export { CollapsibleController } from "./CollapsibleController.js";
2
+ export { defineCollapsible, PrimitivCollapsibleContentElement, PrimitivCollapsibleElement, PrimitivCollapsibleTriggerElement, PrimitivCollapsibleTriggerIconElement, } from "./elements.js";
@@ -286,6 +286,236 @@
286
286
  }
287
287
  ]
288
288
  },
289
+ {
290
+ "kind": "javascript-module",
291
+ "path": "src/collapsible/elements.ts",
292
+ "declarations": [
293
+ {
294
+ "kind": "class",
295
+ "description": "`<primitiv-collapsible>` — the state owner of a single disclosure: a trigger\nthat shows and hides one panel.\n\nThe root finds its own parts, so a collapsible nested in a panel keeps its\nown, and reconciles their ARIA from its reflected state whenever a part\narrives, leaves, or changes an attribute the root reads (the lifecycle is\n`PrimitivRootElement`'s).",
296
+ "name": "PrimitivCollapsibleElement",
297
+ "members": [
298
+ {
299
+ "kind": "field",
300
+ "name": "#shown",
301
+ "privacy": "private",
302
+ "type": {
303
+ "text": "boolean | undefined"
304
+ },
305
+ "description": "Whether the panel was open at the last reconcile, so opening fires\nprimitiv-collapsible-show; `undefined` until the first render."
306
+ },
307
+ {
308
+ "kind": "field",
309
+ "name": "controller",
310
+ "type": {
311
+ "text": "CollapsibleController"
312
+ },
313
+ "privacy": "protected",
314
+ "readonly": true,
315
+ "default": "new CollapsibleController(this, this.#host())"
316
+ },
317
+ {
318
+ "kind": "field",
319
+ "name": "reconcileOn",
320
+ "type": {
321
+ "text": "readonly string[]"
322
+ },
323
+ "privacy": "protected",
324
+ "readonly": true,
325
+ "default": "[\"open\", \"disabled\", \"force-mount\", \"collapsed-height\"]"
326
+ },
327
+ {
328
+ "kind": "field",
329
+ "name": "open",
330
+ "type": {
331
+ "text": "boolean"
332
+ },
333
+ "description": "Whether the panel is open, reflected as the boolean `open` attribute."
334
+ },
335
+ {
336
+ "kind": "method",
337
+ "name": "#host",
338
+ "privacy": "private",
339
+ "return": {
340
+ "type": {
341
+ "text": "CollapsibleControllerHost"
342
+ }
343
+ },
344
+ "description": "The controller's view of this element: state lives in reflected attributes."
345
+ },
346
+ {
347
+ "kind": "method",
348
+ "name": "reconcile",
349
+ "privacy": "protected",
350
+ "return": {
351
+ "type": {
352
+ "text": "void"
353
+ }
354
+ }
355
+ }
356
+ ],
357
+ "events": [
358
+ {
359
+ "type": {
360
+ "text": "CustomEvent<{ open: boolean }>"
361
+ },
362
+ "description": "Bubbles, cancelable, fired before a user-driven toggle is applied, with the open state afterwards. Call `preventDefault()` to veto it.",
363
+ "name": "primitiv-collapsible-change"
364
+ }
365
+ ],
366
+ "attributes": [
367
+ {
368
+ "type": {
369
+ "text": "boolean"
370
+ },
371
+ "description": "Whether the panel is open. Reflected: it changes as the user toggles, so a server re-render or a history snapshot sees the live state.",
372
+ "name": "open"
373
+ },
374
+ {
375
+ "type": {
376
+ "text": "boolean"
377
+ },
378
+ "description": "Disables the collapsible: the trigger's button stays focusable, is marked `aria-disabled`, and never toggles.",
379
+ "name": "disabled"
380
+ }
381
+ ],
382
+ "superclass": {
383
+ "name": "PrimitivRootElement",
384
+ "module": "/src/internal/RootElement.ts"
385
+ },
386
+ "tagName": "primitiv-collapsible",
387
+ "customElement": true,
388
+ "summary": "The state owner of a single disclosure: a trigger that shows and hides one panel."
389
+ },
390
+ {
391
+ "kind": "class",
392
+ "description": "`<primitiv-collapsible-trigger>` — an interactive part. It decorates exactly\none child the consumer writes, a `<button>`, which receives `aria-expanded`,\n`aria-controls` and the state attributes, so the rendered DOM matches React's\n`<button aria-expanded>` (RFC 0033 D24).",
393
+ "name": "PrimitivCollapsibleTriggerElement",
394
+ "superclass": {
395
+ "name": "ElementBase",
396
+ "module": "/src/internal/element.ts"
397
+ },
398
+ "tagName": "primitiv-collapsible-trigger",
399
+ "customElement": true
400
+ },
401
+ {
402
+ "kind": "class",
403
+ "description": "`<primitiv-collapsible-content>` — the panel. Structural: the root writes its\nid and state and hides it while closed.",
404
+ "name": "PrimitivCollapsibleContentElement",
405
+ "events": [
406
+ {
407
+ "type": {
408
+ "text": "CustomEvent"
409
+ },
410
+ "description": "Bubbles from the panel as it opens (not on first render), so a lazy panel can fetch its content.",
411
+ "name": "primitiv-collapsible-show"
412
+ }
413
+ ],
414
+ "attributes": [
415
+ {
416
+ "type": {
417
+ "text": "boolean"
418
+ },
419
+ "description": "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.",
420
+ "name": "force-mount"
421
+ },
422
+ {
423
+ "type": {
424
+ "text": "string"
425
+ },
426
+ "description": "Shows a clamped preview while closed instead of hiding the panel (the read-more pattern). A bare number is pixels; anything else is a CSS length, used verbatim. Published as the `--primitiv-collapsible-collapsed-height` custom property, with `data-clamped=\"true\"`; the panel is never hidden, since the preview is real content.",
427
+ "name": "collapsed-height"
428
+ }
429
+ ],
430
+ "superclass": {
431
+ "name": "ElementBase",
432
+ "module": "/src/internal/element.ts"
433
+ },
434
+ "tagName": "primitiv-collapsible-content",
435
+ "customElement": true
436
+ },
437
+ {
438
+ "kind": "class",
439
+ "description": "`<primitiv-collapsible-trigger-icon>` — a decorative indicator, usually inside\nthe trigger's button. The root hides it from assistive technology and mirrors\nthe `data-state`, for a chevron that flips.",
440
+ "name": "PrimitivCollapsibleTriggerIconElement",
441
+ "superclass": {
442
+ "name": "ElementBase",
443
+ "module": "/src/internal/element.ts"
444
+ },
445
+ "tagName": "primitiv-collapsible-trigger-icon",
446
+ "customElement": true
447
+ },
448
+ {
449
+ "kind": "function",
450
+ "name": "defineCollapsible",
451
+ "return": {
452
+ "type": {
453
+ "text": "void"
454
+ }
455
+ },
456
+ "parameters": [
457
+ {
458
+ "name": "registry",
459
+ "default": "globalThis.customElements",
460
+ "type": {
461
+ "text": "ElementRegistry | undefined"
462
+ }
463
+ }
464
+ ],
465
+ "description": "Define the four Collapsible elements on `registry` (the global\n`customElements` by default). Safe to call more than once, and a no-op where\nthere is no registry (a server render)."
466
+ }
467
+ ],
468
+ "exports": [
469
+ {
470
+ "kind": "js",
471
+ "name": "ElementRegistry",
472
+ "declaration": {
473
+ "name": "ElementRegistry",
474
+ "module": "src/collapsible/elements.ts"
475
+ }
476
+ },
477
+ {
478
+ "kind": "js",
479
+ "name": "PrimitivCollapsibleElement",
480
+ "declaration": {
481
+ "name": "PrimitivCollapsibleElement",
482
+ "module": "src/collapsible/elements.ts"
483
+ }
484
+ },
485
+ {
486
+ "kind": "js",
487
+ "name": "PrimitivCollapsibleTriggerElement",
488
+ "declaration": {
489
+ "name": "PrimitivCollapsibleTriggerElement",
490
+ "module": "src/collapsible/elements.ts"
491
+ }
492
+ },
493
+ {
494
+ "kind": "js",
495
+ "name": "PrimitivCollapsibleContentElement",
496
+ "declaration": {
497
+ "name": "PrimitivCollapsibleContentElement",
498
+ "module": "src/collapsible/elements.ts"
499
+ }
500
+ },
501
+ {
502
+ "kind": "js",
503
+ "name": "PrimitivCollapsibleTriggerIconElement",
504
+ "declaration": {
505
+ "name": "PrimitivCollapsibleTriggerIconElement",
506
+ "module": "src/collapsible/elements.ts"
507
+ }
508
+ },
509
+ {
510
+ "kind": "js",
511
+ "name": "defineCollapsible",
512
+ "declaration": {
513
+ "name": "defineCollapsible",
514
+ "module": "src/collapsible/elements.ts"
515
+ }
516
+ }
517
+ ]
518
+ },
289
519
  {
290
520
  "kind": "javascript-module",
291
521
  "path": "src/tabs/elements.ts",
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Side-effect entry: importing this module defines the four Collapsible
3
+ * elements (`<primitiv-collapsible>`, `-trigger`, `-content`, `-trigger-icon`)
4
+ * on the global `customElements` registry.
5
+ *
6
+ * ```html
7
+ * <script type="module">import "@primitiv-ui/web/define/collapsible";</script>
8
+ * ```
9
+ */
10
+ import { defineCollapsible } from "../collapsible/elements.js";
11
+ defineCollapsible();
@@ -3,4 +3,5 @@
3
3
  * Import `@primitiv-ui/web/define/<component>` instead to define only one.
4
4
  */
5
5
  import "./accordion.js";
6
+ import "./collapsible.js";
6
7
  import "./tabs.js";
@@ -3,4 +3,5 @@
3
3
  * Import `@primitiv-ui/web/define/<component>` instead to define only one.
4
4
  */
5
5
  import "./accordion.js";
6
+ import "./collapsible.js";
6
7
  import "./tabs.js";
@@ -11,3 +11,7 @@ export type PrimitivTabsChangeEvent = CustomEvent<{
11
11
  export type PrimitivAccordionChangeEvent = CustomEvent<{
12
12
  value: string[];
13
13
  }>;
14
+ /** The `primitiv-collapsible-change` event: bubbles, cancelable, fired before the change applies, with the open state afterwards. */
15
+ export type PrimitivCollapsibleChangeEvent = CustomEvent<{
16
+ open: boolean;
17
+ }>;
@@ -19,13 +19,14 @@
19
19
  */
20
20
  import type { DetailedHTMLProps, HTMLAttributes } from "react";
21
21
  import type { PrimitivAccordionContentElement, PrimitivAccordionElement, PrimitivAccordionItemElement, PrimitivAccordionTriggerElement, PrimitivAccordionTriggerIconElement } from "../accordion/elements.js";
22
+ import type { PrimitivCollapsibleContentElement, PrimitivCollapsibleElement, PrimitivCollapsibleTriggerElement, PrimitivCollapsibleTriggerIconElement } from "../collapsible/elements.js";
22
23
  import type { PrimitivTabsContentElement, PrimitivTabsElement, PrimitivTabsListElement, PrimitivTabsTriggerElement } from "../tabs/elements.js";
23
- import type { PrimitivAccordionChangeEvent, PrimitivTabsChangeEvent } from "./events.js";
24
+ import type { PrimitivAccordionChangeEvent, PrimitivCollapsibleChangeEvent, PrimitivTabsChangeEvent } from "./events.js";
24
25
  /** React's own props for an element, plus `class`, which React 19 passes to custom elements. */
25
26
  type ElementProps<E extends HTMLElement> = DetailedHTMLProps<HTMLAttributes<E>, E> & {
26
27
  class?: string;
27
28
  };
28
- export type { PrimitivAccordionChangeEvent, PrimitivTabsChangeEvent };
29
+ export type { PrimitivAccordionChangeEvent, PrimitivCollapsibleChangeEvent, PrimitivTabsChangeEvent };
29
30
  declare module "react" {
30
31
  namespace JSX {
31
32
  interface IntrinsicElements {
@@ -46,6 +47,18 @@ declare module "react" {
46
47
  "onprimitiv-accordion-show"?: (event: CustomEvent) => void;
47
48
  };
48
49
  "primitiv-accordion-trigger-icon": ElementProps<PrimitivAccordionTriggerIconElement>;
50
+ "primitiv-collapsible": ElementProps<PrimitivCollapsibleElement> & {
51
+ open?: boolean;
52
+ disabled?: boolean;
53
+ "onprimitiv-collapsible-change"?: (event: PrimitivCollapsibleChangeEvent) => void;
54
+ };
55
+ "primitiv-collapsible-trigger": ElementProps<PrimitivCollapsibleTriggerElement>;
56
+ "primitiv-collapsible-content": ElementProps<PrimitivCollapsibleContentElement> & {
57
+ "force-mount"?: boolean;
58
+ "collapsed-height"?: string | number;
59
+ "onprimitiv-collapsible-show"?: (event: CustomEvent) => void;
60
+ };
61
+ "primitiv-collapsible-trigger-icon": ElementProps<PrimitivCollapsibleTriggerIconElement>;
49
62
  "primitiv-tabs": ElementProps<PrimitivTabsElement> & {
50
63
  value?: string;
51
64
  orientation?: "horizontal" | "vertical";
@@ -18,8 +18,9 @@
18
18
  */
19
19
  import type { HTMLAttributes } from "svelte/elements";
20
20
  import type { PrimitivAccordionContentElement, PrimitivAccordionElement, PrimitivAccordionItemElement, PrimitivAccordionTriggerElement, PrimitivAccordionTriggerIconElement } from "../accordion/elements.js";
21
+ import type { PrimitivCollapsibleContentElement, PrimitivCollapsibleElement, PrimitivCollapsibleTriggerElement, PrimitivCollapsibleTriggerIconElement } from "../collapsible/elements.js";
21
22
  import type { PrimitivTabsContentElement, PrimitivTabsElement, PrimitivTabsListElement, PrimitivTabsTriggerElement } from "../tabs/elements.js";
22
- import type { PrimitivAccordionChangeEvent, PrimitivTabsChangeEvent } from "./events.js";
23
+ import type { PrimitivAccordionChangeEvent, PrimitivCollapsibleChangeEvent, PrimitivTabsChangeEvent } from "./events.js";
23
24
  /** Svelte's own attributes and events for an element. */
24
25
  type Attributes<E extends HTMLElement> = HTMLAttributes<E>;
25
26
  declare module "svelte/elements" {
@@ -41,6 +42,18 @@ declare module "svelte/elements" {
41
42
  "onprimitiv-accordion-show"?: (event: CustomEvent) => void;
42
43
  };
43
44
  "primitiv-accordion-trigger-icon": Attributes<PrimitivAccordionTriggerIconElement>;
45
+ "primitiv-collapsible": Attributes<PrimitivCollapsibleElement> & {
46
+ open?: boolean;
47
+ disabled?: boolean;
48
+ "onprimitiv-collapsible-change"?: (event: PrimitivCollapsibleChangeEvent) => void;
49
+ };
50
+ "primitiv-collapsible-trigger": Attributes<PrimitivCollapsibleTriggerElement>;
51
+ "primitiv-collapsible-content": Attributes<PrimitivCollapsibleContentElement> & {
52
+ "force-mount"?: boolean;
53
+ "collapsed-height"?: string | number;
54
+ "onprimitiv-collapsible-show"?: (event: CustomEvent) => void;
55
+ };
56
+ "primitiv-collapsible-trigger-icon": Attributes<PrimitivCollapsibleTriggerIconElement>;
44
57
  "primitiv-tabs": Attributes<PrimitivTabsElement> & {
45
58
  value?: string;
46
59
  orientation?: "horizontal" | "vertical";
@@ -19,7 +19,7 @@
19
19
  * JSR does not allow a published module to do.
20
20
  */
21
21
  import type { DefineComponent, EmitsOptions } from "vue";
22
- import type { PrimitivAccordionChangeEvent, PrimitivTabsChangeEvent } from "./events.js";
22
+ import type { PrimitivAccordionChangeEvent, PrimitivCollapsibleChangeEvent, PrimitivTabsChangeEvent } from "./events.js";
23
23
  /** A tag as Vue's template checker sees it: its props, and the events it emits. */
24
24
  type Tag<Props, Events extends EmitsOptions = Record<never, never>> = DefineComponent<Props, Record<never, never>, Record<never, never>, Record<never, never>, Record<never, never>, Record<never, never>, Record<never, never>, Events>;
25
25
  declare module "vue" {
@@ -43,6 +43,20 @@ declare module "vue" {
43
43
  "primitiv-accordion-show": (event: CustomEvent) => void;
44
44
  }>;
45
45
  "primitiv-accordion-trigger-icon": Tag<Record<never, never>>;
46
+ "primitiv-collapsible": Tag<{
47
+ open?: boolean | "";
48
+ disabled?: boolean | "";
49
+ }, {
50
+ "primitiv-collapsible-change": (event: PrimitivCollapsibleChangeEvent) => void;
51
+ }>;
52
+ "primitiv-collapsible-trigger": Tag<Record<never, never>>;
53
+ "primitiv-collapsible-content": Tag<{
54
+ forceMount?: boolean | "";
55
+ collapsedHeight?: string | number;
56
+ }, {
57
+ "primitiv-collapsible-show": (event: CustomEvent) => void;
58
+ }>;
59
+ "primitiv-collapsible-trigger-icon": Tag<Record<never, never>>;
46
60
  "primitiv-tabs": Tag<{
47
61
  value?: string;
48
62
  orientation?: "horizontal" | "vertical";
package/dist/index.d.ts CHANGED
@@ -4,4 +4,5 @@
4
4
  * `@primitiv-ui/web/define` (or `/define/<component>`) to register the tags.
5
5
  */
6
6
  export * from "./accordion/index.js";
7
+ export * from "./collapsible/index.js";
7
8
  export * from "./tabs/index.js";
package/dist/index.js CHANGED
@@ -4,4 +4,5 @@
4
4
  * `@primitiv-ui/web/define` (or `/define/<component>`) to register the tags.
5
5
  */
6
6
  export * from "./accordion/index.js";
7
+ export * from "./collapsible/index.js";
7
8
  export * from "./tabs/index.js";
@@ -1 +1 @@
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
+ function h(e,t,o){return`${e}-${t}-${o}`}function C({orientation:e,dir:t,homeEnd:o=!1,activate:r=!1}){let s={};if(e==="horizontal"||e==="both"){let c=t==="rtl"?"ArrowLeft":"ArrowRight",a=t==="rtl"?"ArrowRight":"ArrowLeft";s[c]="next",s[a]="prev"}return(e==="vertical"||e==="both")&&(s.ArrowDown="next",s.ArrowUp="prev"),o&&(s.Home="first",s.End="last"),r&&(s.Enter="activate",s[" "]="activate"),s}function w({action:e,navigable:t,currentKey:o}){let r=t.length;if(r===0)return null;switch(e){case"activate":return{target:o};case"first":return{target:t[0]};case"last":return{target:t[r-1]};case void 0:return null;case"next":case"prev":{let s=t.indexOf(o);if(s===-1)return null;let c=e==="next"?1:r-1;return{target:t[(s+c)%r]}}}}function B(e){return e.active??e.values[0]}function O(e,t){return e.disabled.has(t)||e.active===t?null:t}function q(e,t,o){let r=C({orientation:e.orientation,dir:e.dir,homeEnd:!0,activate:!0})[o],s=w({action:r,navigable:e.values,currentKey:t});if(!s)return null;let c=s.target,a=e.activationMode==="automatic"||r==="activate";return{focus:c,activate:a?O(e,c):null}}function F(e,t){return e.disabled.has(t)?null:e.expanded.includes(t)?e.expanded.filter(o=>o!==t):e.multiple?[...e.expanded,t]:[t]}function U(e,t,o){let r=C({orientation:e.orientation,dir:e.dir,homeEnd:!0,activate:!0})[o];if(r==="activate")return e.disabled.has(t)?null:{focus:null,toggle:t};let s=e.values.filter(a=>!e.disabled.has(a)),c=w({action:r,navigable:s,currentKey:t});return c&&{focus:c.target,toggle:null}}function R(e){return e.disabled?null:!e.open}function W(e,t){if(t!=="Enter"&&t!==" ")return null;let o=R(e);return o===null?null:{open:o}}var b=class{#t;#e=new Map;#o=null;constructor(t){this.#t=t}registerTrigger(t,o,r={}){this.#e.set(o,{value:t,disabled:r.disabled??!1})}setTriggers(t){this.#e.clear();for(let{value:o,element:r,disabled:s}of t)this.registerTrigger(o,r,{disabled:s})}unregisterTrigger(t){this.#e.delete(t)}connect(){if(this.#o)return;this.#o=new AbortController;let{signal:t}=this.#o;this.#t.addEventListener("click",o=>this.#i(o,this.onTriggerClick),{signal:t}),this.#t.addEventListener("keydown",o=>this.#i(o,this.onTriggerKeydown),{signal:t})}disconnect(){this.#o?.abort(),this.#o=null}ordered(){return[...this.#e].sort(([t],[o])=>t.compareDocumentPosition(o)&Node.DOCUMENT_POSITION_FOLLOWING?-1:1)}disabled(){let t=new Set;for(let o of this.#e.values())o.disabled&&t.add(o.value);return t}#i(t,o){for(let[r,s]of this.#e)if(r.contains(t.target))return o.call(this,s,t)}};var x=class extends b{#t;constructor(t,o){super(t),this.#t=o}#e(t){let{expanded:o,multiple:r}=this.#t.state(),s=F({disabled:this.disabled(),expanded:o,multiple:r},t);s!==null&&this.#t.change(s)}onTriggerClick(t){this.#e(t.value)}onTriggerKeydown(t,o){let{expanded:r,multiple:s,orientation:c,dir:a}=this.#t.state(),p=this.ordered(),l=p.map(([,d])=>d.value),i=U({values:l,disabled:this.disabled(),expanded:r,multiple:s,orientation:c,dir:a},t.value,o.key);i&&(o.preventDefault(),i.toggle!==null&&this.#e(i.toggle),i.focus!==null&&p[l.indexOf(i.focus)][0].focus())}};var g=globalThis.HTMLElement??class{};function n(e,t,o){e.getAttribute(t)!==o&&(o===null?e.removeAttribute(t):e.setAttribute(t,o))}var tt=new WeakSet;function f(e,t){tt.has(e)||(tt.add(e),console.error(t,e))}function v(e,t){if(e)for(let[o,r]of t)e.get(o)||e.define(o,r)}function y(e,t,o){return[...e.querySelectorAll(t)].filter(r=>r.closest(o)===e)}var lt=0,m=class extends g{#t;#e=new MutationObserver(()=>this.reconcile());connectedCallback(){this.controller.connect(),this.#e.observe(this,{subtree:!0,childList:!0,attributes:!0,attributeFilter:[...this.reconcileOn]}),this.reconcile()}disconnectedCallback(){this.controller.disconnect(),this.#e.disconnect()}rootId(){return this.id||(this.#t??=`${this.localName}-${++lt}`)}direction(){return this.matches(":dir(rtl)")?"rtl":"ltr"}parts(t){return y(this,t,this.localName)}announce(t,o){return this.dispatchEvent(new CustomEvent(t,{bubbles:!0,cancelable:!0,detail:o}))}};var at="primitiv-accordion",T="primitiv-accordion-item",_="primitiv-accordion-trigger",et="primitiv-accordion-content",ot="primitiv-accordion-trigger-icon";function ct(e){return(e??"").split(/\s+/).filter(Boolean)}function dt(e){return e.childElementCount===1?e.firstElementChild:(f(e,`<${_}> needs exactly one element child (a <button>) to decorate.`),null)}var M=class extends m{#t=new Set;#e=!1;controller=new x(this,this.#o());reconcileOn=["value","disabled","orientation","force-mount"];get value(){return ct(this.getAttribute("value"))}set value(t){this.setAttribute("value",typeof t=="string"?t:t.join(" "))}#o(){return{state:()=>({expanded:this.value,multiple:this.hasAttribute("multiple"),orientation:this.#i(),dir:this.direction()}),change:t=>{this.announce("primitiv-accordion-change",{value:t})&&(this.value=t)}}}#i(){return this.getAttribute("orientation")==="horizontal"?"horizontal":"vertical"}reconcile(){let t=new Set(this.value),o=this.rootId();n(this,"data-orientation",this.#i());let r=[];for(let s of this.parts(T)){let c=s.getAttribute("value");if(c===null){f(s,`<${T}> needs a value, which pairs it with the root's value.`);continue}let a=t.has(c),p=a?"open":"closed",[l]=y(s,_,T),[i]=y(s,et,T),d=l?dt(l):null;if(d){let u=l.hasAttribute("disabled");r.push({value:c,element:d,disabled:u}),d.id||(d.id=h(o,"heading",c)),n(d,"aria-expanded",String(a)),n(d,"aria-disabled",String(u)),n(d,"data-disabled",String(u)),n(d,"data-state",p)}if(i){i.id||(i.id=h(o,"panel",c));let u=i.hasAttribute("force-mount");i.hasAttribute("role")||i.setAttribute("role","region"),n(i,"aria-labelledby",d?d.id:null),n(i,"hidden",a||u?null:""),n(i,"aria-hidden",a?null:"true"),n(i,"data-state",p),this.#e&&a&&!this.#t.has(c)&&i.dispatchEvent(new CustomEvent("primitiv-accordion-show",{bubbles:!0}))}d&&n(d,"aria-controls",i?i.id:null);for(let u of y(s,ot,T))n(u,"aria-hidden","true"),n(u,"data-state",p)}this.controller.setTriggers(r),this.#t=t,this.#e=!0}},H=class extends g{},K=class extends g{},L=class extends g{},S=class extends g{};function j(e=globalThis.customElements){v(e,[[at,M],[T,H],[_,K],[et,L],[ot,S]])}var E=class extends b{#t;constructor(t,o){super(t),this.#t=o}onTriggerClick(t){let o=R({open:this.#t.state().open,disabled:t.disabled});o!==null&&this.#t.change(o)}onTriggerKeydown(t,o){let r=W({open:this.#t.state().open,disabled:t.disabled},o.key);r&&(o.preventDefault(),this.#t.change(r.open))}};var pt="primitiv-collapsible",V="primitiv-collapsible-trigger",it="primitiv-collapsible-content",nt="primitiv-collapsible-trigger-icon",ut="--primitiv-collapsible-collapsed-height";function gt(e){return e.childElementCount===1?e.firstElementChild:(f(e,`<${V}> needs exactly one element child (a <button>) to decorate.`),null)}function bt(e){return Number.isNaN(Number(e))?e:`${e}px`}function mt(e,t,o){e.style.getPropertyValue(t)!==o&&(e.style.setProperty(t,o),e.getAttribute("style")===""&&e.removeAttribute("style"))}var I=class extends m{#t;controller=new E(this,this.#e());reconcileOn=["open","disabled","force-mount","collapsed-height"];get open(){return this.hasAttribute("open")}set open(t){this.toggleAttribute("open",t)}#e(){return{state:()=>({open:this.open}),change:t=>{this.announce("primitiv-collapsible-change",{open:t})&&(this.open=t)}}}reconcile(){let t=this.open,o=t?"open":"closed",r=this.hasAttribute("disabled"),s=this.rootId();n(this,"data-state",o),n(this,"data-disabled",String(r));let[c]=this.parts(V),[a]=this.parts(it),p=c?gt(c):null;if(p&&(p.id||(p.id=`${s}-trigger`),n(p,"aria-expanded",String(t)),n(p,"aria-disabled",r?"true":null),n(p,"data-disabled",String(r)),n(p,"data-state",o)),a){a.id||(a.id=`${s}-content`);let l=a.getAttribute("collapsed-height"),i=l!==null,d=i||a.hasAttribute("force-mount");n(a,"hidden",t||d?null:""),n(a,"aria-hidden",t||i?null:"true"),n(a,"data-state",o),n(a,"data-disabled",String(r)),n(a,"data-clamped",i?"true":null),mt(a,ut,i?bt(l):""),t&&this.#t===!1&&a.dispatchEvent(new CustomEvent("primitiv-collapsible-show",{bubbles:!0}))}p&&n(p,"aria-controls",a?a.id:null);for(let l of this.parts(nt))n(l,"aria-hidden","true"),n(l,"data-state",o);this.controller.setTriggers(p?[{value:"trigger",element:p,disabled:r}]:[]),this.#t=t}},k=class extends g{},N=class extends g{},D=class extends g{};function J(e=globalThis.customElements){v(e,[[pt,I],[V,k],[it,N],[nt,D]])}var A=class extends b{#t;constructor(t,o){super(t),this.#t=o}onTriggerClick(t){let{active:o}=this.#t.state(),r=O({disabled:this.disabled(),active:o},t.value);if(r!==null){let s=this.ordered().map(([,c])=>c.value);this.#t.activate(r,s.indexOf(r))}}onTriggerKeydown(t,o){let{active:r,orientation:s,dir:c,activationMode:a}=this.#t.state(),p=this.ordered(),l=p.map(([,d])=>d.value),i=q({values:l,disabled:this.disabled(),active:r,orientation:s,dir:c,activationMode:a},t.value,o.key);i&&(o.preventDefault(),i.activate!==null&&this.#t.activate(i.activate,l.indexOf(i.activate)),p[l.indexOf(i.focus)][0].focus())}};var ht="primitiv-tabs",rt="primitiv-tabs-list",Q="primitiv-tabs-trigger",st="primitiv-tabs-content";function ft(e){let t=e.getAttribute("value");return e.childElementCount===1&&t!==null?{tab:e.firstElementChild,value:t}:(f(e,`<${Q}> needs a value and exactly one element child (a <button>, an <a href> or a router link) to decorate.`),null)}var P=class extends m{#t;#e=!1;controller=new A(this,this.#o());reconcileOn=["value","disabled","label","orientation"];get value(){return this.getAttribute("value")??void 0}set value(t){this.setAttribute("value",t)}#o(){return{state:()=>({active:this.value,orientation:this.#n(),dir:this.direction(),activationMode:this.#i()}),activate:(t,o)=>{this.announce("primitiv-tabs-change",{value:t,index:o})&&(this.value=t)}}}#i(){let t=this.getAttribute("activation-mode");return t==="manual"?t:"automatic"}#n(){return this.getAttribute("orientation")==="vertical"?"vertical":"horizontal"}reconcile(){let t=this.#n(),o=this.value,r=this.rootId();n(this,"data-orientation",t);for(let l of this.parts(rt)){n(l,"role","tablist"),n(l,"aria-orientation",t),n(l,"data-orientation",t);let i=l.getAttribute("label");i!==null&&n(l,"aria-label",i)}let s=new Map;for(let l of this.parts(st)){let i=l.getAttribute("value");i!==null&&s.set(i,l)}let c=this.parts(Q).flatMap(l=>{let i=ft(l);return i?[{trigger:l,...i}]:[]}),a=B({values:c.map(l=>l.value),active:o});this.controller.setTriggers(c.map(({trigger:l,tab:i,value:d})=>({value:d,element:i,disabled:l.hasAttribute("disabled")})));for(let{trigger:l,tab:i,value:d}of c){let u=s.get(d),Y=d===o,Z=l.hasAttribute("disabled");i.id||(i.id=h(r,"trigger",d)),u&&!u.id&&(u.id=h(r,"panel",d)),n(i,"role","tab"),n(i,"aria-selected",String(Y)),n(i,"aria-controls",u?u.id:null),n(i,"tabindex",d===a?"0":"-1"),n(i,"aria-disabled",Z?"true":null),n(i,"data-disabled",Z?"":null),n(i,"data-state",Y?"active":"inactive"),n(i,"data-orientation",t),u&&n(u,"aria-labelledby",i.id)}for(let[l,i]of s){let d=l===o;n(i,"role","tabpanel"),n(i,"tabindex","0"),n(i,"hidden",d?null:""),n(i,"data-state",d?"active":"inactive"),n(i,"data-orientation",t)}let p=s.get(o);this.#e&&o!==this.#t&&p&&p.dispatchEvent(new CustomEvent("primitiv-tabs-show",{bubbles:!0})),this.#t=o,this.#e=!0}},$=class extends g{},G=class extends g{},z=class extends g{};function X(e=globalThis.customElements){v(e,[[ht,P],[rt,$],[Q,G],[st,z]])}j();J();X();export{x as AccordionController,E as CollapsibleController,L as PrimitivAccordionContentElement,M as PrimitivAccordionElement,H as PrimitivAccordionItemElement,K as PrimitivAccordionTriggerElement,S as PrimitivAccordionTriggerIconElement,N as PrimitivCollapsibleContentElement,I as PrimitivCollapsibleElement,k as PrimitivCollapsibleTriggerElement,D as PrimitivCollapsibleTriggerIconElement,z as PrimitivTabsContentElement,P as PrimitivTabsElement,$ as PrimitivTabsListElement,G as PrimitivTabsTriggerElement,A as TabsController,j as defineAccordion,J as defineCollapsible,X as defineTabs};
package/dist/tags.d.ts CHANGED
@@ -13,6 +13,7 @@
13
13
  * JSR does not allow a published module to do (RFC 0033 §5.6.4).
14
14
  */
15
15
  import type { PrimitivAccordionContentElement, PrimitivAccordionElement, PrimitivAccordionItemElement, PrimitivAccordionTriggerElement, PrimitivAccordionTriggerIconElement } from "./accordion/elements.js";
16
+ import type { PrimitivCollapsibleContentElement, PrimitivCollapsibleElement, PrimitivCollapsibleTriggerElement, PrimitivCollapsibleTriggerIconElement } from "./collapsible/elements.js";
16
17
  import type { PrimitivTabsContentElement, PrimitivTabsElement, PrimitivTabsListElement, PrimitivTabsTriggerElement } from "./tabs/elements.js";
17
18
  declare global {
18
19
  interface HTMLElementTagNameMap {
@@ -21,6 +22,10 @@ declare global {
21
22
  "primitiv-accordion-trigger": PrimitivAccordionTriggerElement;
22
23
  "primitiv-accordion-content": PrimitivAccordionContentElement;
23
24
  "primitiv-accordion-trigger-icon": PrimitivAccordionTriggerIconElement;
25
+ "primitiv-collapsible": PrimitivCollapsibleElement;
26
+ "primitiv-collapsible-trigger": PrimitivCollapsibleTriggerElement;
27
+ "primitiv-collapsible-content": PrimitivCollapsibleContentElement;
28
+ "primitiv-collapsible-trigger-icon": PrimitivCollapsibleTriggerIconElement;
24
29
  "primitiv-tabs": PrimitivTabsElement;
25
30
  "primitiv-tabs-list": PrimitivTabsListElement;
26
31
  "primitiv-tabs-trigger": PrimitivTabsTriggerElement;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@primitiv-ui/web",
3
- "version": "0.1.46",
3
+ "version": "0.1.47",
4
4
  "description": "Primitiv's headless components as standard custom elements: light DOM, no framework, no runtime dependencies. Works in plain HTML, server-rendered stacks, React, Vue, Angular and anything else that renders HTML.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -14,6 +14,10 @@
14
14
  "types": "./dist/accordion/index.d.ts",
15
15
  "default": "./dist/accordion/index.js"
16
16
  },
17
+ "./collapsible": {
18
+ "types": "./dist/collapsible/index.d.ts",
19
+ "default": "./dist/collapsible/index.js"
20
+ },
17
21
  "./tabs": {
18
22
  "types": "./dist/tabs/index.d.ts",
19
23
  "default": "./dist/tabs/index.js"
@@ -26,6 +30,10 @@
26
30
  "types": "./dist/define/accordion.d.ts",
27
31
  "default": "./dist/define/accordion.js"
28
32
  },
33
+ "./define/collapsible": {
34
+ "types": "./dist/define/collapsible.d.ts",
35
+ "default": "./dist/define/collapsible.js"
36
+ },
29
37
  "./define/tabs": {
30
38
  "types": "./dist/define/tabs.d.ts",
31
39
  "default": "./dist/define/tabs.js"
@@ -60,7 +68,7 @@
60
68
  "directory": "packages/web"
61
69
  },
62
70
  "dependencies": {
63
- "@primitiv-ui/core": "0.1.46"
71
+ "@primitiv-ui/core": "0.1.47"
64
72
  },
65
73
  "devDependencies": {
66
74
  "@custom-elements-manifest/analyzer": "0.11.0",
@@ -1 +0,0 @@
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};