Clean up FocusScope native impl

This commit is contained in:
Eric Bailey
2025-08-04 13:07:54 -05:00
parent 18b6d7cf6a
commit 51011bd758
+36 -18
View File
@@ -1,20 +1,20 @@
import { import {
Children, Children,
cloneElement, cloneElement,
ReactNode, isValidElement,
useRef, type ReactElement,
useMemo, type ReactNode,
useCallback, useCallback,
useEffect, useEffect,
isValidElement, useMemo,
FunctionComponentElement, useRef,
} from 'react' } from 'react'
import { import {
AccessibilityInfo, AccessibilityInfo,
Pressable,
View,
Text,
findNodeHandle, findNodeHandle,
Pressable,
Text,
type View,
} from 'react-native' } from 'react-native'
import {msg} from '@lingui/macro' import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
@@ -31,10 +31,22 @@ function FocusTrap({children}: {children: ReactNode}) {
const {_} = useLingui() const {_} = useLingui()
const child = useRef<View>(null) const child = useRef<View>(null)
const content = useMemo(() => { /*
* Here we add a ref to the first child of this component. This currently
* overrides any ref already on that first child, so we throw an error here
* to prevent us from ever accidentally doing this.
*/
const decoratedChildren = useMemo(() => {
return Children.toArray(children).map((node, i) => { return Children.toArray(children).map((node, i) => {
if (i === 0 && isValidElement(node)) { if (i === 0 && isValidElement(node)) {
return cloneElement(node as FunctionComponentElement<any>, { const n = node as ReactElement<any>
if (n.props.ref !== undefined) {
throw new Error(
'FocusScope needs to override the ref on its first child.',
)
}
return cloneElement(n, {
...n.props,
ref: child, ref: child,
}) })
} }
@@ -42,7 +54,7 @@ function FocusTrap({children}: {children: ReactNode}) {
}) })
}, [children]) }, [children])
const focus = useCallback((ref: View | null) => { const focusNode = useCallback((ref: View | null) => {
if (!ref) return if (!ref) return
const node = findNodeHandle(ref) const node = findNodeHandle(ref)
if (node) { if (node) {
@@ -52,37 +64,43 @@ function FocusTrap({children}: {children: ReactNode}) {
useEffect(() => { useEffect(() => {
setTimeout(() => { setTimeout(() => {
focus(child.current) focusNode(child.current)
}, 1e3) }, 1e3)
}, [focus]) }, [focusNode])
return ( return (
<> <>
<Pressable <Pressable
accessible accessible
accessibilityLabel={_( accessibilityLabel={_(
msg`You've reached the start of the active content. Please go back, or activate to focus the first item.`, msg`You've reached the start of the active content.`,
)}
accessibilityHint={_(
msg`Please go back, or activate this element to return to the start of the active content.`,
)} )}
accessibilityActions={[{name: 'activate', label: 'activate'}]} accessibilityActions={[{name: 'activate', label: 'activate'}]}
onAccessibilityAction={event => { onAccessibilityAction={event => {
switch (event.nativeEvent.actionName) { switch (event.nativeEvent.actionName) {
case 'activate': { case 'activate': {
focus(child.current) focusNode(child.current)
} }
} }
}}> }}>
<Noop /> <Noop />
</Pressable> </Pressable>
{content} {decoratedChildren}
<Pressable <Pressable
accessibilityLabel={_( accessibilityLabel={_(
msg`You've reached the end of the active content. Please go back, or activate to go back to the beginning.`, msg`You've reached the end of the active content.`,
)}
accessibilityHint={_(
msg`Please go back, or activate this element to return to the start of the active content.`,
)} )}
accessibilityActions={[{name: 'activate', label: 'activate'}]} accessibilityActions={[{name: 'activate', label: 'activate'}]}
onAccessibilityAction={event => { onAccessibilityAction={event => {
switch (event.nativeEvent.actionName) { switch (event.nativeEvent.actionName) {
case 'activate': { case 'activate': {
focus(child.current) focusNode(child.current)
} }
} }
}}> }}>