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

Add ability to pass containerElements to focus-trap #179

Merged
merged 4 commits into from
Nov 17, 2020
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
5 changes: 5 additions & 0 deletions .changeset/young-crews-report.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'focus-trap-react': minor
---

Add ability to pass containerElements to focus-trap #179. This PR is made possible because of https://github.com/focus-trap/focus-trap/pull/217 and the released version 6.2.0 of focus-trap.
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,12 @@ Type: `Boolean`, optional

If you would like to pause or unpause the focus trap (see [`focus-trap`'s documentation](https://github.com/focus-trap/focus-trap#focustrappause)), toggle this prop.

#### containerElements

Type: `Array of HTMLElement`, optional

If passed in, these elements will be used as the boundaries for the focus-trap, instead of the child. These get passed as arguments to focus-trap's updateContainerElements method.
stefcameron marked this conversation as resolved.
Show resolved Hide resolved

## Contributing

See [CONTRIBUTING](CONTRIBUTING.md).
Expand Down
60 changes: 60 additions & 0 deletions cypress/integration/focus-trap-demo.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -241,4 +241,64 @@ describe('<FocusTrap> component', () => {
verifyCrucialFocusTrapOnClicking('@trapChild');
});
});

describe('demo: containerElements prop', () => {
it('containerElements can be passed in and used as multiple boundaries to keep the focus within', () => {
cy.get('#demo-containerelements').as('testRoot');

// activate trap
cy.get('@testRoot')
.findByRole('button', { name: 'activate trap' })
.as('lastlyFocusedElementBeforeTrapIsActivated')
.click();

// 1st element should be focused
cy.get('@testRoot')
.findByRole('link', { name: 'with' })
.as('firstElementInTrap')
.should('be.focused');

// trap is active(keep focus in trap by tabbing through the focus trap's tabbable elements)
cy.get('@firstElementInTrap')
.tab()
.should('have.text', 'some')
.should('be.focused')
.tab()
.should('have.text', 'focusable')
.should('be.focused')
.tab()
.should('have.text', 'See')
.should('be.focused')
.tab()
.should('have.text', 'how')
.should('be.focused')
.tab()
.should('have.text', 'works')
.should('be.focused')
.tab()
.should('have.text', 'with')
.should('be.focused')
.tab({ shift: true })
.should('have.text', 'works')
.should('be.focused')
.tab({ shift: true })
.should('have.text', 'how')
.should('be.focused')
.tab({ shift: true })
.should('have.text', 'See')
.should('be.focused')
.tab({ shift: true })
.should('have.text', 'focusable')
.should('be.focused')
.tab({ shift: true })
.should('have.text', 'some')
.should('be.focused')
.tab({ shift: true })
.should('have.text', 'with')
.should('be.focused')
.tab({ shift: true })
.should('have.text', 'works')
.should('be.focused');
});
});
});
6 changes: 6 additions & 0 deletions demo/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,12 @@ <h2>demo autofocus</h2>
</p>
<div id="demo-autofocus"></div>

<h2>demo containerElements</h2>
<p>
You may pass in an array prop `containerElements` that contains nodes to trap focus within, which if passed will be used instead of the direct child.
</p>
<div id="demo-containerelements"></div>

<p>
<span style="font-size:2em;vertical-align:middle;">☜</span>
<a href="https://github.com/focus-trap/focus-trap-react" style="vertical-align:middle;">Return to the repository</a>
Expand Down
82 changes: 82 additions & 0 deletions demo/js/demo-containerelements.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
const React = require('react');
const ReactDOM = require('react-dom');
const FocusTrap = require('../../dist/focus-trap-react');

const container = document.getElementById('demo-containerelements');

class DemoContainerElements extends React.Component {
constructor(props) {
super(props);

this.state = {
activeTrap: false,
};

this.mountTrap = this.mountTrap.bind(this);
this.unmountTrap = this.unmountTrap.bind(this);
this.setElementRef = this.setElementRef.bind(this);

this.elementRef1 = React.createRef();
this.elementRef2 = React.createRef();
}

mountTrap() {
this.setState({ activeTrap: true });
}

unmountTrap() {
this.setState({ activeTrap: false });
}

setElementRef(refName) {
return (element) => {
if (!this[refName].current) {
this[refName].current = element;
this.forceUpdate();
}
};
}

render() {
const trap = this.state.activeTrap ? (
<FocusTrap
containerElements={[this.elementRef1.current, this.elementRef2.current]}
focusTrapOptions={{
onDeactivate: this.unmountTrap,
clickOutsideDeactivates: true,
}}
>
<div className="trap is-active">
<p ref={this.setElementRef('elementRef1')}>
Here is a focus trap <a href="#">with</a> <a href="#">some</a>
<a href="#">focusable</a> parts.
</p>
<p>
Here is <a href="#">something</a>.
</p>
<p ref={this.setElementRef('elementRef2')}>
Here is a another focus trap element. <a href="#">See</a>{' '}
<a href="#">how</a>
it <a href="#">works</a>.
</p>
<p>
<button onClick={this.unmountTrap}>deactivate trap</button>
</p>
</div>
</FocusTrap>
) : (
false
);

return (
<div>
<p>
<button onClick={this.mountTrap}>activate trap</button>
</p>
{trap}
</div>
);
}
}

