Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: correct focus order for non-autofocusable modals, fixes #340 #350

Merged
merged 1 commit into from
Dec 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 9 additions & 5 deletions src/Trap.js
Original file line number Diff line number Diff line change
Expand Up @@ -104,15 +104,13 @@ const activateTrap = () => {
const workingNode = observed || (lastPortaledElement && lastPortaledElement.portaledElement);

// check if lastActiveFocus is still reachable
if (focusOnBody() && lastActiveFocus) {
if (focusOnBody() && lastActiveFocus && lastActiveFocus !== document.body) {
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

focus on the body and the last element is body?
Can happen.

Unfortunately lastActiveFocus is used to hold the state of the Trap, and it is not just "last active element within trap", it's just "last active element recorded".
For autoFocus:false cases it will be body

if (
// it was removed
!document.body.contains(lastActiveFocus)
// or not focusable (this is expensive operation)!
|| isNotFocusable(lastActiveFocus)
) {
lastActiveFocus = null;
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

causes a "loop"


const newTarget = tryRestoreFocus();
if (newTarget) {
newTarget.focus();
Expand Down Expand Up @@ -159,6 +157,10 @@ const activateTrap = () => {
|| focusIsPortaledPair(activeElement, workingNode)
)
) {
// in case there no yet selected element(first activation),
// but there is some active element
// and autofocus is off
// - we blur currently active element and move focus to the body
if (document && !lastActiveFocus && activeElement && !autoFocus) {
// Check if blur() exists, which is missing on certain elements on IE
if (activeElement.blur) {
Expand All @@ -170,9 +172,11 @@ const activateTrap = () => {
lastPortaledElement = {};
}
}
focusWasOutsideWindow = false;
lastActiveFocus = document && document.activeElement;
tryRestoreFocus = captureFocusRestore(lastActiveFocus);
if (lastActiveFocus !== document.body) {
tryRestoreFocus = captureFocusRestore(lastActiveFocus);
}
focusWasOutsideWindow = false;
}
}

Expand Down
103 changes: 82 additions & 21 deletions stories/Exotic.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,29 +2,41 @@ import * as React from 'react';
import FocusLock from '../src/index';

export class Video extends React.Component {
state = {
disabled: true,
}
state = {
disabled: true,
}

toggle = () => this.setState({ disabled: !this.state.disabled });
toggle = () => this.setState({ disabled: !this.state.disabled });

render() {
const { disabled } = this.state;
return (
<div>
<button onClick={this.toggle}>!ACTIVATE THE TRAP!</button>
<FocusLock disabled={disabled} _whiteList={(node) => { console.log(node); return false; }}>
<button onClick={this.toggle}>deactivate</button>
<video controls width="250">
<source src="https://interactive-examples.mdn.mozilla.net/media/examples/flower.webm" type="video/webm" />
<source src="https://interactive-examples.mdn.mozilla.net/media/examples/flower.mp4" type="video/mp4" />
Sorry, your browser doesn't support embedded videos.
</video>
<button onClick={this.toggle}>deactivate</button>
</FocusLock>
</div>
);
}
render() {
const { disabled } = this.state;
return (
<div>
<button onClick={this.toggle}>!ACTIVATE THE TRAP!</button>
<FocusLock
disabled={disabled}
_whiteList={(node) => {
console.log(node);
return false;
}}
>
<button onClick={this.toggle}>deactivate</button>
<video controls width="250">
<source
src="https://interactive-examples.mdn.mozilla.net/media/examples/flower.webm"
type="video/webm"
/>
<source
src="https://interactive-examples.mdn.mozilla.net/media/examples/flower.mp4"
type="video/mp4"
/>
Sorry, your browser doesn't support embedded videos.
</video>
<button onClick={this.toggle}>deactivate</button>
</FocusLock>
</div>
);
}
}

export const FormOverride = () => (
Expand All @@ -36,3 +48,52 @@ export const FormOverride = () => (
</form>
</FocusLock>
);


const ModalWithoutAutoFocus = () => (
<div>
<dialog open style={{ border: '1px solid black' }}>
<FocusLock autoFocus={false}>
<div>
<h4>Title</h4>
<div>
<button>Button A</button>
</div>
<div>
<button>Button B</button>
</div>
<div>
<button>Button C</button>
</div>
</div>
</FocusLock>
</dialog>
</div>
);
export const NonAutofocusModal = () => {
const [isOpen, togglerIsOpen] = React.useState(false);

return (
<div className="App">
<div>
<div>
<button onClick={() => togglerIsOpen(true)}>Open modal</button>
</div>
<div>
<button>Other Button</button>
</div>
<div>
<button>Other Button</button>
</div>
<div>
<button>Other Button</button>
</div>
<div>
<button>Other Button</button>
</div>
</div>
<div>{isOpen && <ModalWithoutAutoFocus />}</div>
<button>Other Button</button>
</div>
);
};
5 changes: 3 additions & 2 deletions stories/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import { MUISelect, MUISelectWhite } from './MUI';
import Fight from './FocusFighting';
import { StyledComponent, StyledSection } from './Custom';
import { DisabledForm, DisabledFormWithTabIndex } from './Disabled';
import { FormOverride, Video } from './Exotic';
import { FormOverride, Video, NonAutofocusModal } from './Exotic';
import { TabbableParent } from './TabbableParent';
import { ControlTrapExample, GroupRowingFocusExample, RowingFocusExample } from './control';

Expand Down Expand Up @@ -83,7 +83,8 @@ storiesOf('Exotic', module)
.add('iframe - Sandbox', () => <Frame><SandboxedIFrame /></Frame>)
.add('sidecar', () => <Frame><SideCar /></Frame>)
.add('tabbable parent', () => <Frame><TabbableParent /></Frame>)
.add('form override', () => <Frame><FormOverride /></Frame>);
.add('form override', () => <Frame><FormOverride /></Frame>)
.add('non autofocusable', () => <Frame><NonAutofocusModal /></Frame>);

storiesOf('FocusScope', module)
.add('keyboard navigation', () => <Frame><ControlTrapExample /></Frame>)
Expand Down
Loading