Skip to content

Commit

Permalink
fix: add global layers
Browse files Browse the repository at this point in the history
  • Loading branch information
renrizzolo committed Dec 23, 2022
1 parent 7463518 commit 007d3a5
Show file tree
Hide file tree
Showing 3 changed files with 159 additions and 12 deletions.
59 changes: 50 additions & 9 deletions src/components/DismissibleFocusTrap/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import PropTypes from 'prop-types';
import { getFocusableNodes } from '../../lib/focus';
import { useClickOutside } from '../../hooks';

const layers = new Set();

const DismissibleFocusTrap = ({
loop = true,
focusOnMount = true,
Expand All @@ -17,17 +19,31 @@ const DismissibleFocusTrap = ({
const contentRef = React.useRef();
const clickedOutsideRef = React.useRef();

const getIsHighestLayer = React.useCallback(() => {
const layersArr = Array.from(layers);
const isHighestLayer = layersArr.indexOf(contentRef.current) === Math.max(layers.size - 1, 0);
return { isHighestLayer, size: layers.size };
}, []);

const clickOutsideHandler = React.useCallback(
(event) => {
if (disabled) return;
if (event.defaultPrevented) return;

if (onClickOutside) {
// don't steal focus if closing via clicking outside
clickedOutsideRef.current = true;
onClickOutside(event);
const { isHighestLayer, size } = getIsHighestLayer();
if (isHighestLayer) {
if (size === 1) {
// don't steal focus if closing via clicking outside
clickedOutsideRef.current = true;
}
onClickOutside?.(event);
event.stopPropagation();
event.preventDefault();
}
}
},
[onClickOutside, disabled]
[onClickOutside, getIsHighestLayer, disabled]
);

useClickOutside(contentRef, clickOutsideHandler);
Expand All @@ -36,6 +52,7 @@ const DismissibleFocusTrap = ({
if (disabled || !focusOnMount) return;
const previousFocusEl = document.activeElement;
const nodes = getFocusableNodes(contentRef?.current, { tabbable: true });

// timeouts are a hack for cases where the state that causes this component to mount/unmount
// also toggles the visibility of the previousFocusEl/parent
window.setTimeout(() => {
Expand All @@ -50,6 +67,16 @@ const DismissibleFocusTrap = ({
};
}, [disabled, focusOnMount]);

React.useEffect(() => {
if (disabled) return;
const node = contentRef.current;
node && layers.add(node);

return () => {
node && layers.delete(node);
};
}, [contentRef, disabled]);

const handleKeyDown = React.useCallback(
(event) => {
if (disabled) return;
Expand All @@ -73,14 +100,28 @@ const DismissibleFocusTrap = ({
}
}
}
},
[disabled, onTabExit, loop, onShiftTabExit]
);

React.useEffect(() => {
const onEscapeKeyDown = (event) => {
if (event.key === 'Escape') {
if (disabled) return;
if (event.defaultPrevented) return;
event.stopPropagation();
onEscape?.(event);
const { isHighestLayer } = getIsHighestLayer();
if (isHighestLayer) {
onEscape?.(event);
event.stopPropagation();
event.preventDefault();
}
}
},
[disabled, loop, onTabExit, onEscape, onShiftTabExit]
);
};
document.addEventListener('keydown', onEscapeKeyDown);
return () => {
document.removeEventListener('keydown', onEscapeKeyDown);
};
}, [getIsHighestLayer, disabled, onEscape]);

return (
<div data-testid="focus-trap" ref={contentRef} onKeyDown={handleKeyDown} {...rest}>
Expand Down
75 changes: 74 additions & 1 deletion src/components/DismissibleFocusTrap/index.spec.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ describe('<FocusTrap />', () => {
expect(onEscape).toBeCalledTimes(1);
});

it('shouldwork with onClickOutside', () => {
it('should work with onClickOutside', () => {
const onClickOutside = jest.fn();
const { getAllByRole, getByTestId } = render(
<div data-testid="outer">
Expand Down Expand Up @@ -241,4 +241,77 @@ describe('<FocusTrap />', () => {
});
expect(document.body).toHaveFocus();
});

it('should be nestable', () => {
const Comp = () => {
const [open1, setOpen1] = React.useState(false);
const [open2, setOpen2] = React.useState(false);

return (
<div>
<button onClick={() => setOpen1(true)}>test 3</button>

{open1 && (
<FocusTrap
onEscape={() => {
console.log('esc1');
setOpen1(false);
}}
>
<button onClick={() => setOpen2(true)}>test 1</button>
{open2 && (
<FocusTrap
onEscape={() => {
console.log('esc2');
setOpen2(false);
}}
>
<button>test 2</button>
</FocusTrap>
)}
</FocusTrap>
)}
</div>
);
};

const { getByText } = render(<Comp />);

expect(getByText('test 3')).toBeInTheDocument();
act(() => {
userEvent.tab();
userEvent.keyboard('[Enter]');
});
expect(getByText('test 1')).toBeInTheDocument();

act(() => {
jest.runAllTimers();
});

act(() => {
expect(getByText('test 1')).toHaveFocus();
userEvent.keyboard('[Enter]');
});

act(() => {
jest.runAllTimers();
});

expect(getByText('test 2')).toBeInTheDocument();
expect(getByText('test 2')).toHaveFocus();

act(() => {
userEvent.keyboard('[Escape]');
});
act(() => {
jest.runAllTimers();
});

expect(getByText('test 1')).toHaveFocus();

act(() => {
userEvent.keyboard('[Escape]');
jest.runAllTimers();
});
});
});
37 changes: 35 additions & 2 deletions www/examples/ActionPanel.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -91,13 +91,19 @@ class Example extends React.PureComponent {
render() {
return (
<React.Fragment>
<Button onClick={this.toggleActionPanel}>Action Panel as a modal</Button>
<Popover
popoverContent={<Button onClick={this.toggleActionPanel}>Action Panel as a modal</Button>}
triggers="click"
isMenu
>
<button>popover</button>
</Popover>
{this.state.showActionPanel && (
<ActionPanel
title="Action Panel"
size="small"
onClose={this.toggleActionPanel}
visuallyHidden={this.state.showActionPanel2}
// visuallyHidden={this.state.showActionPanel2} onEscapeClose={() => console.log('this')}
actionButton={<Button onClick={this.toggleActionPanel}>Save</Button>}
isModal
children={
Expand All @@ -114,6 +120,7 @@ class Example extends React.PureComponent {
{this.state.showActionPanel2 && (
<ActionPanel
title="Action Panel 2"
onEscapeClose={() => console.log('this')}
size="medium"
onClose={this.toggleActionPanel2}
cancelText="Back"
Expand All @@ -130,10 +137,36 @@ class Example extends React.PureComponent {
isModal
children={
<div>
<Popover popoverContent={<button>1</button>} isMenu>
<button>popover</button>
</Popover>
<DatePicker
className="form-control"
dateFormat="DD MMM YYYY"
selected={new Date()}
onChange={() => {}}
placeholderText="Date e.g. 03 Sep 2016"
disableInlineEditing={false}
/>
Native mammals include the dingoes or wild dogs, numbats, quolls, and Tasmanian devils. Dingoes
are the largest carnivorous mammals that populate the wilds of mainland Australia. But the
smaller numbats and Tasmanian devils, which are house cat-like size can be seen only in wildlife
parks. You can also spot them in the wilds of Tasmania.
<Select
isInModal
isClearable={false}
name="countriesSelect"
noOptionsMessage={() => "Sorry, couldn't find that country."}
options={[
{ label: 'a', value: 'A' },
{ label: 'b', value: 'b' },
{ label: 'c', value: 'c' },
]}
placeholder="Countries"
value={{}}
onChange={() => {}}
dts="test-dts"
/>
</div>
}
/>
Expand Down

0 comments on commit 007d3a5

Please sign in to comment.