@takazudo/zudo-doc 5.26.3 → 5.26.4

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/CHANGELOG.md CHANGED
@@ -8,6 +8,13 @@ The format is based on Keep a Changelog, and release notes are generated from th
8
8
 
9
9
  No unreleased changes.
10
10
 
11
+ ## [5.26.4] - 2026-09-22
12
+
13
+ ### Bug Fixes
14
+
15
+ - The mobile sidebar drawer now closes when Escape is pressed, and focus returns to the toggle button. The drawer is a modal-style surface — it renders above a full-viewport backdrop, locks body scroll while open, and is `inert` while closed — so Escape is the dismissal that keyboard-only and assistive-technology users reach for first, and it previously did nothing at all: the `SidebarToggle` island registered no key handler. The listener is now attached to `document` only while the drawer is open, so a closed drawer never swallows an Escape meant for another surface such as the language switcher or the theme-pack flyout. Focus is restored to the toggle as the drawer closes; because the panel becomes `inert` again, focus left inside it would otherwise strand on `<body>`. The handler ignores an Escape that ends an IME composition, so cancelling a Japanese conversion in the drawer's own filter field no longer dismisses the drawer. (10036db43, 4445f1972)
16
+ - The toggle's X now closes the drawer when clicked. While the drawer was open the toggle rendered the close icon but sat underneath the backdrop — `z-modal-backdrop` (50) covers the header's `z-toolbar` (20), so `document.elementFromPoint()` at the button's own centre returned the backdrop element and every real pointer event was intercepted. The toggle now joins the drawer's own tier (`relative z-modal`, 60) for exactly as long as the drawer is open, so the X dismisses the drawer as its icon implies; the rest of the header stays dimmed and non-interactive underneath, and the closed-state markup is unchanged. Together with the Escape fix above, this restores both the keyboard and the pointer dismissal — before the two, clicking the backdrop was the only working way out of an open drawer, and nothing in the interface advertised it. (ffe0f728b)
17
+
11
18
  ## [5.26.3] - 2026-09-21
12
19
 
13
20
  ### Bug Fixes
@@ -1,6 +1,6 @@
1
1
  "use client";
2
2
  import { Fragment, jsx, jsxs } from "preact/jsx-runtime";
3
- import { useState, useEffect } from "preact/hooks";
3
+ import { useState, useEffect, useRef } from "preact/hooks";
4
4
  import { AFTER_NAVIGATE_EVENT, ensureNestedIslandPropsRefresh } from "../transitions/index.js";
5
5
  import { SidebarTree } from "../sidebar-tree-island/index.js";
6
6
  ensureNestedIslandPropsRefresh();
