src/drawer.ts (1829 bytes)
1 // The menu. On a phone it is a drawer over the page. On a desktop it is a 2 // side panel that always shows. 3 4 /** The width from which the menu is a side panel. The same as the media query in style.css. */ 5 const desktop = window.matchMedia('(min-width: 900px)'); 6 7 export interface Drawer { 8 /** True while the drawer covers the page: a phone with the drawer open. */ 9 covers(): boolean; 10 /** True while the menu shows: a desktop, or an open drawer. */ 11 shows(): boolean; 12 /** Opens or closes the drawer. */ 13 set(open: boolean): void; 14 /** Closes the drawer on a phone, so the user sees the page after an action in the menu. */ 15 closeOnPhone(): void; 16 } 17 18 /** `onOpen` runs each time the drawer opens. */ 19 export function setupDrawer( 20 ui: { menu: HTMLButtonElement; panel: HTMLElement }, 21 onOpen: () => void, 22 ): Drawer { 23 const open = (): boolean => document.body.classList.contains('menu-open'); 24 const covers = (): boolean => !desktop.matches && open(); 25 26 function set(value: boolean): void { 27 if (value === open()) return; 28 document.body.classList.toggle('menu-open', value); 29 ui.menu.setAttribute('aria-expanded', String(value)); 30 if (value) onOpen(); 31 } 32 33 ui.menu.addEventListener('click', () => set(!open())); 34 35 // With the drawer open, a tap outside it only closes it. The capture 36 // phase runs before the handlers of the page, so the tap does no lookup. 37 document.addEventListener( 38 'click', 39 (e) => { 40 if (!covers()) return; 41 const target = e.target as Node; 42 if (ui.panel.contains(target) || ui.menu.contains(target)) return; 43 e.stopPropagation(); 44 e.preventDefault(); 45 set(false); 46 }, 47 true, 48 ); 49 50 return { 51 covers, 52 shows: () => desktop.matches || open(), 53 set, 54 closeOnPhone() { 55 if (!desktop.matches) set(false); 56 }, 57 }; 58 }