@vakwerk/navigation 0.0.0-stage → 0.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.
@@ -0,0 +1,324 @@
1
+ import { readConfig } from './config.js';
2
+ import { Overlay } from './overlay.js';
3
+ import { Submenus } from './submenus.js';
4
+ export const EVENTS = {
5
+ open: 'ui-navigation:open',
6
+ close: 'ui-navigation:close',
7
+ modechange: 'ui-navigation:modechange',
8
+ submenu: 'ui-navigation:submenu',
9
+ };
10
+ /** Attributes the controller sets on the host as state hooks for CSS. */
11
+ const HOST_HOOKS = ['data-enhanced', 'data-mode', 'data-submenus', 'data-justify', 'data-open'];
12
+ /**
13
+ * Owns the state of one enhanced navigation: configuration, mode, listeners and timers.
14
+ * Every listener uses `signal` and every timer goes through `setTimer`, so `destroy()` can undo
15
+ * all of it.
16
+ */
17
+ export class NavigationController {
18
+ host;
19
+ config;
20
+ mode;
21
+ #options;
22
+ #view;
23
+ #abort;
24
+ #timers = new Set();
25
+ /** Host hook values from before enhancement; `null` means the attribute was absent. */
26
+ #originalHooks = new Map();
27
+ #onDestroy;
28
+ #submenus;
29
+ #overlay;
30
+ #list;
31
+ /** Pending hover close timers per list item. */
32
+ #hoverTimers = new Map();
33
+ #query;
34
+ constructor(host, nav, list, options, onDestroy) {
35
+ this.host = host;
36
+ this.#options = options;
37
+ this.#onDestroy = onDestroy;
38
+ this.#view = host.ownerDocument.defaultView;
39
+ this.#abort = new this.#view.AbortController();
40
+ for (const name of HOST_HOOKS)
41
+ this.#originalHooks.set(name, host.getAttribute(name));
42
+ this.config = readConfig(host, options);
43
+ this.#query = this.#watchBreakpoint();
44
+ this.mode = this.#computeMode();
45
+ this.#submenus = new Submenus(nav, (change) => this.dispatch(EVENTS.submenu, { ...change, mode: this.mode }));
46
+ this.#list = list;
47
+ this.#overlay = new Overlay(nav, list);
48
+ this.#applyState();
49
+ this.#listen();
50
+ }
51
+ get signal() {
52
+ return this.#abort.signal;
53
+ }
54
+ /** Re-reads the configuration and re-applies state, keeping the options given to `init()`. */
55
+ update() {
56
+ const previousBreakpoint = this.config.breakpoint;
57
+ this.config = readConfig(this.host, this.#options);
58
+ if (this.config.breakpoint !== previousBreakpoint) {
59
+ this.#query.removeEventListener('change', this.#onQueryChange);
60
+ this.#query = this.#watchBreakpoint();
61
+ }
62
+ this.#setMode(this.#computeMode());
63
+ this.#applyState();
64
+ }
65
+ /** Opens the overlay panel; does nothing in inline mode or when it is open. */
66
+ open() {
67
+ if (this.mode !== 'overlay' || this.#overlay.isOpen)
68
+ return;
69
+ this.#overlay.dialog.showModal();
70
+ this.host.setAttribute('data-open', '');
71
+ this.#overlay.button.setAttribute('aria-expanded', 'true');
72
+ this.dispatch(EVENTS.open, { mode: this.mode });
73
+ }
74
+ /** Closes the overlay panel and returns focus to the menu button. */
75
+ close() {
76
+ if (!this.#overlay.isOpen)
77
+ return;
78
+ this.#overlay.dialog.close();
79
+ this.#markClosed(true);
80
+ }
81
+ /**
82
+ * Syncs state after the dialog closed. Idempotent: the dialog's `close` event comes as a task,
83
+ * after a close the controller started has already run this, and must not move focus again.
84
+ * Native `<dialog>` returns focus to whatever was focused before `showModal()`, which is not the
85
+ * menu button after `open()` from script, so focus is returned explicitly.
86
+ */
87
+ #markClosed(returnFocus) {
88
+ if (!this.host.hasAttribute('data-open'))
89
+ return;
90
+ this.host.removeAttribute('data-open');
91
+ this.#overlay.button.setAttribute('aria-expanded', 'false');
92
+ this.dispatch(EVENTS.close, { mode: this.mode });
93
+ if (returnFocus)
94
+ this.#overlay.button.focus();
95
+ }
96
+ /** Closes every open submenu without moving focus. */
97
+ closeSubmenus() {
98
+ this.#submenus.collapseAll();
99
+ }
100
+ dispatch(name, detail) {
101
+ this.host.dispatchEvent(new this.#view.CustomEvent(name, { bubbles: true, detail }));
102
+ }
103
+ setTimer(callback, ms) {
104
+ const id = this.#view.setTimeout(() => {
105
+ this.#timers.delete(id);
106
+ callback();
107
+ }, ms);
108
+ this.#timers.add(id);
109
+ return id;
110
+ }
111
+ clearTimer(id) {
112
+ this.#view.clearTimeout(id);
113
+ this.#timers.delete(id);
114
+ }
115
+ destroy() {
116
+ this.#abort.abort();
117
+ this.#query.removeEventListener('change', this.#onQueryChange);
118
+ for (const id of this.#timers)
119
+ this.#view.clearTimeout(id);
120
+ this.#timers.clear();
121
+ this.#submenus.destroy();
122
+ this.#overlay.detach();
123
+ for (const [name, value] of this.#originalHooks) {
124
+ if (value === null)
125
+ this.host.removeAttribute(name);
126
+ else
127
+ this.host.setAttribute(name, value);
128
+ }
129
+ this.#onDestroy();
130
+ }
131
+ #listen() {
132
+ const options = { signal: this.signal };
133
+ const doc = this.host.ownerDocument;
134
+ this.host.addEventListener('click', this.#onHostClick, options);
135
+ this.host.addEventListener('focusin', this.#onFocusIn, options);
136
+ this.host.addEventListener('focusout', this.#onFocusOut, options);
137
+ doc.addEventListener('click', this.#onDocumentClick, options);
138
+ doc.addEventListener('keydown', this.#onKeydown, options);
139
+ const { button, closeButton, dialog } = this.#overlay;
140
+ button.addEventListener('click', () => this.open(), options);
141
+ closeButton.addEventListener('click', () => this.close(), options);
142
+ // A click on the backdrop targets the dialog itself, but so does a click on the panel's empty
143
+ // area below a short menu; only a point outside the dialog's box is the backdrop.
144
+ dialog.addEventListener('click', (event) => event.target === dialog && this.#outsideDialog(event) && this.close(), options);
145
+ // Escape and the back gesture close the dialog natively.
146
+ dialog.addEventListener('close', () => this.#markClosed(true), options);
147
+ for (const item of this.#submenus.parentItems) {
148
+ item.addEventListener('pointerenter', (event) => this.#onPointerEnter(item, event), options);
149
+ item.addEventListener('pointerleave', (event) => this.#onPointerLeave(item, event), options);
150
+ }
151
+ }
152
+ /** Hover opens submenus only inline, only in hover mode and only for a mouse or pen. */
153
+ #hoverApplies(event) {
154
+ return this.mode === 'inline' && this.config.submenuVisibility === 'hover' && event.pointerType !== 'touch';
155
+ }
156
+ #onPointerEnter(item, event) {
157
+ if (!this.#hoverApplies(event))
158
+ return;
159
+ this.#cancelHoverClose(item);
160
+ if (this.#submenus.isOpen(item))
161
+ return;
162
+ this.#submenus.collapseSiblings(item);
163
+ this.#submenus.expand(item, 'hover');
164
+ }
165
+ /** Leaving a hover-opened submenu closes it after `hover-close-delay`, so the pointer can cross a gap. */
166
+ #onPointerLeave(item, event) {
167
+ if (!this.#hoverApplies(event) || this.#submenus.openedBy.get(item) !== 'hover')
168
+ return;
169
+ this.#cancelHoverClose(item);
170
+ const id = this.setTimer(() => {
171
+ this.#hoverTimers.delete(item);
172
+ if (this.#submenus.openedBy.get(item) === 'hover')
173
+ this.#submenus.collapse(item);
174
+ }, this.config.hoverCloseDelay);
175
+ this.#hoverTimers.set(item, id);
176
+ }
177
+ #cancelHoverClose(item) {
178
+ const id = this.#hoverTimers.get(item);
179
+ if (id === undefined)
180
+ return;
181
+ this.clearTimer(id);
182
+ this.#hoverTimers.delete(item);
183
+ }
184
+ #cancelAllHoverCloses() {
185
+ for (const item of [...this.#hoverTimers.keys()])
186
+ this.#cancelHoverClose(item);
187
+ }
188
+ #onHostClick = (event) => {
189
+ const target = event.target;
190
+ const toggle = target.closest('.ui-nav__toggle');
191
+ const parent = toggle ? this.#submenus.parentOfToggle(toggle) : undefined;
192
+ if (toggle && parent) {
193
+ this.#onToggleClick(parent, toggle);
194
+ return;
195
+ }
196
+ // Activating a link inside a dropdown closes the dropdowns.
197
+ const link = target.closest('a');
198
+ if (link && this.mode === 'overlay' && this.#overlay.dialog.contains(link)) {
199
+ this.close();
200
+ return;
201
+ }
202
+ const item = link?.closest('li');
203
+ if (this.mode === 'inline' && link && item && this.#submenus.topLevelItemOf(link) !== item) {
204
+ this.closeSubmenus();
205
+ }
206
+ };
207
+ #onToggleClick(parent, toggle) {
208
+ // Safari does not focus a button on click (WebKit #22261); focus return and focusin/focusout need it.
209
+ if (this.host.ownerDocument.activeElement !== toggle)
210
+ toggle.focus();
211
+ if (this.#submenus.openedBy.get(parent) === 'hover') {
212
+ this.#cancelHoverClose(parent);
213
+ this.#submenus.pin(parent);
214
+ return;
215
+ }
216
+ if (this.#submenus.isOpen(parent)) {
217
+ this.#submenus.collapse(parent);
218
+ return;
219
+ }
220
+ if (this.mode === 'inline')
221
+ this.#submenus.collapseSiblings(parent);
222
+ this.#submenus.expand(parent, 'click');
223
+ }
224
+ #onDocumentClick = (event) => {
225
+ if (this.mode === 'inline' && !this.host.contains(event.target))
226
+ this.closeSubmenus();
227
+ };
228
+ /** Escape closes the innermost open submenu around focus, or all of them when there is none. */
229
+ #onKeydown = (event) => {
230
+ // An Escape another component already handled (a modal of its own, say) is not ours.
231
+ if (event.defaultPrevented || this.mode !== 'inline' || event.key !== 'Escape')
232
+ return;
233
+ const active = this.host.ownerDocument.activeElement;
234
+ const innermost = active && this.host.contains(active) ? this.#submenus.innermostOpen(active) : undefined;
235
+ if (!innermost) {
236
+ this.closeSubmenus();
237
+ return;
238
+ }
239
+ this.#submenus.collapse(innermost);
240
+ this.#submenus.toggleOf(innermost)?.focus();
241
+ };
242
+ /** Focus on a top-level item closes the submenus of the other top-level items. */
243
+ #onFocusIn = (event) => {
244
+ if (this.mode !== 'inline')
245
+ return;
246
+ const item = this.#submenus.topLevelItemOf(event.target);
247
+ if (item)
248
+ this.#submenus.collapseSiblings(item);
249
+ };
250
+ /** Focus leaving the navigation closes everything; a `null` target (Safari, on click) does not count. */
251
+ #onFocusOut = (event) => {
252
+ const next = event.relatedTarget;
253
+ if (this.mode === 'inline' && next && !this.host.contains(next))
254
+ this.closeSubmenus();
255
+ };
256
+ #onQueryChange = () => {
257
+ this.#setMode(this.#computeMode());
258
+ };
259
+ #watchBreakpoint() {
260
+ const query = this.#view.matchMedia(`(min-width: ${this.config.breakpoint}px)`);
261
+ query.addEventListener('change', this.#onQueryChange, { signal: this.signal });
262
+ return query;
263
+ }
264
+ #computeMode() {
265
+ if (this.config.overlay === 'always')
266
+ return 'overlay';
267
+ if (this.config.overlay === 'never')
268
+ return 'inline';
269
+ return this.#query.matches ? 'inline' : 'overlay';
270
+ }
271
+ #outsideDialog(event) {
272
+ const box = this.#overlay.dialog.getBoundingClientRect();
273
+ return event.clientX < box.left || event.clientX > box.right || event.clientY < box.top || event.clientY > box.bottom;
274
+ }
275
+ /**
276
+ * A mode switch closes all submenus and an open overlay. When focus was in the navigation it
277
+ * goes to the menu button in overlay mode, and in inline mode back to the same element if it
278
+ * is still in the menu, otherwise to the first item.
279
+ */
280
+ #setMode(mode) {
281
+ if (mode === this.mode)
282
+ return;
283
+ const focused = this.host.ownerDocument.activeElement;
284
+ const hadFocus = !!focused && this.host.contains(focused);
285
+ this.#cancelAllHoverCloses();
286
+ this.closeSubmenus();
287
+ if (this.#overlay.isOpen) {
288
+ this.#overlay.dialog.close();
289
+ this.#markClosed(false);
290
+ }
291
+ this.mode = mode;
292
+ this.#applyState();
293
+ this.dispatch(EVENTS.modechange, { mode });
294
+ if (!hadFocus)
295
+ return;
296
+ if (mode === 'overlay') {
297
+ this.#overlay.button.focus();
298
+ return;
299
+ }
300
+ // A nested item is hidden once its submenu closed and cannot keep focus; then take the first item.
301
+ const inMenu = focused.isConnected && this.#list.contains(focused);
302
+ if (inMenu)
303
+ focused.focus();
304
+ if (!inMenu || this.host.ownerDocument.activeElement !== focused) {
305
+ this.#list.querySelector('a[href], button')?.focus();
306
+ }
307
+ }
308
+ /** Writes the host hooks and adds or removes the submenu toggles to match config and mode. */
309
+ #applyState() {
310
+ this.host.setAttribute('data-enhanced', '');
311
+ this.host.setAttribute('data-mode', this.mode);
312
+ this.host.setAttribute('data-submenus', this.config.submenuVisibility);
313
+ this.host.setAttribute('data-justify', this.config.justify);
314
+ this.#overlay.setLabels(this.config.labelOpen, this.config.labelClose);
315
+ this.#overlay.moveList(this.mode === 'overlay');
316
+ this.#submenus.syncToggles(this.#togglesPresent(), this.config.labelSubmenu);
317
+ }
318
+ /** Toggles exist only where submenus can be opened and closed; otherwise they are always shown. */
319
+ #togglesPresent() {
320
+ const { submenuVisibility, overlaySubmenus } = this.config;
321
+ return submenuVisibility !== 'always' && !(this.mode === 'overlay' && overlaySubmenus === 'expanded');
322
+ }
323
+ }
324
+ //# sourceMappingURL=controller.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"controller.js","sourceRoot":"","sources":["../src/controller.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAsE,MAAM,aAAa,CAAC;AAC7G,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AACvC,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAEzC,MAAM,CAAC,MAAM,MAAM,GAAG;IACpB,IAAI,EAAE,oBAAoB;IAC1B,KAAK,EAAE,qBAAqB;IAC5B,UAAU,EAAE,0BAA0B;IACtC,OAAO,EAAE,uBAAuB;CACxB,CAAC;AAEX,yEAAyE;AACzE,MAAM,UAAU,GAAG,CAAC,eAAe,EAAE,WAAW,EAAE,eAAe,EAAE,cAAc,EAAE,WAAW,CAAU,CAAC;AAEzG;;;;GAIG;AACH,MAAM,OAAO,oBAAoB;IACtB,IAAI,CAAc;IAC3B,MAAM,CAAmB;IACzB,IAAI,CAAiB;IAEZ,QAAQ,CAAoB;IAC5B,KAAK,CAA6B;IAClC,MAAM,CAAkB;IACxB,OAAO,GAAG,IAAI,GAAG,EAAU,CAAC;IACrC,uFAAuF;IAC9E,cAAc,GAAG,IAAI,GAAG,EAAyB,CAAC;IAClD,UAAU,CAAa;IACvB,SAAS,CAAW;IACpB,QAAQ,CAAU;IAClB,KAAK,CAAmB;IACjC,gDAAgD;IACvC,YAAY,GAAG,IAAI,GAAG,EAAyB,CAAC;IACzD,MAAM,CAAiB;IAEvB,YACE,IAAiB,EACjB,GAAgB,EAChB,IAAsB,EACtB,OAA0B,EAC1B,SAAqB;QAErB,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;QACjB,IAAI,CAAC,QAAQ,GAAG,OAAO,CAAC;QACxB,IAAI,CAAC,UAAU,GAAG,SAAS,CAAC;QAC5B,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC,aAAa,CAAC,WAAyC,CAAC;QAC1E,IAAI,CAAC,MAAM,GAAG,IAAI,IAAI,CAAC,KAAK,CAAC,eAAe,EAAE,CAAC;QAC/C,KAAK,MAAM,IAAI,IAAI,UAAU;YAAE,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC,IAAI,EAAE,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC,CAAC,CAAC;QAEtF,IAAI,CAAC,MAAM,GAAG,UAAU,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;QACxC,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC,gBAAgB,EAAE,CAAC;QACtC,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC,YAAY,EAAE,CAAC;QAChC,IAAI,CAAC,SAAS,GAAG,IAAI,QAAQ,CAAC,GAAG,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,OAAO,EAAE,EAAE,GAAG,MAAM,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC;QAC9G,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC;QAClB,IAAI,CAAC,QAAQ,GAAG,IAAI,OAAO,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;QACvC,IAAI,CAAC,WAAW,EAAE,CAAC;QACnB,IAAI,CAAC,OAAO,EAAE,CAAC;IACjB,CAAC;IAED,IAAI,MAAM;QACR,OAAO,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC;IAC5B,CAAC;IAED,8FAA8F;IAC9F,MAAM;QACJ,MAAM,kBAAkB,GAAG,IAAI,CAAC,MAAM,CAAC,UAAU,CAAC;QAClD,IAAI,CAAC,MAAM,GAAG,UAAU,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,QAAQ,CAAC,CAAC;QACnD,IAAI,IAAI,CAAC,MAAM,CAAC,UAAU,KAAK,kBAAkB,EAAE,CAAC;YAClD,IAAI,CAAC,MAAM,CAAC,mBAAmB,CAAC,QAAQ,EAAE,IAAI,CAAC,cAAc,CAAC,CAAC;YAC/D,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC,gBAAgB,EAAE,CAAC;QACxC,CAAC;QACD,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,YAAY,EAAE,CAAC,CAAC;QACnC,IAAI,CAAC,WAAW,EAAE,CAAC;IACrB,CAAC;IAED,+EAA+E;IAC/E,IAAI;QACF,IAAI,IAAI,CAAC,IAAI,KAAK,SAAS,IAAI,IAAI,CAAC,QAAQ,CAAC,MAAM;YAAE,OAAO;QAC5D,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,SAAS,EAAE,CAAC;QACjC,IAAI,CAAC,IAAI,CAAC,YAAY,CAAC,WAAW,EAAE,EAAE,CAAC,CAAC;QACxC,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,YAAY,CAAC,eAAe,EAAE,MAAM,CAAC,CAAC;QAC3D,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,IAAI,EAAE,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC;IAClD,CAAC;IAED,qEAAqE;IACrE,KAAK;QACH,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,MAAM;YAAE,OAAO;QAClC,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC;QAC7B,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,CAAC;IACzB,CAAC;IAED;;;;;OAKG;IACH,WAAW,CAAC,WAAoB;QAC9B,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,YAAY,CAAC,WAAW,CAAC;YAAE,OAAO;QACjD,IAAI,CAAC,IAAI,CAAC,eAAe,CAAC,WAAW,CAAC,CAAC;QACvC,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,YAAY,CAAC,eAAe,EAAE,OAAO,CAAC,CAAC;QAC5D,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,KAAK,EAAE,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC;QACjD,IAAI,WAAW;YAAE,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC;IAChD,CAAC;IAED,sDAAsD;IACtD,aAAa;QACX,IAAI,CAAC,SAAS,CAAC,WAAW,EAAE,CAAC;IAC/B,CAAC;IAED,QAAQ,CAAC,IAAY,EAAE,MAAc;QACnC,IAAI,CAAC,IAAI,CAAC,aAAa,CAAC,IAAI,IAAI,CAAC,KAAK,CAAC,WAAW,CAAC,IAAI,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC,CAAC,CAAC;IACvF,CAAC;IAED,QAAQ,CAAC,QAAoB,EAAE,EAAU;QACvC,MAAM,EAAE,GAAG,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC,GAAG,EAAE;YACpC,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;YACxB,QAAQ,EAAE,CAAC;QACb,CAAC,EAAE,EAAE,CAAC,CAAC;QACP,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;QACrB,OAAO,EAAE,CAAC;IACZ,CAAC;IAED,UAAU,CAAC,EAAU;QACnB,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,EAAE,CAAC,CAAC;QAC5B,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;IAC1B,CAAC;IAED,OAAO;QACL,IAAI,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC;QACpB,IAAI,CAAC,MAAM,CAAC,mBAAmB,CAAC,QAAQ,EAAE,IAAI,CAAC,cAAc,CAAC,CAAC;QAC/D,KAAK,MAAM,EAAE,IAAI,IAAI,CAAC,OAAO;YAAE,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,EAAE,CAAC,CAAC;QAC3D,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,CAAC;QACrB,IAAI,CAAC,SAAS,CAAC,OAAO,EAAE,CAAC;QACzB,IAAI,CAAC,QAAQ,CAAC,MAAM,EAAE,CAAC;QACvB,KAAK,MAAM,CAAC,IAAI,EAAE,KAAK,CAAC,IAAI,IAAI,CAAC,cAAc,EAAE,CAAC;YAChD,IAAI,KAAK,KAAK,IAAI;gBAAE,IAAI,CAAC,IAAI,CAAC,eAAe,CAAC,IAAI,CAAC,CAAC;;gBAC/C,IAAI,CAAC,IAAI,CAAC,YAAY,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;QAC3C,CAAC;QACD,IAAI,CAAC,UAAU,EAAE,CAAC;IACpB,CAAC;IAED,OAAO;QACL,MAAM,OAAO,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,CAAC;QACxC,MAAM,GAAG,GAAG,IAAI,CAAC,IAAI,CAAC,aAAa,CAAC;QACpC,IAAI,CAAC,IAAI,CAAC,gBAAgB,CAAC,OAAO,EAAE,IAAI,CAAC,YAAY,EAAE,OAAO,CAAC,CAAC;QAChE,IAAI,CAAC,IAAI,CAAC,gBAAgB,CAAC,SAAS,EAAE,IAAI,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC;QAChE,IAAI,CAAC,IAAI,CAAC,gBAAgB,CAAC,UAAU,EAAE,IAAI,CAAC,WAAW,EAAE,OAAO,CAAC,CAAC;QAClE,GAAG,CAAC,gBAAgB,CAAC,OAAO,EAAE,IAAI,CAAC,gBAAgB,EAAE,OAAO,CAAC,CAAC;QAC9D,GAAG,CAAC,gBAAgB,CAAC,SAAS,EAAE,IAAI,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC;QAC1D,MAAM,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,GAAG,IAAI,CAAC,QAAQ,CAAC;QACtD,MAAM,CAAC,gBAAgB,CAAC,OAAO,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,IAAI,EAAE,EAAE,OAAO,CAAC,CAAC;QAC7D,WAAW,CAAC,gBAAgB,CAAC,OAAO,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,KAAK,EAAE,EAAE,OAAO,CAAC,CAAC;QACnE,8FAA8F;QAC9F,kFAAkF;QAClF,MAAM,CAAC,gBAAgB,CACrB,OAAO,EACP,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,MAAM,KAAK,MAAM,IAAI,IAAI,CAAC,cAAc,CAAC,KAAmB,CAAC,IAAI,IAAI,CAAC,KAAK,EAAE,EAC9F,OAAO,CACR,CAAC;QACF,yDAAyD;QACzD,MAAM,CAAC,gBAAgB,CAAC,OAAO,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,EAAE,OAAO,CAAC,CAAC;QACxE,KAAK,MAAM,IAAI,IAAI,IAAI,CAAC,SAAS,CAAC,WAAW,EAAE,CAAC;YAC9C,IAAI,CAAC,gBAAgB,CAAC,cAAc,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,IAAI,CAAC,eAAe,CAAC,IAAI,EAAE,KAAK,CAAC,EAAE,OAAO,CAAC,CAAC;YAC7F,IAAI,CAAC,gBAAgB,CAAC,cAAc,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,IAAI,CAAC,eAAe,CAAC,IAAI,EAAE,KAAK,CAAC,EAAE,OAAO,CAAC,CAAC;QAC/F,CAAC;IACH,CAAC;IAED,wFAAwF;IACxF,aAAa,CAAC,KAAmB;QAC/B,OAAO,IAAI,CAAC,IAAI,KAAK,QAAQ,IAAI,IAAI,CAAC,MAAM,CAAC,iBAAiB,KAAK,OAAO,IAAI,KAAK,CAAC,WAAW,KAAK,OAAO,CAAC;IAC9G,CAAC;IAED,eAAe,CAAC,IAAmB,EAAE,KAAmB;QACtD,IAAI,CAAC,IAAI,CAAC,aAAa,CAAC,KAAK,CAAC;YAAE,OAAO;QACvC,IAAI,CAAC,iBAAiB,CAAC,IAAI,CAAC,CAAC;QAC7B,IAAI,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,IAAI,CAAC;YAAE,OAAO;QACxC,IAAI,CAAC,SAAS,CAAC,gBAAgB,CAAC,IAAI,CAAC,CAAC;QACtC,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;IACvC,CAAC;IAED,0GAA0G;IAC1G,eAAe,CAAC,IAAmB,EAAE,KAAmB;QACtD,IAAI,CAAC,IAAI,CAAC,aAAa,CAAC,KAAK,CAAC,IAAI,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,KAAK,OAAO;YAAE,OAAO;QACxF,IAAI,CAAC,iBAAiB,CAAC,IAAI,CAAC,CAAC;QAC7B,MAAM,EAAE,GAAG,IAAI,CAAC,QAAQ,CAAC,GAAG,EAAE;YAC5B,IAAI,CAAC,YAAY,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;YAC/B,IAAI,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,KAAK,OAAO;gBAAE,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;QACnF,CAAC,EAAE,IAAI,CAAC,MAAM,CAAC,eAAe,CAAC,CAAC;QAChC,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;IAClC,CAAC;IAED,iBAAiB,CAAC,IAAmB;QACnC,MAAM,EAAE,GAAG,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QACvC,IAAI,EAAE,KAAK,SAAS;YAAE,OAAO;QAC7B,IAAI,CAAC,UAAU,CAAC,EAAE,CAAC,CAAC;QACpB,IAAI,CAAC,YAAY,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;IACjC,CAAC;IAED,qBAAqB;QACnB,KAAK,MAAM,IAAI,IAAI,CAAC,GAAG,IAAI,CAAC,YAAY,CAAC,IAAI,EAAE,CAAC;YAAE,IAAI,CAAC,iBAAiB,CAAC,IAAI,CAAC,CAAC;IACjF,CAAC;IAEQ,YAAY,GAAG,CAAC,KAAY,EAAQ,EAAE;QAC7C,MAAM,MAAM,GAAG,KAAK,CAAC,MAAiB,CAAC;QACvC,MAAM,MAAM,GAAG,MAAM,CAAC,OAAO,CAAC,iBAAiB,CAAC,CAAC;QACjD,MAAM,MAAM,GAAG,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,cAAc,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;QAC1E,IAAI,MAAM,IAAI,MAAM,EAAE,CAAC;YACrB,IAAI,CAAC,cAAc,CAAC,MAAM,EAAE,MAA2B,CAAC,CAAC;YACzD,OAAO;QACT,CAAC;QACD,4DAA4D;QAC5D,MAAM,IAAI,GAAG,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;QACjC,IAAI,IAAI,IAAI,IAAI,CAAC,IAAI,KAAK,SAAS,IAAI,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC;YAC3E,IAAI,CAAC,KAAK,EAAE,CAAC;YACb,OAAO;QACT,CAAC;QACD,MAAM,IAAI,GAAG,IAAI,EAAE,OAAO,CAAC,IAAI,CAAC,CAAC;QACjC,IAAI,IAAI,CAAC,IAAI,KAAK,QAAQ,IAAI,IAAI,IAAI,IAAI,IAAI,IAAI,CAAC,SAAS,CAAC,cAAc,CAAC,IAAI,CAAC,KAAK,IAAI,EAAE,CAAC;YAC3F,IAAI,CAAC,aAAa,EAAE,CAAC;QACvB,CAAC;IACH,CAAC,CAAC;IAEF,cAAc,CAAC,MAAqB,EAAE,MAAyB;QAC7D,sGAAsG;QACtG,IAAI,IAAI,CAAC,IAAI,CAAC,aAAa,CAAC,aAAa,KAAK,MAAM;YAAE,MAAM,CAAC,KAAK,EAAE,CAAC;QACrE,IAAI,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,GAAG,CAAC,MAAM,CAAC,KAAK,OAAO,EAAE,CAAC;YACpD,IAAI,CAAC,iBAAiB,CAAC,MAAM,CAAC,CAAC;YAC/B,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;YAC3B,OAAO;QACT,CAAC;QACD,IAAI,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,CAAC;YAClC,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;YAChC,OAAO;QACT,CAAC;QACD,IAAI,IAAI,CAAC,IAAI,KAAK,QAAQ;YAAE,IAAI,CAAC,SAAS,CAAC,gBAAgB,CAAC,MAAM,CAAC,CAAC;QACpE,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACzC,CAAC;IAEQ,gBAAgB,GAAG,CAAC,KAAY,EAAQ,EAAE;QACjD,IAAI,IAAI,CAAC,IAAI,KAAK,QAAQ,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,MAAc,CAAC;YAAE,IAAI,CAAC,aAAa,EAAE,CAAC;IAChG,CAAC,CAAC;IAEF,gGAAgG;IACvF,UAAU,GAAG,CAAC,KAAoB,EAAQ,EAAE;QACnD,qFAAqF;QACrF,IAAI,KAAK,CAAC,gBAAgB,IAAI,IAAI,CAAC,IAAI,KAAK,QAAQ,IAAI,KAAK,CAAC,GAAG,KAAK,QAAQ;YAAE,OAAO;QACvF,MAAM,MAAM,GAAG,IAAI,CAAC,IAAI,CAAC,aAAa,CAAC,aAAa,CAAC;QACrD,MAAM,SAAS,GAAG,MAAM,IAAI,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,aAAa,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;QAC1G,IAAI,CAAC,SAAS,EAAE,CAAC;YACf,IAAI,CAAC,aAAa,EAAE,CAAC;YACrB,OAAO;QACT,CAAC;QACD,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC;QACnC,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,SAAS,CAAC,EAAE,KAAK,EAAE,CAAC;IAC9C,CAAC,CAAC;IAEF,kFAAkF;IACzE,UAAU,GAAG,CAAC,KAAiB,EAAQ,EAAE;QAChD,IAAI,IAAI,CAAC,IAAI,KAAK,QAAQ;YAAE,OAAO;QACnC,MAAM,IAAI,GAAG,IAAI,CAAC,SAAS,CAAC,cAAc,CAAC,KAAK,CAAC,MAAiB,CAAC,CAAC;QACpE,IAAI,IAAI;YAAE,IAAI,CAAC,SAAS,CAAC,gBAAgB,CAAC,IAAI,CAAC,CAAC;IAClD,CAAC,CAAC;IAEF,yGAAyG;IAChG,WAAW,GAAG,CAAC,KAAiB,EAAQ,EAAE;QACjD,MAAM,IAAI,GAAG,KAAK,CAAC,aAA4B,CAAC;QAChD,IAAI,IAAI,CAAC,IAAI,KAAK,QAAQ,IAAI,IAAI,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC;YAAE,IAAI,CAAC,aAAa,EAAE,CAAC;IACxF,CAAC,CAAC;IAEO,cAAc,GAAG,GAAS,EAAE;QACnC,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,YAAY,EAAE,CAAC,CAAC;IACrC,CAAC,CAAC;IAEF,gBAAgB;QACd,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC,eAAe,IAAI,CAAC,MAAM,CAAC,UAAU,KAAK,CAAC,CAAC;QAChF,KAAK,CAAC,gBAAgB,CAAC,QAAQ,EAAE,IAAI,CAAC,cAAc,EAAE,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,CAAC,CAAC;QAC/E,OAAO,KAAK,CAAC;IACf,CAAC;IAED,YAAY;QACV,IAAI,IAAI,CAAC,MAAM,CAAC,OAAO,KAAK,QAAQ;YAAE,OAAO,SAAS,CAAC;QACvD,IAAI,IAAI,CAAC,MAAM,CAAC,OAAO,KAAK,OAAO;YAAE,OAAO,QAAQ,CAAC;QACrD,OAAO,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,SAAS,CAAC;IACpD,CAAC;IAED,cAAc,CAAC,KAAiB;QAC9B,MAAM,GAAG,GAAG,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,qBAAqB,EAAE,CAAC;QACzD,OAAO,KAAK,CAAC,OAAO,GAAG,GAAG,CAAC,IAAI,IAAI,KAAK,CAAC,OAAO,GAAG,GAAG,CAAC,KAAK,IAAI,KAAK,CAAC,OAAO,GAAG,GAAG,CAAC,GAAG,IAAI,KAAK,CAAC,OAAO,GAAG,GAAG,CAAC,MAAM,CAAC;IACxH,CAAC;IAED;;;;OAIG;IACH,QAAQ,CAAC,IAAoB;QAC3B,IAAI,IAAI,KAAK,IAAI,CAAC,IAAI;YAAE,OAAO;QAC/B,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,CAAC,aAAa,CAAC,aAAmC,CAAC;QAC5E,MAAM,QAAQ,GAAG,CAAC,CAAC,OAAO,IAAI,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC;QAE1D,IAAI,CAAC,qBAAqB,EAAE,CAAC;QAC7B,IAAI,CAAC,aAAa,EAAE,CAAC;QACrB,IAAI,IAAI,CAAC,QAAQ,CAAC,MAAM,EAAE,CAAC;YACzB,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC;YAC7B,IAAI,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC;QAC1B,CAAC;QACD,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;QACjB,IAAI,CAAC,WAAW,EAAE,CAAC;QACnB,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,UAAU,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC;QAE3C,IAAI,CAAC,QAAQ;YAAE,OAAO;QACtB,IAAI,IAAI,KAAK,SAAS,EAAE,CAAC;YACvB,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC;YAC7B,OAAO;QACT,CAAC;QACD,mGAAmG;QACnG,MAAM,MAAM,GAAG,OAAO,CAAC,WAAW,IAAI,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC;QACnE,IAAI,MAAM;YAAE,OAAO,CAAC,KAAK,EAAE,CAAC;QAC5B,IAAI,CAAC,MAAM,IAAI,IAAI,CAAC,IAAI,CAAC,aAAa,CAAC,aAAa,KAAK,OAAO,EAAE,CAAC;YACjE,IAAI,CAAC,KAAK,CAAC,aAAa,CAAc,iBAAiB,CAAC,EAAE,KAAK,EAAE,CAAC;QACpE,CAAC;IACH,CAAC;IAED,8FAA8F;IAC9F,WAAW;QACT,IAAI,CAAC,IAAI,CAAC,YAAY,CAAC,eAAe,EAAE,EAAE,CAAC,CAAC;QAC5C,IAAI,CAAC,IAAI,CAAC,YAAY,CAAC,WAAW,EAAE,IAAI,CAAC,IAAI,CAAC,CAAC;QAC/C,IAAI,CAAC,IAAI,CAAC,YAAY,CAAC,eAAe,EAAE,IAAI,CAAC,MAAM,CAAC,iBAAiB,CAAC,CAAC;QACvE,IAAI,CAAC,IAAI,CAAC,YAAY,CAAC,cAAc,EAAE,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;QAC5D,IAAI,CAAC,QAAQ,CAAC,SAAS,CAAC,IAAI,CAAC,MAAM,CAAC,SAAS,EAAE,IAAI,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC;QACvE,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,KAAK,SAAS,CAAC,CAAC;QAChD,IAAI,CAAC,SAAS,CAAC,WAAW,CAAC,IAAI,CAAC,eAAe,EAAE,EAAE,IAAI,CAAC,MAAM,CAAC,YAAY,CAAC,CAAC;IAC/E,CAAC;IAED,mGAAmG;IACnG,eAAe;QACb,MAAM,EAAE,iBAAiB,EAAE,eAAe,EAAE,GAAG,IAAI,CAAC,MAAM,CAAC;QAC3D,OAAO,iBAAiB,KAAK,QAAQ,IAAI,CAAC,CAAC,IAAI,CAAC,IAAI,KAAK,SAAS,IAAI,eAAe,KAAK,UAAU,CAAC,CAAC;IACxG,CAAC;CACF","sourcesContent":["import { readConfig, type NavigationConfig, type NavigationMode, type NavigationOptions } from './config.js';\nimport { Overlay } from './overlay.js';\nimport { Submenus } from './submenus.js';\n\nexport const EVENTS = {\n open: 'ui-navigation:open',\n close: 'ui-navigation:close',\n modechange: 'ui-navigation:modechange',\n submenu: 'ui-navigation:submenu',\n} as const;\n\n/** Attributes the controller sets on the host as state hooks for CSS. */\nconst HOST_HOOKS = ['data-enhanced', 'data-mode', 'data-submenus', 'data-justify', 'data-open'] as const;\n\n/**\n * Owns the state of one enhanced navigation: configuration, mode, listeners and timers.\n * Every listener uses `signal` and every timer goes through `setTimer`, so `destroy()` can undo\n * all of it.\n */\nexport class NavigationController {\n readonly host: HTMLElement;\n config: NavigationConfig;\n mode: NavigationMode;\n\n readonly #options: NavigationOptions;\n readonly #view: Window & typeof globalThis;\n readonly #abort: AbortController;\n readonly #timers = new Set<number>();\n /** Host hook values from before enhancement; `null` means the attribute was absent. */\n readonly #originalHooks = new Map<string, string | null>();\n readonly #onDestroy: () => void;\n readonly #submenus: Submenus;\n readonly #overlay: Overlay;\n readonly #list: HTMLUListElement;\n /** Pending hover close timers per list item. */\n readonly #hoverTimers = new Map<HTMLLIElement, number>();\n #query: MediaQueryList;\n\n constructor(\n host: HTMLElement,\n nav: HTMLElement,\n list: HTMLUListElement,\n options: NavigationOptions,\n onDestroy: () => void,\n ) {\n this.host = host;\n this.#options = options;\n this.#onDestroy = onDestroy;\n this.#view = host.ownerDocument.defaultView as Window & typeof globalThis;\n this.#abort = new this.#view.AbortController();\n for (const name of HOST_HOOKS) this.#originalHooks.set(name, host.getAttribute(name));\n\n this.config = readConfig(host, options);\n this.#query = this.#watchBreakpoint();\n this.mode = this.#computeMode();\n this.#submenus = new Submenus(nav, (change) => this.dispatch(EVENTS.submenu, { ...change, mode: this.mode }));\n this.#list = list;\n this.#overlay = new Overlay(nav, list);\n this.#applyState();\n this.#listen();\n }\n\n get signal(): AbortSignal {\n return this.#abort.signal;\n }\n\n /** Re-reads the configuration and re-applies state, keeping the options given to `init()`. */\n update(): void {\n const previousBreakpoint = this.config.breakpoint;\n this.config = readConfig(this.host, this.#options);\n if (this.config.breakpoint !== previousBreakpoint) {\n this.#query.removeEventListener('change', this.#onQueryChange);\n this.#query = this.#watchBreakpoint();\n }\n this.#setMode(this.#computeMode());\n this.#applyState();\n }\n\n /** Opens the overlay panel; does nothing in inline mode or when it is open. */\n open(): void {\n if (this.mode !== 'overlay' || this.#overlay.isOpen) return;\n this.#overlay.dialog.showModal();\n this.host.setAttribute('data-open', '');\n this.#overlay.button.setAttribute('aria-expanded', 'true');\n this.dispatch(EVENTS.open, { mode: this.mode });\n }\n\n /** Closes the overlay panel and returns focus to the menu button. */\n close(): void {\n if (!this.#overlay.isOpen) return;\n this.#overlay.dialog.close();\n this.#markClosed(true);\n }\n\n /**\n * Syncs state after the dialog closed. Idempotent: the dialog's `close` event comes as a task,\n * after a close the controller started has already run this, and must not move focus again.\n * Native `<dialog>` returns focus to whatever was focused before `showModal()`, which is not the\n * menu button after `open()` from script, so focus is returned explicitly.\n */\n #markClosed(returnFocus: boolean): void {\n if (!this.host.hasAttribute('data-open')) return;\n this.host.removeAttribute('data-open');\n this.#overlay.button.setAttribute('aria-expanded', 'false');\n this.dispatch(EVENTS.close, { mode: this.mode });\n if (returnFocus) this.#overlay.button.focus();\n }\n\n /** Closes every open submenu without moving focus. */\n closeSubmenus(): void {\n this.#submenus.collapseAll();\n }\n\n dispatch(name: string, detail: object): void {\n this.host.dispatchEvent(new this.#view.CustomEvent(name, { bubbles: true, detail }));\n }\n\n setTimer(callback: () => void, ms: number): number {\n const id = this.#view.setTimeout(() => {\n this.#timers.delete(id);\n callback();\n }, ms);\n this.#timers.add(id);\n return id;\n }\n\n clearTimer(id: number): void {\n this.#view.clearTimeout(id);\n this.#timers.delete(id);\n }\n\n destroy(): void {\n this.#abort.abort();\n this.#query.removeEventListener('change', this.#onQueryChange);\n for (const id of this.#timers) this.#view.clearTimeout(id);\n this.#timers.clear();\n this.#submenus.destroy();\n this.#overlay.detach();\n for (const [name, value] of this.#originalHooks) {\n if (value === null) this.host.removeAttribute(name);\n else this.host.setAttribute(name, value);\n }\n this.#onDestroy();\n }\n\n #listen(): void {\n const options = { signal: this.signal };\n const doc = this.host.ownerDocument;\n this.host.addEventListener('click', this.#onHostClick, options);\n this.host.addEventListener('focusin', this.#onFocusIn, options);\n this.host.addEventListener('focusout', this.#onFocusOut, options);\n doc.addEventListener('click', this.#onDocumentClick, options);\n doc.addEventListener('keydown', this.#onKeydown, options);\n const { button, closeButton, dialog } = this.#overlay;\n button.addEventListener('click', () => this.open(), options);\n closeButton.addEventListener('click', () => this.close(), options);\n // A click on the backdrop targets the dialog itself, but so does a click on the panel's empty\n // area below a short menu; only a point outside the dialog's box is the backdrop.\n dialog.addEventListener(\n 'click',\n (event) => event.target === dialog && this.#outsideDialog(event as MouseEvent) && this.close(),\n options,\n );\n // Escape and the back gesture close the dialog natively.\n dialog.addEventListener('close', () => this.#markClosed(true), options);\n for (const item of this.#submenus.parentItems) {\n item.addEventListener('pointerenter', (event) => this.#onPointerEnter(item, event), options);\n item.addEventListener('pointerleave', (event) => this.#onPointerLeave(item, event), options);\n }\n }\n\n /** Hover opens submenus only inline, only in hover mode and only for a mouse or pen. */\n #hoverApplies(event: PointerEvent): boolean {\n return this.mode === 'inline' && this.config.submenuVisibility === 'hover' && event.pointerType !== 'touch';\n }\n\n #onPointerEnter(item: HTMLLIElement, event: PointerEvent): void {\n if (!this.#hoverApplies(event)) return;\n this.#cancelHoverClose(item);\n if (this.#submenus.isOpen(item)) return;\n this.#submenus.collapseSiblings(item);\n this.#submenus.expand(item, 'hover');\n }\n\n /** Leaving a hover-opened submenu closes it after `hover-close-delay`, so the pointer can cross a gap. */\n #onPointerLeave(item: HTMLLIElement, event: PointerEvent): void {\n if (!this.#hoverApplies(event) || this.#submenus.openedBy.get(item) !== 'hover') return;\n this.#cancelHoverClose(item);\n const id = this.setTimer(() => {\n this.#hoverTimers.delete(item);\n if (this.#submenus.openedBy.get(item) === 'hover') this.#submenus.collapse(item);\n }, this.config.hoverCloseDelay);\n this.#hoverTimers.set(item, id);\n }\n\n #cancelHoverClose(item: HTMLLIElement): void {\n const id = this.#hoverTimers.get(item);\n if (id === undefined) return;\n this.clearTimer(id);\n this.#hoverTimers.delete(item);\n }\n\n #cancelAllHoverCloses(): void {\n for (const item of [...this.#hoverTimers.keys()]) this.#cancelHoverClose(item);\n }\n\n readonly #onHostClick = (event: Event): void => {\n const target = event.target as Element;\n const toggle = target.closest('.ui-nav__toggle');\n const parent = toggle ? this.#submenus.parentOfToggle(toggle) : undefined;\n if (toggle && parent) {\n this.#onToggleClick(parent, toggle as HTMLButtonElement);\n return;\n }\n // Activating a link inside a dropdown closes the dropdowns.\n const link = target.closest('a');\n if (link && this.mode === 'overlay' && this.#overlay.dialog.contains(link)) {\n this.close();\n return;\n }\n const item = link?.closest('li');\n if (this.mode === 'inline' && link && item && this.#submenus.topLevelItemOf(link) !== item) {\n this.closeSubmenus();\n }\n };\n\n #onToggleClick(parent: HTMLLIElement, toggle: HTMLButtonElement): void {\n // Safari does not focus a button on click (WebKit #22261); focus return and focusin/focusout need it.\n if (this.host.ownerDocument.activeElement !== toggle) toggle.focus();\n if (this.#submenus.openedBy.get(parent) === 'hover') {\n this.#cancelHoverClose(parent);\n this.#submenus.pin(parent);\n return;\n }\n if (this.#submenus.isOpen(parent)) {\n this.#submenus.collapse(parent);\n return;\n }\n if (this.mode === 'inline') this.#submenus.collapseSiblings(parent);\n this.#submenus.expand(parent, 'click');\n }\n\n readonly #onDocumentClick = (event: Event): void => {\n if (this.mode === 'inline' && !this.host.contains(event.target as Node)) this.closeSubmenus();\n };\n\n /** Escape closes the innermost open submenu around focus, or all of them when there is none. */\n readonly #onKeydown = (event: KeyboardEvent): void => {\n // An Escape another component already handled (a modal of its own, say) is not ours.\n if (event.defaultPrevented || this.mode !== 'inline' || event.key !== 'Escape') return;\n const active = this.host.ownerDocument.activeElement;\n const innermost = active && this.host.contains(active) ? this.#submenus.innermostOpen(active) : undefined;\n if (!innermost) {\n this.closeSubmenus();\n return;\n }\n this.#submenus.collapse(innermost);\n this.#submenus.toggleOf(innermost)?.focus();\n };\n\n /** Focus on a top-level item closes the submenus of the other top-level items. */\n readonly #onFocusIn = (event: FocusEvent): void => {\n if (this.mode !== 'inline') return;\n const item = this.#submenus.topLevelItemOf(event.target as Element);\n if (item) this.#submenus.collapseSiblings(item);\n };\n\n /** Focus leaving the navigation closes everything; a `null` target (Safari, on click) does not count. */\n readonly #onFocusOut = (event: FocusEvent): void => {\n const next = event.relatedTarget as Node | null;\n if (this.mode === 'inline' && next && !this.host.contains(next)) this.closeSubmenus();\n };\n\n readonly #onQueryChange = (): void => {\n this.#setMode(this.#computeMode());\n };\n\n #watchBreakpoint(): MediaQueryList {\n const query = this.#view.matchMedia(`(min-width: ${this.config.breakpoint}px)`);\n query.addEventListener('change', this.#onQueryChange, { signal: this.signal });\n return query;\n }\n\n #computeMode(): NavigationMode {\n if (this.config.overlay === 'always') return 'overlay';\n if (this.config.overlay === 'never') return 'inline';\n return this.#query.matches ? 'inline' : 'overlay';\n }\n\n #outsideDialog(event: MouseEvent): boolean {\n const box = this.#overlay.dialog.getBoundingClientRect();\n return event.clientX < box.left || event.clientX > box.right || event.clientY < box.top || event.clientY > box.bottom;\n }\n\n /**\n * A mode switch closes all submenus and an open overlay. When focus was in the navigation it\n * goes to the menu button in overlay mode, and in inline mode back to the same element if it\n * is still in the menu, otherwise to the first item.\n */\n #setMode(mode: NavigationMode): void {\n if (mode === this.mode) return;\n const focused = this.host.ownerDocument.activeElement as HTMLElement | null;\n const hadFocus = !!focused && this.host.contains(focused);\n\n this.#cancelAllHoverCloses();\n this.closeSubmenus();\n if (this.#overlay.isOpen) {\n this.#overlay.dialog.close();\n this.#markClosed(false);\n }\n this.mode = mode;\n this.#applyState();\n this.dispatch(EVENTS.modechange, { mode });\n\n if (!hadFocus) return;\n if (mode === 'overlay') {\n this.#overlay.button.focus();\n return;\n }\n // A nested item is hidden once its submenu closed and cannot keep focus; then take the first item.\n const inMenu = focused.isConnected && this.#list.contains(focused);\n if (inMenu) focused.focus();\n if (!inMenu || this.host.ownerDocument.activeElement !== focused) {\n this.#list.querySelector<HTMLElement>('a[href], button')?.focus();\n }\n }\n\n /** Writes the host hooks and adds or removes the submenu toggles to match config and mode. */\n #applyState(): void {\n this.host.setAttribute('data-enhanced', '');\n this.host.setAttribute('data-mode', this.mode);\n this.host.setAttribute('data-submenus', this.config.submenuVisibility);\n this.host.setAttribute('data-justify', this.config.justify);\n this.#overlay.setLabels(this.config.labelOpen, this.config.labelClose);\n this.#overlay.moveList(this.mode === 'overlay');\n this.#submenus.syncToggles(this.#togglesPresent(), this.config.labelSubmenu);\n }\n\n /** Toggles exist only where submenus can be opened and closed; otherwise they are always shown. */\n #togglesPresent(): boolean {\n const { submenuVisibility, overlaySubmenus } = this.config;\n return submenuVisibility !== 'always' && !(this.mode === 'overlay' && overlaySubmenus === 'expanded');\n }\n}\n"]}
package/dist/dom.d.ts ADDED
@@ -0,0 +1,7 @@
1
+ /** Returns `prefix` plus a number that no element in `doc` uses as its id yet. */
2
+ export declare function uniqueId(doc: Document, prefix: string): string;
3
+ /** Text for assistive technology only; hidden visually by `.ui-nav__visually-hidden` in the CSS. */
4
+ export declare function createVisuallyHidden(doc: Document, text: string): HTMLSpanElement;
5
+ /** A decorative chevron that inherits the text colour. */
6
+ export declare function createChevron(doc: Document): SVGSVGElement;
7
+ //# sourceMappingURL=dom.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dom.d.ts","sourceRoot":"","sources":["../src/dom.ts"],"names":[],"mappings":"AAIA,kFAAkF;AAClF,wBAAgB,QAAQ,CAAC,GAAG,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAK9D;AAED,oGAAoG;AACpG,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,GAAG,eAAe,CAKjF;AAED,0DAA0D;AAC1D,wBAAgB,aAAa,CAAC,GAAG,EAAE,QAAQ,GAAG,aAAa,CAe1D"}
package/dist/dom.js ADDED
@@ -0,0 +1,35 @@
1
+ const SVG_NS = 'http://www.w3.org/2000/svg';
2
+ let counter = 0;
3
+ /** Returns `prefix` plus a number that no element in `doc` uses as its id yet. */
4
+ export function uniqueId(doc, prefix) {
5
+ let id;
6
+ do
7
+ id = `${prefix}${++counter}`;
8
+ while (doc.getElementById(id));
9
+ return id;
10
+ }
11
+ /** Text for assistive technology only; hidden visually by `.ui-nav__visually-hidden` in the CSS. */
12
+ export function createVisuallyHidden(doc, text) {
13
+ const span = doc.createElement('span');
14
+ span.className = 'ui-nav__visually-hidden';
15
+ span.textContent = text;
16
+ return span;
17
+ }
18
+ /** A decorative chevron that inherits the text colour. */
19
+ export function createChevron(doc) {
20
+ const svg = doc.createElementNS(SVG_NS, 'svg');
21
+ svg.setAttribute('class', 'ui-nav__chevron');
22
+ svg.setAttribute('viewBox', '0 0 24 24');
23
+ svg.setAttribute('aria-hidden', 'true');
24
+ svg.setAttribute('focusable', 'false');
25
+ const path = doc.createElementNS(SVG_NS, 'path');
26
+ path.setAttribute('d', 'm6 9 6 6 6-6');
27
+ path.setAttribute('fill', 'none');
28
+ path.setAttribute('stroke', 'currentColor');
29
+ path.setAttribute('stroke-width', '2');
30
+ path.setAttribute('stroke-linecap', 'round');
31
+ path.setAttribute('stroke-linejoin', 'round');
32
+ svg.append(path);
33
+ return svg;
34
+ }
35
+ //# sourceMappingURL=dom.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dom.js","sourceRoot":"","sources":["../src/dom.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,GAAG,4BAA4B,CAAC;AAE5C,IAAI,OAAO,GAAG,CAAC,CAAC;AAEhB,kFAAkF;AAClF,MAAM,UAAU,QAAQ,CAAC,GAAa,EAAE,MAAc;IACpD,IAAI,EAAU,CAAC;IACf;QAAG,EAAE,GAAG,GAAG,MAAM,GAAG,EAAE,OAAO,EAAE,CAAC;WACzB,GAAG,CAAC,cAAc,CAAC,EAAE,CAAC,EAAE;IAC/B,OAAO,EAAE,CAAC;AACZ,CAAC;AAED,oGAAoG;AACpG,MAAM,UAAU,oBAAoB,CAAC,GAAa,EAAE,IAAY;IAC9D,MAAM,IAAI,GAAG,GAAG,CAAC,aAAa,CAAC,MAAM,CAAC,CAAC;IACvC,IAAI,CAAC,SAAS,GAAG,yBAAyB,CAAC;IAC3C,IAAI,CAAC,WAAW,GAAG,IAAI,CAAC;IACxB,OAAO,IAAI,CAAC;AACd,CAAC;AAED,0DAA0D;AAC1D,MAAM,UAAU,aAAa,CAAC,GAAa;IACzC,MAAM,GAAG,GAAG,GAAG,CAAC,eAAe,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;IAC/C,GAAG,CAAC,YAAY,CAAC,OAAO,EAAE,iBAAiB,CAAC,CAAC;IAC7C,GAAG,CAAC,YAAY,CAAC,SAAS,EAAE,WAAW,CAAC,CAAC;IACzC,GAAG,CAAC,YAAY,CAAC,aAAa,EAAE,MAAM,CAAC,CAAC;IACxC,GAAG,CAAC,YAAY,CAAC,WAAW,EAAE,OAAO,CAAC,CAAC;IACvC,MAAM,IAAI,GAAG,GAAG,CAAC,eAAe,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjD,IAAI,CAAC,YAAY,CAAC,GAAG,EAAE,cAAc,CAAC,CAAC;IACvC,IAAI,CAAC,YAAY,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAClC,IAAI,CAAC,YAAY,CAAC,QAAQ,EAAE,cAAc,CAAC,CAAC;IAC5C,IAAI,CAAC,YAAY,CAAC,cAAc,EAAE,GAAG,CAAC,CAAC;IACvC,IAAI,CAAC,YAAY,CAAC,gBAAgB,EAAE,OAAO,CAAC,CAAC;IAC7C,IAAI,CAAC,YAAY,CAAC,iBAAiB,EAAE,OAAO,CAAC,CAAC;IAC9C,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;IACjB,OAAO,GAAG,CAAC;AACb,CAAC","sourcesContent":["const SVG_NS = 'http://www.w3.org/2000/svg';\n\nlet counter = 0;\n\n/** Returns `prefix` plus a number that no element in `doc` uses as its id yet. */\nexport function uniqueId(doc: Document, prefix: string): string {\n let id: string;\n do id = `${prefix}${++counter}`;\n while (doc.getElementById(id));\n return id;\n}\n\n/** Text for assistive technology only; hidden visually by `.ui-nav__visually-hidden` in the CSS. */\nexport function createVisuallyHidden(doc: Document, text: string): HTMLSpanElement {\n const span = doc.createElement('span');\n span.className = 'ui-nav__visually-hidden';\n span.textContent = text;\n return span;\n}\n\n/** A decorative chevron that inherits the text colour. */\nexport function createChevron(doc: Document): SVGSVGElement {\n const svg = doc.createElementNS(SVG_NS, 'svg');\n svg.setAttribute('class', 'ui-nav__chevron');\n svg.setAttribute('viewBox', '0 0 24 24');\n svg.setAttribute('aria-hidden', 'true');\n svg.setAttribute('focusable', 'false');\n const path = doc.createElementNS(SVG_NS, 'path');\n path.setAttribute('d', 'm6 9 6 6 6-6');\n path.setAttribute('fill', 'none');\n path.setAttribute('stroke', 'currentColor');\n path.setAttribute('stroke-width', '2');\n path.setAttribute('stroke-linecap', 'round');\n path.setAttribute('stroke-linejoin', 'round');\n svg.append(path);\n return svg;\n}\n"]}
@@ -0,0 +1,10 @@
1
+ import type { NavigationInstance } from './navigation.js';
2
+ type Init = (element: HTMLElement) => NavigationInstance;
3
+ /**
4
+ * Creates the custom element class. A factory instead of a top-level class, because evaluating
5
+ * `class extends HTMLElement` fails where `HTMLElement` does not exist (a server-side import), and
6
+ * because `customElements.define()` refuses one class under two tag names.
7
+ */
8
+ export declare function createElementClass(init: Init): CustomElementConstructor;
9
+ export {};
10
+ //# sourceMappingURL=element.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"element.d.ts","sourceRoot":"","sources":["../src/element.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;AAE1D,KAAK,IAAI,GAAG,CAAC,OAAO,EAAE,WAAW,KAAK,kBAAkB,CAAC;AAEzD;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,IAAI,GAAG,wBAAwB,CAqCvE"}
@@ -0,0 +1,38 @@
1
+ import { ATTRIBUTES } from './config.js';
2
+ /**
3
+ * Creates the custom element class. A factory instead of a top-level class, because evaluating
4
+ * `class extends HTMLElement` fails where `HTMLElement` does not exist (a server-side import), and
5
+ * because `customElements.define()` refuses one class under two tag names.
6
+ */
7
+ export function createElementClass(init) {
8
+ return class UiNavigationElement extends HTMLElement {
9
+ static observedAttributes = [...ATTRIBUTES];
10
+ #instance;
11
+ #pending;
12
+ connectedCallback() {
13
+ const doc = this.ownerDocument;
14
+ // A classic script in <head> upgrades the element on its opening tag, before <nav> exists.
15
+ if (doc.readyState === 'loading') {
16
+ this.#pending?.abort();
17
+ this.#pending = new doc.defaultView.AbortController();
18
+ doc.addEventListener('DOMContentLoaded', () => {
19
+ this.#pending = undefined;
20
+ this.#instance = init(this);
21
+ }, { once: true, signal: this.#pending.signal });
22
+ return;
23
+ }
24
+ this.#instance = init(this);
25
+ }
26
+ disconnectedCallback() {
27
+ this.#pending?.abort();
28
+ this.#pending = undefined;
29
+ this.#instance?.destroy();
30
+ this.#instance = undefined;
31
+ }
32
+ attributeChangedCallback(_name, oldValue, newValue) {
33
+ if (oldValue !== newValue)
34
+ this.#instance?.update();
35
+ }
36
+ };
37
+ }
38
+ //# sourceMappingURL=element.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"element.js","sourceRoot":"","sources":["../src/element.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAKzC;;;;GAIG;AACH,MAAM,UAAU,kBAAkB,CAAC,IAAU;IAC3C,OAAO,MAAM,mBAAoB,SAAQ,WAAW;QAClD,MAAM,CAAC,kBAAkB,GAAG,CAAC,GAAG,UAAU,CAAC,CAAC;QAE5C,SAAS,CAAiC;QAC1C,QAAQ,CAA8B;QAEtC,iBAAiB;YACf,MAAM,GAAG,GAAG,IAAI,CAAC,aAAa,CAAC;YAC/B,2FAA2F;YAC3F,IAAI,GAAG,CAAC,UAAU,KAAK,SAAS,EAAE,CAAC;gBACjC,IAAI,CAAC,QAAQ,EAAE,KAAK,EAAE,CAAC;gBACvB,IAAI,CAAC,QAAQ,GAAG,IAAK,GAAG,CAAC,WAA0C,CAAC,eAAe,EAAE,CAAC;gBACtF,GAAG,CAAC,gBAAgB,CAClB,kBAAkB,EAClB,GAAG,EAAE;oBACH,IAAI,CAAC,QAAQ,GAAG,SAAS,CAAC;oBAC1B,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC,IAAI,CAAC,CAAC;gBAC9B,CAAC,EACD,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,QAAQ,CAAC,MAAM,EAAE,CAC7C,CAAC;gBACF,OAAO;YACT,CAAC;YACD,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC,IAAI,CAAC,CAAC;QAC9B,CAAC;QAED,oBAAoB;YAClB,IAAI,CAAC,QAAQ,EAAE,KAAK,EAAE,CAAC;YACvB,IAAI,CAAC,QAAQ,GAAG,SAAS,CAAC;YAC1B,IAAI,CAAC,SAAS,EAAE,OAAO,EAAE,CAAC;YAC1B,IAAI,CAAC,SAAS,GAAG,SAAS,CAAC;QAC7B,CAAC;QAED,wBAAwB,CAAC,KAAa,EAAE,QAAuB,EAAE,QAAuB;YACtF,IAAI,QAAQ,KAAK,QAAQ;gBAAE,IAAI,CAAC,SAAS,EAAE,MAAM,EAAE,CAAC;QACtD,CAAC;KACF,CAAC;AACJ,CAAC","sourcesContent":["import { ATTRIBUTES } from './config.js';\nimport type { NavigationInstance } from './navigation.js';\n\ntype Init = (element: HTMLElement) => NavigationInstance;\n\n/**\n * Creates the custom element class. A factory instead of a top-level class, because evaluating\n * `class extends HTMLElement` fails where `HTMLElement` does not exist (a server-side import), and\n * because `customElements.define()` refuses one class under two tag names.\n */\nexport function createElementClass(init: Init): CustomElementConstructor {\n return class UiNavigationElement extends HTMLElement {\n static observedAttributes = [...ATTRIBUTES];\n\n #instance: NavigationInstance | undefined;\n #pending: AbortController | undefined;\n\n connectedCallback(): void {\n const doc = this.ownerDocument;\n // A classic script in <head> upgrades the element on its opening tag, before <nav> exists.\n if (doc.readyState === 'loading') {\n this.#pending?.abort();\n this.#pending = new (doc.defaultView as Window & typeof globalThis).AbortController();\n doc.addEventListener(\n 'DOMContentLoaded',\n () => {\n this.#pending = undefined;\n this.#instance = init(this);\n },\n { once: true, signal: this.#pending.signal },\n );\n return;\n }\n this.#instance = init(this);\n }\n\n disconnectedCallback(): void {\n this.#pending?.abort();\n this.#pending = undefined;\n this.#instance?.destroy();\n this.#instance = undefined;\n }\n\n attributeChangedCallback(_name: string, oldValue: string | null, newValue: string | null): void {\n if (oldValue !== newValue) this.#instance?.update();\n }\n };\n}\n"]}
@@ -0,0 +1,32 @@
1
+ import { EVENTS } from './controller.js';
2
+ import type { NavigationMode, NavigationOptions } from './config.js';
3
+ export { EVENTS };
4
+ export type { Justify, NavigationConfig, NavigationMode, NavigationOptions, OverlaySetting, OverlaySubmenus, SubmenuVisibility, } from './config.js';
5
+ export interface NavigationInstance {
6
+ readonly mode: NavigationMode;
7
+ /** Opens the overlay panel; does nothing in inline mode. */
8
+ open(): void;
9
+ /** Closes the overlay panel and returns focus to the menu button. */
10
+ close(): void;
11
+ /** Closes every open submenu without moving focus. */
12
+ closeSubmenus(): void;
13
+ update(): void;
14
+ destroy(): void;
15
+ }
16
+ export interface NavigationEventDetail {
17
+ mode: NavigationMode;
18
+ }
19
+ export interface SubmenuEventDetail {
20
+ open: boolean;
21
+ label: string;
22
+ mode: NavigationMode;
23
+ by: 'click' | 'hover' | 'pin';
24
+ }
25
+ /**
26
+ * Enhances the `<nav>` inside `element` and returns its instance. Calling it again for the same
27
+ * element returns the same instance until `destroy()`.
28
+ */
29
+ export declare function init(element: HTMLElement, options?: NavigationOptions): NavigationInstance;
30
+ /** Registers the custom element under `tag`; does nothing when the tag is already defined. */
31
+ export declare function define(tag?: string): void;
32
+ //# sourceMappingURL=navigation.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"navigation.d.ts","sourceRoot":"","sources":["../src/navigation.ts"],"names":[],"mappings":"AAAA,OAAO,EAAwB,MAAM,EAAE,MAAM,iBAAiB,CAAC;AAE/D,OAAO,KAAK,EAAE,cAAc,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAErE,OAAO,EAAE,MAAM,EAAE,CAAC;AAClB,YAAY,EACV,OAAO,EACP,gBAAgB,EAChB,cAAc,EACd,iBAAiB,EACjB,cAAc,EACd,eAAe,EACf,iBAAiB,GAClB,MAAM,aAAa,CAAC;AAErB,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,IAAI,EAAE,cAAc,CAAC;IAC9B,4DAA4D;IAC5D,IAAI,IAAI,IAAI,CAAC;IACb,qEAAqE;IACrE,KAAK,IAAI,IAAI,CAAC;IACd,sDAAsD;IACtD,aAAa,IAAI,IAAI,CAAC;IACtB,MAAM,IAAI,IAAI,CAAC;IACf,OAAO,IAAI,IAAI,CAAC;CACjB;AAED,MAAM,WAAW,qBAAqB;IACpC,IAAI,EAAE,cAAc,CAAC;CACtB;AAED,MAAM,WAAW,kBAAkB;IACjC,IAAI,EAAE,OAAO,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,cAAc,CAAC;IACrB,EAAE,EAAE,OAAO,GAAG,OAAO,GAAG,KAAK,CAAC;CAC/B;AAcD;;;GAGG;AACH,wBAAgB,IAAI,CAAC,OAAO,EAAE,WAAW,EAAE,OAAO,GAAE,iBAAsB,GAAG,kBAAkB,CAU9F;AAgBD,8FAA8F;AAC9F,wBAAgB,MAAM,CAAC,GAAG,SAAkB,GAAG,IAAI,CAGlD"}
@@ -0,0 +1,50 @@
1
+ import { NavigationController, EVENTS } from './controller.js';
2
+ import { createElementClass } from './element.js';
3
+ export { EVENTS };
4
+ const instances = new WeakMap();
5
+ /** Returned for a host without a `<nav>`: there is nothing to enhance. */
6
+ const INERT = Object.freeze({
7
+ mode: 'inline',
8
+ open() { },
9
+ close() { },
10
+ closeSubmenus() { },
11
+ update() { },
12
+ destroy() { },
13
+ });
14
+ /**
15
+ * Enhances the `<nav>` inside `element` and returns its instance. Calling it again for the same
16
+ * element returns the same instance until `destroy()`.
17
+ */
18
+ export function init(element, options = {}) {
19
+ const existing = instances.get(element);
20
+ if (existing)
21
+ return existing;
22
+ const nav = element.querySelector('nav');
23
+ const list = nav?.querySelector(':scope > ul');
24
+ if (!nav || !list)
25
+ return INERT;
26
+ const controller = new NavigationController(element, nav, list, options, () => instances.delete(element));
27
+ const instance = publicApi(controller);
28
+ instances.set(element, instance);
29
+ return instance;
30
+ }
31
+ /** The public API of a controller, without its internals (config, host, timers, listeners). */
32
+ function publicApi(controller) {
33
+ return Object.freeze({
34
+ get mode() {
35
+ return controller.mode;
36
+ },
37
+ open: () => controller.open(),
38
+ close: () => controller.close(),
39
+ closeSubmenus: () => controller.closeSubmenus(),
40
+ update: () => controller.update(),
41
+ destroy: () => controller.destroy(),
42
+ });
43
+ }
44
+ /** Registers the custom element under `tag`; does nothing when the tag is already defined. */
45
+ export function define(tag = 'ui-navigation') {
46
+ if (customElements.get(tag))
47
+ return;
48
+ customElements.define(tag, createElementClass((element) => init(element)));
49
+ }
50
+ //# sourceMappingURL=navigation.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"navigation.js","sourceRoot":"","sources":["../src/navigation.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAC;AAC/D,OAAO,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAGlD,OAAO,EAAE,MAAM,EAAE,CAAC;AAkClB,MAAM,SAAS,GAAG,IAAI,OAAO,EAAmC,CAAC;AAEjE,0EAA0E;AAC1E,MAAM,KAAK,GAAuB,MAAM,CAAC,MAAM,CAAC;IAC9C,IAAI,EAAE,QAAiB;IACvB,IAAI,KAAI,CAAC;IACT,KAAK,KAAI,CAAC;IACV,aAAa,KAAI,CAAC;IAClB,MAAM,KAAI,CAAC;IACX,OAAO,KAAI,CAAC;CACb,CAAC,CAAC;AAEH;;;GAGG;AACH,MAAM,UAAU,IAAI,CAAC,OAAoB,EAAE,UAA6B,EAAE;IACxE,MAAM,QAAQ,GAAG,SAAS,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;IACxC,IAAI,QAAQ;QAAE,OAAO,QAAQ,CAAC;IAC9B,MAAM,GAAG,GAAG,OAAO,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC;IACzC,MAAM,IAAI,GAAG,GAAG,EAAE,aAAa,CAAmB,aAAa,CAAC,CAAC;IACjE,IAAI,CAAC,GAAG,IAAI,CAAC,IAAI;QAAE,OAAO,KAAK,CAAC;IAChC,MAAM,UAAU,GAAG,IAAI,oBAAoB,CAAC,OAAO,EAAE,GAAG,EAAE,IAAI,EAAE,OAAO,EAAE,GAAG,EAAE,CAAC,SAAS,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC;IAC1G,MAAM,QAAQ,GAAG,SAAS,CAAC,UAAU,CAAC,CAAC;IACvC,SAAS,CAAC,GAAG,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC;IACjC,OAAO,QAAQ,CAAC;AAClB,CAAC;AAED,+FAA+F;AAC/F,SAAS,SAAS,CAAC,UAAgC;IACjD,OAAO,MAAM,CAAC,MAAM,CAAC;QACnB,IAAI,IAAI;YACN,OAAO,UAAU,CAAC,IAAI,CAAC;QACzB,CAAC;QACD,IAAI,EAAE,GAAG,EAAE,CAAC,UAAU,CAAC,IAAI,EAAE;QAC7B,KAAK,EAAE,GAAG,EAAE,CAAC,UAAU,CAAC,KAAK,EAAE;QAC/B,aAAa,EAAE,GAAG,EAAE,CAAC,UAAU,CAAC,aAAa,EAAE;QAC/C,MAAM,EAAE,GAAG,EAAE,CAAC,UAAU,CAAC,MAAM,EAAE;QACjC,OAAO,EAAE,GAAG,EAAE,CAAC,UAAU,CAAC,OAAO,EAAE;KACpC,CAAC,CAAC;AACL,CAAC;AAED,8FAA8F;AAC9F,MAAM,UAAU,MAAM,CAAC,GAAG,GAAG,eAAe;IAC1C,IAAI,cAAc,CAAC,GAAG,CAAC,GAAG,CAAC;QAAE,OAAO;IACpC,cAAc,CAAC,MAAM,CAAC,GAAG,EAAE,kBAAkB,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC;AAC7E,CAAC","sourcesContent":["import { NavigationController, EVENTS } from './controller.js';\nimport { createElementClass } from './element.js';\nimport type { NavigationMode, NavigationOptions } from './config.js';\n\nexport { EVENTS };\nexport type {\n Justify,\n NavigationConfig,\n NavigationMode,\n NavigationOptions,\n OverlaySetting,\n OverlaySubmenus,\n SubmenuVisibility,\n} from './config.js';\n\nexport interface NavigationInstance {\n readonly mode: NavigationMode;\n /** Opens the overlay panel; does nothing in inline mode. */\n open(): void;\n /** Closes the overlay panel and returns focus to the menu button. */\n close(): void;\n /** Closes every open submenu without moving focus. */\n closeSubmenus(): void;\n update(): void;\n destroy(): void;\n}\n\nexport interface NavigationEventDetail {\n mode: NavigationMode;\n}\n\nexport interface SubmenuEventDetail {\n open: boolean;\n label: string;\n mode: NavigationMode;\n by: 'click' | 'hover' | 'pin';\n}\n\nconst instances = new WeakMap<HTMLElement, NavigationInstance>();\n\n/** Returned for a host without a `<nav>`: there is nothing to enhance. */\nconst INERT: NavigationInstance = Object.freeze({\n mode: 'inline' as const,\n open() {},\n close() {},\n closeSubmenus() {},\n update() {},\n destroy() {},\n});\n\n/**\n * Enhances the `<nav>` inside `element` and returns its instance. Calling it again for the same\n * element returns the same instance until `destroy()`.\n */\nexport function init(element: HTMLElement, options: NavigationOptions = {}): NavigationInstance {\n const existing = instances.get(element);\n if (existing) return existing;\n const nav = element.querySelector('nav');\n const list = nav?.querySelector<HTMLUListElement>(':scope > ul');\n if (!nav || !list) return INERT;\n const controller = new NavigationController(element, nav, list, options, () => instances.delete(element));\n const instance = publicApi(controller);\n instances.set(element, instance);\n return instance;\n}\n\n/** The public API of a controller, without its internals (config, host, timers, listeners). */\nfunction publicApi(controller: NavigationController): NavigationInstance {\n return Object.freeze({\n get mode() {\n return controller.mode;\n },\n open: () => controller.open(),\n close: () => controller.close(),\n closeSubmenus: () => controller.closeSubmenus(),\n update: () => controller.update(),\n destroy: () => controller.destroy(),\n });\n}\n\n/** Registers the custom element under `tag`; does nothing when the tag is already defined. */\nexport function define(tag = 'ui-navigation'): void {\n if (customElements.get(tag)) return;\n customElements.define(tag, createElementClass((element) => init(element)));\n}\n"]}
@@ -0,0 +1,19 @@
1
+ /**
2
+ * The menu button and the `<dialog>` panel of one navigation. Both exist while the navigation is
3
+ * enhanced; the navigation's own `<ul>` moves into the panel in overlay mode and back to its place
4
+ * in `<nav>` in inline mode, so there is only ever one list.
5
+ */
6
+ export declare class Overlay {
7
+ #private;
8
+ readonly button: HTMLButtonElement;
9
+ readonly dialog: HTMLDialogElement;
10
+ readonly closeButton: HTMLButtonElement;
11
+ constructor(nav: HTMLElement, list: HTMLUListElement);
12
+ get isOpen(): boolean;
13
+ setLabels(open: string, close: string): void;
14
+ /** Puts the list in the panel (`true`) or back in its place in `<nav>` (`false`). */
15
+ moveList(toPanel: boolean): void;
16
+ /** Removes the button and the panel and puts the list back. */
17
+ detach(): void;
18
+ }
19
+ //# sourceMappingURL=overlay.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"overlay.d.ts","sourceRoot":"","sources":["../src/overlay.ts"],"names":[],"mappings":"AAEA;;;;GAIG;AACH,qBAAa,OAAO;;IAClB,QAAQ,CAAC,MAAM,EAAE,iBAAiB,CAAC;IACnC,QAAQ,CAAC,MAAM,EAAE,iBAAiB,CAAC;IACnC,QAAQ,CAAC,WAAW,EAAE,iBAAiB,CAAC;gBAQ5B,GAAG,EAAE,WAAW,EAAE,IAAI,EAAE,gBAAgB;IAsCpD,IAAI,MAAM,IAAI,OAAO,CAEpB;IAED,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI;IAK5C,qFAAqF;IACrF,QAAQ,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI;IAQhC,+DAA+D;IAC/D,MAAM,IAAI,IAAI;CAMf"}