Skip to content

Commit b4474d6

Browse files
feat: Add <FocusTrap> docs (#258)
* Add initial files * Add web docs * Touch up * Fix logic of includeTriggerInFocusTrap to focus on initial render * Add test * Fix build error * Update changelogs
1 parent 1080deb commit b4474d6

File tree

17 files changed

+237
-5
lines changed

17 files changed

+237
-5
lines changed

apps/docs/docgen.config.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,7 @@ module.exports = {
145145
'numpad/Numpad',
146146
'overlays/Alert',
147147
'overlays/OverlayContentContext',
148+
'overlays/FocusTrap',
148149
'overlays/FullscreenAlert',
149150
'overlays/modal/Modal',
150151
'overlays/modal/ModalHeader',
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
:::note Before using FocusTrap
2+
`<FocusTrap>` is intended to prevent keyboard users from interacting with elements on the page that a mouse user cannot interact with either. An example of this is `<Modal>` where the user cannot click the items behind the modal. If you want to shift focus based on UI events, consider using the [.focus()](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/focus) method instead.
3+
:::
4+
5+
:::warning Accessibility
6+
It is **required** that when using a `<FocusTrap>` there is logic using only key presses to escape the focus trap. Otherwise, keyboard users will be blocked after they enter a `<FocusTrap>`.
7+
:::
8+
9+
## Basic example
10+
11+
:::note
12+
All the examples have controls to enable / disable the focus trap so that page keyboard navigation isn't blocked.
13+
:::
14+
15+
When enabled, only the children of the `<FocusTrap>` will be able to receive focus. Otherwise, standard DOM focus order follows.
16+
17+
```jsx live
18+
function Example() {
19+
const [focusTrapEnabled, setFocusTrapEnabled] = useState(false);
20+
21+
return (
22+
<VStack gap={2}>
23+
<Checkbox
24+
checked={focusTrapEnabled}
25+
onChange={() => setFocusTrapEnabled((prev) => !prev)}
26+
label="Focus trap enabled"
27+
/>
28+
{focusTrapEnabled && (
29+
<FocusTrap>
30+
<VStack gap={2} background="bgAlternate" padding={2}>
31+
<Text>Inside FocusTrap</Text>
32+
<HStack gap={1}>
33+
<Button>1</Button>
34+
<Button>2</Button>
35+
<Button>3</Button>
36+
</HStack>
37+
<Checkbox
38+
checked={focusTrapEnabled}
39+
onChange={() => setFocusTrapEnabled((prev) => !prev)}
40+
label="Focus trap enabled"
41+
/>
42+
</VStack>
43+
</FocusTrap>
44+
)}
45+
</VStack>
46+
);
47+
}
48+
```
49+
50+
## Include trigger in FocusTrap
51+
52+
The `includeTriggerInFocusTrap` prop includes the triggering element causing the `<FocusTrap>` to render as part of the focus order.
53+
54+
```jsx live
55+
function Example() {
56+
const [focusTrapEnabled, setFocusTrapEnabled] = useState(false);
57+
58+
return (
59+
<VStack gap={2}>
60+
<Checkbox
61+
checked={focusTrapEnabled}
62+
onChange={() => setFocusTrapEnabled((prev) => !prev)}
63+
label="Focus trap enabled"
64+
/>
65+
{focusTrapEnabled && (
66+
<FocusTrap includeTriggerInFocusTrap>
67+
<VStack gap={2} background="bgAlternate" padding={2}>
68+
<Text>Inside FocusTrap</Text>
69+
<HStack gap={1}>
70+
<Button>1</Button>
71+
<Button>2</Button>
72+
<Button>3</Button>
73+
</HStack>
74+
<Checkbox
75+
checked={focusTrapEnabled}
76+
onChange={() => setFocusTrapEnabled((prev) => !prev)}
77+
label="Focus trap enabled"
78+
/>
79+
</VStack>
80+
</FocusTrap>
81+
)}
82+
</VStack>
83+
);
84+
}
85+
```
86+
87+
## Restore focus on unmount
88+
89+
The `restoreFocusOnUnmount` prop returns focus to the triggering element when the `<FocusTrap>` is unmounted.
90+
91+
```jsx live
92+
function Example() {
93+
const [focusTrapEnabled, setFocusTrapEnabled] = useState(false);
94+
95+
return (
96+
<VStack gap={2}>
97+
<Checkbox
98+
checked={focusTrapEnabled}
99+
onChange={() => setFocusTrapEnabled((prev) => !prev)}
100+
label="Focus trap enabled"
101+
/>
102+
{focusTrapEnabled && (
103+
<FocusTrap restoreFocusOnUnmount>
104+
<VStack gap={2} background="bgAlternate" padding={2}>
105+
<Text>Inside FocusTrap</Text>
106+
<HStack gap={1}>
107+
<Button>1</Button>
108+
<Button>2</Button>
109+
<Button>3</Button>
110+
</HStack>
111+
<Checkbox
112+
checked={focusTrapEnabled}
113+
onChange={() => setFocusTrapEnabled((prev) => !prev)}
114+
label="Focus trap enabled"
115+
/>
116+
</VStack>
117+
</FocusTrap>
118+
)}
119+
</VStack>
120+
);
121+
}
122+
```
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import ComponentPropsTable from '@site/src/components/page/ComponentPropsTable';
2+
import webPropsData from ':docgen/web/overlays/FocusTrap/data';
3+
import { sharedParentTypes } from ':docgen/_types/sharedParentTypes';
4+
import { sharedTypeAliases } from ':docgen/_types/sharedTypeAliases';
5+
6+
<ComponentPropsTable
7+
props={webPropsData}
8+
sharedTypeAliases={sharedTypeAliases}
9+
sharedParentTypes={sharedParentTypes}
10+
/>
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
---
2+
id: focusTrap
3+
title: FocusTrap
4+
platform_switcher_options: { web: true, mobile: false }
5+
hide_title: true
6+
---
7+
8+
import { VStack } from '@coinbase/cds-web/layout';
9+
import { ComponentHeader } from '@site/src/components/page/ComponentHeader';
10+
import { ComponentTabsContainer } from '@site/src/components/page/ComponentTabsContainer';
11+
12+
import webPropsToc from ':docgen/web/overlays/FocusTrap/toc-props';
13+
import WebPropsTable from './_webPropsTable.mdx';
14+
import WebExamples, { toc as webExamplesToc } from './_webExamples.mdx';
15+
import webMetadata from './webMetadata.json';
16+
17+
<VStack gap={5}>
18+
<ComponentHeader title="FocusTrap" webMetadata={webMetadata} />
19+
<ComponentTabsContainer
20+
webPropsTable={<WebPropsTable />}
21+
webExamples={<WebExamples />}
22+
webExamplesToc={webExamplesToc}
23+
webPropsToc={webPropsToc}
24+
/>
25+
</VStack>
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{
2+
"import": "import { FocusTrap } from '@coinbase/cds-web/overlays/FocusTrap'",
3+
"source": "https://github.com/coinbase/cds/blob/master/packages/web/src/overlays/FocusTrap.tsx",
4+
"description": "FocusTrap is a component that traps focus within its children.",
5+
"relatedComponents": [
6+
{
7+
"label": "Modal",
8+
"url": "/components/overlay/Modal/"
9+
},
10+
{
11+
"label": "Tray",
12+
"url": "/components/overlay/Tray/"
13+
}
14+
]
15+
}

apps/docs/sidebars.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -439,6 +439,7 @@ const sidebars: SidebarsConfig = {
439439
label: 'Overlay',
440440
items: [
441441
{ type: 'doc', id: 'components/overlay/Alert/alert', label: 'Alert' },
442+
{ type: 'doc', id: 'components/overlay/FocusTrap/focusTrap', label: 'FocusTrap' },
442443
{
443444
type: 'doc',
444445
id: 'components/overlay/FullscreenAlert/fullscreenAlert',

packages/common/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@ All notable changes to this project will be documented in this file.
88

99
<!-- template-start -->
1010

11+
## 8.37.1 ((1/14/2026, 12:37 PM PST))
12+
13+
This is an artificial version bump with no new change.
14+
1115
## 8.37.0 ((1/12/2026, 02:16 PM PST))
1216

1317
This is an artificial version bump with no new change.

packages/common/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@coinbase/cds-common",
3-
"version": "8.37.0",
3+
"version": "8.37.1",
44
"description": "Coinbase Design System - Common",
55
"repository": {
66
"type": "git",

packages/mcp-server/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@ All notable changes to this project will be documented in this file.
88

99
<!-- template-start -->
1010

11+
## 8.37.1 ((1/14/2026, 12:37 PM PST))
12+
13+
This is an artificial version bump with no new change.
14+
1115
## 8.37.0 ((1/12/2026, 02:16 PM PST))
1216

1317
This is an artificial version bump with no new change.

packages/mcp-server/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@coinbase/cds-mcp-server",
3-
"version": "8.37.0",
3+
"version": "8.37.1",
44
"description": "Coinbase Design System - MCP Server",
55
"repository": {
66
"type": "git",

0 commit comments

Comments
 (0)