Bluesky app fork with some witchin' additions 💫

Improve dialogs (#2933)

* Improve dialogs

* Remove comment, revert storybook

* Hacky fix

* Comments

authored by

Eric Bailey and committed by
GitHub
b52a7429 da62a77f

+123 -85
+70 -51
src/components/Dialog/index.tsx
··· 8 8 } from '@gorhom/bottom-sheet' 9 9 import {useSafeAreaInsets} from 'react-native-safe-area-context' 10 10 11 - import {useTheme, atoms as a} from '#/alf' 11 + import {useTheme, atoms as a, flatten} from '#/alf' 12 12 import {Portal} from '#/components/Portal' 13 13 import {createInput} from '#/components/forms/TextField' 14 14 ··· 36 36 const hasSnapPoints = !!sheetOptions.snapPoints 37 37 const insets = useSafeAreaInsets() 38 38 39 - const open = React.useCallback<DialogControlProps['open']>((i = 0) => { 40 - sheet.current?.snapToIndex(i) 41 - }, []) 39 + /* 40 + * Used to manage open/closed, but index is otherwise handled internally by `BottomSheet` 41 + */ 42 + const [openIndex, setOpenIndex] = React.useState(-1) 43 + 44 + /* 45 + * `openIndex` is the index of the snap point to open the bottom sheet to. If >0, the bottom sheet is open. 46 + */ 47 + const isOpen = openIndex > -1 48 + 49 + const open = React.useCallback<DialogControlProps['open']>( 50 + ({index} = {}) => { 51 + // can be set to any index of `snapPoints`, but `0` is the first i.e. "open" 52 + setOpenIndex(index || 0) 53 + }, 54 + [setOpenIndex], 55 + ) 42 56 43 57 const close = React.useCallback(() => { 44 58 sheet.current?.close() ··· 57 71 (index: number) => { 58 72 if (index === -1) { 59 73 onClose?.() 74 + setOpenIndex(-1) 60 75 } 61 76 }, 62 - [onClose], 77 + [onClose, setOpenIndex], 63 78 ) 64 79 65 80 const context = React.useMemo(() => ({close}), [close]) 66 81 67 82 return ( 68 - <Portal> 69 - <BottomSheet 70 - enableDynamicSizing={!hasSnapPoints} 71 - enablePanDownToClose 72 - keyboardBehavior="interactive" 73 - android_keyboardInputMode="adjustResize" 74 - keyboardBlurBehavior="restore" 75 - topInset={insets.top} 76 - {...sheetOptions} 77 - ref={sheet} 78 - index={-1} 79 - backgroundStyle={{backgroundColor: 'transparent'}} 80 - backdropComponent={props => ( 81 - <BottomSheetBackdrop 82 - opacity={0.4} 83 - appearsOnIndex={0} 84 - disappearsOnIndex={-1} 85 - {...props} 86 - /> 87 - )} 88 - handleIndicatorStyle={{backgroundColor: t.palette.primary_500}} 89 - handleStyle={{display: 'none'}} 90 - onChange={onChange}> 91 - <Context.Provider value={context}> 92 - <View 93 - style={[ 94 - a.absolute, 95 - a.inset_0, 96 - t.atoms.bg, 97 - { 98 - borderTopLeftRadius: 40, 99 - borderTopRightRadius: 40, 100 - height: Dimensions.get('window').height * 2, 101 - }, 102 - ]} 103 - /> 104 - {children} 105 - </Context.Provider> 106 - </BottomSheet> 107 - </Portal> 83 + isOpen && ( 84 + <Portal> 85 + <BottomSheet 86 + enableDynamicSizing={!hasSnapPoints} 87 + enablePanDownToClose 88 + keyboardBehavior="interactive" 89 + android_keyboardInputMode="adjustResize" 90 + keyboardBlurBehavior="restore" 91 + topInset={insets.top} 92 + {...sheetOptions} 93 + ref={sheet} 94 + index={openIndex} 95 + backgroundStyle={{backgroundColor: 'transparent'}} 96 + backdropComponent={props => ( 97 + <BottomSheetBackdrop 98 + opacity={0.4} 99 + appearsOnIndex={0} 100 + disappearsOnIndex={-1} 101 + {...props} 102 + /> 103 + )} 104 + handleIndicatorStyle={{backgroundColor: t.palette.primary_500}} 105 + handleStyle={{display: 'none'}} 106 + onChange={onChange}> 107 + <Context.Provider value={context}> 108 + <View 109 + style={[ 110 + a.absolute, 111 + a.inset_0, 112 + t.atoms.bg, 113 + { 114 + borderTopLeftRadius: 40, 115 + borderTopRightRadius: 40, 116 + height: Dimensions.get('window').height * 2, 117 + }, 118 + ]} 119 + /> 120 + {hasSnapPoints ? children : <View>{children}</View>} 121 + </Context.Provider> 122 + </BottomSheet> 123 + </Portal> 124 + ) 108 125 ) 109 126 } 110 127 111 - // TODO a11y props here, or is that handled by the sheet? 112 - export function Inner(props: DialogInnerProps) { 128 + export function Inner({children, style}: DialogInnerProps) { 113 129 const insets = useSafeAreaInsets() 114 130 return ( 115 131 <BottomSheetView 116 132 style={[ 117 - a.p_lg, 133 + a.p_xl, 118 134 { 119 135 paddingTop: 40, 120 136 borderTopLeftRadius: 40, 121 137 borderTopRightRadius: 40, 122 138 paddingBottom: insets.bottom + a.pb_5xl.paddingBottom, 123 139 }, 140 + flatten(style), 124 141 ]}> 125 - {props.children} 142 + {children} 126 143 </BottomSheetView> 127 144 ) 128 145 } 129 146 130 - export function ScrollableInner(props: DialogInnerProps) { 147 + export function ScrollableInner({children, style}: DialogInnerProps) { 131 148 const insets = useSafeAreaInsets() 132 149 return ( 133 150 <BottomSheetScrollView ··· 136 153 style={[ 137 154 a.flex_1, // main diff is this 138 155 a.p_xl, 156 + a.h_full, 139 157 { 140 158 paddingTop: 40, 141 159 borderTopLeftRadius: 40, 142 160 borderTopRightRadius: 40, 143 161 }, 162 + flatten(style), 144 163 ]}> 145 - {props.children} 164 + {children} 146 165 <View style={{height: insets.bottom + a.pt_5xl.paddingTop}} /> 147 166 </BottomSheetScrollView> 148 167 )
+31 -26
src/components/Dialog/index.web.tsx
··· 5 5 import {msg} from '@lingui/macro' 6 6 import {useLingui} from '@lingui/react' 7 7 8 - import {useTheme, atoms as a, useBreakpoints, web} from '#/alf' 8 + import {useTheme, atoms as a, useBreakpoints, web, flatten} from '#/alf' 9 9 import {Portal} from '#/components/Portal' 10 10 11 11 import {DialogOuterProps, DialogInnerProps} from '#/components/Dialog/types' 12 12 import {Context} from '#/components/Dialog/context' 13 + import {Button, ButtonIcon} from '#/components/Button' 14 + import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times' 13 15 14 16 export {useDialogControl, useDialogContext} from '#/components/Dialog/context' 15 17 export * from '#/components/Dialog/types' ··· 18 20 const stopPropagation = (e: any) => e.stopPropagation() 19 21 20 22 export function Outer({ 23 + children, 21 24 control, 22 25 onClose, 23 - children, 24 26 }: React.PropsWithChildren<DialogOuterProps>) { 25 27 const {_} = useLingui() 26 28 const t = useTheme() ··· 147 149 a.rounded_md, 148 150 a.w_full, 149 151 a.border, 150 - gtMobile ? a.p_xl : a.p_lg, 152 + gtMobile ? a.p_2xl : a.p_xl, 151 153 t.atoms.bg, 152 154 { 153 155 maxWidth: 600, ··· 156 158 shadowOpacity: t.name === 'light' ? 0.1 : 0.4, 157 159 shadowRadius: 30, 158 160 }, 159 - ...(Array.isArray(style) ? style : [style || {}]), 161 + flatten(style), 160 162 ]}> 161 163 {children} 162 164 </Animated.View> ··· 170 172 return null 171 173 } 172 174 173 - /** 174 - * TODO(eric) unused rn 175 - */ 176 - // export function Close() { 177 - // const {_} = useLingui() 178 - // const t = useTheme() 179 - // const {close} = useDialogContext() 180 - // return ( 181 - // <View 182 - // style={[ 183 - // a.absolute, 184 - // a.z_10, 185 - // { 186 - // top: a.pt_lg.paddingTop, 187 - // right: a.pr_lg.paddingRight, 188 - // }, 189 - // ]}> 190 - // <Button onPress={close} label={_(msg`Close active dialog`)}> 191 - // </Button> 192 - // </View> 193 - // ) 194 - // } 175 + export function Close() { 176 + const {_} = useLingui() 177 + const {close} = React.useContext(Context) 178 + return ( 179 + <View 180 + style={[ 181 + a.absolute, 182 + a.z_10, 183 + { 184 + top: a.pt_md.paddingTop, 185 + right: a.pr_md.paddingRight, 186 + }, 187 + ]}> 188 + <Button 189 + size="small" 190 + variant="ghost" 191 + color="primary" 192 + shape="round" 193 + onPress={close} 194 + label={_(msg`Close active dialog`)}> 195 + <ButtonIcon icon={X} size="md" /> 196 + </Button> 197 + </View> 198 + ) 199 + }
+15 -6
src/components/Dialog/types.ts
··· 1 1 import React from 'react' 2 - import type {ViewStyle, AccessibilityProps} from 'react-native' 2 + import type {AccessibilityProps} from 'react-native' 3 3 import {BottomSheetProps} from '@gorhom/bottom-sheet' 4 + 5 + import {ViewStyleProp} from '#/alf' 4 6 5 7 type A11yProps = Required<AccessibilityProps> 6 8 ··· 8 10 close: () => void 9 11 } 10 12 13 + export type DialogControlOpenOptions = { 14 + /** 15 + * NATIVE ONLY 16 + * 17 + * Optional index of the snap point to open the bottom sheet to. Defaults to 18 + * 0, which is the first snap point (i.e. "open"). 19 + */ 20 + index?: number 21 + } 22 + 11 23 export type DialogControlProps = { 12 - open: (index?: number) => void 24 + open: (options?: DialogControlOpenOptions) => void 13 25 close: () => void 14 26 } 15 27 ··· 26 38 webOptions?: {} 27 39 } 28 40 29 - type DialogInnerPropsBase<T> = React.PropsWithChildren<{ 30 - style?: ViewStyle 31 - }> & 32 - T 41 + type DialogInnerPropsBase<T> = React.PropsWithChildren<ViewStyleProp> & T 33 42 export type DialogInnerProps = 34 43 | DialogInnerPropsBase<{ 35 44 label?: undefined
+1 -1
src/components/Prompt.tsx
··· 41 41 <Dialog.Inner 42 42 accessibilityLabelledBy={titleId} 43 43 accessibilityDescribedBy={descriptionId} 44 - style={{width: 'auto', maxWidth: 400}}> 44 + style={[{width: 'auto', maxWidth: 400}]}> 45 45 {children} 46 46 </Dialog.Inner> 47 47 </Context.Provider>
+5
src/components/icons/Times.tsx
··· 1 + import {createSinglePathSVG} from './TEMPLATE' 2 + 3 + export const TimesLarge_Stroke2_Corner0_Rounded = createSinglePathSVG({ 4 + path: 'M4.293 4.293a1 1 0 0 1 1.414 0L12 10.586l6.293-6.293a1 1 0 1 1 1.414 1.414L13.414 12l6.293 6.293a1 1 0 0 1-1.414 1.414L12 13.414l-6.293 6.293a1 1 0 0 1-1.414-1.414L10.586 12 4.293 5.707a1 1 0 0 1 0-1.414Z', 5 + })
+1 -1
src/view/screens/Storybook/Dialogs.tsx
··· 50 50 51 51 <Dialog.Outer 52 52 control={control} 53 - nativeOptions={{sheet: {snapPoints: ['90%']}}}> 53 + nativeOptions={{sheet: {snapPoints: ['100%']}}}> 54 54 <Dialog.Handle /> 55 55 56 56 <Dialog.ScrollableInner