ReactDOM.render(<DemoContainerElements />, container);
1 change: 1 addition & 0 deletions demo/js/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ require('./demo-defaults');
require('./demo-ffne');
require('./demo-special-element');
require('./demo-autofocus');
require('./demo-containerelements');
1 change: 1 addition & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ declare namespace FocusTrap {
active?: boolean;
paused?: boolean;
focusTrapOptions?: FocusTrapOptions;
containerElements?: Array<HTMLElement>;
}
}

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@
"typescript": "^4.0.5"
},
"dependencies": {
"focus-trap": "^6.1.4"
"focus-trap": "^6.2.0"
},
"peerDependencies": {
"prop-types": "^15.7.2",
Expand Down
88 changes: 56 additions & 32 deletions src/focus-trap-react.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,43 +57,62 @@ class FocusTrap extends React.Component {
}
}

componentDidMount() {
const focusTrapElementDOMNode = ReactDOM.findDOMNode(this.focusTrapElement);

// eslint-disable-next-line react/prop-types -- _createFocusTrap is an internal prop
this.focusTrap = this.props._createFocusTrap(
focusTrapElementDOMNode,
this.tailoredFocusTrapOptions
);

if (this.props.active) {
this.focusTrap.activate();
setupFocusTrap() {
if (!this.focusTrap) {
const focusTrapElementDOMNodes = this.focusTrapElements.map(
ReactDOM.findDOMNode
);

const nodesExist = focusTrapElementDOMNodes.some(Boolean);
if (nodesExist) {
// eslint-disable-next-line react/prop-types -- _createFocusTrap is an internal prop
this.focusTrap = this.props._createFocusTrap(
focusTrapElementDOMNodes,
this.tailoredFocusTrapOptions
);

if (this.props.active) {
this.focusTrap.activate();
}

if (this.props.paused) {
this.focusTrap.pause();
}
}
}
}

if (this.props.paused) {
this.focusTrap.pause();
}
componentDidMount() {
this.setupFocusTrap();
}

componentDidUpdate(prevProps) {
if (prevProps.active && !this.props.active) {
// NOTE: we never let the trap return the focus since we do that ourselves
this.focusTrap.deactivate({ returnFocus: false });
if (this.returnFocusOnDeactivate) {
this.returnFocus();
this.setupFocusTrap();

if (this.focusTrap) {
if (prevProps.containerElements !== this.props.containerElements) {
this.focusTrap.updateContainerElements(this.props.containerElements);
}
return; // un/pause does nothing on an inactive trap
}

if (!prevProps.active && this.props.active) {
this.updatePreviousElement();
this.focusTrap.activate();
}
if (prevProps.active && !this.props.active) {
// NOTE: we never let the trap return the focus since we do that ourselves
this.focusTrap.deactivate({ returnFocus: false });
if (this.returnFocusOnDeactivate) {
this.returnFocus();
}
return; // un/pause does nothing on an inactive trap
}

if (!prevProps.active && this.props.active) {
this.updatePreviousElement();
this.focusTrap.activate();
}

if (prevProps.paused && !this.props.paused) {
this.focusTrap.unpause();
} else if (!prevProps.paused && this.props.paused) {
this.focusTrap.pause();
if (prevProps.paused && !this.props.paused) {
this.focusTrap.unpause();
} else if (!prevProps.paused && this.props.paused) {
this.focusTrap.pause();
}
}
}

Expand All @@ -105,20 +124,24 @@ class FocusTrap extends React.Component {
}
}

setFocusTrapElement = (element) => {
this.focusTrapElement = element;
setFocusTrapElements = (elements) => {
this.focusTrapElements = elements;
};

render() {
const child = React.Children.only(this.props.children);

const composedRefCallback = (element) => {
this.setFocusTrapElement(element);
const { containerElements } = this.props;

if (typeof child.ref === 'function') {
child.ref(element);
} else if (child.ref) {
child.ref.current = element;
}

const elements = containerElements ? containerElements : [element];
this.setFocusTrapElements(elements);
};

const childWithRef = React.cloneElement(child, {
Expand Down Expand Up @@ -159,6 +182,7 @@ FocusTrap.propTypes = {
allowOutsideClick: PropTypes.oneOfType([PropTypes.bool, PropTypes.func]),
preventScroll: PropTypes.bool,
}),
containerElements: PropTypes.arrayOf(PropTypes.instanceOf(ElementType)),
children: PropTypes.oneOfType([
PropTypes.element, // React element
PropTypes.instanceOf(ElementType), // DOM element
Expand Down
Loading