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