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
+1
View File
@@ -70,6 +70,7 @@
"@segment/analytics-react-native": "^2.10.1",
"@segment/sovran-react-native": "^0.4.5",
"@sentry/react-native": "5.5.0",
"@tamagui/focus-scope": "^1.84.1",
"@tanstack/react-query": "^5.8.1",
"@tiptap/core": "^2.0.0-beta.220",
"@tiptap/extension-document": "^2.0.0-beta.220",
+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 />
+25
View File
@@ -6921,6 +6921,31 @@
"@svgr/plugin-svgo" "^5.5.0"
loader-utils "^2.0.0"
"@tamagui/compose-refs@1.84.1":
version "1.84.1"
resolved "https://registry.yarnpkg.com/@tamagui/compose-refs/-/compose-refs-1.84.1.tgz#244735edc3ac2e617389297f005d5bc25872465f"
integrity sha512-oZ0rUmQABlGm/QKQITxAW9WLV3qjyq1ehgoWcZVmtc1Kc/hkFQe2J+wRQV726CmTAnuUgUXi3eoNMwBVoZksfQ==
"@tamagui/constants@1.84.1":
version "1.84.1"
resolved "https://registry.yarnpkg.com/@tamagui/constants/-/constants-1.84.1.tgz#62e41837dbe844d14e255f3eea9c2583044d2509"
integrity sha512-QmvyCqtEIugqXutQI35GJQ1hlpSapYCdOHx9QlgsOWjAY34pu55MaY/tDrQeQ0AUmI/qx30vy7TsCJxB4QFEoQ==
"@tamagui/focus-scope@^1.84.1":
version "1.84.1"
resolved "https://registry.yarnpkg.com/@tamagui/focus-scope/-/focus-scope-1.84.1.tgz#e9f061184048c75f87da023f54b9c5abccdd460d"
integrity sha512-0E1Wc3jmKhafETfH1dUuJYmGK1bDNA/9TySbOeTjTToxUoL3V0G2W5JSwSMCDqR1Bl+xrGlGwzXTUhouw8qSog==
dependencies:
"@tamagui/compose-refs" "1.84.1"
"@tamagui/use-event" "1.84.1"
"@tamagui/use-event@1.84.1":
version "1.84.1"
resolved "https://registry.yarnpkg.com/@tamagui/use-event/-/use-event-1.84.1.tgz#a095a1bde9c40c4a397226c57c3fa32f6018f504"
integrity sha512-U88WCxvMz7ZSfMFMJEFbG3tJjK/Lf+PHlmtYvlx1V+YiqRBoj5+milzoM8PclENn5vZMiJW0ozYRgzI/cdE7Eg==
dependencies:
"@tamagui/constants" "1.84.1"
"@tanstack/query-core@5.8.1":
version "5.8.1"
resolved "https://registry.yarnpkg.com/@tanstack/query-core/-/query-core-5.8.1.tgz#5215a028370d9b2f32e83787a0ea119e2f977996"