@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:
|
|
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=
|
|
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.
|
|
169
|
-
|
|
170
|
-
it is
|
|
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
|
+
"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.
|
|
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",
|