@@ -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+
221386function requireElement ( selector : string , root : ParentNode = document ) {
222387 const element = root . querySelector ( selector )
223388 if ( ! element ) throw new Error ( `Missing fixture element ${ selector } ` )
0 commit comments