-
Notifications
You must be signed in to change notification settings - Fork 2.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add focus trap #39520
Add focus trap #39520
Changes from 3 commits
ec50a62
623f6ca
7700359
ee852a4
d64cb37
cfdab9a
76887e4
7f25987
3425512
9740843
368610e
5ced21b
9928153
12846dd
41862ad
eb4b654
6e3ba8c
23f47d4
1ac3596
bc9e6eb
296e9a1
e707d95
7c78aaa
d0d29a2
7c6626b
9c995b5
69b14e3
745f06f
aff1627
11fda35
12b2bae
7f0b86d
a9743ce
eb3b538
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
import SCREENS from '@src/SCREENS'; | ||
|
||
const BOTTOM_TAB_SCREENS: string[] = [SCREENS.HOME, SCREENS.SETTINGS.ROOT]; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Instead of |
||
|
||
export default BOTTOM_TAB_SCREENS; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
type FocusTrapForModalProps = { | ||
children: React.ReactNode; | ||
active: boolean; | ||
}; | ||
|
||
export default FocusTrapForModalProps; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
import type FocusTrapForModalProps from './FocusTrapForModalProps'; | ||
|
||
function FocusTrapForModal({children}: FocusTrapForModalProps) { | ||
return children; | ||
roryabraham marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
|
||
FocusTrapForModal.displayName = 'FocusTrapForModal'; | ||
|
||
export default FocusTrapForModal; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
import FocusTrapOriginal from 'focus-trap-react'; | ||
roryabraham marked this conversation as resolved.
Show resolved
Hide resolved
|
||
import React from 'react'; | ||
import sharedTrapStack from '@components/FocusTrap/sharedTrapStack'; | ||
import type FocusTrapForModalProps from './FocusTrapForModalProps'; | ||
|
||
function FocusTrapForModal({children, active}: FocusTrapForModalProps) { | ||
return ( | ||
<FocusTrapOriginal | ||
active={active} | ||
focusTrapOptions={{ | ||
trapStack: sharedTrapStack, | ||
allowOutsideClick: true, | ||
fallbackFocus: document.body, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We should disable the initial focus to prevent default selection on popup menus.#43659 |
||
}} | ||
> | ||
{children} | ||
</FocusTrapOriginal> | ||
); | ||
} | ||
|
||
FocusTrapForModal.displayName = 'FocusTrapForModal'; | ||
|
||
export default FocusTrapForModal; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
type FocusTrapForScreenProps = { | ||
children: React.ReactNode; | ||
}; | ||
|
||
export default FocusTrapForScreenProps; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
import type FocusTrapProps from './FocusTrapProps'; | ||
|
||
function FocusTrapView({children}: FocusTrapProps) { | ||
return children; | ||
} | ||
|
||
FocusTrapView.displayName = 'FocusTrapView'; | ||
|
||
export default FocusTrapView; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,68 @@ | ||
import {useFocusEffect, useIsFocused, useRoute} from '@react-navigation/native'; | ||
import FocusTrapOriginal from 'focus-trap-react'; | ||
import React, {useMemo, useRef} from 'react'; | ||
import BOTTOM_TAB_SCREENS from '@components/FocusTrap/BOTTOM_TAB_SCREENS'; | ||
import SCREENS_WITH_AUTOFOCUS from '@components/FocusTrap/SCREENS_WITH_AUTOFOCUS'; | ||
import sharedTrapStack from '@components/FocusTrap/sharedTrapStack'; | ||
import WIDE_LAYOUT_INACTIVE_SCREENS from '@components/FocusTrap/WIDE_LAYOUT_INACTIVE_SCREENS'; | ||
import useWindowDimensions from '@hooks/useWindowDimensions'; | ||
import type FocusTrapProps from './FocusTrapProps'; | ||
|
||
let activeRouteName = ''; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we need There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This variable is shared between instances of |
||
|
||
function FocusTrap({children}: FocusTrapProps) { | ||
const isFocused = useIsFocused(); | ||
const route = useRoute(); | ||
const {isSmallScreenWidth} = useWindowDimensions(); | ||
|
||
const isActive = useMemo(() => { | ||
// Focus trap can't be active on bottom tab screens because it would block access to the tab bar. | ||
if (BOTTOM_TAB_SCREENS.includes(route.name)) { | ||
return false; | ||
} | ||
|
||
// Focus trap can't be active on these screens if the layout is wide because they may be displayed side by side. | ||
if (WIDE_LAYOUT_INACTIVE_SCREENS.includes(route.name) && !isSmallScreenWidth) { | ||
return false; | ||
} | ||
return true; | ||
}, [isSmallScreenWidth, route]); | ||
|
||
useFocusEffect(() => { | ||
activeRouteName = route.name; | ||
}); | ||
|
||
const focusTrapRef = useRef<FocusTrapOriginal | null>(null); | ||
|
||
return ( | ||
<FocusTrapOriginal | ||
ref={focusTrapRef} | ||
active={isActive} | ||
paused={!isFocused} | ||
focusTrapOptions={{ | ||
trapStack: sharedTrapStack, | ||
allowOutsideClick: true, | ||
fallbackFocus: document.body, | ||
// We don't want to ovverride autofocus on these screens. | ||
initialFocus: () => { | ||
if (SCREENS_WITH_AUTOFOCUS.includes(activeRouteName)) { | ||
return false; | ||
} | ||
return undefined; | ||
}, | ||
setReturnFocus: (element) => { | ||
if (SCREENS_WITH_AUTOFOCUS.includes(activeRouteName)) { | ||
return false; | ||
} | ||
return element; | ||
}, | ||
}} | ||
> | ||
{children} | ||
</FocusTrapOriginal> | ||
); | ||
} | ||
|
||
FocusTrap.displayName = 'FocusTrapView'; | ||
|
||
export default FocusTrap; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
import SCREENS from '@src/SCREENS'; | ||
|
||
const SCREENS_WITH_AUTOFOCUS: string[] = [ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This seems pretty manual/brittle. Is there a way we can derive which screens have auto-focus or not, rather than having a const we need to remember to update? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think there is any other way to configure this. Maybe only by rewriting configuration in There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If I remember correctly I tried two different approaches:
When I created this PR, the array with const seemed as the simplest and most reliable solution. If we unify how we handle autofocus, we could once again consider option nr. 2. Or maybe there is another option I haven't consider |
||
SCREENS.WORKSPACE_SWITCHER.ROOT, | ||
SCREENS.SEARCH_ROOT, | ||
SCREENS.REPORT, | ||
SCREENS.REPORT_DESCRIPTION_ROOT, | ||
SCREENS.PRIVATE_NOTES.EDIT, | ||
SCREENS.SETTINGS.PROFILE.STATUS, | ||
SCREENS.SETTINGS.PROFILE.PRONOUNS, | ||
SCREENS.NEW_TASK.DETAILS, | ||
SCREENS.MONEY_REQUEST.CREATE, | ||
]; | ||
|
||
export default SCREENS_WITH_AUTOFOCUS; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
import NAVIGATORS from '@src/NAVIGATORS'; | ||
import SCREENS from '@src/SCREENS'; | ||
|
||
const WIDE_LAYOUT_INACTIVE_SCREENS: string[] = [ | ||
roryabraham marked this conversation as resolved.
Show resolved
Hide resolved
|
||
NAVIGATORS.BOTTOM_TAB_NAVIGATOR, | ||
SCREENS.HOME, | ||
SCREENS.SETTINGS.ROOT, | ||
SCREENS.REPORT, | ||
SCREENS.SETTINGS.PROFILE.ROOT, | ||
SCREENS.SETTINGS.PREFERENCES.ROOT, | ||
SCREENS.SETTINGS.SECURITY, | ||
SCREENS.SETTINGS.WALLET.ROOT, | ||
SCREENS.SETTINGS.ABOUT, | ||
SCREENS.SETTINGS.WORKSPACES, | ||
|
||
SCREENS.WORKSPACE.INITIAL, | ||
|
||
SCREENS.WORKSPACE.PROFILE, | ||
SCREENS.WORKSPACE.CARD, | ||
SCREENS.WORKSPACE.WORKFLOWS, | ||
SCREENS.WORKSPACE.WORKFLOWS_APPROVER, | ||
SCREENS.WORKSPACE.WORKFLOWS_AUTO_REPORTING_FREQUENCY, | ||
SCREENS.WORKSPACE.WORKFLOWS_AUTO_REPORTING_MONTHLY_OFFSET, | ||
SCREENS.WORKSPACE.REIMBURSE, | ||
SCREENS.WORKSPACE.BILLS, | ||
SCREENS.WORKSPACE.INVOICES, | ||
SCREENS.WORKSPACE.TRAVEL, | ||
SCREENS.WORKSPACE.MEMBERS, | ||
SCREENS.WORKSPACE.CATEGORIES, | ||
SCREENS.WORKSPACE.MORE_FEATURES, | ||
SCREENS.WORKSPACE.TAGS, | ||
SCREENS.WORKSPACE.TAXES, | ||
SCREENS.WORKSPACE.DISTANCE_RATES, | ||
]; | ||
|
||
export default WIDE_LAYOUT_INACTIVE_SCREENS; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
import type {FocusTrap as FocusTrapHandler} from 'focus-trap'; | ||
|
||
const trapStack: FocusTrapHandler[] = []; | ||
roryabraham marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
export default trapStack; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Request to add library
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Request was resolved successfully