Small logic cleanups (#3449)
* Small logic cleanups * Small logic cleanups (#3451) * remove a few things * oops * stop swallowing the error * queue callbacks * oops * log error if caught * no need to be nullable * move isClosing=true up * reset `isClosing` and `closeCallbacks` on close completion and open * run queued callbacks on `open` if there are any pending * rm unnecessary ref and check * ensure order of calls is always correct * call `snapToIndex()` on open * add tester to storybook --------- Co-authored-by: Hailey <me@haileyok.com>zio/stable
parent
a49a5a351d
commit
c96bc92042
|
@ -39,8 +39,7 @@ export function useDialogControl(): DialogOuterProps['control'] {
|
||||||
control.current.open()
|
control.current.open()
|
||||||
},
|
},
|
||||||
close: cb => {
|
close: cb => {
|
||||||
control.current.close()
|
control.current.close(cb)
|
||||||
cb?.()
|
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
[id, control],
|
[id, control],
|
||||||
|
|
|
@ -83,7 +83,7 @@ export function Outer({
|
||||||
const sheetOptions = nativeOptions?.sheet || {}
|
const sheetOptions = nativeOptions?.sheet || {}
|
||||||
const hasSnapPoints = !!sheetOptions.snapPoints
|
const hasSnapPoints = !!sheetOptions.snapPoints
|
||||||
const insets = useSafeAreaInsets()
|
const insets = useSafeAreaInsets()
|
||||||
const closeCallback = React.useRef<() => void>()
|
const closeCallbacks = React.useRef<(() => void)[]>([])
|
||||||
const {setDialogIsOpen} = useDialogStateControlContext()
|
const {setDialogIsOpen} = useDialogStateControlContext()
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
@ -96,22 +96,51 @@ export function Outer({
|
||||||
*/
|
*/
|
||||||
const isOpen = openIndex > -1
|
const isOpen = openIndex > -1
|
||||||
|
|
||||||
|
const callQueuedCallbacks = React.useCallback(() => {
|
||||||
|
for (const cb of closeCallbacks.current) {
|
||||||
|
try {
|
||||||
|
cb()
|
||||||
|
} catch (e: any) {
|
||||||
|
logger.error('Error running close callback', e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
closeCallbacks.current = []
|
||||||
|
}, [])
|
||||||
|
|
||||||
const open = React.useCallback<DialogControlProps['open']>(
|
const open = React.useCallback<DialogControlProps['open']>(
|
||||||
({index} = {}) => {
|
({index} = {}) => {
|
||||||
|
// Run any leftover callbacks that might have been queued up before calling `.open()`
|
||||||
|
callQueuedCallbacks()
|
||||||
|
|
||||||
setDialogIsOpen(control.id, true)
|
setDialogIsOpen(control.id, true)
|
||||||
// can be set to any index of `snapPoints`, but `0` is the first i.e. "open"
|
// can be set to any index of `snapPoints`, but `0` is the first i.e. "open"
|
||||||
setOpenIndex(index || 0)
|
setOpenIndex(index || 0)
|
||||||
|
sheet.current?.snapToIndex(index || 0)
|
||||||
},
|
},
|
||||||
[setOpenIndex, setDialogIsOpen, control.id],
|
[setDialogIsOpen, control.id, callQueuedCallbacks],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// This is the function that we call when we want to dismiss the dialog.
|
||||||
const close = React.useCallback<DialogControlProps['close']>(cb => {
|
const close = React.useCallback<DialogControlProps['close']>(cb => {
|
||||||
if (cb && typeof cb === 'function') {
|
if (typeof cb === 'function') {
|
||||||
closeCallback.current = cb
|
closeCallbacks.current.push(cb)
|
||||||
}
|
}
|
||||||
sheet.current?.close()
|
sheet.current?.close()
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
// This is the actual thing we are doing once we "confirm" the dialog. We want the dialog's close animation to
|
||||||
|
// happen before we run this. It is passed to the `BottomSheet` component.
|
||||||
|
const onCloseAnimationComplete = React.useCallback(() => {
|
||||||
|
// This removes the dialog from our list of stored dialogs. Not super necessary on iOS, but on Android this
|
||||||
|
// tells us that we need to toggle the accessibility overlay setting
|
||||||
|
setDialogIsOpen(control.id, false)
|
||||||
|
setOpenIndex(-1)
|
||||||
|
|
||||||
|
callQueuedCallbacks()
|
||||||
|
onClose?.()
|
||||||
|
}, [callQueuedCallbacks, control.id, onClose, setDialogIsOpen])
|
||||||
|
|
||||||
useImperativeHandle(
|
useImperativeHandle(
|
||||||
control.ref,
|
control.ref,
|
||||||
() => ({
|
() => ({
|
||||||
|
@ -121,21 +150,6 @@ export function Outer({
|
||||||
[open, close],
|
[open, close],
|
||||||
)
|
)
|
||||||
|
|
||||||
const onCloseInner = React.useCallback(() => {
|
|
||||||
try {
|
|
||||||
closeCallback.current?.()
|
|
||||||
} catch (e: any) {
|
|
||||||
logger.error(`Dialog closeCallback failed`, {
|
|
||||||
message: e.message,
|
|
||||||
})
|
|
||||||
} finally {
|
|
||||||
closeCallback.current = undefined
|
|
||||||
}
|
|
||||||
setDialogIsOpen(control.id, false)
|
|
||||||
onClose?.()
|
|
||||||
setOpenIndex(-1)
|
|
||||||
}, [control.id, onClose, setDialogIsOpen])
|
|
||||||
|
|
||||||
const context = React.useMemo(() => ({close}), [close])
|
const context = React.useMemo(() => ({close}), [close])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -163,7 +177,7 @@ export function Outer({
|
||||||
backdropComponent={Backdrop}
|
backdropComponent={Backdrop}
|
||||||
handleIndicatorStyle={{backgroundColor: t.palette.primary_500}}
|
handleIndicatorStyle={{backgroundColor: t.palette.primary_500}}
|
||||||
handleStyle={{display: 'none'}}
|
handleStyle={{display: 'none'}}
|
||||||
onClose={onCloseInner}>
|
onClose={onCloseAnimationComplete}>
|
||||||
<Context.Provider value={context}>
|
<Context.Provider value={context}>
|
||||||
<View
|
<View
|
||||||
style={[
|
style={[
|
||||||
|
|
|
@ -33,40 +33,41 @@ export function Outer({
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const {gtMobile} = useBreakpoints()
|
const {gtMobile} = useBreakpoints()
|
||||||
const [isOpen, setIsOpen] = React.useState(false)
|
const [isOpen, setIsOpen] = React.useState(false)
|
||||||
const [isVisible, setIsVisible] = React.useState(true)
|
|
||||||
const {setDialogIsOpen} = useDialogStateControlContext()
|
const {setDialogIsOpen} = useDialogStateControlContext()
|
||||||
|
|
||||||
const open = React.useCallback(() => {
|
const open = React.useCallback(() => {
|
||||||
setIsOpen(true)
|
|
||||||
setDialogIsOpen(control.id, true)
|
setDialogIsOpen(control.id, true)
|
||||||
|
setIsOpen(true)
|
||||||
}, [setIsOpen, setDialogIsOpen, control.id])
|
}, [setIsOpen, setDialogIsOpen, control.id])
|
||||||
|
|
||||||
const onCloseInner = React.useCallback(async () => {
|
|
||||||
setIsVisible(false)
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 150))
|
|
||||||
setIsOpen(false)
|
|
||||||
setIsVisible(true)
|
|
||||||
setDialogIsOpen(control.id, false)
|
|
||||||
onClose?.()
|
|
||||||
}, [control.id, onClose, setDialogIsOpen])
|
|
||||||
|
|
||||||
const close = React.useCallback<DialogControlProps['close']>(
|
const close = React.useCallback<DialogControlProps['close']>(
|
||||||
cb => {
|
cb => {
|
||||||
|
setDialogIsOpen(control.id, false)
|
||||||
|
setIsOpen(false)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (cb && typeof cb === 'function') {
|
if (cb && typeof cb === 'function') {
|
||||||
cb()
|
// This timeout ensures that the callback runs at the same time as it would on native. I.e.
|
||||||
|
// console.log('Step 1') -> close(() => console.log('Step 3')) -> console.log('Step 2')
|
||||||
|
// This should always output 'Step 1', 'Step 2', 'Step 3', but without the timeout it would output
|
||||||
|
// 'Step 1', 'Step 3', 'Step 2'.
|
||||||
|
setTimeout(cb)
|
||||||
}
|
}
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
logger.error(`Dialog closeCallback failed`, {
|
logger.error(`Dialog closeCallback failed`, {
|
||||||
message: e.message,
|
message: e.message,
|
||||||
})
|
})
|
||||||
} finally {
|
|
||||||
onCloseInner()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onClose?.()
|
||||||
},
|
},
|
||||||
[onCloseInner],
|
[control.id, onClose, setDialogIsOpen],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const handleBackgroundPress = React.useCallback(async () => {
|
||||||
|
close()
|
||||||
|
}, [close])
|
||||||
|
|
||||||
useImperativeHandle(
|
useImperativeHandle(
|
||||||
control.ref,
|
control.ref,
|
||||||
() => ({
|
() => ({
|
||||||
|
@ -103,7 +104,7 @@ export function Outer({
|
||||||
<TouchableWithoutFeedback
|
<TouchableWithoutFeedback
|
||||||
accessibilityHint={undefined}
|
accessibilityHint={undefined}
|
||||||
accessibilityLabel={_(msg`Close active dialog`)}
|
accessibilityLabel={_(msg`Close active dialog`)}
|
||||||
onPress={onCloseInner}>
|
onPress={handleBackgroundPress}>
|
||||||
<View
|
<View
|
||||||
style={[
|
style={[
|
||||||
web(a.fixed),
|
web(a.fixed),
|
||||||
|
@ -113,17 +114,15 @@ export function Outer({
|
||||||
gtMobile ? a.p_lg : a.p_md,
|
gtMobile ? a.p_lg : a.p_md,
|
||||||
{overflowY: 'auto'},
|
{overflowY: 'auto'},
|
||||||
]}>
|
]}>
|
||||||
{isVisible && (
|
<Animated.View
|
||||||
<Animated.View
|
entering={FadeIn.duration(150)}
|
||||||
entering={FadeIn.duration(150)}
|
// exiting={FadeOut.duration(150)}
|
||||||
// exiting={FadeOut.duration(150)}
|
style={[
|
||||||
style={[
|
web(a.fixed),
|
||||||
web(a.fixed),
|
a.inset_0,
|
||||||
a.inset_0,
|
{opacity: 0.8, backgroundColor: t.palette.black},
|
||||||
{opacity: 0.8, backgroundColor: t.palette.black},
|
]}
|
||||||
]}
|
/>
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<View
|
<View
|
||||||
style={[
|
style={[
|
||||||
|
@ -135,7 +134,7 @@ export function Outer({
|
||||||
minHeight: web('calc(90vh - 36px)') || undefined,
|
minHeight: web('calc(90vh - 36px)') || undefined,
|
||||||
},
|
},
|
||||||
]}>
|
]}>
|
||||||
{isVisible ? children : null}
|
{children}
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</TouchableWithoutFeedback>
|
</TouchableWithoutFeedback>
|
||||||
|
|
|
@ -123,6 +123,13 @@ export function Action({
|
||||||
cta,
|
cta,
|
||||||
testID,
|
testID,
|
||||||
}: {
|
}: {
|
||||||
|
/**
|
||||||
|
* Callback to run when the action is pressed. The method is called _after_
|
||||||
|
* the dialog closes.
|
||||||
|
*
|
||||||
|
* Note: The dialog will close automatically when the action is pressed, you
|
||||||
|
* should NOT close the dialog as a side effect of this method.
|
||||||
|
*/
|
||||||
onPress: () => void
|
onPress: () => void
|
||||||
color?: ButtonColor
|
color?: ButtonColor
|
||||||
/**
|
/**
|
||||||
|
@ -165,6 +172,13 @@ export function Basic({
|
||||||
description: string
|
description: string
|
||||||
cancelButtonCta?: string
|
cancelButtonCta?: string
|
||||||
confirmButtonCta?: string
|
confirmButtonCta?: string
|
||||||
|
/**
|
||||||
|
* Callback to run when the Confirm button is pressed. The method is called
|
||||||
|
* _after_ the dialog closes.
|
||||||
|
*
|
||||||
|
* Note: The dialog will close automatically when the action is pressed, you
|
||||||
|
* should NOT close the dialog as a side effect of this method.
|
||||||
|
*/
|
||||||
onConfirm: () => void
|
onConfirm: () => void
|
||||||
confirmButtonColor?: ButtonColor
|
confirmButtonColor?: ButtonColor
|
||||||
}>) {
|
}>) {
|
||||||
|
|
|
@ -507,9 +507,7 @@ export const ComposePost = observer(function ComposePost({
|
||||||
control={discardPromptControl}
|
control={discardPromptControl}
|
||||||
title={_(msg`Discard draft?`)}
|
title={_(msg`Discard draft?`)}
|
||||||
description={_(msg`Are you sure you'd like to discard this draft?`)}
|
description={_(msg`Are you sure you'd like to discard this draft?`)}
|
||||||
onConfirm={() => {
|
onConfirm={onClose}
|
||||||
discardPromptControl.close(onClose)
|
|
||||||
}}
|
|
||||||
confirmButtonCta={_(msg`Discard`)}
|
confirmButtonCta={_(msg`Discard`)}
|
||||||
confirmButtonColor="negative"
|
confirmButtonColor="negative"
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -6,12 +6,13 @@ import {atoms as a} from '#/alf'
|
||||||
import {Button, ButtonText} from '#/components/Button'
|
import {Button, ButtonText} from '#/components/Button'
|
||||||
import * as Dialog from '#/components/Dialog'
|
import * as Dialog from '#/components/Dialog'
|
||||||
import * as Prompt from '#/components/Prompt'
|
import * as Prompt from '#/components/Prompt'
|
||||||
import {H3, P} from '#/components/Typography'
|
import {H3, P, Text} from '#/components/Typography'
|
||||||
|
|
||||||
export function Dialogs() {
|
export function Dialogs() {
|
||||||
const scrollable = Dialog.useDialogControl()
|
const scrollable = Dialog.useDialogControl()
|
||||||
const basic = Dialog.useDialogControl()
|
const basic = Dialog.useDialogControl()
|
||||||
const prompt = Prompt.usePromptControl()
|
const prompt = Prompt.usePromptControl()
|
||||||
|
const testDialog = Dialog.useDialogControl()
|
||||||
const {closeAllDialogs} = useDialogStateControlContext()
|
const {closeAllDialogs} = useDialogStateControlContext()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -60,6 +61,15 @@ export function Dialogs() {
|
||||||
<ButtonText>Open prompt</ButtonText>
|
<ButtonText>Open prompt</ButtonText>
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="solid"
|
||||||
|
color="primary"
|
||||||
|
size="small"
|
||||||
|
onPress={testDialog.open}
|
||||||
|
label="one">
|
||||||
|
<ButtonText>Open Tester</ButtonText>
|
||||||
|
</Button>
|
||||||
|
|
||||||
<Prompt.Outer control={prompt}>
|
<Prompt.Outer control={prompt}>
|
||||||
<Prompt.TitleText>This is a prompt</Prompt.TitleText>
|
<Prompt.TitleText>This is a prompt</Prompt.TitleText>
|
||||||
<Prompt.DescriptionText>
|
<Prompt.DescriptionText>
|
||||||
|
@ -122,6 +132,131 @@ export function Dialogs() {
|
||||||
</View>
|
</View>
|
||||||
</Dialog.ScrollableInner>
|
</Dialog.ScrollableInner>
|
||||||
</Dialog.Outer>
|
</Dialog.Outer>
|
||||||
|
|
||||||
|
<Dialog.Outer control={testDialog}>
|
||||||
|
<Dialog.Handle />
|
||||||
|
|
||||||
|
<Dialog.ScrollableInner
|
||||||
|
accessibilityDescribedBy="dialog-description"
|
||||||
|
accessibilityLabelledBy="dialog-title">
|
||||||
|
<View style={[a.relative, a.gap_md, a.w_full]}>
|
||||||
|
<Text>
|
||||||
|
Watch the console logs to test each of these dialog edge cases.
|
||||||
|
Functionality should be consistent across both native and web. If
|
||||||
|
not then *sad face* something is wrong.
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
color="primary"
|
||||||
|
size="small"
|
||||||
|
onPress={() => {
|
||||||
|
testDialog.close(() => {
|
||||||
|
console.log('close callback')
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
label="Close It">
|
||||||
|
<ButtonText>Normal Use (Should just log)</ButtonText>
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
color="primary"
|
||||||
|
size="small"
|
||||||
|
onPress={() => {
|
||||||
|
testDialog.close(() => {
|
||||||
|
console.log('close callback')
|
||||||
|
})
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
testDialog.open()
|
||||||
|
}, 100)
|
||||||
|
}}
|
||||||
|
label="Close It">
|
||||||
|
<ButtonText>
|
||||||
|
Calls `.open()` in 100ms (Should log when the animation switches
|
||||||
|
to open)
|
||||||
|
</ButtonText>
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
color="primary"
|
||||||
|
size="small"
|
||||||
|
onPress={() => {
|
||||||
|
setTimeout(() => {
|
||||||
|
testDialog.open()
|
||||||
|
}, 2e3)
|
||||||
|
|
||||||
|
testDialog.close(() => {
|
||||||
|
console.log('close callback')
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
label="Close It">
|
||||||
|
<ButtonText>
|
||||||
|
Calls `.open()` in 2000ms (Should log after close animation and
|
||||||
|
not log on open)
|
||||||
|
</ButtonText>
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
color="primary"
|
||||||
|
size="small"
|
||||||
|
onPress={() => {
|
||||||
|
testDialog.close(() => {
|
||||||
|
console.log('close callback')
|
||||||
|
})
|
||||||
|
setTimeout(() => {
|
||||||
|
testDialog.close(() => {
|
||||||
|
console.log('close callback after 100ms')
|
||||||
|
})
|
||||||
|
}, 100)
|
||||||
|
}}
|
||||||
|
label="Close It">
|
||||||
|
<ButtonText>
|
||||||
|
Calls `.close()` then again in 100ms (should log twice)
|
||||||
|
</ButtonText>
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
color="primary"
|
||||||
|
size="small"
|
||||||
|
onPress={() => {
|
||||||
|
testDialog.close(() => {
|
||||||
|
console.log('close callback')
|
||||||
|
})
|
||||||
|
testDialog.close(() => {
|
||||||
|
console.log('close callback 2')
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
label="Close It">
|
||||||
|
<ButtonText>
|
||||||
|
Call `close()` twice immediately (should just log twice)
|
||||||
|
</ButtonText>
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
color="primary"
|
||||||
|
size="small"
|
||||||
|
onPress={() => {
|
||||||
|
console.log('Step 1')
|
||||||
|
testDialog.close(() => {
|
||||||
|
console.log('Step 3')
|
||||||
|
})
|
||||||
|
console.log('Step 2')
|
||||||
|
}}
|
||||||
|
label="Close It">
|
||||||
|
<ButtonText>
|
||||||
|
Log before `close()`, after `close()` and in the `close()`
|
||||||
|
callback. Should be an order of 1 2 3
|
||||||
|
</ButtonText>
|
||||||
|
</Button>
|
||||||
|
</View>
|
||||||
|
</Dialog.ScrollableInner>
|
||||||
|
</Dialog.Outer>
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue