Skip to content

Commit

Permalink
feat(react): add focusTrap options to AnchoredOverlay (#1775)
Browse files Browse the repository at this point in the history
  • Loading branch information
scurker authored Dec 20, 2024
1 parent 2b0bb38 commit 0a00d24
Show file tree
Hide file tree
Showing 3 changed files with 90 additions and 1 deletion.
10 changes: 10 additions & 0 deletions docs/pages/components/AnchoredOverlay.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,16 @@ function AnchoredOverlayWithOffsetExample() {
type: 'number',
description: 'An optional offset number to position the anchor element from its anchored target.'
},
{
name: 'focusTrap',
type: 'boolean',
description: 'When set, traps focus within the AnchoredOverlay.'
},
{
name: 'focusTrapOptions',
type: 'object',
description: 'When `focusTrap` is true, optional arguments to configure the focus trap.'
},
{
name: 'as',
type: 'React.ElementType',
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import { render, screen, within } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import AnchoredOverlay from './';
import axe from '../../axe';
Expand Down Expand Up @@ -123,6 +123,76 @@ test('should call onPlacementChange with initial placement', () => {
expect(onPlacementChange).toHaveBeenCalledWith('top');
});

test('should not trap focus when focusTrap is false', async () => {
const targetRef = { current: document.createElement('button') };
const user = userEvent.setup();

render(
<>
<button>Outside Before</button>
<AnchoredOverlay target={targetRef} open focusTrap={false}>
<button>Inside</button>
</AnchoredOverlay>
<button>Outside After</button>
</>
);

const buttons = screen.getAllByRole('button');

buttons[0].focus();
expect(buttons[0]).toHaveFocus();
await user.tab();
expect(buttons[1]).toHaveFocus();
await user.tab();
expect(buttons[2]).toHaveFocus();
});

test('should trap focus when focusTrap is true', async () => {
const targetRef = { current: document.createElement('button') };
const user = userEvent.setup();

render(
<>
<button>Outside Before</button>
<AnchoredOverlay target={targetRef} open focusTrap data-testid="overlay">
<button>First</button>
<button>Second</button>
<button>Third</button>
</AnchoredOverlay>
<button>Outside After</button>
</>
);

const buttons = within(screen.getByTestId('overlay')).getAllByRole('button');

expect(buttons[0]).toHaveFocus();
await user.tab();
expect(buttons[1]).toHaveFocus();
await user.tab();
expect(buttons[2]).toHaveFocus();
await user.tab();
expect(buttons[0]).toHaveFocus();
});

test('should restore focus when focusTrap is unmounted', async () => {
const targetRef = { current: document.createElement('button') };
const outsideButton = document.createElement('button');
document.body.appendChild(outsideButton);
outsideButton.focus();

const { unmount } = render(
<AnchoredOverlay target={targetRef} open focusTrap data-testid="overlay">
<button>Inside Button</button>
</AnchoredOverlay>
);

expect(screen.getByText('Inside Button')).toHaveFocus();
unmount();
expect(outsideButton).toHaveFocus();

document.body.removeChild(outsideButton);
});

test('should support ref prop', () => {
const targetRef = { current: document.createElement('button') };
const ref = React.createRef<HTMLDivElement>();
Expand Down
9 changes: 9 additions & 0 deletions packages/react/src/components/AnchoredOverlay/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { type PolymorphicProps } from '../../utils/polymorphicComponent';
import resolveElement from '../../utils/resolveElement';
import useSharedRef from '../../utils/useSharedRef';
import useEscapeKey from '../../utils/useEscapeKey';
import useFocusTrap from '../../utils/useFocusTrap';

type AnchoredOverlayProps<
Overlay extends HTMLElement,
Expand All @@ -27,6 +28,10 @@ type AnchoredOverlayProps<
onPlacementChange?: (placement: Placement) => void;
/** An optional offset number to position the anchor element from its anchored target. */
offset?: number;
/** When set, traps focus within the AnchoredOverlay. */
focusTrap?: boolean;
/** When `focusTrap` is true, optional arguments to configure the focus trap. */
focusTrapOptions?: Parameters<typeof useFocusTrap>[1];
children?: React.ReactNode;
} & PolymorphicProps<React.HTMLAttributes<Overlay>>;

Expand Down Expand Up @@ -56,6 +61,8 @@ const AnchoredOverlay = forwardRef(
style,
open = false,
offset,
focusTrap,
focusTrapOptions,
onOpenChange,
onPlacementChange,
...props
Expand Down Expand Up @@ -99,6 +106,8 @@ const AnchoredOverlay = forwardRef(
}
});

useFocusTrap(ref, !focusTrap ? { disabled: true } : focusTrapOptions);

useEffect(() => {
if (typeof onPlacementChange === 'function') {
onPlacementChange(placement);
Expand Down

0 comments on commit 0a00d24

Please sign in to comment.