Rough sketch
This commit is contained in:
@@ -0,0 +1,13 @@
|
||||
import {createContext, useContext} from 'react'
|
||||
|
||||
import {type ToastApi} from '#/components/Toast/types'
|
||||
|
||||
export type ToastContext = ToastApi
|
||||
|
||||
export const Context = createContext<ToastContext>({
|
||||
show() {},
|
||||
})
|
||||
|
||||
export function useToast() {
|
||||
return useContext(Context)
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import {createPortalGroup} from '#/components/Portal'
|
||||
|
||||
const portal = createPortalGroup()
|
||||
|
||||
export const Provider = portal.Provider
|
||||
export const Outlet = portal.Outlet
|
||||
export const Portal = portal.Portal
|
||||
@@ -1,5 +1,5 @@
|
||||
import {useEffect, useMemo, useRef, useState} from 'react'
|
||||
import {AccessibilityInfo} from 'react-native'
|
||||
import {useCallback, useEffect, useMemo, useRef, useState} from 'react'
|
||||
import {AccessibilityInfo, View} from 'react-native'
|
||||
import {
|
||||
Gesture,
|
||||
GestureDetector,
|
||||
@@ -18,15 +18,100 @@ import Animated, {
|
||||
} from 'react-native-reanimated'
|
||||
import RootSiblings from 'react-native-root-siblings'
|
||||
import {useSafeAreaInsets} from 'react-native-safe-area-context'
|
||||
import {nanoid} from 'nanoid/non-secure'
|
||||
|
||||
import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
|
||||
import {atoms as a} from '#/alf'
|
||||
import {DEFAULT_TOAST_DURATION} from '#/components/Toast/const'
|
||||
import {Context} from '#/components/Toast/Context'
|
||||
import * as Portal from '#/components/Toast/Portal'
|
||||
import {Toast} from '#/components/Toast/Toast'
|
||||
import {type ToastApi, type ToastType} from '#/components/Toast/types'
|
||||
import {
|
||||
type ToastApi,
|
||||
type ToastProps,
|
||||
type ToastType,
|
||||
} from '#/components/Toast/types'
|
||||
|
||||
export {useToast} from '#/components/Toast/Context'
|
||||
export {Outlet} from '#/components/Toast/Portal'
|
||||
|
||||
const TOAST_ANIMATION_DURATION = 300
|
||||
|
||||
export function ToastProvider({children}: {children: React.ReactNode}) {
|
||||
const {top} = useSafeAreaInsets()
|
||||
const [toasts, setToasts] = useState<
|
||||
{
|
||||
id: string
|
||||
toast: ToastProps
|
||||
timeout: NodeJS.Timeout
|
||||
}[]
|
||||
>([])
|
||||
|
||||
const show = useCallback<ToastApi['show']>(props => {
|
||||
AccessibilityInfo.announceForAccessibility(props.a11yLabel)
|
||||
|
||||
setToasts(prevToasts => {
|
||||
const id = nanoid()
|
||||
return [
|
||||
...prevToasts,
|
||||
{
|
||||
id,
|
||||
toast: props,
|
||||
timeout: setTimeout(() => {
|
||||
setToasts(currentToasts => currentToasts.filter(t => t.id !== id))
|
||||
}, props.duration || DEFAULT_TOAST_DURATION),
|
||||
},
|
||||
]
|
||||
})
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
setToasts(toasts => {
|
||||
toasts.map(({timeout}) => {
|
||||
clearTimeout(timeout)
|
||||
})
|
||||
return toasts
|
||||
})
|
||||
}
|
||||
}, [])
|
||||
|
||||
const ctx = useMemo(
|
||||
() => ({
|
||||
show,
|
||||
}),
|
||||
[show],
|
||||
)
|
||||
|
||||
return (
|
||||
<Context.Provider value={ctx}>
|
||||
<Portal.Provider>
|
||||
{children}
|
||||
|
||||
{toasts.length ? (
|
||||
<Portal.Portal>
|
||||
<View
|
||||
style={[
|
||||
a.absolute,
|
||||
a.gap_sm,
|
||||
{
|
||||
flexDirection: 'column-reverse',
|
||||
top: top,
|
||||
left: a.px_lg.paddingLeft,
|
||||
right: a.px_lg.paddingLeft,
|
||||
},
|
||||
]}>
|
||||
{toasts.map(({id, toast}) => (
|
||||
<Toast key={id} type={toast.type} content={toast.content} />
|
||||
))}
|
||||
</View>
|
||||
</Portal.Portal>
|
||||
) : null}
|
||||
</Portal.Provider>
|
||||
</Context.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export function ToastContainer() {
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -2,15 +2,26 @@
|
||||
* Note: relies on styles in #/styles.css
|
||||
*/
|
||||
|
||||
import {useEffect, useState} from 'react'
|
||||
import {useCallback, useEffect, useMemo, useState} from 'react'
|
||||
import {AccessibilityInfo, Pressable, View} from 'react-native'
|
||||
import {useSafeAreaInsets} from 'react-native-safe-area-context'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {nanoid} from 'nanoid/non-secure'
|
||||
|
||||
import {atoms as a, useBreakpoints} from '#/alf'
|
||||
import {DEFAULT_TOAST_DURATION} from '#/components/Toast/const'
|
||||
import {Context} from '#/components/Toast/Context'
|
||||
import * as Portal from '#/components/Toast/Portal'
|
||||
import {Toast} from '#/components/Toast/Toast'
|
||||
import {type ToastApi, type ToastType} from '#/components/Toast/types'
|
||||
import {
|
||||
type ToastApi,
|
||||
type ToastProps,
|
||||
type ToastType,
|
||||
} from '#/components/Toast/types'
|
||||
|
||||
export {useToast} from '#/components/Toast/Context'
|
||||
export {Outlet} from '#/components/Toast/Portal'
|
||||
|
||||
const TOAST_ANIMATION_STYLES = {
|
||||
entering: {
|
||||
@@ -31,6 +42,85 @@ let globalSetActiveToast: GlobalSetActiveToast | undefined
|
||||
let toastTimeout: NodeJS.Timeout | undefined
|
||||
type ToastContainerProps = {}
|
||||
|
||||
export function ToastProvider({children}: {children: React.ReactNode}) {
|
||||
const {bottom} = useSafeAreaInsets()
|
||||
const {gtPhone} = useBreakpoints()
|
||||
const [toasts, setToasts] = useState<
|
||||
{
|
||||
id: string
|
||||
toast: ToastProps
|
||||
timeout: NodeJS.Timeout
|
||||
}[]
|
||||
>([])
|
||||
|
||||
const show = useCallback<ToastApi['show']>(props => {
|
||||
setToasts(prevToasts => {
|
||||
const id = nanoid()
|
||||
return [
|
||||
...prevToasts,
|
||||
{
|
||||
id,
|
||||
toast: props,
|
||||
timeout: setTimeout(() => {
|
||||
setToasts(currentToasts => currentToasts.filter(t => t.id !== id))
|
||||
}, props.duration || DEFAULT_TOAST_DURATION),
|
||||
},
|
||||
]
|
||||
})
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
setToasts(toasts => {
|
||||
toasts.map(({timeout}) => {
|
||||
clearTimeout(timeout)
|
||||
})
|
||||
return toasts
|
||||
})
|
||||
}
|
||||
}, [])
|
||||
|
||||
const ctx = useMemo(
|
||||
() => ({
|
||||
show,
|
||||
}),
|
||||
[show],
|
||||
)
|
||||
|
||||
return (
|
||||
<Context.Provider value={ctx}>
|
||||
<Portal.Provider>
|
||||
{children}
|
||||
|
||||
{toasts.length && (
|
||||
<Portal.Portal>
|
||||
<View
|
||||
style={[
|
||||
a.fixed,
|
||||
a.gap_sm,
|
||||
{
|
||||
flexDirection: 'column-reverse',
|
||||
left: a.px_xl.paddingLeft,
|
||||
right: a.px_xl.paddingLeft,
|
||||
bottom: Math.max(bottom, a.px_xl.paddingLeft),
|
||||
},
|
||||
gtPhone && [
|
||||
{
|
||||
maxWidth: 380,
|
||||
},
|
||||
],
|
||||
]}>
|
||||
{toasts.map(({id, toast}) => (
|
||||
<Toast key={id} content={toast.content} type={toast.type} />
|
||||
))}
|
||||
</View>
|
||||
</Portal.Portal>
|
||||
)}
|
||||
</Portal.Provider>
|
||||
</Context.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export const ToastContainer: React.FC<ToastContainerProps> = ({}) => {
|
||||
const {_} = useLingui()
|
||||
const {gtPhone} = useBreakpoints()
|
||||
|
||||
@@ -1,24 +1,26 @@
|
||||
export type ToastType = 'default' | 'success' | 'error' | 'warning' | 'info'
|
||||
|
||||
export type ToastApi = {
|
||||
show: (props: {
|
||||
/**
|
||||
* The type of toast to show. This determines the styling and icon used.
|
||||
*/
|
||||
type: ToastType
|
||||
/**
|
||||
* A string, `Text`, or `Span` components to render inside the toast. This
|
||||
* allows additional formatting of the content, but should not be used for
|
||||
* interactive elements link links or buttons.
|
||||
*/
|
||||
content: React.ReactNode | string
|
||||
/**
|
||||
* Accessibility label for the toast, used for screen readers.
|
||||
*/
|
||||
a11yLabel: string
|
||||
/**
|
||||
* Defaults to `DEFAULT_TOAST_DURATION` from `#components/Toast/const`.
|
||||
*/
|
||||
duration?: number
|
||||
}) => void
|
||||
export type ToastProps = {
|
||||
/**
|
||||
* The type of toast to show. This determines the styling and icon used.
|
||||
*/
|
||||
type: ToastType
|
||||
/**
|
||||
* A string, `Text`, or `Span` components to render inside the toast. This
|
||||
* allows additional formatting of the content, but should not be used for
|
||||
* interactive elements link links or buttons.
|
||||
*/
|
||||
content: React.ReactNode | string
|
||||
/**
|
||||
* Accessibility label for the toast, used for screen readers.
|
||||
*/
|
||||
a11yLabel: string
|
||||
/**
|
||||
* Defaults to `DEFAULT_TOAST_DURATION` from `#components/Toast/const`.
|
||||
*/
|
||||
duration?: number
|
||||
}
|
||||
|
||||
export type ToastApi = {
|
||||
show: (props: ToastProps) => void
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user