@agencecinq/disclosure-navigation 0.0.0-stage → 2.0.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 CHANGED
@@ -1,3 +1,321 @@
1
- # Temporary Holding Version
1
+ [![](https://img.shields.io/npm/v/@agencecinq/disclosure-navigation)](https://www.npmjs.com/package/@agencecinq/disclosure-navigation)
2
+ [![](https://img.shields.io/npm/dm/@agencecinq/disclosure-navigation)](https://www.npmjs.com/package/@agencecinq/disclosure-navigation)
2
3
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
4
+ # @agencecinq/disclosure-navigation
5
+
6
+ > Accessible, WAI-ARIA disclosure navigation as a lightweight Web Component.
7
+
8
+ A site navigation whose buttons show and hide submenus. `<cinq-disclosure-navigation>`
9
+ updates `aria-expanded` and `hidden`, keeps one branch open at a time, adds the keyboard
10
+ support of the APG example, and can open the submenus on hover.
11
+
12
+ Implementation follows the
13
+ [WAI-ARIA Authoring Practices disclosure navigation example](https://www.w3.org/WAI/ARIA/apg/patterns/disclosure/examples/disclosure-navigation/).
14
+ Inspired by [`@19h47/disclosure-navigation`](https://github.com/19h47/19h47-disclosure-navigation).
15
+
16
+ - **Navigation, not a menu.** A site navigation is a list of links. Buttons announce
17
+ _collapsed_ or _expanded_, links stay links. The ARIA `menu` role is meant for
18
+ application menus.
19
+ - **Your HTML is the source of truth.** The component reads `aria-expanded` and
20
+ `aria-controls`, and never adds a missing attribute.
21
+ - **Built on the platform.** Native `<button>`, the `hidden` attribute,
22
+ [`ariaControlsElements`](https://developer.mozilla.org/en-US/docs/Web/API/Element/ariaControlsElements),
23
+ [`ToggleEvent`](https://developer.mozilla.org/en-US/docs/Web/API/ToggleEvent) as popovers
24
+ fire it, `AbortController`. No dependency.
25
+
26
+ ## Installation
27
+
28
+ ```bash
29
+ pnpm add @agencecinq/disclosure-navigation
30
+ ```
31
+
32
+ ## Usage
33
+
34
+ ```js
35
+ import "@agencecinq/disclosure-navigation";
36
+ ```
37
+
38
+ ```html
39
+ <cinq-disclosure-navigation>
40
+ <nav aria-label="Main">
41
+ <ul>
42
+ <li><a href="/">Home</a></li>
43
+ <li>
44
+ <button type="button" aria-expanded="false" aria-controls="about-menu">About</button>
45
+ <ul id="about-menu" hidden>
46
+ <li><a href="/about/overview">Overview</a></li>
47
+ <li><a href="/about/team" aria-current="page">Team</a></li>
48
+ </ul>
49
+ </li>
50
+ </ul>
51
+ </nav>
52
+ </cinq-disclosure-navigation>
53
+ ```
54
+
55
+ Importing the package registers the custom element. Connecting it only adds listeners.
56
+
57
+ > **HTML is the source of truth.** The component does not set a missing
58
+ > `aria-expanded`, `hidden` or label, nor warns about them. Use an accessibility linter
59
+ > (axe-core, Lighthouse) to catch invalid markup.
60
+
61
+ ### Required markup
62
+
63
+ | Attribute | On | Role |
64
+ | --------------- | ----------- | ---------------------------------------------------------------------------------------- |
65
+ | `aria-expanded` | `<button>` | **Required.** The state. A button without it is ignored. The component updates it. |
66
+ | `aria-controls` | `<button>` | **Required.** ID(s) of the submenu(s), inside the host. Several IDs work together. |
67
+ | `hidden` | Submenu | Present while collapsed, or `hidden="until-found"` to keep its links searchable. |
68
+ | `aria-label` | `<nav>` | Names the landmark when the page has more than one navigation. |
69
+ | `aria-current` | `<a>` | Optional: marks the current page. |
70
+
71
+ To render a submenu open, write `aria-expanded="true"` on its button and leave out
72
+ `hidden`.
73
+
74
+ ### Attributes
75
+
76
+ | Attribute | On | Effect |
77
+ | ------------------------ | ---- | ------------------------------------------------------------------------- |
78
+ | `data-hover` | Host | A mouse opens the submenus on hover. See [Opening on hover](#opening-on-hover). |
79
+ | `data-arrow-keys="false"` | Host | Leaves the arrow, Home and End keys to the browser. |
80
+
81
+ Both are read on each interaction: setting or removing them needs no call.
82
+
83
+ ### Styling
84
+
85
+ Style the state the component writes:
86
+
87
+ ```css
88
+ button[aria-expanded="true"]::after {
89
+ rotate: 225deg;
90
+ }
91
+
92
+ /* Needed only if your CSS gives .submenu a display value */
93
+ .submenu[hidden]:not([hidden="until-found"]) {
94
+ display: none;
95
+ }
96
+ ```
97
+
98
+ Position submenus with logical properties, such as `inset-inline-start`, so they follow
99
+ the reading direction. The host is inline, as any custom element: give it a `display`
100
+ if you lay it out.
101
+
102
+ ### Nested submenus
103
+
104
+ A submenu can hold buttons with their own submenus. Opening one keeps its parents open
105
+ and closes the other branches. Closing a submenu closes the ones nested in it.
106
+
107
+ In a submenu, the arrow keys stay at its level, whatever the layout: Tab moves into a
108
+ nested submenu, and Escape back to its button.
109
+
110
+ ### Opening on hover
111
+
112
+ With `data-hover`, a mouse opens a submenu when it enters its button, and closes it when
113
+ it leaves the button and the submenu. Touch and pen keep the click.
114
+
115
+ ```html
116
+ <cinq-disclosure-navigation data-hover>…</cinq-disclosure-navigation>
117
+ ```
118
+
119
+ - **The click still works.** The first click after the mouse opened a submenu keeps it
120
+ open, as the user meant to open it. The next click closes it.
121
+ - **No gap.** The pointer goes from the button to the submenu: leave no space between
122
+ them, or fill it with the submenu's padding.
123
+ - **Escape closes it wherever the focus is.** Content shown on hover must be dismissible
124
+ without moving the pointer
125
+ ([WCAG 1.4.13](https://www.w3.org/WAI/WCAG22/Understanding/content-on-hover-or-focus.html)).
126
+ - **Hover only adds.** Keyboard and screen reader users open the same submenus with the
127
+ buttons.
128
+
129
+ ### Find in page
130
+
131
+ Write `hidden="until-found"` on collapsed submenus and their links stay searchable. When
132
+ find in page or a link to a fragment reveals a submenu, the component opens it, nested
133
+ ones included, and keeps `until-found` when it closes again.
134
+
135
+ ```html
136
+ <ul id="about-menu" hidden="until-found">…</ul>
137
+ ```
138
+
139
+ A stylesheet that sets `[hidden] { display: none }` keeps the links out of the search:
140
+ Bootstrap's Reboot does, with `!important`, while Tailwind 4's preflight leaves
141
+ `until-found` out. Restore `display` for `[hidden="until-found"]`. The browser reveals
142
+ the submenu whatever a listener does:
143
+ canceling `beforetoggle` leaves the button collapsed next to a visible submenu.
144
+
145
+ ## Keyboard
146
+
147
+ | Key | Focus on | Action |
148
+ | ------------------- | -------------------------- | ------------------------------------------------ |
149
+ | Tab / Shift + Tab | Anywhere | Native order. Collapsed submenus are skipped. |
150
+ | Enter / Space | Button | Toggles its submenu (native button activation). |
151
+ | Escape | Button or top-level link | Closes the open submenu. |
152
+ | Escape | Submenu | Closes the innermost one and focuses its button. |
153
+ | Escape | Outside the navigation | Closes the open submenu, focus stays. |
154
+ | ArrowDown | Expanded top-level button | Moves focus to the first item of its submenu. |
155
+ | Arrow keys | Top-level item | Previous or next button or link, wrapping. |
156
+ | Arrow keys | Submenu item | Previous or next item, wrapping. |
157
+ | Home / End | Top-level or submenu item | First or last item. |
158
+
159
+ Moving focus out of the navigation closes the open submenu. Keys pressed with Alt, Ctrl
160
+ or Meta are left to the browser, and so are arrow keys in a text field.
161
+
162
+ In a right-to-left context, ArrowLeft and ArrowRight swap. The component reads the
163
+ direction from the markup with `:dir(rtl)`: a `dir` attribute on the page or the
164
+ navigation is enough.
165
+
166
+ ### In a dialog or a popover
167
+
168
+ Escape is also a
169
+ [close request](https://html.spec.whatwg.org/multipage/interaction.html#close-requests):
170
+ it closes the topmost dialog or popover. While a submenu is open and focus is in the
171
+ navigation, the component cancels the key press, so each press closes one layer: the
172
+ submenu, then the dialog. With every submenu closed, or focus elsewhere, the browser
173
+ handles the key. An Escape another script already canceled is left alone.
174
+
175
+ ## API
176
+
177
+ | Member | Description |
178
+ | ----------------- | -------------------------------------------------------------------------------------------- |
179
+ | `init()` | Binds listeners. Runs on connect. Call `destroy()` first if already bound. |
180
+ | `destroy()` | Removes listeners. Runs on disconnect. The markup stays in its current state. |
181
+ | `open($button)` | Opens the submenu of `$button` and closes the other branches. Canceling its `beforetoggle` leaves every submenu as it is. |
182
+ | `close($button?)` | Closes the submenu of `$button`, or every open submenu. Focus inside a closing submenu moves to its button. |
183
+ | `toggle($button)` | Opens or closes the submenu of `$button`. |
184
+
185
+ Buttons are found inside the host with `button[aria-expanded][aria-controls]`, read from
186
+ the DOM on each interaction: adding or removing items needs no call. Closing the submenu
187
+ once a link is followed is up to you:
188
+
189
+ ```js
190
+ const $navigation = document.querySelector("cinq-disclosure-navigation");
191
+
192
+ $navigation.addEventListener("click", (event) => {
193
+ if (event.target.closest("a")) {
194
+ $navigation.close();
195
+ }
196
+ });
197
+ ```
198
+
199
+ ### Closing from outside
200
+
201
+ The open submenu closes when focus leaves the navigation, on Escape, and with
202
+ `data-hover` when the mouse leaves it. Safari does not focus a button on click, by
203
+ design: after opening a submenu with a click, clicking elsewhere moves no focus and the
204
+ submenu stays open. If you need it, close it yourself:
205
+
206
+ ```js
207
+ document.addEventListener("click", (event) => {
208
+ if (!$navigation.contains(event.target)) {
209
+ $navigation.close();
210
+ }
211
+ });
212
+ ```
213
+
214
+ ## Events
215
+
216
+ The component fires the platform's events, the way a popover does: a
217
+ [`ToggleEvent`](https://developer.mozilla.org/en-US/docs/Web/API/ToggleEvent) named
218
+ `beforetoggle`, then `toggle`, at each submenu. The same listener works for `<details>`,
219
+ popovers and `<dialog>`.
220
+
221
+ | Event | Fires | Cancelable |
222
+ | -------------- | ----------------------------------------------------------------------------------------- | ----------------------- |
223
+ | `beforetoggle` | **Before** the DOM changes. | When the submenu opens. |
224
+ | `toggle` | **After**, in a later task. Changes made before it fires coalesce into one event, from the first old state to the last new state. | No. |
225
+
226
+ `event.target` is the submenu, `event.source` its button, and `oldState` and `newState`
227
+ are `'open'` or `'closed'`. As on the platform, the events do not bubble: otherwise a
228
+ submenu's `toggle` would reach the `ontoggle` of a `<details>` or a `<dialog>` holding the
229
+ navigation. Listen on the submenu, or on the host in the capture phase.
230
+
231
+ ```js
232
+ $navigation.addEventListener(
233
+ "beforetoggle",
234
+ (event) => {
235
+ if ("open" === event.newState && "services" === event.source.id) {
236
+ event.preventDefault(); // Services stays closed
237
+ }
238
+ },
239
+ { capture: true },
240
+ );
241
+
242
+ $navigation.addEventListener(
243
+ "toggle",
244
+ (event) => console.log(event.target.id, event.newState),
245
+ { capture: true },
246
+ );
247
+ ```
248
+
249
+ Canceling an opening leaves every submenu as it is, the open one included: the other
250
+ branches close after `beforetoggle`. Closing cannot be canceled. A button that controls
251
+ several elements fires at each one, and opens none of them when one opening is canceled.
252
+ In TypeScript, the DOM library already types both events as `ToggleEvent`.
253
+
254
+ ## Browser support
255
+
256
+ Chrome and Edge 135, Firefox 136 and Safari 17: submenus are resolved with
257
+ [`ariaControlsElements`](https://developer.mozilla.org/en-US/docs/Web/API/Element/ariaControlsElements)
258
+ and events are [`ToggleEvent`](https://developer.mozilla.org/en-US/docs/Web/API/ToggleEvent)s.
259
+ `event.source` needs Chrome and Edge 140, Firefox 145 and Safari 26.5: older browsers
260
+ fire the events without it.
261
+
262
+ Find in page relies on `hidden="until-found"`: Chrome and Edge 102, Firefox 139,
263
+ Safari 26.2. Elsewhere it acts as `hidden`. The package is an ES module.
264
+
265
+ ## Migration
266
+
267
+ ### From 1.x to 2.0
268
+
269
+ 1. **Expect ArrowDown to stay at the level of a submenu.** On an expanded button in a
270
+ submenu, it moved into the nested submenu, while ArrowUp went to the previous item.
271
+ It now goes to the next item. Tab still moves into the nested submenu. ArrowDown on
272
+ an expanded top-level button moves into its submenu, as before.
273
+
274
+ ### From `@19h47/disclosure-navigation` 5.x
275
+
276
+ The class becomes a custom element: the host replaces `$nav`. Markup inside it, styles,
277
+ methods and events do not change.
278
+
279
+ 1. **Wrap the navigation in the host**, and drop the instance.
280
+
281
+ ```diff
282
+ + <cinq-disclosure-navigation>
283
+ <nav aria-label="Main">…</nav>
284
+ + </cinq-disclosure-navigation>
285
+ ```
286
+
287
+ ```diff
288
+ - import DisclosureNavigation from "@19h47/disclosure-navigation";
289
+ -
290
+ - const disclosureNavigation = new DisclosureNavigation($nav);
291
+ -
292
+ - disclosureNavigation.init();
293
+ + import "@agencecinq/disclosure-navigation";
294
+ ```
295
+
296
+ 2. **Call the methods on the host.** `open()`, `close()` and `toggle()` take the same
297
+ arguments. `init()` and `destroy()` run on connect and disconnect. `$el` is gone.
298
+ 3. **Listen on the host.** `beforetoggle` and `toggle` still fire at the submenus,
299
+ without bubbling: listen on the host in the capture phase, instead of `$nav`.
300
+ 4. **Replace `useArrowKeys = false`** with `data-arrow-keys="false"` on the host.
301
+ 5. **Expect Escape outside the navigation** to close the open submenu, without
302
+ canceling the key.
303
+
304
+ ### From several `<cinq-disclosure-button>`
305
+
306
+ A navigation built from one `<cinq-disclosure-button>` per entry, with a script that
307
+ closes the others or opens on hover, becomes one host.
308
+
309
+ 1. **Remove the `<cinq-disclosure-button>` around each button**, and wrap the navigation
310
+ in `<cinq-disclosure-navigation>`. Add `data-hover` to open on hover.
311
+ 2. **Remove the script.** One branch open at a time, Escape, closing when focus leaves
312
+ and hover are built in.
313
+ 3. **Replace the `disclosure-button:*` listeners** with `beforetoggle` and `toggle` on
314
+ the host, in the capture phase.
315
+
316
+ ## Development
317
+
318
+ ```bash
319
+ pnpm --filter @agencecinq/disclosure-navigation build
320
+ pnpm --filter @agencecinq/disclosure-navigation test # Chromium, Firefox and WebKit
321
+ ```
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Disclosure navigation Web Component around a site navigation whose buttons show
3
+ * and hide submenus. The markup is the source of truth: `aria-expanded` on each
4
+ * button holds the state, and the submenus are the elements its `aria-controls`
5
+ * references. The component never adds a missing attribute; it only updates
6
+ * `aria-expanded` and `hidden` when the user interacts, and fires `beforetoggle`
7
+ * and `toggle` at the submenus, as popovers do.
8
+ *
9
+ * @see https://www.w3.org/WAI/ARIA/apg/patterns/disclosure/examples/disclosure-navigation/
10
+ * @see https://w3c.github.io/aria/#dom-ariamixin-ariacontrolselements
11
+ */
12
+ export declare class DisclosureNavigation extends HTMLElement {
13
+ #private;
14
+ connectedCallback(): void;
15
+ disconnectedCallback(): void;
16
+ /** Binds listeners. Call `destroy()` first if already bound. */
17
+ init(): void;
18
+ /** Removes listeners. The markup stays in its current state. */
19
+ destroy(): void;
20
+ /**
21
+ * Opens the submenu of `$button` and closes the open ones outside its branch,
22
+ * as popovers do. Canceling its `beforetoggle` leaves every submenu as it is.
23
+ */
24
+ open($button: HTMLButtonElement): void;
25
+ /** Closes the submenu of `$button` and the ones nested in it, or every open submenu. */
26
+ close($button?: HTMLButtonElement): void;
27
+ toggle($button: HTMLButtonElement): void;
28
+ }
29
+ //# sourceMappingURL=disclosure-navigation.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"disclosure-navigation.d.ts","sourceRoot":"","sources":["../src/disclosure-navigation.ts"],"names":[],"mappings":"AA2BA;;;;;;;;;;GAUG;AACH,qBAAa,oBAAqB,SAAQ,WAAW;;IAOnD,iBAAiB,IAAI,IAAI;IAIzB,oBAAoB,IAAI,IAAI;IAI5B,gEAAgE;IAChE,IAAI,IAAI,IAAI;IAcZ,gEAAgE;IAChE,OAAO,IAAI,IAAI;IAKf;;;OAGG;IACH,IAAI,CAAC,OAAO,EAAE,iBAAiB,GAAG,IAAI;IActC,wFAAwF;IACxF,KAAK,CAAC,OAAO,CAAC,EAAE,iBAAiB,GAAG,IAAI;IAUxC,MAAM,CAAC,OAAO,EAAE,iBAAiB,GAAG,IAAI;CAoOzC"}
@@ -0,0 +1,3 @@
1
+ import { DisclosureNavigation } from './disclosure-navigation.js';
2
+ export { DisclosureNavigation };
3
+ //# 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,oBAAoB,EAAE,MAAM,4BAA4B,CAAC;AAElE,OAAO,EAAE,oBAAoB,EAAE,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,125 @@
1
+ //#region src/utils.ts
2
+ var e = (e) => e === "open" ? "closed" : "open", t = /* @__PURE__ */ new WeakMap(), n = (t, n, r) => {
3
+ let i = new ToggleEvent("beforetoggle", {
4
+ cancelable: n === "open",
5
+ oldState: e(n),
6
+ newState: n,
7
+ source: r
8
+ });
9
+ return t.dispatchEvent(i);
10
+ }, r = (n, r, i) => {
11
+ let a = t.get(n), o = a?.oldState ?? e(r);
12
+ clearTimeout(a?.timeout);
13
+ let s = setTimeout(() => {
14
+ let e = new ToggleEvent("toggle", {
15
+ oldState: o,
16
+ newState: r,
17
+ source: i
18
+ });
19
+ t.delete(n), n.dispatchEvent(e);
20
+ });
21
+ t.set(n, {
22
+ oldState: o,
23
+ timeout: s
24
+ });
25
+ }, i = (e, t, n) => {
26
+ if (e.altKey || e.ctrlKey || e.metaKey) return;
27
+ let r = t.length - 1, i = n === 0 ? r : n - 1, a = n === r ? 0 : n + 1, o = t[n].matches(":dir(rtl)"), s = {
28
+ ArrowUp: i,
29
+ ArrowLeft: o ? a : i,
30
+ ArrowDown: a,
31
+ ArrowRight: o ? i : a,
32
+ Home: 0,
33
+ End: r
34
+ }[e.key];
35
+ s !== void 0 && (e.preventDefault(), t[s].focus());
36
+ }, a = "button[aria-expanded][aria-controls]", o = `a[href], ${a}`, s = "a[href], button:enabled, [tabindex]:not([tabindex^=\"-\"])", c = (e) => e.ariaExpanded === "true", l = (e) => e.ariaControlsElements ?? [], u = (e, t) => l(e).some((e) => e.contains(t)), d = (e, t) => t !== null && (e.contains(t) || u(e, t)), f = (e, t) => l(e).every((r) => n(r, t, e)), p = class extends HTMLElement {
37
+ #e = null;
38
+ #t = /* @__PURE__ */ new WeakMap();
39
+ #n = null;
40
+ connectedCallback() {
41
+ this.init();
42
+ }
43
+ disconnectedCallback() {
44
+ this.destroy();
45
+ }
46
+ init() {
47
+ this.#e = new AbortController();
48
+ let { signal: e } = this.#e;
49
+ this.addEventListener("click", this.#l, { signal: e }), this.addEventListener("keydown", this.#h, { signal: e }), this.addEventListener("focusout", this.#f, { signal: e }), this.addEventListener("beforematch", this.#p, { signal: e }), this.addEventListener("pointerover", this.#u, { signal: e }), this.addEventListener("pointerout", this.#d, { signal: e }), document.addEventListener("keydown", this.#m, { signal: e });
50
+ }
51
+ destroy() {
52
+ this.#e?.abort(), this.#n = null;
53
+ }
54
+ open(e) {
55
+ if (c(e) || !f(e, "open")) return;
56
+ let t = this.#i.filter((t) => !u(t, e) && !u(e, t));
57
+ this.#o(t), this.#s(e, !0);
58
+ }
59
+ close(e) {
60
+ let t = e ? this.#i.filter((t) => t === e || u(e, t)) : this.#i;
61
+ this.#o(t);
62
+ }
63
+ toggle(e) {
64
+ return c(e) ? this.close(e) : this.open(e);
65
+ }
66
+ get #r() {
67
+ return [...this.querySelectorAll(a)];
68
+ }
69
+ get #i() {
70
+ return this.#r.filter(c);
71
+ }
72
+ #a(e, t) {
73
+ let n = this.#r.flatMap(l).filter((t) => e.some((e) => e !== t && e.contains(t)));
74
+ return e.flatMap((e) => [...e.querySelectorAll(t)]).filter((e) => !n.some((t) => t.contains(e)));
75
+ }
76
+ #o(e) {
77
+ e.toReversed().forEach((e) => {
78
+ f(e, "closed"), this.#s(e, !1);
79
+ });
80
+ }
81
+ #s(e, t) {
82
+ let n = t ? "open" : "closed";
83
+ e.ariaExpanded = t.toString(), !t && e === this.#n && (this.#n = null), l(e).forEach((i) => {
84
+ r(i, n, e), t ? (this.#t.set(i, i.hidden || !0), i.hidden = !1) : (i.matches(":focus-within") && e.focus(), i.hidden = this.#t.get(i) ?? !0);
85
+ });
86
+ }
87
+ #c(e) {
88
+ return e.pointerType === "mouse" && this.hasAttribute("data-hover");
89
+ }
90
+ #l = (e) => {
91
+ let t = e.target.closest(a);
92
+ if (!t || !this.contains(t)) return;
93
+ let n = t === this.#n;
94
+ this.#n = null, n || this.toggle(t);
95
+ };
96
+ #u = (e) => {
97
+ let t = e.target.closest(a), n = e.relatedTarget;
98
+ this.#c(e) && t && this.contains(t) && !c(t) && !d(t, n) && (this.open(t), c(t) && (this.#n = t));
99
+ };
100
+ #d = (e) => {
101
+ if (!this.#c(e)) return;
102
+ let t = e.target, n = e.relatedTarget;
103
+ this.#i.filter((e) => d(e, t) && !d(e, n)).forEach((e) => this.close(e));
104
+ };
105
+ #f = (e) => {
106
+ this.contains(e.relatedTarget) || this.matches(":active") || this.close();
107
+ };
108
+ #p = (e) => {
109
+ let t = e.target, n = this.#r.find((e) => l(e).includes(t));
110
+ n && (this.open(n), t.hidden = "until-found");
111
+ };
112
+ #m = (e) => {
113
+ if (e.key !== "Escape" || e.defaultPrevented || !this.#i.length) return;
114
+ let t = e.target;
115
+ return this.contains(t) ? (e.preventDefault(), this.close(this.#i.findLast((e) => u(e, t)))) : this.close();
116
+ };
117
+ #h = (e) => {
118
+ if (this.dataset.arrowKeys === "false") return;
119
+ let t = e.target, n = this.#i.findLast((e) => u(e, t)), r = n ? this.#a(l(n), s) : this.#a([this], o), a = r.indexOf(t);
120
+ if (a !== -1) return !n && e.key === "ArrowDown" && t instanceof HTMLButtonElement && c(t) ? (e.preventDefault(), this.#a(l(t), s)[0]?.focus()) : i(e, r, a);
121
+ };
122
+ };
123
+ customElements.get("cinq-disclosure-navigation") || customElements.define("cinq-disclosure-navigation", p);
124
+ //#endregion
125
+ export { p as DisclosureNavigation };
@@ -0,0 +1,21 @@
1
+ export type State = "open" | "closed";
2
+ /**
3
+ * Fires `beforetoggle` at `$element`, as a popover does: before the change,
4
+ * without bubbling, cancelable only when the element opens.
5
+ *
6
+ * @returns `false` if a listener canceled the opening.
7
+ */
8
+ export declare const dispatchBeforetoggle: ($element: HTMLElement, newState: State, $source: HTMLButtonElement) => boolean;
9
+ /**
10
+ * Fires `toggle` at `$element` in a later task, as a popover does. Changes made
11
+ * before it fires coalesce into one event, from the first old state to the last
12
+ * new state.
13
+ */
14
+ export declare const queueToggle: ($element: HTMLElement, newState: State, $source: HTMLButtonElement) => void;
15
+ /**
16
+ * Moves focus between `elements` with the arrow, Home and End keys, wrapping at
17
+ * both ends. Left and right follow the reading direction of the focused element.
18
+ * Keys pressed with a modifier are left to the browser.
19
+ */
20
+ export declare const focusByKey: (event: KeyboardEvent, elements: HTMLElement[], index: number) => void;
21
+ //# sourceMappingURL=utils.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,KAAK,GAAG,MAAM,GAAG,QAAQ,CAAC;AAUtC;;;;;GAKG;AACH,eAAO,MAAM,oBAAoB,GAC/B,UAAU,WAAW,EACrB,UAAU,KAAK,EACf,SAAS,iBAAiB,KACzB,OASF,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,WAAW,GACtB,UAAU,WAAW,EACrB,UAAU,KAAK,EACf,SAAS,iBAAiB,KACzB,IAkBF,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,UAAU,GACrB,OAAO,aAAa,EACpB,UAAU,WAAW,EAAE,EACvB,OAAO,MAAM,KACZ,IAyBF,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,52 @@
1
1
  {
2
2
  "name": "@agencecinq/disclosure-navigation",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "2.0.0",
4
+ "description": "Accessible WAI-ARIA disclosure navigation Web Component: submenus opened by buttons, on hover too.",
5
+ "keywords": [
6
+ "disclosure",
7
+ "navigation",
8
+ "nav",
9
+ "menu",
10
+ "web-component",
11
+ "accessibility",
12
+ "a11y",
13
+ "wai-aria",
14
+ "apg"
15
+ ],
16
+ "type": "module",
17
+ "license": "MIT",
18
+ "author": "Jérémy Levron <jeremy@agencecinq.com> (https://agencecinq.com)",
19
+ "repository": {
20
+ "type": "git",
21
+ "url": "https://github.com/agencecinq/ui.git",
22
+ "directory": "packages/disclosure-navigation"
23
+ },
24
+ "exports": {
25
+ ".": {
26
+ "types": "./dist/index.d.ts",
27
+ "import": "./dist/index.js"
28
+ }
29
+ },
30
+ "main": "./dist/index.js",
31
+ "module": "./dist/index.js",
32
+ "types": "./dist/index.d.ts",
33
+ "files": [
34
+ "dist"
35
+ ],
36
+ "devDependencies": {
37
+ "@vitest/browser-playwright": "^4.1.11",
38
+ "playwright": "^1.64.1",
39
+ "vite": "^8.2.0",
40
+ "vite-plugin-dts": "^5.0.3",
41
+ "vitest": "^4.1.11"
42
+ },
43
+ "publishConfig": {
44
+ "access": "public"
45
+ },
46
+ "scripts": {
47
+ "build": "vite build",
48
+ "dev": "vite build --watch",
49
+ "test": "vitest run",
50
+ "test:watch": "vitest"
51
+ }
6
52
  }