diff --git a/jump.js b/jump.js index c878cc0..75778fc 100644 --- a/jump.js +++ b/jump.js @@ -1,6 +1,7 @@ export function jumpKeyUX() { return window => { let jumps = [] + let lastFocusedElement = null function focus(next) { let current = window.document.activeElement @@ -8,27 +9,44 @@ export function jumpKeyUX() { jumps.push(new WeakRef(current)) } next.focus({ focusVisible: true }) + lastFocusedElement = null } - function back() { - let ref = jumps.pop() - if (!ref) { - window.document.activeElement.blur() - return + function jumpBack() { + let ref = jumps.pop(); + if (ref) { + let el = ref.deref(); + + if (el && el.isConnected) { + el.focus(); + return true; + } + return jumpBack(); } - let el = ref.deref() - if (el && el.isConnected) { - el.focus() - } else { - back() + return false; + } + + function blur() { + let activeElement = document.activeElement; + if (activeElement && activeElement !== document.body) { + lastFocusedElement = activeElement; } + activeElement.blur(); } - + + function jumpBackOrBlur() { + let successfullyJumped = jumpBack(); + if (!successfullyJumped) { + blur(); + } + } + let tries = 0 let finding function jump(from) { clearInterval(finding) + tries = 0 let ariaControls = from.getAttribute('aria-controls') finding = setInterval(() => { if (tries++ > 50) { @@ -53,14 +71,21 @@ export function jumpKeyUX() { }, 50) } + function restoreFocus(event) { + event.preventDefault() + lastFocusedElement.focus({ focusVisible: true }) + lastFocusedElement = null + } + function keyDown(event) { - if (event.target.getAttribute('aria-controls')) { - if (event.key === 'Enter') { - jump(event.target) - } + if (event.target.getAttribute('aria-controls') && event.key === 'Enter') { + jump(event.target) } - if (event.key === 'Escape') { - back() + + if (event.key === 'Tab' && lastFocusedElement) { + restoreFocus(event) + } else if (event.key === 'Escape') { + jumpBackOrBlur() } } diff --git a/package.json b/package.json index 25516b4..a7c2f22 100644 --- a/package.json +++ b/package.json @@ -85,7 +85,7 @@ "import": { "./index.js": "{ startKeyUX, hotkeyKeyUX, pressKeyUX, focusGroupKeyUX, jumpKeyUX, hiddenKeyUX, likelyWithKeyboard, getHotKeyHint, hotkeyOverrides, hotkeyMacCompat }" }, - "limit": "2162 B" + "limit": "2208 B" } ], "clean-publish": { diff --git a/test/jump.test.ts b/test/jump.test.ts index fa2f595..f82703e 100644 --- a/test/jump.test.ts +++ b/test/jump.test.ts @@ -6,8 +6,35 @@ import { setTimeout } from 'node:timers/promises' import { jumpKeyUX, startKeyUX } from '../index.js' import { keyboardClick, mouseClick, press } from './utils.js' +test('jumps to next next elemnt, blur and restore focus on the last element', async () => { + let { window } = new JSDOM() + global.document = window.document + + startKeyUX(window, [jumpKeyUX()]) + window.document.body.innerHTML = + '' + + '' + let step1 = window.document.querySelector('#step1')! + let step2 = window.document.querySelector('#step1')! + + step1.focus() + + press(window, 'Tab') + equal(window.document.activeElement, step1) + await setTimeout(50) + equal(window.document.activeElement, step2) + + press(window, 'Escape') + equal(window.document.activeElement, window.document.body) + + press(window, 'Tab') + equal(window.document.activeElement, step2) +}) + test('jumps to next area by click and back by escape', async () => { - let window = new JSDOM().window + let { window } = new JSDOM() + global.document = window.document + startKeyUX(window, [jumpKeyUX()]) window.document.body.innerHTML = '' + @@ -26,6 +53,7 @@ test('jumps to next area by click and back by escape', async () => { step1.focus() keyboardClick(window, step1) + equal(window.document.activeElement, step1) await setTimeout(50) equal(window.document.activeElement, step2button) @@ -71,6 +99,7 @@ test('stops event tracking', async () => { keyboardClick(window, step1) await setTimeout(50) equal(window.document.activeElement, window.document.body) + }) test('ignores mouse click', async () => {