Skip to content

Commit

Permalink
fix: auto-focus in case of activeElement disappeareance, fixes #321
Browse files Browse the repository at this point in the history
  • Loading branch information
theKashey committed Aug 24, 2024
1 parent 57999e2 commit a109314
Show file tree
Hide file tree
Showing 3 changed files with 118 additions and 10 deletions.
6 changes: 3 additions & 3 deletions .size-limit.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[
{
"path": "dist/cjs/UI.js",
"limit": "3.8 KB",
"limit": "3.9 KB",
"ignore": [
"prop-types",
"@babel/runtime",
Expand All @@ -10,7 +10,7 @@
},
{
"path": "dist/es2015/sidecar.js",
"limit": "4.6 KB",
"limit": "4.8 KB",
"ignore": [
"prop-types",
"@babel/runtime",
Expand All @@ -19,7 +19,7 @@
},
{
"path": "dist/es2015/index.js",
"limit": "6.9 KB",
"limit": "7.1 KB",
"ignore": [
"prop-types",
"@babel/runtime",
Expand Down
92 changes: 87 additions & 5 deletions _tests/keep-focus.spec.js
Original file line number Diff line number Diff line change
@@ -1,20 +1,102 @@
import React from 'react';
import {
render, screen,
render,
} from '@testing-library/react';
import { expect } from 'chai';
import FocusLock from '../src';
import { deferAction } from '../src/util';

describe('Focus restoration', () => {
describe('Focus restoration', async () => {
it('maintains focus on element removal', async () => {
const { rerender } = render(
<FocusLock>
<button type="button" key={1}>btn1</button>
</FocusLock>,
);
//
expect(document.activeElement.innerHTML).to.be.equal('btn1');

rerender(
<FocusLock>
<button type="button" key={2}>new button</button>
</FocusLock>,
);

// wait
await new Promise(deferAction);

expect(document.activeElement.innerHTML).to.be.equal('new button');
});

it('handles disabled elements', async () => {
const { rerender } = render(
<FocusLock>
<button type="button">btn1</button>
<button type="button">btn2</button>
</FocusLock>,
);
//
expect(document.activeElement.innerHTML).to.be.equal('btn1');


// https://github.com/jsdom/jsdom/issues/3029 - jsdom does trigger blur on disabled
document.activeElement.blur();
document.body.focus();

rerender(
<FocusLock>
<button type="button" disabled>btn1</button>
<button type="button">btn2</button>
</FocusLock>,
);

// wait
await new Promise(deferAction);

expect(document.activeElement.innerHTML).to.be.equal('btn2');
});

it('moves focus to the nearest element', async () => {
render(
<FocusLock>
<button type="button">test</button>
<button type="button">btn1</button>
<button type="button">btn2</button>
<button type="button" id="middle-button">btn3</button>
<button type="button">btn4</button>
<button type="button">btn5</button>
</FocusLock>,
);
//
expect(document.activeElement).to.be.equal(screen.getByRole('button'));
const middleButton = document.getElementById('middle-button');
middleButton.focus();
expect(document.activeElement).to.be.equal(middleButton);

middleButton.parentElement.removeChild(middleButton);
// wait
await new Promise(deferAction);

// btn4 "replaces" bnt3 in visual order
expect(document.activeElement.innerHTML).to.be.equal('btn4');
});

it.todo('selects closes element to restore focus');
it('moves focus to the nearest element before', async () => {
render(
<FocusLock>
<button type="button">btn1</button>
<button type="button">btn2</button>
<button type="button" id="middle-button">btn3</button>
</FocusLock>,
);
//
const middleButton = document.getElementById('middle-button');
middleButton.focus();
expect(document.activeElement).to.be.equal(middleButton);

middleButton.parentElement.removeChild(middleButton);
// wait
await new Promise(deferAction);

// btn2 is just before bnt3
expect(document.activeElement.innerHTML).to.be.equal('btn2');
});
});
30 changes: 28 additions & 2 deletions src/Trap.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ import PropTypes from 'prop-types';
import withSideEffect from 'react-clientside-effect';
import {
moveFocusInside, focusInside,
focusIsHidden, expandFocusableNodes,
focusIsHidden,
expandFocusableNodes,
getFocusableNodes,
focusNextElement,
focusPrevElement,
focusFirstElement,
Expand All @@ -22,6 +24,7 @@ const isFreeFocus = () => focusOnBody() || focusIsHidden();

let lastActiveTrap = null;
let lastActiveFocus = null;
let tryRestoreFocus = () => null;

let lastPortaledElement = null;

Expand Down Expand Up @@ -86,13 +89,35 @@ const withinHost = (activeElement, workingArea) => (
workingArea.some(area => checkInHost(activeElement, area, area))
);

const isNotFocusable = node => (
!getFocusableNodes([node.parentNode], new Map()).some(el => el.node === node)
);

const activateTrap = () => {
let result = false;
if (lastActiveTrap) {
const {
observed, persistentFocus, autoFocus, shards, crossFrame, focusOptions,
} = lastActiveTrap;
const workingNode = observed || (lastPortaledElement && lastPortaledElement.portaledElement);

// check if lastActiveFocus is still reachable
if (focusOnBody() && lastActiveFocus) {
if (
// it was removed
!document.body.contains(lastActiveFocus)
// or not focusable (this is expensive operation)!
|| isNotFocusable(lastActiveFocus)
) {
lastActiveFocus = null;

const newTarget = tryRestoreFocus();
if (newTarget) {
newTarget.focus();
}
}
}

const activeElement = document && document.activeElement;
if (workingNode) {
const workingArea = [
Expand Down Expand Up @@ -130,11 +155,12 @@ const activateTrap = () => {
}
focusWasOutsideWindow = false;
lastActiveFocus = document && document.activeElement;
tryRestoreFocus = captureFocusRestore(lastActiveFocus);
}
}

if (document
// element was changed
// element was changed by moveFocusInside
&& activeElement !== document.activeElement
// fast check for any auto-guard
&& document.querySelector('[data-focus-auto-guard]')) {
Expand Down

0 comments on commit a109314

Please sign in to comment.