Add Dialog, Portal

This commit is contained in:
Eric Bailey
2024-01-10 16:13:02 -06:00
parent 22f0c16cf1
commit 2bac965da4
9 changed files with 226 additions and 2 deletions
+5 -1
View File
@@ -41,6 +41,7 @@ import {
import {Provider as UnreadNotifsProvider} from 'state/queries/notifications/unread'
import * as persisted from '#/state/persisted'
import {Splash} from '#/Splash'
import {Provider as PortalProvider, Outlet as PortalOutlet} from '#/view/com/Portal'
SplashScreen.preventAutoHideAsync()
@@ -76,6 +77,7 @@ function InnerApp() {
<GestureHandlerRootView style={s.h100pct}>
<TestCtrls />
<Shell />
<PortalOutlet />
</GestureHandlerRootView>
</RootSiblingParent>
</ThemeProvider>
@@ -113,7 +115,9 @@ function App() {
<ModalStateProvider>
<LightboxStateProvider>
<I18nProvider>
<InnerApp />
<PortalProvider>
<InnerApp />
</PortalProvider>
</I18nProvider>
</LightboxStateProvider>
</ModalStateProvider>
+5 -1
View File
@@ -30,6 +30,7 @@ import {
} from 'state/session'
import {Provider as UnreadNotifsProvider} from 'state/queries/notifications/unread'
import * as persisted from '#/state/persisted'
import {Provider as PortalProvider, Outlet as PortalOutlet} from '#/view/com/Portal'
function InnerApp() {
const {isInitialLoad, currentAccount} = useSession()
@@ -58,6 +59,7 @@ function InnerApp() {
<RootSiblingParent>
<SafeAreaProvider>
<Shell />
<PortalOutlet />
</SafeAreaProvider>
</RootSiblingParent>
<ToastContainer />
@@ -94,7 +96,9 @@ function App() {
<ModalStateProvider>
<LightboxStateProvider>
<I18nProvider>
<InnerApp />
<PortalProvider>
<InnerApp />
</PortalProvider>
</I18nProvider>
</LightboxStateProvider>
</ModalStateProvider>
+7
View File
@@ -0,0 +1,7 @@
import React from 'react'
import {DialogProps} from '#/view/com/Dialog/types'
export function Dialog(props: React.PropsWithChildren<DialogProps>) {
return null
}
+96
View File
@@ -0,0 +1,96 @@
import React from 'react'
import {View, TouchableWithoutFeedback, DimensionValue} from 'react-native'
import {FocusScope} from '@tamagui/focus-scope'
import Animated, {FadeInDown, FadeIn, FadeOut} from 'react-native-reanimated'
import {useTheme, atoms as a, useBreakpoints} from '#/alf'
import {Portal} from '#/view/com/Portal'
import {DialogProps} from '#/view/com/Dialog/types'
const Context = React.createContext<{
dismiss: () => void
}>({
dismiss: () => {},
})
export function useDialog() {
return React.useContext(Context)
}
export function Dialog({
isOpen,
onDismiss,
children,
}: React.PropsWithChildren<DialogProps>) {
const t = useTheme()
const {gtMobile} = useBreakpoints()
const dismiss = React.useCallback(() => {
onDismiss()
}, [onDismiss])
React.useEffect(() => {
function handler(e: KeyboardEvent) {
if (e.key === 'Escape') dismiss()
}
document.addEventListener('keydown', handler)
return () => document.removeEventListener('keydown', handler)
}, [dismiss])
const context = React.useMemo(() => ({dismiss}), [dismiss])
return (
<>
{isOpen && (
<Portal>
<Context.Provider value={context}>
<TouchableWithoutFeedback
accessibilityRole="button"
onPress={onDismiss}>
<View
style={[
a.absolute,
a.inset_0,
a.z_10,
a.flex_row,
a.justify_center,
]}>
<Animated.View
entering={FadeIn.duration(200)}
exiting={FadeOut.duration(200)}
style={[
a.absolute,
a.inset_0,
t.atoms.bg_contrast_100,
{opacity: 0.8},
]}
/>
<View
style={[
a.w_full,
a.z_20,
{
maxWidth: 600,
paddingTop: gtMobile ? ('10vh' as DimensionValue) : 0,
},
]}>
<FocusScope loop enabled trapped>
<Animated.View
entering={FadeInDown.duration(200)}
exiting={FadeOut.duration(200)}
aria-role="dialog">
{children}
</Animated.View>
</FocusScope>
</View>
</View>
</TouchableWithoutFeedback>
</Context.Provider>
</Portal>
)}
</>
)
}
+4
View File
@@ -0,0 +1,4 @@
export type DialogProps = {
isOpen: boolean
onDismiss: () => void
}
+58
View File
@@ -0,0 +1,58 @@
import React from 'react'
type Component = React.ReactElement
type ContextType = {
outlet: Component | null
append(id: string, component: Component): void
remove(id: string): void
}
type ComponentMap = {
[id: string]: Component
}
export const Context = React.createContext<ContextType>({
outlet: null,
append: () => {},
remove: () => {},
})
export function Provider(props: React.PropsWithChildren<{}>) {
const map = React.useRef<ComponentMap>({})
const [outlet, setOutlet] = React.useState<ContextType['outlet']>(null)
const append = React.useCallback<ContextType['append']>((id, component) => {
if (map.current[id]) return
map.current[id] = <React.Fragment key={id}>{component}</React.Fragment>
setOutlet(<>{Object.values(map.current)}</>)
}, [])
const remove = React.useCallback<ContextType['remove']>(id => {
delete map.current[id]
setOutlet(<>{Object.values(map.current)}</>)
}, [])
console.log(outlet)
return (
<Context.Provider value={{outlet, append, remove}}>
{props.children}
</Context.Provider>
)
}
export function Outlet() {
const ctx = React.useContext(Context)
return ctx.outlet
}
export function Portal({children}: React.PropsWithChildren<{}>) {
const {append, remove} = React.useContext(Context)
const id = React.useId()
React.useEffect(() => {
append(id, children as Component)
return () => remove(id)
}, [id, children, append, remove])
return null
}
+25
View File
@@ -12,6 +12,7 @@ import {Text, H1, H2, H3, H4, H5, H6} from '#/view/com/Typography'
import {InputText} from '#/view/com/forms/InputText'
import {InputDate, utils} from '#/view/com/forms/InputDate'
import {Logo} from '#/view/icons/Logo'
import {Dialog} from '#/view/com/Dialog'
function ThemeSelector() {
const setColorMode = useSetColorMode()
@@ -316,6 +317,28 @@ function Forms() {
)
}
function Dialogs() {
const [isOpen, setIsOpen] = React.useState(false)
console.log({isOpen})
return (
<>
<Button type='secondary' size='small' onPress={() => setIsOpen(true)} accessibilityLabel='Open basic dialog' accessibilityHint='Open basic dialog'>
Open basic dialog
</Button>
<Dialog isOpen={isOpen} onDismiss={() => setIsOpen(false)}>
<Button type='primary' size='small' onPress={() => setIsOpen(false)} accessibilityLabel='Open basic dialog' accessibilityHint='Open basic dialog'>
Close basic dialog
</Button>
<Button type='secondary' size='small' onPress={() => setIsOpen(false)} accessibilityLabel='Open basic dialog' accessibilityHint='Open basic dialog'>
Close basic dialog
</Button>
</Dialog>
</>
)
}
export function DebugScreen() {
const t = useTheme()
@@ -325,6 +348,8 @@ export function DebugScreen() {
<View style={[a.p_xl, a.gap_xxl, {paddingBottom: 200}]}>
<ThemeSelector />
<Dialogs />
<Forms />
<Buttons />