@hubpav/hwio-web-runtime 0.9.1 → 0.10.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.
@@ -7,6 +7,6 @@
7
7
  * - Escape, with focus back on its summary when focus was inside;
8
8
  * - focus leaving it (Tab past the last item);
9
9
  * - another dropdown opening, or another runtime announcing a popup (HWioHeader's mega panels and
10
- * drawer, through `hwio:popupopen`). Opening a dropdown announces it, so an open mega panel closes.
10
+ * small-screen menu, through `hwio:popupopen`). Opening a dropdown announces it, so an open mega panel closes.
11
11
  */
12
12
  export declare function hwioDropdownRuntime(): import("./mount.js").HWioBinder;
package/dist/dropdown.js CHANGED
@@ -9,7 +9,7 @@ import { hwioAnnouncePopup, hwioOnPopup } from './popup.js';
9
9
  * - Escape, with focus back on its summary when focus was inside;
10
10
  * - focus leaving it (Tab past the last item);
11
11
  * - another dropdown opening, or another runtime announcing a popup (HWioHeader's mega panels and
12
- * drawer, through `hwio:popupopen`). Opening a dropdown announces it, so an open mega panel closes.
12
+ * small-screen menu, through `hwio:popupopen`). Opening a dropdown announces it, so an open mega panel closes.
13
13
  */
