diff --git a/src/components/Dialog/DialogAnchor.tsx b/src/components/Dialog/DialogAnchor.tsx index 20c7434f0..8eac6aa75 100644 --- a/src/components/Dialog/DialogAnchor.tsx +++ b/src/components/Dialog/DialogAnchor.tsx @@ -49,14 +49,18 @@ export function useDialogAnchor({ type DialogAnchorProps = PropsWithChildren> & { id: string; + focus?: boolean; + trapFocus?: boolean; } & ComponentProps<'div'>; export const DialogAnchor = ({ children, className, + focus = true, id, placement = 'auto', referenceElement = null, + trapFocus, ...restDivProps }: DialogAnchorProps) => { const open = useDialogIsOpen(id); @@ -66,6 +70,43 @@ export const DialogAnchor = ({ referenceElement, }); + // handle focus and focus trap inside the dialog + useEffect(() => { + if (!popperElementRef.current || !focus || !open) return; + const container = popperElementRef.current; + container.focus(); + + if (!trapFocus) return; + const handleKeyDownWithTabRoundRobin = (event: KeyboardEvent) => { + if (event.key !== 'Tab') return; + + const focusableElements = getFocusableElements(container); + if (focusableElements.length === 0) return; + + const firstElement = focusableElements[0] as HTMLElement; + const lastElement = focusableElements[focusableElements.length - 1] as HTMLElement; + if (firstElement === lastElement) { + event.preventDefault(); + firstElement.focus(); + } + + // Trap focus within the group + if (event.shiftKey && document.activeElement === firstElement) { + // If Shift + Tab on the first element, move focus to the last element + event.preventDefault(); + lastElement.focus(); + } else if (!event.shiftKey && document.activeElement === lastElement) { + // If Tab on the last element, move focus to the first element + event.preventDefault(); + firstElement.focus(); + } + }; + + container.addEventListener('keydown', handleKeyDownWithTabRoundRobin); + + return () => container.removeEventListener('keydown', handleKeyDownWithTabRoundRobin); + }, [focus, popperElementRef, open, trapFocus]); + return (
{children}
); }; + +function getFocusableElements(container: HTMLElement) { + return container.querySelectorAll( + 'a[href], button, textarea, input, select, [tabindex]:not([tabindex="-1"])', + ); +} diff --git a/src/components/MessageActions/MessageActions.tsx b/src/components/MessageActions/MessageActions.tsx index 41dd364e7..cdae62daa 100644 --- a/src/components/MessageActions/MessageActions.tsx +++ b/src/components/MessageActions/MessageActions.tsx @@ -149,6 +149,7 @@ export const MessageActions = < id={dialogId} placement={isMine ? 'top-end' : 'top-start'} referenceElement={actionsBoxButtonRef.current} + trapFocus >