@@ -17,6 +17,7 @@ function SidebarToggle({
17
17
  dateFormats
18
18
  }) {
19
19
  const [open, setOpen] = useState(false);
20
+ const hamburgerRef = useRef(null);
20
21
  useEffect(() => {
21
22
  if (open) {
22
23
  document.body.style.overflow = "hidden";
@@ -34,13 +35,39 @@ function SidebarToggle({
34
35
  document.addEventListener(AFTER_NAVIGATE_EVENT, handleSwap);
35
36
  return () => document.removeEventListener(AFTER_NAVIGATE_EVENT, handleSwap);
36
37
  }, []);
38
+ useEffect(() => {
39
+ if (!open) return;
40
+ function handleKeyDown(event) {
41
+ if (event.isComposing) return;
42
+ if (event.key === "Escape") {
43
+ setOpen(false);
44
+ hamburgerRef.current?.focus();
45
+ }
46
+ }
47
+ document.addEventListener("keydown", handleKeyDown);
48
+ return () => document.removeEventListener("keydown", handleKeyDown);
49
+ }, [open]);
37
50
  return /* @__PURE__ */ jsxs(Fragment, { children: [
38
51
  /* @__PURE__ */ jsxs(
39
52
  "button",
40
53
  {
54
+ ref: hamburgerRef,
41
55
  type: "button",
42
56
  onClick: () => setOpen(!open),
43
- className: "lg:hidden shrink-0 px-hsp-sm py-vsp-xs -ml-hsp-sm mr-hsp-sm text-muted hover:text-fg",
57
+ className: cx(
58
+ "lg:hidden shrink-0 px-hsp-sm py-vsp-xs -ml-hsp-sm mr-hsp-sm text-muted hover:text-fg",
59
+ // While open, the button IS the close control (it shows the X), so it
60
+ // has to sit in the drawer's own tier rather than under the backdrop —
61
+ // `z-modal-backdrop` (50) otherwise intercepts every pointer event at
62
+ // the button's own centre and the X is unclickable
63
+ // (zudolab/zudo-doc#4369). `relative` is required: a bare `z-index`
64
+ // has no effect on a statically-positioned element.
65
+ // Conditioning on `open` is load-bearing, not cosmetic: SSR always
66
+ // renders `open=false`, so the closed-state markup stays
67
+ // byte-identical (hydration-stable, and the A2 no-stub parity hashes
68
+ // for this directory do not move).
69
+ open && "relative z-modal"
70
+ ),
44
71
  "aria-label": open ? "Close sidebar" : "Open sidebar",
45
72
  "aria-expanded": open,
46
73
  children: [
@@ -4,7 +4,7 @@
4
4
  /** @jsxImportSource preact */
5
5
  // Use preact hook entrypoints directly — the "react" → "preact/compat" alias
6
6
  // lets us consume React-typed components in this Preact app.
7
- import { useState, useEffect } from "preact/hooks";
7
+ import { useState, useEffect, useRef } from "preact/hooks";
8
8
  // After zudolab/zudo-doc#1335 the host components pull lifecycle event names
9
9
  // from the v2 transitions module rather than hard-coding `astro:*` literals.
10
10
  // `ensureNestedIslandPropsRefresh` is imported through the barrel (not the
@@ -87,6 +87,9 @@ export function SidebarToggle({
87
87
  // same shape regardless of `open`, preventing Preact from re-mounting
88
88
  // the subtree (which can drop click handlers on the hamburger button).
89
89
  const [open, setOpen] = useState(false);
90
+ // Focus-restore target for the Escape handler below. A `ref` does not
91
+ // serialise, so SSR/hydration markup is unaffected (zudolab/zudo-doc#4366).
92
+ const hamburgerRef = useRef<HTMLButtonElement>(null);
90
93
 
91
94
  useEffect(() => {
92
95
  if (open) {
@@ -108,6 +111,35 @@ export function SidebarToggle({
108
111
  return () => document.removeEventListener(AFTER_NAVIGATE_EVENT, handleSwap);
109
112
  }, []);
110
113
 
114
+ // Escape-to-close (zudolab/zudo-doc#4366). Deliberately inlined rather than
115
+ // reusing `connectEscapeToClose` from theme-pack-switcher/switcher-state.js
116
+ // — this island is ejectable and eject's `rewireImports` would rewrite that
117
+ // relative import to `@takazudo/zudo-doc/theme-pack-switcher`, a subpath
118
+ // absent from the package's `exports` map, shipping a broken ejected copy.
119
+ // The listener is registered only while `open` is true: a closed drawer
120
+ // must never swallow an Escape meant for another document-level listener
121
+ // (language switcher, find bar, theme-pack flyout). Focus is restored to
122
+ // the hamburger synchronously in the handler — the `<aside>` goes `inert`
123
+ // on close, so without this, focus left inside the drawer would strand on
124
+ // `<body>` and defeat the point of the fix for keyboard/AT users.
125
+ useEffect(() => {
126
+ if (!open) return;
127
+ function handleKeyDown(event: KeyboardEvent) {
128
+ // An Escape that ends an IME composition belongs to the composition, not
129
+ // to the drawer: cancelling a Japanese conversion in the drawer's own
130
+ // "Filter navigation" input would otherwise dismiss the whole drawer and
131
+ // yank focus to the hamburger. Same guard the sibling document-level
132
+ // shortcut in `sidebar-tree-island/index.tsx` already uses.
133
+ if (event.isComposing) return;
134
+ if (event.key === "Escape") {
135
+ setOpen(false);
136
+ hamburgerRef.current?.focus();
137
+ }
138
+ }
139
+ document.addEventListener("keydown", handleKeyDown);
140
+ return () => document.removeEventListener("keydown", handleKeyDown);
141
+ }, [open]);
142
+
111
143
  return (
112
144
  <>
113
145
  {/* Hamburger button - visible only on mobile.
@@ -117,9 +149,23 @@ export function SidebarToggle({
117
149
  Preact's hydration walk sees byte-stable markup and keeps the
118
150
  click handler attached. */}
119
151
  <button
152
+ ref={hamburgerRef}
120
153
  type="button"
121
154
  onClick={() => setOpen(!open)}
122
- className="lg:hidden shrink-0 px-hsp-sm py-vsp-xs -ml-hsp-sm mr-hsp-sm text-muted hover:text-fg"
155
+ className={cx(
156
+ "lg:hidden shrink-0 px-hsp-sm py-vsp-xs -ml-hsp-sm mr-hsp-sm text-muted hover:text-fg",
157
+ // While open, the button IS the close control (it shows the X), so it
158
+ // has to sit in the drawer's own tier rather than under the backdrop —
159
+ // `z-modal-backdrop` (50) otherwise intercepts every pointer event at
160
+ // the button's own centre and the X is unclickable
161
+ // (zudolab/zudo-doc#4369). `relative` is required: a bare `z-index`
162
+ // has no effect on a statically-positioned element.
163
+ // Conditioning on `open` is load-bearing, not cosmetic: SSR always
164
+ // renders `open=false`, so the closed-state markup stays
165
+ // byte-identical (hydration-stable, and the A2 no-stub parity hashes
166
+ // for this directory do not move).
167
+ open && "relative z-modal",
168
+ )}
123
169
  aria-label={open ? "Close sidebar" : "Open sidebar"}
124
170
  aria-expanded={open}
125
171
  >
@@ -165,9 +211,13 @@ export function SidebarToggle({
165
211
  mount/unmount across the hydration boundary).
166
212
  `z-modal-backdrop` (50) intentionally sits ABOVE the header
167
213
  (`z-toolbar`, 20): the open mobile drawer is a modal surface that
168
- dims the whole viewport, header included. Closing is via tapping the
169
- backdrop (onClick below), so the header hamburger being dimmed under
170
- it is fine. */}
214
+ dims the whole viewport, header included. The toggle button is the
215
+ one exception — while open it renders the X and advertises itself as
216
+ the close control, so it is lifted to `z-modal` (60) for exactly as
217
+ long as the drawer is open (see its className above). Everything else
218
+ in the header stays dimmed and non-interactive underneath.
219
+ Backdrop tapping (onClick below) remains a valid dismissal, as does
220
+ Escape (zudolab/zudo-doc#4366). */}
171
221
  <div
172
222
  className={cx("fixed inset-0 z-modal-backdrop bg-overlay/30 lg:hidden", !open && "hidden")}
173
223
  aria-hidden={!open}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@takazudo/zudo-doc",
3
- "version": "5.26.3",
3
+ "version": "5.26.4",
4
4
  "type": "module",
5
5
  "description": "zudo-doc framework primitives layer that sits on top of zfb's engine — sidebar, theme, TOC, breadcrumb, layouts, head injection, View Transitions, SSR-skip wrappers (per ADR-003).",
6
6
  "license": "MIT",
@@ -771,7 +771,7 @@
771
771
  "typescript": "^5.0.0",
772
772
  "vitest": "^4.1.0",
773
773
  "zod": "^4.3.6",
774
- "@takazudo/zudo-doc-history-server": "5.26.3"
774
+ "@takazudo/zudo-doc-history-server": "5.26.4"
775
775
  },
776
776
  "scripts": {
777
777
  "gen:search-widget-script": "node scripts/gen-search-widget-script.mjs",