@agencecinq/disclosure-navigation 0.0.0-stage → 1.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,311 @@
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
+ ### Opening on hover
108
+
109
+ With `data-hover`, a mouse opens a submenu when it enters its button, and closes it when
110
+ it leaves the button and the submenu. Touch and pen keep the click.
111
+
112
+ ```html
113
+ <cinq-disclosure-navigation data-hover>…</cinq-disclosure-navigation>
114
+ ```
115
+
116
+ - **The click still works.** The first click after the mouse opened a submenu keeps it
117
+ open, as the user meant to open it. The next click closes it.
118
+ - **No gap.** The pointer goes from the button to the submenu: leave no space between
119
+ them, or fill it with the submenu's padding.
120
+ - **Escape closes it wherever the focus is.** Content shown on hover must be dismissible
121
+ without moving the pointer
122
+ ([WCAG 1.4.13](https://www.w3.org/WAI/WCAG22/Understanding/content-on-hover-or-focus.html)).
123
+ - **Hover only adds.** Keyboard and screen reader users open the same submenus with the
124
+ buttons.
125
+
126
+ ### Find in page
127
+
128
+ Write `hidden="until-found"` on collapsed submenus and their links stay searchable. When
129
+ find in page or a link to a fragment reveals a submenu, the component opens it, nested
130
+ ones included, and keeps `until-found` when it closes again.
131
+
132
+ ```html
133
+ <ul id="about-menu" hidden="until-found">…</ul>
134
+ ```
135
+
136
+ A stylesheet that sets `[hidden] { display: none }` keeps the links out of the search:
137
+ Bootstrap's Reboot does, with `!important`, while Tailwind 4's preflight leaves
138
+ `until-found` out. Restore `display` for `[hidden="until-found"]`. The browser reveals
139
+ the submenu whatever a listener does:
140
+ canceling `beforetoggle` leaves the button collapsed next to a visible submenu.
141
+
142
+ ## Keyboard
143
+
144
+ | Key | Focus on | Action |
145
+ | ------------------- | -------------------------- | ------------------------------------------------ |
146
+ | Tab / Shift + Tab | Anywhere | Native order. Collapsed submenus are skipped. |
147
+ | Enter / Space | Button | Toggles its submenu (native button activation). |
148
+ | Escape | Button or top-level link | Closes the open submenu. |
149
+ | Escape | Submenu | Closes the innermost one and focuses its button. |
150
+ | Escape | Outside the navigation | Closes the open submenu, focus stays. |
151
+ | ArrowDown | Expanded button, any level | Moves focus to the first item of its submenu. |
152
+ | Arrow keys | Top-level item | Previous or next button or link, wrapping. |
153
+ | Arrow keys | Submenu item | Previous or next item, wrapping. |
154
+ | Home / End | Top-level or submenu item | First or last item. |
155
+
156
+ Moving focus out of the navigation closes the open submenu. Keys pressed with Alt, Ctrl
157
+ or Meta are left to the browser, and so are arrow keys in a text field.
158
+
159
+ In a right-to-left context, ArrowLeft and ArrowRight swap. The component reads the
160
+ direction from the markup with `:dir(rtl)`: a `dir` attribute on the page or the
161
+ navigation is enough.
162
+
163
+ ### In a dialog or a popover
164
+
165
+ Escape is also a
166
+ [close request](https://html.spec.whatwg.org/multipage/interaction.html#close-requests):
167
+ it closes the topmost dialog or popover. While a submenu is open and focus is in the
168
+ navigation, the component cancels the key press, so each press closes one layer: the
169
+ submenu, then the dialog. With every submenu closed, or focus elsewhere, the browser
170
+ handles the key. An Escape another script already canceled is left alone.
171
+
172
+ ## API
173
+
174
+ | Member | Description |
175
+ | ----------------- | -------------------------------------------------------------------------------------------- |
176
+ | `init()` | Binds listeners. Runs on connect. Call `destroy()` first if already bound. |
177
+ | `destroy()` | Removes listeners. Runs on disconnect. The markup stays in its current state. |
178
+ | `open($button)` | Opens the submenu of `$button` and closes the other branches. Canceling its `beforetoggle` leaves every submenu as it is. |
179
+ | `close($button?)` | Closes the submenu of `$button`, or every open submenu. Focus inside a closing submenu moves to its button. |
180
+ | `toggle($button)` | Opens or closes the submenu of `$button`. |
181
+
182
+ Buttons are found inside the host with `button[aria-expanded][aria-controls]`, read from
183
+ the DOM on each interaction: adding or removing items needs no call. Closing the submenu
184
+ once a link is followed is up to you:
185
+
186
+ ```js
187
+ const $navigation = document.querySelector("cinq-disclosure-navigation");
188
+
189
+ $navigation.addEventListener("click", (event) => {
190
+ if (event.target.closest("a")) {
191
+ $navigation.close();
192
+ }
193
+ });
194
+ ```
195
+
196
+ ### Closing from outside
197
+
198
+ The open submenu closes when focus leaves the navigation, on Escape, and with
199
+ `data-hover` when the mouse leaves it. Safari does not focus a button on click, by
200
+ design: after opening a submenu with a click, clicking elsewhere moves no focus and the
201
+ submenu stays open. If you need it, close it yourself:
202
+
203
+ ```js
204
+ document.addEventListener("click", (event) => {
205
+ if (!$navigation.contains(event.target)) {
206
+ $navigation.close();
207
+ }
208
+ });
209
+ ```
210
+
211
+ ## Events
212
+
213
+ The component fires the platform's events, the way a popover does: a
214
+ [`ToggleEvent`](https://developer.mozilla.org/en-US/docs/Web/API/ToggleEvent) named
215
+ `beforetoggle`, then `toggle`, at each submenu. The same listener works for `<details>`,
216
+ popovers and `<dialog>`.
217
+
218
+ | Event | Fires | Cancelable |
219
+ | -------------- | ----------------------------------------------------------------------------------------- | ----------------------- |
220
+ | `beforetoggle` | **Before** the DOM changes. | When the submenu opens. |
221
+ | `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. |
222
+
223
+ `event.target` is the submenu, `event.source` its button, and `oldState` and `newState`
224
+ are `'open'` or `'closed'`. As on the platform, the events do not bubble: otherwise a
225
+ submenu's `toggle` would reach the `ontoggle` of a `<details>` or a `<dialog>` holding the
226
+ navigation. Listen on the submenu, or on the host in the capture phase.
227
+
228
+ ```js
229
+ $navigation.addEventListener(
230
+ "beforetoggle",
231
+ (event) => {
232
+ if ("open" === event.newState && "services" === event.source.id) {
233
+ event.preventDefault(); // Services stays closed
234
+ }
235
+ },
236
+ { capture: true },
237
+ );
238
+
239
+ $navigation.addEventListener(
240
+ "toggle",
241
+ (event) => console.log(event.target.id, event.newState),
242
+ { capture: true },
243
+ );
244
+ ```
245
+
246
+ Canceling an opening leaves every submenu as it is, the open one included: the other
247
+ branches close after `beforetoggle`. Closing cannot be canceled. A button that controls
248
+ several elements fires at each one, and opens none of them when one opening is canceled.
249
+ In TypeScript, the DOM library already types both events as `ToggleEvent`.
250
+
251
+ ## Browser support
252
+
253
+ Chrome and Edge 135, Firefox 136 and Safari 17: submenus are resolved with
254
+ [`ariaControlsElements`](https://developer.mozilla.org/en-US/docs/Web/API/Element/ariaControlsElements)
255
+ and events are [`ToggleEvent`](https://developer.mozilla.org/en-US/docs/Web/API/ToggleEvent)s.
256
+ `event.source` needs Chrome and Edge 140, Firefox 145 and Safari 26.5: older browsers
257
+ fire the events without it.
258
+
259
+ Find in page relies on `hidden="until-found"`: Chrome and Edge 102, Firefox 139,
260
+ Safari 26.2. Elsewhere it acts as `hidden`. The package is an ES module.
261
+
262
+ ## Migration
263
+
264
+ ### From `@19h47/disclosure-navigation` 5.x
265
+
266
+ The class becomes a custom element: the host replaces `$nav`. Markup inside it, styles,
267
+ methods and events do not change.
268
+
269
+ 1. **Wrap the navigation in the host**, and drop the instance.
270
+
271
+ ```diff
272
+ + <cinq-disclosure-navigation>
273
+ <nav aria-label="Main">…</nav>
274
+ + </cinq-disclosure-navigation>
275
+ ```
276
+
277
+ ```diff
278
+ - import DisclosureNavigation from "@19h47/disclosure-navigation";
279
+ -
280
+ - const disclosureNavigation = new DisclosureNavigation($nav);
281
+ -
282
+ - disclosureNavigation.init();
283
+ + import "@agencecinq/disclosure-navigation";
284
+ ```
285
+
286
+ 2. **Call the methods on the host.** `open()`, `close()` and `toggle()` take the same
287
+ arguments. `init()` and `destroy()` run on connect and disconnect. `$el` is gone.
288
+ 3. **Listen on the host.** `beforetoggle` and `toggle` still fire at the submenus,
289
+ without bubbling: listen on the host in the capture phase, instead of `$nav`.
290
+ 4. **Replace `useArrowKeys = false`** with `data-arrow-keys="false"` on the host.
291
+ 5. **Expect Escape outside the navigation** to close the open submenu, without
292
+ canceling the key.
293
+
294
+ ### From several `<cinq-disclosure-button>`
295
+
296
+ A navigation built from one `<cinq-disclosure-button>` per entry, with a script that
297
+ closes the others or opens on hover, becomes one host.
298
+
299
+ 1. **Remove the `<cinq-disclosure-button>` around each button**, and wrap the navigation
300
+ in `<cinq-disclosure-navigation>`. Add `data-hover` to open on hover.
301
+ 2. **Remove the script.** One branch open at a time, Escape, closing when focus leaves
302
+ and hover are built in.
303
+ 3. **Replace the `disclosure-button:*` listeners** with `beforetoggle` and `toggle` on
304
+ the host, in the capture phase.
305
+
306
+ ## Development
307
+
308
+ ```bash
309
+ pnpm --filter @agencecinq/disclosure-navigation build
310
+ pnpm --filter @agencecinq/disclosure-navigation test # Chromium, Firefox and WebKit
311
+ ```
@@ -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;CAkOzC"}
@@ -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 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": "1.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
  }