14
14
  export function hwioDropdownRuntime() {
15
15
  let controller = null;
package/dist/nav.d.ts CHANGED
@@ -1,12 +1,13 @@
1
1
  /**
2
- * Header behaviours: drives the DaisyUI drawer from its [data-hwio-drawer-toggle] buttons (Enter and
3
- * Space work because they are buttons; aria-expanded follows the checkbox, focus moves to the panel's
4
- * first control on open and back to the trigger on close, Escape closes, a followed link closes),
5
- * opens HWioHeader's mega panels
2
+ * Header behaviours: opens HWioHeader's small-screen menu from its [data-hwio-menu-toggle] button into its
3
+ * [data-hwio-menu-panel] sheet (aria-expanded and the button's label follow; the page is locked without
4
+ * moving, its scrollbar's width kept as padding, and made inert behind the sheet; focus moves to the first
5
+ * item without scrolling and back to the toggle on close; the toggle, Escape, a followed link, another
6
+ * popup and a resize to the desktop layout close it), opens HWioHeader's mega panels
6
7
  * ([data-hwio-mega-trigger] buttons and their [data-hwio-mega-panel]; hover with intent on pointer
7
8
  * devices, click, ArrowDown into the panel, Escape / outside click / focus leaving close; one at a
8
9
  * time; the click that follows a hover-open keeps the panel open, the next one closes it), and marks the nav link whose section is in view ([data-hwio-scrollspy] nav with same-page
9
- * anchors) with aria-current="location". Opening a mega panel or the drawer announces it
10
+ * anchors) with aria-current="location". Opening a mega panel or the small-screen menu announces it
10
11
  * (`hwio:popupopen`, popup.ts), so an open dropdown (HWioLanguageSwitcher) closes; a dropdown that
11
12
  * opens closes the mega panels the same way.
12
13
  */
package/dist/nav.js CHANGED
@@ -1,21 +1,26 @@
1
1
  import { hwioMount } from './mount.js';
2
2
  import { hwioAnnouncePopup, hwioOnPopup } from './popup.js';
3
3
  /**
4
- * Header behaviours: drives the DaisyUI drawer from its [data-hwio-drawer-toggle] buttons (Enter and
5
- * Space work because they are buttons; aria-expanded follows the checkbox, focus moves to the panel's
6
- * first control on open and back to the trigger on close, Escape closes, a followed link closes),
7
- * opens HWioHeader's mega panels
4
+ * Header behaviours: opens HWioHeader's small-screen menu from its [data-hwio-menu-toggle] button into its
5
+ * [data-hwio-menu-panel] sheet (aria-expanded and the button's label follow; the page is locked without
6
+ * moving, its scrollbar's width kept as padding, and made inert behind the sheet; focus moves to the first
7
+ * item without scrolling and back to the toggle on close; the toggle, Escape, a followed link, another
8
+ * popup and a resize to the desktop layout close it), opens HWioHeader's mega panels
8
9
  * ([data-hwio-mega-trigger] buttons and their [data-hwio-mega-panel]; hover with intent on pointer
9
10
  * devices, click, ArrowDown into the panel, Escape / outside click / focus leaving close; one at a
10
11
  * time; the click that follows a hover-open keeps the panel open, the next one closes it), and marks the nav link whose section is in view ([data-hwio-scrollspy] nav with same-page
11
- * anchors) with aria-current="location". Opening a mega panel or the drawer announces it
12
+ * anchors) with aria-current="location". Opening a mega panel or the small-screen menu announces it
12
13
  * (`hwio:popupopen`, popup.ts), so an open dropdown (HWioLanguageSwitcher) closes; a dropdown that
13
14
  * opens closes the mega panels the same way.
14
15
  */
15
16
  export function hwioNavRuntime() {
16
17
  let controller = null;
17
18
  let observer = null;
19
+ let closeMenus = [];
18
20
  function cleanup() {
21
+ // A page swap with the menu open must not leave the next page locked or inert.
22
+ closeMenus.forEach((close) => close());
23
+ closeMenus = [];
19
24
  controller?.abort();
20
25
  controller = null;
21
26
  observer?.disconnect();
@@ -25,46 +30,89 @@ export function hwioNavRuntime() {
25
30
  cleanup();
26
31
  controller = new AbortController();
27
32
  const { signal } = controller;
28
- document.querySelectorAll('input.drawer-toggle').forEach((input) => {
29
- const side = input.parentElement?.querySelector('.drawer-side') ?? null;
30
- const toggles = Array.from(document.querySelectorAll(`[data-hwio-drawer-toggle="${input.id}"]`));
31
- const trigger = toggles.find((t) => !side?.contains(t)) ?? null;
32
- const sync = () => toggles.forEach((t) => t.setAttribute('aria-expanded', String(input.checked)));
33
- // DaisyUI reveals .drawer-side after a 100 ms visibility delay; retry per frame until the control takes focus.
34
- const focusWhenVisible = (el, frames = 40) => {
35
- if (!input.checked)
36
- return;
37
- el.focus();
38
- if (document.activeElement !== el && frames > 0)
39
- requestAnimationFrame(() => focusWhenVisible(el, frames - 1));
33
+ document.querySelectorAll('[data-hwio-menu-toggle]').forEach((toggle) => {
34
+ const panel = document.querySelector(`[data-hwio-menu-panel="${toggle.dataset.hwioMenuToggle}"]`);
35
+ if (!panel)
36
+ return;
37
+ const header = toggle.closest('header');
38
+ const root = document.documentElement;
39
+ const reduced = window.matchMedia('(prefers-reduced-motion: reduce)');
40
+ const desktop = toggle.dataset.hwioMenuUntil ? window.matchMedia(`(min-width: ${toggle.dataset.hwioMenuUntil})`) : null;
41
+ let inerted = [];
42
+ let hideTimer = null;
43
+ const isOpen = () => toggle.getAttribute('aria-expanded') === 'true';
44
+ const label = (open) => {
45
+ const text = open ? toggle.dataset.hwioLabelClose : toggle.dataset.hwioLabelOpen;
46
+ if (text)
47
+ toggle.setAttribute('aria-label', text);
40
48
  };
41
- const apply = (restoreFocus) => {
42
- sync();
43
- if (input.checked) {
44
- if (side)
45
- hwioAnnouncePopup(side);
46
- const first = side?.querySelector('button, a[href], input:not([type="hidden"]), select, textarea, summary, [tabindex]:not([tabindex="-1"])');
47
- if (first)
48
- focusWhenVisible(first);
49
+ // Lock the page where it is: overflow hidden on the root, its scrollbar's width kept as padding (so
50
+ // the layout keeps its width), and everything outside the header inert, walking up from the header.
51
+ const lock = (on) => {
52
+ if (on) {
53
+ root.style.setProperty('--hwio-scrollbar', `${Math.max(0, window.innerWidth - root.clientWidth)}px`);
54
+ root.classList.add('hwio-scroll-lock');
55
+ for (let node = header; node && node.parentElement && node !== document.body; node = node.parentElement) {
56
+ for (const sibling of Array.from(node.parentElement.children)) {
57
+ if (sibling !== node && sibling instanceof HTMLElement && !sibling.inert && sibling.tagName !== 'SCRIPT') {
58
+ sibling.inert = true;
59
+ inerted.push(sibling);
60
+ }
61
+ }
62
+ }
49
63
  }
50
- else if (restoreFocus) {
51
- trigger?.focus();
64
+ else {
65
+ root.classList.remove('hwio-scroll-lock');
66
+ root.style.removeProperty('--hwio-scrollbar');
67
+ inerted.forEach((el) => { el.inert = false; });
68
+ inerted = [];
52
69
  }
53
70
  };
54
71
  const setOpen = (open, restoreFocus = true) => {
55
- if (input.checked === open)
72
+ if (isOpen() === open)
56
73
  return;
57
- input.checked = open;
58
- apply(restoreFocus);
74
+ toggle.setAttribute('aria-expanded', String(open));
75
+ label(open);
76
+ if (hideTimer) {
77
+ clearTimeout(hideTimer);
78
+ hideTimer = null;
79
+ }
80
+ if (open) {
81
+ panel.hidden = false;
82
+ lock(true);
83
+ // One frame with the closed state rendered, so the sheet fades in instead of appearing.
84
+ requestAnimationFrame(() => { if (isOpen())
85
+ panel.dataset.state = 'open'; });
86
+ hwioAnnouncePopup(panel);
87
+ panel.querySelector('a[href], button, summary, input:not([type="hidden"])')?.focus({ preventScroll: true });
88
+ }
89
+ else {
90
+ panel.dataset.state = 'closed';
91
+ lock(false);
92
+ const hide = () => { if (!isOpen())
93
+ panel.hidden = true; };
94
+ if (reduced.matches)
95
+ hide();
96
+ else
97
+ hideTimer = setTimeout(hide, 200);
98
+ if (restoreFocus)
99
+ toggle.focus({ preventScroll: true });
100
+ }
59
101
  };
60
- sync();
61
- toggles.forEach((t) => t.addEventListener('click', () => setOpen(!input.checked), { signal }));
62
- // Labels (the overlay) still flip the checkbox natively; follow them.
63
- input.addEventListener('change', () => apply(true), { signal });
64
- // A followed link closes the drawer without stealing focus from the navigation target (same-page anchors).
65
- side?.querySelectorAll('a[href]').forEach((a) => a.addEventListener('click', () => setOpen(false, false), { signal }));
66
- document.addEventListener('keydown', (e) => { if (e.key === 'Escape' && input.checked)
102
+ closeMenus.push(() => { if (isOpen()) {
103
+ setOpen(false, false);
104
+ panel.hidden = true;
105
+ } });
106
+ toggle.addEventListener('click', () => setOpen(!isOpen()), { signal });
107
+ // A followed link closes the menu without stealing focus from the navigation target (same-page anchors).
108
+ panel.querySelectorAll('a[href]').forEach((a) => a.addEventListener('click', () => setOpen(false, false), { signal }));
109
+ document.addEventListener('keydown', (e) => { if (e.key === 'Escape' && isOpen())
67
110
  setOpen(false); }, { signal });
111
+ desktop?.addEventListener('change', (e) => { if (e.matches)
112
+ setOpen(false, false); }, { signal });
113
+ // Another popup (the language dropdown in the bar) opened: close the menu unless it sits in the sheet.
114
+ hwioOnPopup((owner) => { if (owner !== panel && !panel.contains(owner))
115
+ setOpen(false, false); }, signal);
68
116
  });
69
117
  const triggers = Array.from(document.querySelectorAll('[data-hwio-mega-trigger]'));
70
118
  if (triggers.length) {
@@ -135,7 +183,7 @@ export function hwioNavRuntime() {
135
183
  if (!pairs.some((p) => p.trigger.contains(node) || p.panel.contains(node)))
136
184
  closeAll();
137
185
  }, { signal });
138
- // Another popup (a language dropdown, the drawer) opened: close the panels unless it sits in one.
186
+ // Another popup (a language dropdown, the small-screen menu) opened: close the panels unless it sits in one.
139
187
  hwioOnPopup((owner) => {
140
188
  if (!pairs.some((p) => p.panel === owner || p.panel.contains(owner)))
141
189
  closeAll();
package/dist/popup.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * One popup at a time across runtimes. A runtime that opens something which floats over the page
3
- * (HWioHeader's mega panel or drawer, a `details[data-hwio-dropdown]`) announces the element it
3
+ * (HWioHeader's mega panel or small-screen menu, a `details[data-hwio-dropdown]`) announces the element it
4
4
  * opened with `hwio:popupopen`; every other runtime closes what it has open unless that element is
5
5
  * the announced one or contains it. The runtimes stay independent: neither imports the other.
6
6
  */
package/dist/popup.js CHANGED
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * One popup at a time across runtimes. A runtime that opens something which floats over the page
3
- * (HWioHeader's mega panel or drawer, a `details[data-hwio-dropdown]`) announces the element it
3
+ * (HWioHeader's mega panel or small-screen menu, a `details[data-hwio-dropdown]`) announces the element it
4
4
  * opened with `hwio:popupopen`; every other runtime closes what it has open unless that element is
5
5
  * the announced one or contains it. The runtimes stay independent: neither imports the other.
6
6
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hubpav/hwio-web-runtime",
3
- "version": "0.9.1",
3
+ "version": "0.10.0",
4
4
  "description": "Framework-free browser runtime for the HARDWARIO websites: consent (Consent Mode v2 + GTM), HubSpot forms with Turnstile, attribution, theme, CTA tracking and small behaviours. View-transition safe.",
5
5
  "license": "MIT",
6
6
  "type": "module",