Skip to content

Commit 2dacec0

Browse files
committed
update tabbing behaviour for popover component
1 parent a878dd4 commit 2dacec0

3 files changed

Lines changed: 67 additions & 7 deletions

File tree

libs/@guardian/source-development-kitchen/package.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,10 @@
3838
"@storybook/addon-docs": "10.4.0",
3939
"@storybook/addon-links": "10.4.0",
4040
"@storybook/react-vite": "10.4.0",
41+
"@testing-library/dom": "10.4.1",
42+
"@testing-library/jest-dom": "6.9.1",
43+
"@testing-library/react": "16.3.0",
44+
"@testing-library/user-event": "14.6.1",
4145
"@types/chai": "5.2.3",
4246
"@types/jest": "30.0.0",
4347
"@types/react": "18.2.79",
@@ -47,6 +51,7 @@
4751
"react-dom": "18.2.0",
4852
"rollup": "4.60.0",
4953
"storybook": "10.4.0",
54+
"tabbable": "6.4.0",
5055
"ts-jest": "29.4.11",
5156
"tslib": "2.8.1",
5257
"typescript": "6.0.3",

libs/@guardian/source-development-kitchen/src/react-components/popover/Popover.tsx

Lines changed: 40 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
} from '@guardian/source/foundations';
77
import { Button, SvgCross } from '@guardian/source/react-components';
88
import { useEffect, useId, useRef } from 'react';
9+
import { tabbable } from 'tabbable';
910
import { getPositionStyles } from './position';
1011
import { getThemeColours, type ThemePopover } from './theme';
1112

@@ -149,21 +150,55 @@ export const Popover = ({
149150
handleClose();
150151
}
151152
};
152-
/** Respond to escape key by closing Popover */
153-
const dismissOnEsc = (event: KeyboardEvent) => {
153+
154+
/**
155+
* Handles tab key events by moving focus between tabbable elements within the container
156+
*/
157+
const handleTabKey = (event: KeyboardEvent) => {
158+
const popoverEl = document.getElementById(popoverId);
159+
const tabbableItems = popoverEl ? tabbable(popoverEl) : [];
160+
const firstTabbableItem = tabbableItems[0];
161+
const lastTabbableItem = tabbableItems[tabbableItems.length - 1];
162+
// Shift + tab moves focus from first element to last element
163+
if (event.shiftKey && document.activeElement === firstTabbableItem) {
164+
// Move focus to last tabbable element
165+
event.preventDefault();
166+
lastTabbableItem?.focus();
167+
}
168+
// Tab moves focus from last element to first element
169+
else if (document.activeElement === lastTabbableItem) {
170+
// Move focus to first tabbable element
171+
event.preventDefault();
172+
firstTabbableItem?.focus();
173+
}
174+
};
175+
176+
/**
177+
* Handler for keydown event listener
178+
* - Respond to escape key by closing Popover
179+
* - Respond to tab key by keeping focus within Popover area
180+
*/
181+
const handleKeydown = (event: KeyboardEvent) => {
154182
if (event.code === 'Escape') {
183+
// Close on escape key
155184
handleClose();
185+
} else if (event.code !== 'Tab') {
186+
// Early return if not tab key
187+
return;
188+
} else {
189+
// Otherwise, handle tab key
190+
handleTabKey(event);
156191
}
157192
};
158193

159194
document.addEventListener('click', dismissOnClickElsewhere);
160-
document.addEventListener('keydown', dismissOnEsc);
195+
document.addEventListener('keydown', handleKeydown);
161196
// Remove listeners on unmount
162197
return () => {
163198
document.removeEventListener('click', dismissOnClickElsewhere);
164-
document.removeEventListener('keydown', dismissOnEsc);
199+
document.removeEventListener('keydown', handleKeydown);
165200
};
166-
}, [isOpen, handleClose]);
201+
}, [isOpen, handleClose, popoverId]);
167202

168203
return (
169204
<div

pnpm-lock.yaml

Lines changed: 22 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)