Skip to content

Commit 894b79e

Browse files
committed
Improve pageToc and navMenu
1 parent f20a531 commit 894b79e

7 files changed

Lines changed: 271 additions & 3 deletions

File tree

‎packages/ts-components/src/standard/pageToc/pageTocStyle.ts‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -282,6 +282,7 @@ function registerPageTocMobileOverlayStyles(
282282
styleBuilder
283283
.select('.template-doc--has-toc .page-toc__panel-toggle', theme)
284284
.media(mediaMax(BREAKPOINTS.toc))
285+
.display('flex')
285286
.position('absolute')
286287
.left('0')
287288
.right('auto')
@@ -301,7 +302,6 @@ function registerPageTocMobileOverlayStyles(
301302
.zIndex(2)
302303
.userSelect('none')
303304
.cursor('pointer')
304-
.display('flex')
305305
.alignItems('center')
306306
.justifyContent('center')
307307
.textTransform('uppercase')
@@ -323,6 +323,13 @@ function registerPageTocMobileOverlayStyles(
323323
}
324324

325325
function registerPageTocDesktopCollapsedStyles(theme: ThemeMode) {
326+
styleBuilder
327+
.select(
328+
'.template-doc--toc-collapsed .doc-toc:not(.doc-toc--open) .page-toc__panel-toggle',
329+
theme,
330+
)
331+
.media(mediaAbove(BREAKPOINTS.toc))
332+
.display('flex')
326333
styleBuilder
327334
.select('.template-doc--toc-collapsed .doc-toc', theme)
328335
.media(mediaAbove(BREAKPOINTS.toc))

‎packages/ts-page-scripts/src/embed/navMenu.embed.ts‎

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

‎packages/ts-page-scripts/src/embed/pageToc.embed.ts‎

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import { runInDom } from '@purestack/ts-minidom'
2+
import { describe, expect, it, vi } from 'vitest'
3+
4+
describe('navigation resizing', () => {
5+
it.each([false, true])(
6+
'preserves width when collapsed is %s',
7+
async (collapsed) => {
8+
vi.resetModules()
9+
await runInDom(
10+
'<html><body class="template-doc--has-nav"><aside class="doc-sidebar"><nav class="nav__menu"><button data-nav-menu-toggle>Navigation</button></nav></aside></body></html>',
11+
async (document) => {
12+
if (collapsed) localStorage.setItem('ts-ssg:nav-collapsed:root', '1')
13+
const listeners = new Map<string, EventListener>()
14+
Object.assign(document, { getElementById: () => null })
15+
Object.assign(window, {
16+
matchMedia: () => ({ matches: true, addEventListener: vi.fn() }),
17+
addEventListener: (name: string, listener: EventListener) =>
18+
listeners.set(name, listener),
19+
removeEventListener: (name: string) => listeners.delete(name),
20+
})
21+
vi.stubGlobal('getComputedStyle', () => ({
22+
getPropertyValue: () => '',
23+
marginInlineEnd: '0px',
24+
}))
25+
vi.stubGlobal('requestAnimationFrame', (run: FrameRequestCallback) =>
26+
run(0),
27+
)
28+
try {
29+
await import('./navMenu')
30+
window.tsSsgNavMenu?.hydrate()
31+
const sidebar = document.querySelector('.doc-sidebar')
32+
const menu = document.querySelector('.nav__menu')
33+
const toggle = document.querySelector('[data-nav-menu-toggle]')
34+
if (!sidebar || !menu || !toggle)
35+
throw new Error('Missing navigation fixture')
36+
let right = 260
37+
Object.assign(sidebar, {
38+
getBoundingClientRect: () => ({ left: 0 }),
39+
})
40+
Object.assign(menu, { getBoundingClientRect: () => ({ right }) })
41+
const mouse = (type: string, clientX: number) =>
42+
Object.assign(
43+
new MouseEvent(type, {
44+
button: 0,
45+
bubbles: true,
46+
cancelable: true,
47+
}),
48+
{ clientX },
49+
)
50+
if (collapsed) toggle.dispatchEvent(new Event('click'))
51+
menu.dispatchEvent(mouse('mousedown', right))
52+
expect(document.body.classList.contains('resizing')).toBe(true)
53+
listeners.get('mousemove')?.(mouse('mousemove', 420))
54+
listeners.get('mouseup')?.(mouse('mouseup', 420))
55+
const widthProperty = '--ps-doc-layout-user-nav-width'
56+
const widthStorage = 'ts-ssg:doc-layout:nav-width'
57+
expect(document.body.style.getPropertyValue(widthProperty)).toBe(
58+
'420px',
59+
)
60+
expect(localStorage.getItem(widthStorage)).toBe('420')
61+
if (collapsed) {
62+
listeners.get('keydown')?.(
63+
Object.assign(new Event('keydown'), { key: 'Escape' }),
64+
)
65+
expect(sidebar.classList.contains('doc-sidebar--open')).toBe(
66+
false,
67+
)
68+
right = 26
69+
toggle.dispatchEvent(mouse('mousemove', 13))
70+
expect(document.body.classList.contains('col-resize')).toBe(false)
71+
toggle.dispatchEvent(mouse('mousedown', 13))
72+
listeners.get('mouseup')?.(mouse('mouseup', 13))
73+
toggle.dispatchEvent(new Event('click'))
74+
expect(sidebar.classList.contains('doc-sidebar--open')).toBe(true)
75+
}
76+
expect(document.body.classList.contains('resizing')).toBe(false)
77+
expect(document.body.style.getPropertyValue(widthProperty)).toBe(
78+
'420px',
79+
)
80+
expect(localStorage.getItem(widthStorage)).toBe('420')
81+
} finally {
82+
vi.unstubAllGlobals()
83+
}
84+
},
85+
)
86+
},
87+
)
88+
})

‎packages/ts-page-scripts/src/navMenu.ts‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,11 @@ function init(menu: HTMLElement) {
238238

239239
const isOnResizeEdge = (event: MouseEvent) => {
240240
if (!supportsDesktopCollapse()) return false
241+
if (
242+
document.body.classList.contains(NAV_COLLAPSED_CLASS) &&
243+
!sidebar?.classList.contains(NAV_OPEN_CLASS)
244+
)
245+
return false
241246
const rect = menu.getBoundingClientRect()
242247
return Math.abs(event.clientX - rect.right) <= NAV_RESIZE_EDGE_TOLERANCE_PX
243248
}

‎packages/ts-page-scripts/src/pageToc.test.ts‎

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,171 @@ describe('page toc refresh', () => {
218218
})
219219
})
220220

221+
describe('page toc Escape handling', () => {
222+
it.each([
223+
{
224+
layout: 'docked',
225+
narrow: false,
226+
collapsed: false,
227+
prevented: false,
228+
staysOpen: true,
229+
},
230+
{
231+
layout: 'mobile drawer',
232+
narrow: true,
233+
collapsed: false,
234+
prevented: false,
235+
staysOpen: false,
236+
},
237+
{
238+
layout: 'collapsed desktop drawer',
239+
narrow: false,
240+
collapsed: true,
241+
prevented: false,
242+
staysOpen: false,
243+
},
244+
{
245+
layout: 'mobile drawer with Escape handled by search',
246+
narrow: true,
247+
collapsed: false,
248+
prevented: true,
249+
staysOpen: true,
250+
},
251+
{
252+
layout: 'collapsed desktop drawer with Escape handled by search',
253+
narrow: false,
254+
collapsed: true,
255+
prevented: true,
256+
staysOpen: true,
257+
},
258+
])('$layout', async ({ narrow, collapsed, prevented, staysOpen }) => {
259+
vi.resetModules()
260+
await runInDom(
261+
'<html><body class="template-doc--has-toc"><main class="doc-main"><article class="doc-content"></article></main><aside class="doc-toc"><nav class="page-toc"><button class="page-toc__panel-toggle"><span class="page-toc__panel-toggle-label"></span></button><div class="page-toc__content"></div></nav></aside></body></html>',
262+
async (document) => {
263+
document.body.classList.toggle('template-doc--toc-collapsed', collapsed)
264+
const listeners = new Map<string, EventListener>()
265+
Object.assign(window, {
266+
matchMedia: (query: string) => ({
267+
matches: narrow && query === '(max-width: 1320px)',
268+
addEventListener: vi.fn(),
269+
}),
270+
location: { hash: '' },
271+
addEventListener: (name: string, listener: EventListener) => {
272+
listeners.set(name, listener)
273+
},
274+
})
275+
await import('./pageToc')
276+
window.tsSsgPageToc?.hydrate()
277+
const shell = requireElement('.doc-toc')
278+
const toggle = requireElement('.page-toc__panel-toggle')
279+
if (narrow || collapsed) toggle.dispatchEvent(new Event('click'))
280+
expect(shell.classList.contains('doc-toc--open')).toBe(true)
281+
const event = Object.assign(
282+
new Event('keydown', { cancelable: true }),
283+
{ key: 'Escape' },
284+
)
285+
if (prevented) event.preventDefault()
286+
const onKeydown = listeners.get('keydown')
287+
expect(onKeydown).toBeDefined()
288+
onKeydown?.(event)
289+
expect(shell.classList.contains('doc-toc--open')).toBe(staysOpen)
290+
expect(toggle.getAttribute('aria-expanded')).toBe(String(staysOpen))
291+
expect(document.body.classList.contains('doc-toc-open')).toBe(
292+
staysOpen && (narrow || collapsed),
293+
)
294+
},
295+
)
296+
})
297+
})
298+
299+
describe('page toc drawer resizing', () => {
300+
it.each([
301+
{ layout: 'responsive drawer', narrow: true, collapsed: false },
302+
{ layout: 'collapsed desktop drawer', narrow: false, collapsed: true },
303+
])(
304+
'preserves the resized width when reopening the $layout',
305+
async ({ narrow, collapsed }) => {
306+
vi.resetModules()
307+
await runInDom(
308+
'<html><body class="template-doc--has-toc"><main class="doc-main"><article class="doc-content"></article></main><aside class="doc-toc"><nav class="page-toc"><button class="page-toc__panel-toggle"><span class="page-toc__panel-toggle-label"></span></button><div class="page-toc__content"></div></nav></aside></body></html>',
309+
async (document) => {
310+
document.body.classList.toggle(
311+
'template-doc--toc-collapsed',
312+
collapsed,
313+
)
314+
const listeners = new Map<string, EventListener>()
315+
Object.assign(window, {
316+
matchMedia: (query: string) => ({
317+
matches: narrow && query === '(max-width: 1320px)',
318+
addEventListener: vi.fn(),
319+
}),
320+
location: { hash: '' },
321+
addEventListener: (name: string, listener: EventListener) =>
322+
listeners.set(name, listener),
323+
removeEventListener: (name: string) => listeners.delete(name),
324+
})
325+
vi.stubGlobal('getComputedStyle', () => ({
326+
getPropertyValue: () => '',
327+
marginInlineStart: '0px',
328+
}))
329+
try {
330+
await import('./pageToc')
331+
window.tsSsgPageToc?.hydrate()
332+
const shell = requireElement('.doc-toc')
333+
const toc = requireElement('.page-toc')
334+
const toggle = requireElement('.page-toc__panel-toggle')
335+
let left = 960
336+
Object.assign(shell, {
337+
getBoundingClientRect: () => ({ right: 1200 }),
338+
})
339+
Object.assign(toc, { getBoundingClientRect: () => ({ left }) })
340+
const mouse = (type: string, clientX: number) =>
341+
Object.assign(
342+
new MouseEvent(type, {
343+
button: 0,
344+
bubbles: true,
345+
cancelable: true,
346+
}),
347+
{ clientX },
348+
)
349+
toggle.dispatchEvent(new Event('click'))
350+
toc.dispatchEvent(mouse('mousedown', left))
351+
expect(document.body.classList.contains('resizing')).toBe(true)
352+
listeners.get('mousemove')?.(mouse('mousemove', 780))
353+
listeners.get('mouseup')?.(mouse('mouseup', 780))
354+
const widthProperty = '--ps-doc-layout-user-toc-width'
355+
const widthStorage = 'ts-ssg:doc-layout:toc-width'
356+
expect(document.body.style.getPropertyValue(widthProperty)).toBe(
357+
'420px',
358+
)
359+
expect(localStorage.getItem(widthStorage)).toBe('420')
360+
listeners.get('keydown')?.(
361+
Object.assign(new Event('keydown'), { key: 'Escape' }),
362+
)
363+
expect(shell.classList.contains('doc-toc--open')).toBe(false)
364+
left = 1174
365+
// A real button click includes down/up events within the old resize hit area.
366+
toggle.dispatchEvent(mouse('mousemove', 1187))
367+
expect(document.body.classList.contains('col-resize')).toBe(false)
368+
toggle.dispatchEvent(mouse('mousedown', 1187))
369+
listeners.get('mouseup')?.(mouse('mouseup', 1187))
370+
toggle.dispatchEvent(new Event('click'))
371+
expect(shell.classList.contains('doc-toc--open')).toBe(true)
372+
expect(document.body.classList.contains('resizing')).toBe(false)
373+
expect(document.body.style.getPropertyValue(widthProperty)).toBe(
374+
'420px',
375+
)
376+
expect(localStorage.getItem(widthStorage)).toBe('420')
377+
} finally {
378+
vi.unstubAllGlobals()
379+
}
380+
},
381+
)
382+
},
383+
)
384+
})
385+
221386
function requireElement(selector: string, root: ParentNode = document) {
222387
const element = root.querySelector(selector)
223388
if (!element) throw new Error(`Missing fixture element ${selector}`)

‎packages/ts-page-scripts/src/pageToc.ts‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,7 @@ function init(toc: HTMLElement) {
156156

157157
const isOnResizeEdge = (event: MouseEvent) => {
158158
if (!supportsDesktopResize()) return false
159+
if (!tocShell?.classList.contains(TOC_OPEN_CLASS)) return false
159160
const rect = toc.getBoundingClientRect()
160161
return Math.abs(event.clientX - rect.left) <= TOC_RESIZE_EDGE_TOLERANCE_PX
161162
}
@@ -269,6 +270,8 @@ function init(toc: HTMLElement) {
269270
window.addEventListener('keydown', (event) => {
270271
if (
271272
event.key === 'Escape' &&
273+
!event.defaultPrevented &&
274+
isCollapsible() &&
272275
tocShell.classList.contains(TOC_OPEN_CLASS)
273276
) {
274277
setTocOpen(false)

0 commit comments

Comments
 (0)