Skip to content

Commit b557e25

Browse files
committed
feat: handle focus within dialog
1 parent 993358f commit b557e25

File tree

2 files changed

+49
-0
lines changed

2 files changed

+49
-0
lines changed

src/components/Dialog/DialogAnchor.tsx

+48
Original file line numberDiff line numberDiff line change
@@ -49,14 +49,18 @@ export function useDialogAnchor<T extends HTMLElement>({
4949

5050
type DialogAnchorProps = PropsWithChildren<Partial<DialogAnchorOptions>> & {
5151
id: string;
52+
focus?: boolean;
53+
trapFocus?: boolean;
5254
} & ComponentProps<'div'>;
5355

5456
export const DialogAnchor = ({
5557
children,
5658
className,
59+
focus = true,
5760
id,
5861
placement = 'auto',
5962
referenceElement = null,
63+
trapFocus,
6064
...restDivProps
6165
}: DialogAnchorProps) => {
6266
const open = useDialogIsOpen(id);
@@ -66,6 +70,43 @@ export const DialogAnchor = ({
6670
referenceElement,
6771
});
6872

73+
// handle focus and focus trap inside the dialog
74+
useEffect(() => {
75+
if (!popperElementRef.current || !focus || !open) return;
76+
const container = popperElementRef.current;
77+
container.focus();
78+
79+
if (!trapFocus) return;
80+
const handleKeyDownWithTabRoundRobin = (event: KeyboardEvent) => {
81+
if (event.key !== 'Tab') return;
82+
83+
const focusableElements = getFocusableElements(container);
84+
if (focusableElements.length === 0) return;
85+
86+
const firstElement = focusableElements[0] as HTMLElement;
87+
const lastElement = focusableElements[focusableElements.length - 1] as HTMLElement;
88+
if (firstElement === lastElement) {
89+
event.preventDefault();
90+
firstElement.focus();
91+
}
92+
93+
// Trap focus within the group
94+
if (event.shiftKey && document.activeElement === firstElement) {
95+
// If Shift + Tab on the first element, move focus to the last element
96+
event.preventDefault();
97+
lastElement.focus();
98+
} else if (!event.shiftKey && document.activeElement === lastElement) {
99+
// If Tab on the last element, move focus to the first element
100+
event.preventDefault();
101+
firstElement.focus();
102+
}
103+
};
104+
105+
container.addEventListener('keydown', handleKeyDownWithTabRoundRobin);
106+
107+
return () => container.removeEventListener('keydown', handleKeyDownWithTabRoundRobin);
108+
}, [focus, popperElementRef, open, trapFocus]);
109+
69110
return (
70111
<DialogPortalEntry dialogId={id}>
71112
<div
@@ -75,9 +116,16 @@ export const DialogAnchor = ({
75116
data-testid='str-chat__dialog-contents'
76117
ref={popperElementRef}
77118
style={styles.popper}
119+
tabIndex={0}
78120
>
79121
{children}
80122
</div>
81123
</DialogPortalEntry>
82124
);
83125
};
126+
127+
function getFocusableElements(container: HTMLElement) {
128+
return container.querySelectorAll(
129+
'a[href], button, textarea, input, select, [tabindex]:not([tabindex="-1"])',
130+
);
131+
}

src/components/MessageActions/MessageActions.tsx

+1
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,7 @@ export const MessageActions = <
149149
id={dialogId}
150150
placement={isMine ? 'top-end' : 'top-start'}
151151
referenceElement={actionsBoxButtonRef.current}
152+
trapFocus
152153
>
153154
<MessageActionsBox
154155
getMessageActions={getMessageActions}

0 commit comments

Comments
 (0)