diff --git a/docs/pages/components/AnchoredOverlay.mdx b/docs/pages/components/AnchoredOverlay.mdx
index 13bd6cfd4..17b6af5d1 100644
--- a/docs/pages/components/AnchoredOverlay.mdx
+++ b/docs/pages/components/AnchoredOverlay.mdx
@@ -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',
diff --git a/packages/react/src/components/AnchoredOverlay/AnchoredOverlay.test.tsx b/packages/react/src/components/AnchoredOverlay/AnchoredOverlay.test.tsx
index 4fdbd385b..53bb799b3 100644
--- a/packages/react/src/components/AnchoredOverlay/AnchoredOverlay.test.tsx
+++ b/packages/react/src/components/AnchoredOverlay/AnchoredOverlay.test.tsx
@@ -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';
@@ -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(
+ <>
+
+
+
+
+
+ >
+ );
+
+ 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(
+ <>
+
+
+
+
+
+
+
+ >
+ );
+
+ 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(
+
+
+
+ );
+
+ 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();
diff --git a/packages/react/src/components/AnchoredOverlay/index.tsx b/packages/react/src/components/AnchoredOverlay/index.tsx
index 74b40379d..4934bda75 100644
--- a/packages/react/src/components/AnchoredOverlay/index.tsx
+++ b/packages/react/src/components/AnchoredOverlay/index.tsx
@@ -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,
@@ -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[1];
children?: React.ReactNode;
} & PolymorphicProps>;
@@ -56,6 +61,8 @@ const AnchoredOverlay = forwardRef(
style,
open = false,
offset,
+ focusTrap,
+ focusTrapOptions,
onOpenChange,
onPlacementChange,
...props
@@ -99,6 +106,8 @@ const AnchoredOverlay = forwardRef(
}
});
+ useFocusTrap(ref, !focusTrap ? { disabled: true } : focusTrapOptions);
+
useEffect(() => {
if (typeof onPlacementChange === 'function') {
onPlacementChange(placement);