Add Dialog, Portal
This commit is contained in:
@@ -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
@@ -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
@@ -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>
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
import React from 'react'
|
||||
|
||||
import {DialogProps} from '#/view/com/Dialog/types'
|
||||
|
||||
export function Dialog(props: React.PropsWithChildren<DialogProps>) {
|
||||
return null
|
||||
}
|
||||
@@ -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>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export type DialogProps = {
|
||||
isOpen: boolean
|
||||
onDismiss: () => void
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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 />
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user