my fork of the bluesky client
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}