-
Notifications
You must be signed in to change notification settings - Fork 3k
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
prevent initial focus trap while has a focused element. #44932
Changes from 6 commits
a1587bb
21f134f
ecdc861
38e473d
25ab7fd
08c6f03
59496ce
dc87814
8081861
bb24cd9
1e590d4
c6e1bb6
3106ce8
bd4a935
9686926
367e62f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,88 @@ | ||
diff --git a/node_modules/focus-trap/dist/focus-trap.esm.js b/node_modules/focus-trap/dist/focus-trap.esm.js | ||
index 10d56db..a0ed057 100644 | ||
--- a/node_modules/focus-trap/dist/focus-trap.esm.js | ||
+++ b/node_modules/focus-trap/dist/focus-trap.esm.js | ||
@@ -100,8 +100,8 @@ var isKeyForward = function isKeyForward(e) { | ||
var isKeyBackward = function isKeyBackward(e) { | ||
return isTabEvent(e) && e.shiftKey; | ||
}; | ||
-var delay = function delay(fn) { | ||
- return setTimeout(fn, 0); | ||
+var delay = function delay(fn, delayTime = 0) { | ||
+ return setTimeout(fn, delayTime); | ||
}; | ||
|
||
// Array.find/findIndex() are not supported on IE; this replicates enough | ||
@@ -283,7 +283,7 @@ var createFocusTrap = function createFocusTrap(elements, userOptions) { | ||
return node; | ||
}; | ||
var getInitialFocusNode = function getInitialFocusNode() { | ||
- var node = getNodeForOption('initialFocus'); | ||
+ var node = getNodeForOption('initialFocus', state.containers); | ||
|
||
// false explicitly indicates we want no initialFocus at all | ||
if (node === false) { | ||
@@ -744,7 +744,7 @@ var createFocusTrap = function createFocusTrap(elements, userOptions) { | ||
// that caused the focus trap activation. | ||
state.delayInitialFocusTimer = config.delayInitialFocus ? delay(function () { | ||
tryFocus(getInitialFocusNode()); | ||
- }) : tryFocus(getInitialFocusNode()); | ||
+ }, typeof config.delayInitialFocus === 'number' ? config.delayInitialFocus : undefined) : tryFocus(getInitialFocusNode()); | ||
doc.addEventListener('focusin', checkFocusIn, true); | ||
doc.addEventListener('mousedown', checkPointerDown, { | ||
capture: true, | ||
diff --git a/node_modules/focus-trap/index.d.ts b/node_modules/focus-trap/index.d.ts | ||
index 400db1b..69f4b94 100644 | ||
--- a/node_modules/focus-trap/index.d.ts | ||
+++ b/node_modules/focus-trap/index.d.ts | ||
@@ -16,7 +16,7 @@ declare module 'focus-trap' { | ||
* `document.querySelector()` to find the DOM node), `false` to explicitly indicate | ||
* an opt-out, or a function that returns a DOM node or `false`. | ||
*/ | ||
- export type FocusTargetOrFalse = FocusTargetValueOrFalse | (() => FocusTargetValueOrFalse); | ||
+ export type FocusTargetOrFalse = FocusTargetValueOrFalse | ((containers?: HTMLElement[]) => FocusTargetValueOrFalse | undefined); | ||
|
||
type MouseEventToBoolean = (event: MouseEvent | TouchEvent) => boolean; | ||
type KeyboardEventToBoolean = (event: KeyboardEvent) => boolean; | ||
@@ -185,7 +185,7 @@ declare module 'focus-trap' { | ||
* This prevents elements within the focusable element from capturing | ||
* the event that triggered the focus trap activation. | ||
*/ | ||
- delayInitialFocus?: boolean; | ||
+ delayInitialFocus?: boolean | number; | ||
/** | ||
* Default: `window.document`. Document where the focus trap will be active. | ||
* This allows to use FocusTrap in an iFrame context. | ||
diff --git a/node_modules/focus-trap/index.js b/node_modules/focus-trap/index.js | ||
index de8e46a..7ab9d89 100644 | ||
--- a/node_modules/focus-trap/index.js | ||
+++ b/node_modules/focus-trap/index.js | ||
@@ -63,8 +63,8 @@ const isKeyBackward = function (e) { | ||
return isTabEvent(e) && e.shiftKey; | ||
}; | ||
|
||
-const delay = function (fn) { | ||
- return setTimeout(fn, 0); | ||
+const delay = function (fn, delayTime = 0) { | ||
+ return setTimeout(fn, delayTime); | ||
}; | ||
|
||
// Array.find/findIndex() are not supported on IE; this replicates enough | ||
@@ -267,7 +267,7 @@ const createFocusTrap = function (elements, userOptions) { | ||
}; | ||
|
||
const getInitialFocusNode = function () { | ||
- let node = getNodeForOption('initialFocus'); | ||
+ let node = getNodeForOption('initialFocus', state.containers); | ||
|
||
// false explicitly indicates we want no initialFocus at all | ||
if (node === false) { | ||
@@ -817,7 +817,7 @@ const createFocusTrap = function (elements, userOptions) { | ||
state.delayInitialFocusTimer = config.delayInitialFocus | ||
? delay(function () { | ||
tryFocus(getInitialFocusNode()); | ||
- }) | ||
+ }, typeof config.delayInitialFocus === 'number' ? config.delayInitialFocus : undefined) | ||
: tryFocus(getInitialFocusNode()); | ||
|
||
doc.addEventListener('focusin', checkFocusIn, true); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,6 @@ | ||
type FocusTrapForScreenProps = { | ||
children: React.ReactNode; | ||
testID?: string; | ||
suneox marked this conversation as resolved.
Show resolved
Hide resolved
|
||
}; | ||
|
||
export default FocusTrapForScreenProps; |
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
@@ -1,23 +1,18 @@ | ||||||
import {useFocusEffect, useIsFocused, useRoute} from '@react-navigation/native'; | ||||||
import {useIsFocused, useRoute} from '@react-navigation/native'; | ||||||
import FocusTrap from 'focus-trap-react'; | ||||||
import React, {useCallback, useMemo} from 'react'; | ||||||
import {useOnyx} from 'react-native-onyx'; | ||||||
import React, {useMemo} from 'react'; | ||||||
import BOTTOM_TAB_SCREENS from '@components/FocusTrap/BOTTOM_TAB_SCREENS'; | ||||||
import getScreenWithAutofocus from '@components/FocusTrap/SCREENS_WITH_AUTOFOCUS'; | ||||||
import sharedTrapStack from '@components/FocusTrap/sharedTrapStack'; | ||||||
import TOP_TAB_SCREENS from '@components/FocusTrap/TOP_TAB_SCREENS'; | ||||||
import WIDE_LAYOUT_INACTIVE_SCREENS from '@components/FocusTrap/WIDE_LAYOUT_INACTIVE_SCREENS'; | ||||||
import useWindowDimensions from '@hooks/useWindowDimensions'; | ||||||
import ONYXKEYS from '@src/ONYXKEYS'; | ||||||
import CONST from '@src/CONST'; | ||||||
import type FocusTrapProps from './FocusTrapProps'; | ||||||
|
||||||
let activeRouteName = ''; | ||||||
function FocusTrapForScreen({children}: FocusTrapProps) { | ||||||
const isFocused = useIsFocused(); | ||||||
const route = useRoute(); | ||||||
const {isSmallScreenWidth} = useWindowDimensions(); | ||||||
const [isAuthenticated] = useOnyx(ONYXKEYS.SESSION, {selector: (session) => !!session?.authToken}); | ||||||
const screensWithAutofocus = useMemo(() => getScreenWithAutofocus(!!isAuthenticated), [isAuthenticated]); | ||||||
|
||||||
const isActive = useMemo(() => { | ||||||
// Focus trap can't be active on bottom tab screens because it would block access to the tab bar. | ||||||
|
@@ -37,12 +32,6 @@ function FocusTrapForScreen({children}: FocusTrapProps) { | |||||
return true; | ||||||
}, [isFocused, isSmallScreenWidth, route.name]); | ||||||
|
||||||
useFocusEffect( | ||||||
useCallback(() => { | ||||||
activeRouteName = route.name; | ||||||
}, [route]), | ||||||
); | ||||||
|
||||||
return ( | ||||||
<FocusTrap | ||||||
active={isActive} | ||||||
|
@@ -51,18 +40,12 @@ function FocusTrapForScreen({children}: FocusTrapProps) { | |||||
trapStack: sharedTrapStack, | ||||||
allowOutsideClick: true, | ||||||
fallbackFocus: document.body, | ||||||
// We don't want to ovverride autofocus on these screens. | ||||||
initialFocus: () => { | ||||||
if (screensWithAutofocus.includes(activeRouteName)) { | ||||||
return false; | ||||||
suneox marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
} | ||||||
return undefined; | ||||||
}, | ||||||
setReturnFocus: (element) => { | ||||||
if (screensWithAutofocus.includes(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. We should not return focus to the initial screen if we already have a focused element, e.g. in ReportScreen after closing RHP the Composer should remain focused Screen.Recording.2024-07-09.at.12.17.06.AM.movThere 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. @s77rt I’m still working on 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. Yes! 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. When a component using As a result, we can’t retrieve the containers for Screen A when Screen B unmounts. Instead, we can check if the active element is the default (document body). If it is, we can return false to ensure the focus trap does not interfere with the focus transition to Screen A. Otherwise, we keep the current focused element. I have updated the code, and it covers the issue you mentioned above. 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. Works for me but let's also check if |
||||||
delayInitialFocus: CONST.ANIMATED_TRANSITION + CONST.ANIMATION_IN_TIMING, | ||||||
initialFocus: (focusTrapContainers) => { | ||||||
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.
Suggested change
|
||||||
const hasFocusedElementInsideContainer = focusTrapContainers?.some((container) => container.contains(document.activeElement)); | ||||||
if (hasFocusedElementInsideContainer) { | ||||||
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.
Suggested change
NAB |
||||||
return false; | ||||||
} | ||||||
return element; | ||||||
}, | ||||||
}} | ||||||
> | ||||||
|
This file was deleted.
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.
I think we need to keep the extra setTimeout here and add our timeout. After testing I found out that document.activeElement does not return the correct focused element.
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.
After the component unmounts, the focus trap will deactivate, so the default active element becomes the document. So I don't think we need to add another timeout inside the existing timeout
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.
Can you please elaborate what does this has to do with focus deactivation? What I am referring to is the fact that if you log
document.activeElement
you will not get the correct focused element as we are doing this too earlyScreen.Recording.2024-07-08.at.4.42.36.PM.mov
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.
We have a missing
delayInitialFocus
time; it should equalCONST.ANIMATED_TRANSITION + CONST.ANIMATION_IN_TIMING
. I’ll update it with the timing animation in accordingly.Screen.Recording.2024-07-08.at.23.18.32.mov
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.
Updated timing animation
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.
The correct timing is 300ms (
CONST.ANIMATED_TRANSITION
).CONST.ANIMATION_IN_TIMING
is not related to the screen transition.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.
About this change, we need to extend ANIMATION_IN_TIMING. This is because useAutoFocus also runs after ANIMATED_TRANSITION. In that case, input focusing and getting initial focus from the focus trap will run simultaneously, so sometimes the focused element is not correct
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.
Given that this already had it's own
setTimeout
even when no timeout is actually required, we should just add anothersetTimeout
. The former is to fix whatever the library was trying to fix and the later is for our actual wanted delay.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.
I'll try your suggestion
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.
i have updated