mirror of https://git.lenooby09.tech/LeNooby09/social-app.git
1import {
2 Children,
3 cloneElement,
4 isValidElement,
5 type ReactElement,
6 type ReactNode,
7 useCallback,
8 useEffect,
9 useMemo,
10 useRef,
11} from 'react'
12import {
13 AccessibilityInfo,
14 findNodeHandle,
15 Pressable,
16 Text,
17 View,
18} from 'react-native'
19import {msg} from '@lingui/macro'
20import {useLingui} from '@lingui/react'
21
22import {useA11y} from '#/state/a11y'
23
24/**
25 * Conditionally wraps children in a `FocusTrap` component based on whether
26 * screen reader support is enabled. THIS SHOULD BE USED SPARINGLY, only when
27 * no better option is available.
28 */
29export function FocusScope({children}: {children: ReactNode}) {
30 const {screenReaderEnabled} = useA11y()
31
32 return screenReaderEnabled ? <FocusTrap>{children}</FocusTrap> : children
33}
34
35/**
36 * `FocusTrap` is intended as a last-ditch effort to ensure that users keep
37 * focus within a certain section of the app, like an overlay.
38 *
39 * It works by placing "guards" at the start and end of the active content.
40 * Then when the user reaches either of those guards, it will announce that
41 * they have reached the start or end of the content and tell them how to
42 * remain within the active content section.
43 */
44function FocusTrap({children}: {children: ReactNode}) {
45 const {_} = useLingui()
46 const child = useRef<View>(null)
47
48 /*
49 * Here we add a ref to the first child of this component. This currently
50 * overrides any ref already on that first child, so we throw an error here
51 * to prevent us from ever accidentally doing this.
52 */
53 const decoratedChildren = useMemo(() => {
54 return Children.toArray(children).map((node, i) => {
55 if (i === 0 && isValidElement(node)) {
56 const n = node as ReactElement<any>
57 if (n.props.ref !== undefined) {
58 throw new Error(
59 'FocusScope needs to override the ref on its first child.',
60 )
61 }
62 return cloneElement(n, {
63 ...n.props,
64 ref: child,
65 })
66 }
67 return node
68 })
69 }, [children])
70
71 const focusNode = useCallback((ref: View | null) => {
72 if (!ref) return
73 const node = findNodeHandle(ref)
74 if (node) {
75 AccessibilityInfo.setAccessibilityFocus(node)
76 }
77 }, [])
78
79 useEffect(() => {
80 setTimeout(() => {
81 focusNode(child.current)
82 }, 1e3)
83 }, [focusNode])
84
85 return (
86 <>
87 <Pressable
88 accessible
89 accessibilityLabel={_(
90 msg`You've reached the start of the active content.`,
91 )}
92 accessibilityHint={_(
93 msg`Please go back, or activate this element to return to the start of the active content.`,
94 )}
95 accessibilityActions={[{name: 'activate', label: 'activate'}]}
96 onAccessibilityAction={event => {
97 switch (event.nativeEvent.actionName) {
98 case 'activate': {
99 focusNode(child.current)
100 }
101 }
102 }}>
103 <Noop />
104 </Pressable>
105 <View
106 /**
107 * This property traps focus effectively on iOS, but not on Android.
108 */
109 accessibilityViewIsModal>
110 {decoratedChildren}
111 </View>
112 <Pressable
113 accessibilityLabel={_(
114 msg`You've reached the end of the active content.`,
115 )}
116 accessibilityHint={_(
117 msg`Please go back, or activate this element to return to the start of the active content.`,
118 )}
119 accessibilityActions={[{name: 'activate', label: 'activate'}]}
120 onAccessibilityAction={event => {
121 switch (event.nativeEvent.actionName) {
122 case 'activate': {
123 focusNode(child.current)
124 }
125 }
126 }}>
127 <Noop />
128 </Pressable>
129 </>
130 )
131}
132
133function Noop() {
134 return (
135 <Text
136 accessible={false}
137 style={{
138 height: 1,
139 opacity: 0,
140 }}>
141 {' '}
142 </Text>
143 )
144}