Something went wrong. Try again.
Bluesky app fork with some witchin' additions 💫
Something went wrong. Try again.
5.6 kB · 180 lines
TypeScript
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181/** @jest-environment jsdom */
import {handleOverlayNavigation} from '#/features/keyboardShortcuts/overlayNavigation.web'
function createOverlay(role: 'menu' | 'dialog') { const overlay = document.createElement('div') overlay.setAttribute('role', role) overlay.tabIndex = -1 document.body.appendChild(overlay) return overlay}
function addControl(overlay: HTMLElement, role?: string) { const control = document.createElement('button') if (role) { control.setAttribute('role', role) control.tabIndex = -1 } control.getBoundingClientRect = () => ({width: 100, height: 30}) as DOMRect overlay.appendChild(control) return control}
function press(key: string, options: KeyboardEventInit = {}) { const event = new KeyboardEvent('keydown', { key, bubbles: true, cancelable: true, ...options, }) document.activeElement!.dispatchEvent(event) return event}
beforeEach(() => { document.body.replaceChildren() window.addEventListener('keydown', handleOverlayNavigation, true)})
afterEach(() => { window.removeEventListener('keydown', handleOverlayNavigation, true)})
it.each(['menu', 'dialog'] as const)( 'moves and wraps focus in a %s without activating an option', role => { const overlay = createOverlay(role) const first = addControl(overlay, role === 'menu' ? 'menuitem' : undefined) const second = addControl(overlay, role === 'menu' ? 'menuitem' : undefined) const activate = jest.fn() second.addEventListener('click', activate) first.focus()
expect(press('j').defaultPrevented).toBe(true) expect(document.activeElement).toBe(second) press('j', {repeat: true}) expect(document.activeElement).toBe(first) press('k') expect(document.activeElement).toBe(second) expect(activate).not.toHaveBeenCalled() },)
it('chooses the first or last option when the menu container has focus', () => { const menu = createOverlay('menu') const first = addControl(menu, 'menuitemcheckbox') const last = addControl(menu, 'menuitemradio') menu.focus() press('j') expect(document.activeElement).toBe(first) menu.focus() press('k') expect(document.activeElement).toBe(last)})
it('keeps submenu navigation separate from the parent menu', () => { const parent = createOverlay('menu') const trigger = addControl(parent, 'menuitem') const submenu = createOverlay('menu') parent.appendChild(submenu) const first = addControl(submenu, 'menuitem') const last = addControl(submenu, 'menuitem') first.focus() press('k') expect(document.activeElement).toBe(last) trigger.focus() press('j') expect(document.activeElement).toBe(trigger)})
it('navigates a nested discard prompt independently of the composer', () => { const composer = createOverlay('dialog') addControl(composer) const prompt = createOverlay('dialog') composer.appendChild(prompt) const keepEditing = addControl(prompt) const discard = addControl(prompt) keepEditing.focus() press('j') expect(document.activeElement).toBe(discard) press('j') expect(document.activeElement).toBe(keepEditing)})
it('skips unavailable dialog controls', () => { const dialog = createOverlay('dialog') const first = addControl(dialog) addControl(dialog).disabled = true addControl(dialog).setAttribute('aria-disabled', 'true') addControl(dialog).hidden = true addControl(dialog).style.display = 'none' addControl(dialog).style.visibility = 'hidden' addControl(dialog).tabIndex = -1 const hidden = document.createElement('div') hidden.setAttribute('aria-hidden', 'true') dialog.appendChild(hidden) addControl(hidden) const last = addControl(dialog) first.focus() press('j') expect(document.activeElement).toBe(last)})
it.each(['input', 'textarea', 'select', 'contenteditable', 'slider'])( 'leaves j/k alone in a %s', kind => { const dialog = createOverlay('dialog') const field = document.createElement( ['contenteditable', 'slider'].includes(kind) ? 'div' : kind, ) field.tabIndex = 0 if (kind === 'contenteditable') field.setAttribute('contenteditable', 'true') if (kind === 'slider') field.setAttribute('role', 'slider') dialog.appendChild(field) addControl(dialog) field.focus() expect(press('j').defaultPrevented).toBe(false) expect(press('k').defaultPrevented).toBe(false) expect(document.activeElement).toBe(field) },)
it.each([ {ctrlKey: true}, {altKey: true}, {metaKey: true}, {shiftKey: true}, {isComposing: true},])('preserves modified keys and IME input: %o', options => { const menu = createOverlay('menu') const first = addControl(menu, 'menuitem') addControl(menu, 'menuitem') first.focus() expect(press('j', options).defaultPrevented).toBe(false) expect(document.activeElement).toBe(first)})
it('prevents menu typeahead and other handlers from processing j/k', () => { const menu = createOverlay('menu') const first = addControl(menu, 'menuitem') addControl(menu, 'menuitem') const typeahead = jest.fn() menu.addEventListener('keydown', typeahead) first.focus() press('j') expect(typeahead).not.toHaveBeenCalled() expect(press('Enter').defaultPrevented).toBe(false) expect(press('Escape').defaultPrevented).toBe(false) expect(typeahead).toHaveBeenCalledTimes(2)})
it('leaves page navigation and empty overlays alone', () => { const pageButton = addControl(document.body) pageButton.focus() expect(press('j').defaultPrevented).toBe(false) const emptyMenu = createOverlay('menu') emptyMenu.focus() expect(press('j').defaultPrevented).toBe(false)})