|
6 | 6 | } from '@guardian/source/foundations'; |
7 | 7 | import { Button, SvgCross } from '@guardian/source/react-components'; |
8 | 8 | import { useEffect, useId, useRef } from 'react'; |
| 9 | +import { tabbable } from 'tabbable'; |
9 | 10 | import { getPositionStyles } from './position'; |
10 | 11 | import { getThemeColours, type ThemePopover } from './theme'; |
11 | 12 |
|
@@ -149,21 +150,55 @@ export const Popover = ({ |
149 | 150 | handleClose(); |
150 | 151 | } |
151 | 152 | }; |
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) => { |
154 | 182 | if (event.code === 'Escape') { |
| 183 | + // Close on escape key |
155 | 184 | 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); |
156 | 191 | } |
157 | 192 | }; |
158 | 193 |
|
159 | 194 | document.addEventListener('click', dismissOnClickElsewhere); |
160 | | - document.addEventListener('keydown', dismissOnEsc); |
| 195 | + document.addEventListener('keydown', handleKeydown); |
161 | 196 | // Remove listeners on unmount |
162 | 197 | return () => { |
163 | 198 | document.removeEventListener('click', dismissOnClickElsewhere); |
164 | | - document.removeEventListener('keydown', dismissOnEsc); |
| 199 | + document.removeEventListener('keydown', handleKeydown); |
165 | 200 | }; |
166 | | - }, [isOpen, handleClose]); |
| 201 | + }, [isOpen, handleClose, popoverId]); |
167 | 202 |
|
168 | 203 | return ( |
169 | 204 | <div |
|
0 commit comments