Skip to content

Commit 1aac687

Browse files
feat: Focus on first tabbable node when Dialog opens (#1927)
* fix: Prevent screen reader focus on Portal InvisiBox * feat: Add initial focus target logic * Refactor getInitialNode function and unit tests * chore: image-snapshot updates (#1941) Co-authored-by: kvrmd <[email protected]> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: kvrmd <[email protected]>
1 parent 2e258cf commit 1aac687

File tree

19 files changed

+116
-28
lines changed

19 files changed

+116
-28
lines changed

packages/components-providers/src/FocusTrap/utils.ts

Lines changed: 44 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -70,31 +70,58 @@ export const activateFocusTrap = ({
7070
let lastTabbableNode: FocusableElement = element
7171
let mostRecentlyFocusedNode: FocusableElement | null = null
7272

73-
const getInitialFocusNode = () => {
74-
let node
73+
const getInitialFocusNodeByPriority = () => {
74+
// Return the already focused node within element
7575
if (element.contains(document.activeElement)) {
76-
node = document.activeElement as HTMLElement
77-
} else {
78-
// Look for data-autofocus b/c React strips autofocus from dom
79-
// https://github.com/facebook/react/issues/11851
80-
const autoFocusElement = element.querySelector(
81-
'[data-autofocus="true"]'
82-
) as HTMLElement
83-
84-
// In the absence of autofocus, the surface will have initial focus
85-
const surfaceElement = element.querySelector(
86-
'[data-overlay-surface="true"]'
87-
) as HTMLElement
88-
node = autoFocusElement || surfaceElement || element
76+
return document.activeElement as HTMLElement
77+
}
78+
79+
// Look for data-autofocus b/c React strips autofocus from dom
80+
// https://github.com/facebook/react/issues/11851
81+
const autoFocusElement = element.querySelector('[data-autofocus="true"]')
82+
if (autoFocusElement) {
83+
return autoFocusElement
84+
}
85+
86+
// Without autofocus, fallback to a tabbable node by priority, if one exists.
87+
const firstInputElement = element.querySelector('input, textarea, select')
88+
if (firstInputElement) {
89+
return firstInputElement
90+
}
91+
92+
const footerElement = element.querySelector('footer')
93+
const firstTabbableFooterElement = footerElement
94+
? tabbable(footerElement)[0]
95+
: null
96+
if (firstTabbableFooterElement) {
97+
return firstTabbableFooterElement
8998
}
9099

100+
const firstTabbableElement = tabbable(element)[0]
101+
if (firstTabbableElement) {
102+
return firstTabbableElement
103+
}
104+
105+
// In the absence of autofocus and any tabbable element, the surface will have initial focus.
106+
const surfaceElement = element.querySelector(
107+
'[data-overlay-surface="true"]'
108+
)
109+
if (surfaceElement) {
110+
return surfaceElement
111+
}
112+
113+
// default to element
114+
return element
115+
}
116+
117+
const getInitialFocusNode = () => {
118+
const node = getInitialFocusNodeByPriority()
91119
if (!node || !isFocusable(node)) {
92120
throw new Error(
93121
'Your focus trap needs to have at least one focusable element'
94122
)
95123
}
96-
97-
return node
124+
return node as FocusableElement
98125
}
99126

100127
const updateTabbableNodes = () => {
46 Bytes
Loading
38 Bytes
Loading
62 Bytes
Loading
61 Bytes
Loading
56 Bytes
Loading
45 Bytes
Loading
55 Bytes
Loading
51 Bytes
Loading
0 Bytes
Loading

0 commit comments

Comments
 (0)