Add Dialog, Portal
This commit is contained in:
@@ -70,6 +70,7 @@
|
|||||||
"@segment/analytics-react-native": "^2.10.1",
|
"@segment/analytics-react-native": "^2.10.1",
|
||||||
"@segment/sovran-react-native": "^0.4.5",
|
"@segment/sovran-react-native": "^0.4.5",
|
||||||
"@sentry/react-native": "5.5.0",
|
"@sentry/react-native": "5.5.0",
|
||||||
|
"@tamagui/focus-scope": "^1.84.1",
|
||||||
"@tanstack/react-query": "^5.8.1",
|
"@tanstack/react-query": "^5.8.1",
|
||||||
"@tiptap/core": "^2.0.0-beta.220",
|
"@tiptap/core": "^2.0.0-beta.220",
|
||||||
"@tiptap/extension-document": "^2.0.0-beta.220",
|
"@tiptap/extension-document": "^2.0.0-beta.220",
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ import {
|
|||||||
import {Provider as UnreadNotifsProvider} from 'state/queries/notifications/unread'
|
import {Provider as UnreadNotifsProvider} from 'state/queries/notifications/unread'
|
||||||
import * as persisted from '#/state/persisted'
|
import * as persisted from '#/state/persisted'
|
||||||
import {Splash} from '#/Splash'
|
import {Splash} from '#/Splash'
|
||||||
|
import {Provider as PortalProvider, Outlet as PortalOutlet} from '#/view/com/Portal'
|
||||||
|
|
||||||
SplashScreen.preventAutoHideAsync()
|
SplashScreen.preventAutoHideAsync()
|
||||||
|
|
||||||
@@ -76,6 +77,7 @@ function InnerApp() {
|
|||||||
<GestureHandlerRootView style={s.h100pct}>
|
<GestureHandlerRootView style={s.h100pct}>
|
||||||
<TestCtrls />
|
<TestCtrls />
|
||||||
<Shell />
|
<Shell />
|
||||||
|
<PortalOutlet />
|
||||||
</GestureHandlerRootView>
|
</GestureHandlerRootView>
|
||||||
</RootSiblingParent>
|
</RootSiblingParent>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
@@ -113,7 +115,9 @@ function App() {
|
|||||||
<ModalStateProvider>
|
<ModalStateProvider>
|
||||||
<LightboxStateProvider>
|
<LightboxStateProvider>
|
||||||
<I18nProvider>
|
<I18nProvider>
|
||||||
|
<PortalProvider>
|
||||||
<InnerApp />
|
<InnerApp />
|
||||||
|
</PortalProvider>
|
||||||
</I18nProvider>
|
</I18nProvider>
|
||||||
</LightboxStateProvider>
|
</LightboxStateProvider>
|
||||||
</ModalStateProvider>
|
</ModalStateProvider>
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ import {
|
|||||||
} from 'state/session'
|
} from 'state/session'
|
||||||
import {Provider as UnreadNotifsProvider} from 'state/queries/notifications/unread'
|
import {Provider as UnreadNotifsProvider} from 'state/queries/notifications/unread'
|
||||||
import * as persisted from '#/state/persisted'
|
import * as persisted from '#/state/persisted'
|
||||||
|
import {Provider as PortalProvider, Outlet as PortalOutlet} from '#/view/com/Portal'
|
||||||
|
|
||||||
function InnerApp() {
|
function InnerApp() {
|
||||||
const {isInitialLoad, currentAccount} = useSession()
|
const {isInitialLoad, currentAccount} = useSession()
|
||||||
@@ -58,6 +59,7 @@ function InnerApp() {
|
|||||||
<RootSiblingParent>
|
<RootSiblingParent>
|
||||||
<SafeAreaProvider>
|
<SafeAreaProvider>
|
||||||
<Shell />
|
<Shell />
|
||||||
|
<PortalOutlet />
|
||||||
</SafeAreaProvider>
|
</SafeAreaProvider>
|
||||||
</RootSiblingParent>
|
</RootSiblingParent>
|
||||||
<ToastContainer />
|
<ToastContainer />
|
||||||
@@ -94,7 +96,9 @@ function App() {
|
|||||||
<ModalStateProvider>
|
<ModalStateProvider>
|
||||||
<LightboxStateProvider>
|
<LightboxStateProvider>
|
||||||
<I18nProvider>
|
<I18nProvider>
|
||||||
|
<PortalProvider>
|
||||||
<InnerApp />
|
<InnerApp />
|
||||||
|
</PortalProvider>
|
||||||
</I18nProvider>
|
</I18nProvider>
|
||||||
</LightboxStateProvider>
|
</LightboxStateProvider>
|
||||||
</ModalStateProvider>
|
</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 {InputText} from '#/view/com/forms/InputText'
|
||||||
import {InputDate, utils} from '#/view/com/forms/InputDate'
|
import {InputDate, utils} from '#/view/com/forms/InputDate'
|
||||||
import {Logo} from '#/view/icons/Logo'
|
import {Logo} from '#/view/icons/Logo'
|
||||||
|
import {Dialog} from '#/view/com/Dialog'
|
||||||
|
|
||||||
function ThemeSelector() {
|
function ThemeSelector() {
|
||||||
const setColorMode = useSetColorMode()
|
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() {
|
export function DebugScreen() {
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
|
|
||||||
@@ -325,6 +348,8 @@ export function DebugScreen() {
|
|||||||
<View style={[a.p_xl, a.gap_xxl, {paddingBottom: 200}]}>
|
<View style={[a.p_xl, a.gap_xxl, {paddingBottom: 200}]}>
|
||||||
<ThemeSelector />
|
<ThemeSelector />
|
||||||
|
|
||||||
|
<Dialogs />
|
||||||
|
|
||||||
<Forms />
|
<Forms />
|
||||||
<Buttons />
|
<Buttons />
|
||||||
|
|
||||||
|
|||||||
@@ -6921,6 +6921,31 @@
|
|||||||
"@svgr/plugin-svgo" "^5.5.0"
|
"@svgr/plugin-svgo" "^5.5.0"
|
||||||
loader-utils "^2.0.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":
|
"@tanstack/query-core@5.8.1":
|
||||||
version "5.8.1"
|
version "5.8.1"
|
||||||
resolved "https://registry.yarnpkg.com/@tanstack/query-core/-/query-core-5.8.1.tgz#5215a028370d9b2f32e83787a0ea119e2f977996"
|
resolved "https://registry.yarnpkg.com/@tanstack/query-core/-/query-core-5.8.1.tgz#5215a028370d9b2f32e83787a0ea119e2f977996"
|
||||||
|
|||||||
Reference in New Issue
Block a user