my fork of the bluesky client
at main 244 lines 5.9 kB view raw
1import React from 'react' 2import {Pressable, StyleProp, View, ViewStyle} from 'react-native' 3import {msg, Trans} from '@lingui/macro' 4import {useLingui} from '@lingui/react' 5import flattenReactChildren from 'react-keyed-flatten-children' 6 7import {isNative} from '#/platform/detection' 8import {atoms as a, useTheme} from '#/alf' 9import {Button, ButtonText} from '#/components/Button' 10import * as Dialog from '#/components/Dialog' 11import {useInteractionState} from '#/components/hooks/useInteractionState' 12import {Context, ItemContext} from '#/components/Menu/context' 13import { 14 ContextType, 15 GroupProps, 16 ItemIconProps, 17 ItemProps, 18 ItemTextProps, 19 TriggerProps, 20} from '#/components/Menu/types' 21import {Text} from '#/components/Typography' 22 23export { 24 type DialogControlProps as MenuControlProps, 25 useDialogControl as useMenuControl, 26} from '#/components/Dialog' 27 28export function useMemoControlContext() { 29 return React.useContext(Context) 30} 31 32export function Root({ 33 children, 34 control, 35}: React.PropsWithChildren<{ 36 control?: Dialog.DialogOuterProps['control'] 37}>) { 38 const defaultControl = Dialog.useDialogControl() 39 const context = React.useMemo<ContextType>( 40 () => ({ 41 control: control || defaultControl, 42 }), 43 [control, defaultControl], 44 ) 45 46 return <Context.Provider value={context}>{children}</Context.Provider> 47} 48 49export function Trigger({children, label, role = 'button'}: TriggerProps) { 50 const {control} = React.useContext(Context) 51 const {state: focused, onIn: onFocus, onOut: onBlur} = useInteractionState() 52 const { 53 state: pressed, 54 onIn: onPressIn, 55 onOut: onPressOut, 56 } = useInteractionState() 57 58 return children({ 59 isNative: true, 60 control, 61 state: { 62 hovered: false, 63 focused, 64 pressed, 65 }, 66 props: { 67 onPress: control.open, 68 onFocus, 69 onBlur, 70 onPressIn, 71 onPressOut, 72 accessibilityLabel: label, 73 accessibilityRole: role, 74 }, 75 }) 76} 77 78export function Outer({ 79 children, 80 showCancel, 81}: React.PropsWithChildren<{ 82 showCancel?: boolean 83 style?: StyleProp<ViewStyle> 84}>) { 85 const context = React.useContext(Context) 86 const {_} = useLingui() 87 88 return ( 89 <Dialog.Outer 90 control={context.control} 91 nativeOptions={{preventExpansion: true}}> 92 <Dialog.Handle /> 93 {/* Re-wrap with context since Dialogs are portal-ed to root */} 94 <Context.Provider value={context}> 95 <Dialog.ScrollableInner label={_(msg`Menu`)} style={[a.py_sm]}> 96 <View style={[a.gap_lg]}> 97 {children} 98 {isNative && showCancel && <Cancel />} 99 </View> 100 </Dialog.ScrollableInner> 101 </Context.Provider> 102 </Dialog.Outer> 103 ) 104} 105 106export function Item({children, label, style, onPress, ...rest}: ItemProps) { 107 const t = useTheme() 108 const {control} = React.useContext(Context) 109 const {state: focused, onIn: onFocus, onOut: onBlur} = useInteractionState() 110 const { 111 state: pressed, 112 onIn: onPressIn, 113 onOut: onPressOut, 114 } = useInteractionState() 115 116 return ( 117 <Pressable 118 {...rest} 119 accessibilityHint="" 120 accessibilityLabel={label} 121 onFocus={onFocus} 122 onBlur={onBlur} 123 onPress={async e => { 124 await onPress(e) 125 if (!e.defaultPrevented) { 126 control?.close() 127 } 128 }} 129 onPressIn={e => { 130 onPressIn() 131 rest.onPressIn?.(e) 132 }} 133 onPressOut={e => { 134 onPressOut() 135 rest.onPressOut?.(e) 136 }} 137 style={[ 138 a.flex_row, 139 a.align_center, 140 a.gap_sm, 141 a.px_md, 142 a.rounded_md, 143 a.border, 144 t.atoms.bg_contrast_25, 145 t.atoms.border_contrast_low, 146 {minHeight: 44, paddingVertical: 10}, 147 style, 148 (focused || pressed) && !rest.disabled && [t.atoms.bg_contrast_50], 149 ]}> 150 <ItemContext.Provider value={{disabled: Boolean(rest.disabled)}}> 151 {children} 152 </ItemContext.Provider> 153 </Pressable> 154 ) 155} 156 157export function ItemText({children, style}: ItemTextProps) { 158 const t = useTheme() 159 const {disabled} = React.useContext(ItemContext) 160 return ( 161 <Text 162 numberOfLines={1} 163 ellipsizeMode="middle" 164 style={[ 165 a.flex_1, 166 a.text_md, 167 a.font_bold, 168 t.atoms.text_contrast_high, 169 {paddingTop: 3}, 170 style, 171 disabled && t.atoms.text_contrast_low, 172 ]}> 173 {children} 174 </Text> 175 ) 176} 177 178export function ItemIcon({icon: Comp}: ItemIconProps) { 179 const t = useTheme() 180 const {disabled} = React.useContext(ItemContext) 181 return ( 182 <Comp 183 size="lg" 184 fill={ 185 disabled 186 ? t.atoms.text_contrast_low.color 187 : t.atoms.text_contrast_medium.color 188 } 189 /> 190 ) 191} 192 193export function Group({children, style}: GroupProps) { 194 const t = useTheme() 195 return ( 196 <View 197 style={[ 198 a.rounded_md, 199 a.overflow_hidden, 200 a.border, 201 t.atoms.border_contrast_low, 202 style, 203 ]}> 204 {flattenReactChildren(children).map((child, i) => { 205 return React.isValidElement(child) && child.type === Item ? ( 206 <React.Fragment key={i}> 207 {i > 0 ? ( 208 <View style={[a.border_b, t.atoms.border_contrast_low]} /> 209 ) : null} 210 {React.cloneElement(child, { 211 // @ts-ignore 212 style: { 213 borderRadius: 0, 214 borderWidth: 0, 215 }, 216 })} 217 </React.Fragment> 218 ) : null 219 })} 220 </View> 221 ) 222} 223 224function Cancel() { 225 const {_} = useLingui() 226 const {control} = React.useContext(Context) 227 228 return ( 229 <Button 230 label={_(msg`Close this dialog`)} 231 size="small" 232 variant="ghost" 233 color="secondary" 234 onPress={() => control.close()}> 235 <ButtonText> 236 <Trans>Cancel</Trans> 237 </ButtonText> 238 </Button> 239 ) 240} 241 242export function Divider() { 243 return null 244}