@agencecinq/disclosure-button 1.1.0

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 ADDED
@@ -0,0 +1,198 @@
1
+ [![](https://img.shields.io/npm/v/@agencecinq/disclosure-button)](https://www.npmjs.com/package/@agencecinq/disclosure-button)
2
+ [![](https://img.shields.io/npm/dm/@agencecinq/disclosure-button)](https://www.npmjs.com/package/@agencecinq/disclosure-button)
3
+
4
+ # @agencecinq/disclosure-button
5
+
6
+ > Accessible WAI-ARIA disclosure button Web Component.
7
+
8
+ A disclosure button shows or hides a section of content. `<cinq-disclosure-button>`
9
+ wraps a trigger, wires up the `aria-expanded` / `aria-controls` relationship,
10
+ toggles the `hidden` attribute on controlled regions, and dispatches open/close
11
+ events.
12
+
13
+ Implementation follows the
14
+ [WAI-ARIA Authoring Practices disclosure pattern](https://www.w3.org/WAI/ARIA/apg/patterns/disclosure/).
15
+ Inspired by [`@19h47/disclosure-button`](https://github.com/19h47/19h47-disclosure-button/).
16
+
17
+ ## Installation
18
+
19
+ ```bash
20
+ pnpm add @agencecinq/disclosure-button
21
+ ```
22
+
23
+ ## Usage
24
+
25
+ ```js
26
+ import '@agencecinq/disclosure-button';
27
+ ```
28
+
29
+ ```html
30
+ <cinq-disclosure-button>
31
+ <button
32
+ type="button"
33
+ aria-expanded="false"
34
+ aria-controls="details-1"
35
+ >
36
+ Show more
37
+ </button>
38
+ </cinq-disclosure-button>
39
+
40
+ <div id="details-1" class="foo" hidden>
41
+ Disclosure content
42
+ </div>
43
+ ```
44
+
45
+ ```css
46
+ .foo[hidden] {
47
+ display: none;
48
+ }
49
+
50
+ .foo {
51
+ display: flex;
52
+ }
53
+ ```
54
+
55
+ ### Required markup
56
+
57
+ | Attribute / element | Required | Role |
58
+ | ------------------- | -------- | ---- |
59
+ | Inner `<button>` | **Yes** | Focusable trigger inside `<cinq-disclosure-button>`. |
60
+ | `aria-expanded` | **Yes** | Current disclosure state on the trigger. |
61
+ | `aria-controls` | **Yes** | Space-separated IDs of the controlled regions. |
62
+ | `hidden` | **Yes** | Set on each controlled region in its collapsed initial state. |
63
+
64
+ Use `[data-button]` instead of `<button>` when you need a different focusable
65
+ element as the trigger.
66
+
67
+ ### API
68
+
69
+ | Attribute | Required | Description |
70
+ | --------- | -------- | ----------- |
71
+ | `expanded` | No | Reflected state on the host — useful for styling the wrapper. |
72
+
73
+ | Method | Description |
74
+ | ------ | ----------- |
75
+ | `open(emit?)` | Opens the disclosure. |
76
+ | `close(emit?)` | Closes the disclosure. |
77
+ | `toggle()` | Toggles open/closed. |
78
+ | `destroy()` | Detaches listeners. |
79
+
80
+ ### Keyboard & focus
81
+
82
+ The trigger must be a native focusable control — typically
83
+ `<button type="button">`. Enter and Space activate the button through browser
84
+ defaults; the component listens for `click`, which those keys dispatch on
85
+ buttons.
86
+
87
+ | Key | Function |
88
+ | --- | -------- |
89
+ | `Enter` | Toggle the disclosure (native button behavior). |
90
+ | `Space` | Toggle the disclosure (native button behavior). |
91
+
92
+ The component does **not** move focus into the controlled region on open, nor
93
+ restore focus on close — focus stays on the trigger. Once open, users can tab
94
+ into focusable elements inside the region if you include any.
95
+
96
+ While collapsed, `hidden` keeps the region out of the tab order and accessibility
97
+ tree.
98
+
99
+ On `focus` / `blur`, a `.focus` class is toggled on the inner trigger:
100
+
101
+ ```css
102
+ cinq-disclosure-button button.focus {
103
+ outline: 2px solid currentColor;
104
+ outline-offset: 2px;
105
+ }
106
+ ```
107
+
108
+ ### One button, multiple targets
109
+
110
+ `aria-controls` accepts several space-separated IDs — all matched regions open
111
+ and close together.
112
+
113
+ ### Multiple buttons, one target
114
+
115
+ Several triggers can share the same `aria-controls` ID. Use one
116
+ `<cinq-disclosure-button>` per trigger — each instance listens for bubbling
117
+ open/close events and syncs when `event.detail.elements` references a shared DOM
118
+ node.
119
+
120
+ ### Programmatic API
121
+
122
+ ```js
123
+ const $host = document.querySelector('cinq-disclosure-button');
124
+
125
+ $host.open();
126
+ $host.close();
127
+ ```
128
+
129
+ `open()` and `close()` accept an optional `emit` argument (default `true`). Pass
130
+ `false` to update state without dispatching an event. Linked triggers only sync
131
+ when the event is dispatched.
132
+
133
+ ```js
134
+ $openLink.addEventListener("click", () => {
135
+ $host.open();
136
+ });
137
+
138
+ $dismissButton.addEventListener("click", () => {
139
+ $host.close();
140
+ });
141
+ ```
142
+
143
+ Call `destroy()` when removing the element from the DOM to detach listeners.
144
+
145
+ ## Events
146
+
147
+ | Event | Cancelable | Detail | Description |
148
+ | ----- | ---------- | ------ | ----------- |
149
+ | `disclosure-button:open` | Yes | `{ ids, elements, el }` | Fired before the disclosure opens. Cancel to abort. |
150
+ | `disclosure-button:close` | Yes | `{ ids, elements, el }` | Fired before the disclosure closes. Cancel to abort. |
151
+
152
+ ```js
153
+ import { EVENTS } from "@agencecinq/utils";
154
+
155
+ $host.addEventListener(EVENTS.DISCLOSURE_BUTTON_OPEN, (event) => {
156
+ if (!userMayOpen()) {
157
+ event.preventDefault();
158
+ }
159
+ });
160
+ ```
161
+
162
+ `event.detail` carries `{ ids, elements, el }`.
163
+
164
+ ### Updating the button label
165
+
166
+ The component does not change the trigger's visible text. Listen to the open/close
167
+ events and update the label in your app — useful for Show/Hide copy, i18n, or
168
+ custom designs:
169
+
170
+ ```js
171
+ import { EVENTS } from "@agencecinq/utils";
172
+
173
+ const labels = { closed: "Show details", open: "Hide details" };
174
+
175
+ $button.addEventListener(EVENTS.DISCLOSURE_BUTTON_OPEN, () => {
176
+ $button.textContent = labels.open;
177
+ });
178
+
179
+ $button.addEventListener(EVENTS.DISCLOSURE_BUTTON_CLOSE, () => {
180
+ $button.textContent = labels.closed;
181
+ });
182
+ ```
183
+
184
+ ### Read more / Read less
185
+
186
+ Keep a short excerpt visible and hide the rest. Style the trigger as inline text
187
+ and swap between Read more / Read less on open/close events.
188
+
189
+ ## Build setup
190
+
191
+ ```bash
192
+ pnpm -C packages/disclosure-button build
193
+ ```
194
+
195
+ ## Acknowledgments
196
+
197
+ - [Disclosure Pattern (WAI-ARIA Practices)](https://www.w3.org/WAI/ARIA/apg/patterns/disclosure/)
198
+ - [`@19h47/disclosure-button`](https://github.com/19h47/19h47-disclosure-button/) — original implementation
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Disclosure button Web Component wrapping a slotted trigger.
3
+ *
4
+ * @see https://www.w3.org/WAI/ARIA/apg/patterns/disclosure/
5
+ */
6
+ export declare class DisclosureButton extends HTMLElement {
7
+ static observedAttributes: string[];
8
+ $button: HTMLButtonElement | null;
9
+ elements: HTMLElement[];
10
+ private controlIds;
11
+ private observer;
12
+ private reflectingAttribute;
13
+ connectedCallback(): void;
14
+ disconnectedCallback(): void;
15
+ attributeChangedCallback(name: string, _oldValue: string | null, newValue: string | null): void;
16
+ get button(): HTMLButtonElement | null;
17
+ get expanded(): boolean;
18
+ toggle(): boolean;
19
+ close(emit?: boolean): void;
20
+ open(emit?: boolean): void;
21
+ destroy(): void;
22
+ private initEvents;
23
+ private onClick;
24
+ private onFocus;
25
+ private onBlur;
26
+ private onLinkedChange;
27
+ private get detail();
28
+ private isExpanded;
29
+ private updateExpandedFromElements;
30
+ private reflectExpandedAttribute;
31
+ }
32
+ //# sourceMappingURL=disclosure-button.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"disclosure-button.d.ts","sourceRoot":"","sources":["../src/disclosure-button.ts"],"names":[],"mappings":"AAiDA;;;;GAIG;AACH,qBAAa,gBAAiB,SAAQ,WAAW;IAC/C,MAAM,CAAC,kBAAkB,WAAgB;IAEzC,OAAO,EAAE,iBAAiB,GAAG,IAAI,CAAQ;IACzC,QAAQ,EAAE,WAAW,EAAE,CAAM;IAE7B,OAAO,CAAC,UAAU,CAAgB;IAClC,OAAO,CAAC,QAAQ,CAAiC;IACjD,OAAO,CAAC,mBAAmB,CAAS;IAEpC,iBAAiB,IAAI,IAAI;IA+BzB,oBAAoB,IAAI,IAAI;IAO5B,wBAAwB,CACtB,IAAI,EAAE,MAAM,EACZ,SAAS,EAAE,MAAM,GAAG,IAAI,EACxB,QAAQ,EAAE,MAAM,GAAG,IAAI,GACtB,IAAI;IAiBP,IAAI,MAAM,IAAI,iBAAiB,GAAG,IAAI,CAErC;IAED,IAAI,QAAQ,IAAI,OAAO,CAEtB;IAED,MAAM,IAAI,OAAO;IAoBjB,KAAK,CAAC,IAAI,UAAO,GAAG,IAAI;IAaxB,IAAI,CAAC,IAAI,UAAO,GAAG,IAAI;IAavB,OAAO,IAAI,IAAI;IAaf,OAAO,CAAC,UAAU;IAUlB,OAAO,CAAC,OAAO,CAEb;IAEF,OAAO,CAAC,OAAO,CAEb;IAEF,OAAO,CAAC,MAAM,CAEZ;IAEF,OAAO,CAAC,cAAc,CAUpB;IAEF,OAAO,KAAK,MAAM,GAEjB;IAED,OAAO,CAAC,UAAU;IAIlB,OAAO,CAAC,0BAA0B;IAYlC,OAAO,CAAC,wBAAwB;CAajC"}
@@ -0,0 +1,4 @@
1
+ import { DisclosureButton } from './disclosure-button.js';
2
+ export { DisclosureButton };
3
+ export type { DisclosureButtonDetail } from './types.js';
4
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAE1D,OAAO,EAAE,gBAAgB,EAAE,CAAC;AAC5B,YAAY,EAAE,sBAAsB,EAAE,MAAM,YAAY,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,143 @@
1
+ //#region ../utils/dist/index.js
2
+ var e = {
3
+ DRAWER_CLOSE: "drawer-close",
4
+ DRAWER_OPEN: "drawer-open",
5
+ DRAWER_TOGGLE: "drawer-toggle",
6
+ MODAL_CLOSE: "modal-close",
7
+ MODAL_OPEN: "modal-open",
8
+ MODAL_TOGGLE: "modal-toggle",
9
+ SPINBUTTON_CHANGE: "spinbutton-change",
10
+ DISCLOSURE_BUTTON_OPEN: "disclosure-button:open",
11
+ DISCLOSURE_BUTTON_CLOSE: "disclosure-button:close",
12
+ TAB_BEFORE_ACTIVATE: "tab-before-activate",
13
+ TAB_ACTIVATE: "tab-activate",
14
+ TAB_DELETE: "tab-delete",
15
+ CART_BEFORE_ADD: "cart-before-add",
16
+ CART_BEFORE_UPDATE: "cart-before-update",
17
+ CART_UPDATE: "cart-update",
18
+ VARIANT_CHANGE: "variant-change"
19
+ }, t = (e, t) => {
20
+ let n = null, r = null, i = () => {
21
+ r && e(...r), n = null;
22
+ };
23
+ return (...e) => {
24
+ r = e, n ||= setTimeout(i, t);
25
+ };
26
+ }, n = document.documentElement, { body: r } = document;
27
+ n.hasAttribute("data-debug");
28
+ var i = {
29
+ x: 0,
30
+ y: 0
31
+ };
32
+ window.addEventListener("pointermove", t(({ x: e, y: t }) => {
33
+ i.x = e, i.y = t;
34
+ }, 100), { passive: !0 }), window.matchMedia("(width >= 64rem)"), window.matchMedia("(min-width: 1280px)"), window.matchMedia("(min-width: 1440px)"), window.matchMedia("(min-width: 1920px)");
35
+ //#endregion
36
+ //#region src/disclosure-button.ts
37
+ var a = (e, t, n) => e.dispatchEvent(new CustomEvent(n, {
38
+ bubbles: !0,
39
+ cancelable: !0,
40
+ detail: t
41
+ })), o = (e, t) => {
42
+ e.hidden = !t;
43
+ }, s = (e) => {
44
+ e.forEach((e) => {
45
+ o(e, !0);
46
+ });
47
+ }, c = (e) => {
48
+ e.forEach((e) => {
49
+ o(e, !1);
50
+ });
51
+ }, l = (e) => !e.hidden, u = (e) => e ? e.trim().split(/\s+/).map((e) => e.trim()).filter(Boolean) : [], d = (e, t) => e.elements.some((e) => t.includes(e)), f = class extends HTMLElement {
52
+ static observedAttributes = ["expanded"];
53
+ $button = null;
54
+ elements = [];
55
+ controlIds = [];
56
+ observer = null;
57
+ reflectingAttribute = !1;
58
+ connectedCallback() {
59
+ if (this.$button = this.querySelector("[data-button]") || this.querySelector("button"), !this.$button) throw Error("DisclosureButton: button element not found");
60
+ if (this.controlIds = u(this.$button.getAttribute("aria-controls")), this.controlIds.length === 0) return;
61
+ let e = this.controlIds.map((e) => `#${e}`).join(",");
62
+ this.elements = [...document.querySelectorAll(e)], this.initEvents(), this.updateExpandedFromElements(), this.observer = new MutationObserver(() => {
63
+ this.reflectingAttribute || this.reflectExpandedAttribute();
64
+ }), this.observer.observe(this.$button, {
65
+ attributes: !0,
66
+ attributeFilter: ["aria-expanded"]
67
+ }), this.reflectExpandedAttribute();
68
+ }
69
+ disconnectedCallback() {
70
+ this.destroy(), this.$button = null, this.elements = [], this.controlIds = [];
71
+ }
72
+ attributeChangedCallback(e, t, n) {
73
+ if (e !== "expanded" || !this.$button || this.reflectingAttribute) return;
74
+ let r = this.$button.getAttribute("aria-expanded") === "true";
75
+ if (n !== null && !r) {
76
+ this.open();
77
+ return;
78
+ }
79
+ n === null && r && this.close();
80
+ }
81
+ get button() {
82
+ return this.$button;
83
+ }
84
+ get expanded() {
85
+ return this.$button?.getAttribute("aria-expanded") === "true";
86
+ }
87
+ toggle() {
88
+ return this.$button ? this.isExpanded() ? a(this.$button, this.detail, e.DISCLOSURE_BUTTON_CLOSE) ? (this.close(!1), !0) : !1 : a(this.$button, this.detail, e.DISCLOSURE_BUTTON_OPEN) ? (this.open(!1), !0) : !1 : !1;
89
+ }
90
+ close(t = !0) {
91
+ this.$button && (t && this.isExpanded() && !a(this.$button, this.detail, e.DISCLOSURE_BUTTON_CLOSE) || (c(this.elements), this.updateExpandedFromElements()));
92
+ }
93
+ open(t = !0) {
94
+ this.$button && (t && !this.isExpanded() && !a(this.$button, this.detail, e.DISCLOSURE_BUTTON_OPEN) || (s(this.elements), this.updateExpandedFromElements()));
95
+ }
96
+ destroy() {
97
+ this.$button && (this.$button.removeEventListener("click", this.onClick), this.$button.removeEventListener("focus", this.onFocus), this.$button.removeEventListener("blur", this.onBlur), document.removeEventListener(e.DISCLOSURE_BUTTON_OPEN, this.onLinkedChange), document.removeEventListener(e.DISCLOSURE_BUTTON_CLOSE, this.onLinkedChange), this.observer?.disconnect(), this.observer = null);
98
+ }
99
+ initEvents() {
100
+ this.$button && (this.$button.addEventListener("click", this.onClick), this.$button.addEventListener("focus", this.onFocus), this.$button.addEventListener("blur", this.onBlur), document.addEventListener(e.DISCLOSURE_BUTTON_OPEN, this.onLinkedChange), document.addEventListener(e.DISCLOSURE_BUTTON_CLOSE, this.onLinkedChange));
101
+ }
102
+ onClick = () => {
103
+ this.toggle();
104
+ };
105
+ onFocus = () => {
106
+ this.$button?.classList.add("focus");
107
+ };
108
+ onBlur = () => {
109
+ this.$button?.classList.remove("focus");
110
+ };
111
+ onLinkedChange = (e) => {
112
+ if (!(e instanceof CustomEvent) || !this.$button) return;
113
+ let t = e.detail;
114
+ t.el !== this.$button && d(t, this.elements) && queueMicrotask(() => {
115
+ this.updateExpandedFromElements();
116
+ });
117
+ };
118
+ get detail() {
119
+ return {
120
+ ids: this.controlIds,
121
+ elements: this.elements,
122
+ el: this.$button
123
+ };
124
+ }
125
+ isExpanded() {
126
+ return this.$button?.getAttribute("aria-expanded") === "true";
127
+ }
128
+ updateExpandedFromElements() {
129
+ if (!this.$button) return;
130
+ if (this.elements.length === 0) {
131
+ this.$button.setAttribute("aria-expanded", "false");
132
+ return;
133
+ }
134
+ let e = this.elements.every((e) => l(e));
135
+ this.$button.setAttribute("aria-expanded", e ? "true" : "false");
136
+ }
137
+ reflectExpandedAttribute() {
138
+ this.$button && (this.reflectingAttribute = !0, this.$button.getAttribute("aria-expanded") === "true" ? this.setAttribute("expanded", "") : this.removeAttribute("expanded"), this.reflectingAttribute = !1);
139
+ }
140
+ };
141
+ customElements.get("cinq-disclosure-button") || customElements.define("cinq-disclosure-button", f);
142
+ //#endregion
143
+ export { f as DisclosureButton };
@@ -0,0 +1,8 @@
1
+ export interface DisclosureButtonDetail {
2
+ /** Raw tokens from the trigger's `aria-controls` attribute. */
3
+ ids: string[];
4
+ /** Controlled elements resolved from the DOM at `init()`. */
5
+ elements: HTMLElement[];
6
+ el: HTMLElement;
7
+ }
8
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,sBAAsB;IACrC,+DAA+D;IAC/D,GAAG,EAAE,MAAM,EAAE,CAAC;IACd,6DAA6D;IAC7D,QAAQ,EAAE,WAAW,EAAE,CAAC;IACxB,EAAE,EAAE,WAAW,CAAC;CACjB"}
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "@agencecinq/disclosure-button",
3
+ "version": "1.1.0",
4
+ "description": "Accessible WAI-ARIA disclosure button Web Component.",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "author": "Jérémy Levron <jeremy@agencecinq.com> (https://agencecinq.com)",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "https://github.com/agencecinq/shopify.git",
11
+ "directory": "packages/disclosure-button"
12
+ },
13
+ "exports": {
14
+ ".": {
15
+ "types": "./dist/index.d.ts",
16
+ "import": "./dist/index.js"
17
+ }
18
+ },
19
+ "main": "./dist/index.js",
20
+ "module": "./dist/index.js",
21
+ "types": "./dist/index.d.ts",
22
+ "files": [
23
+ "dist"
24
+ ],
25
+ "peerDependencies": {
26
+ "@agencecinq/utils": "*"
27
+ },
28
+ "devDependencies": {
29
+ "vite": "^8.1.3",
30
+ "vite-plugin-dts": "^5.0.3",
31
+ "@agencecinq/utils": "5.0.2"
32
+ },
33
+ "publishConfig": {
34
+ "access": "public"
35
+ },
36
+ "scripts": {
37
+ "build": "vite build",
38
+ "dev": "vite build --watch"
39
+ }
40
+ }