From 2bac965da4b7d6f03e025b3175e6acdec823423a Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Wed, 10 Jan 2024 16:13:02 -0600 Subject: [PATCH] Add Dialog, Portal --- package.json | 1 + src/App.native.tsx | 6 +- src/App.web.tsx | 6 +- src/view/com/Dialog/index.tsx | 7 +++ src/view/com/Dialog/index.web.tsx | 96 +++++++++++++++++++++++++++++++ src/view/com/Dialog/types.ts | 4 ++ src/view/com/Portal.tsx | 58 +++++++++++++++++++ src/view/screens/DebugNew.tsx | 25 ++++++++ yarn.lock | 25 ++++++++ 9 files changed, 226 insertions(+), 2 deletions(-) create mode 100644 src/view/com/Dialog/index.tsx create mode 100644 src/view/com/Dialog/index.web.tsx create mode 100644 src/view/com/Dialog/types.ts create mode 100644 src/view/com/Portal.tsx diff --git a/package.json b/package.json index 7e63ad9a62..01d90da90f 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/App.native.tsx b/src/App.native.tsx index a9dfafe36f..39485ee24f 100644 --- a/src/App.native.tsx +++ b/src/App.native.tsx @@ -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() { + @@ -113,7 +115,9 @@ function App() { - + + + diff --git a/src/App.web.tsx b/src/App.web.tsx index 9ee9a4fd71..5760b0d4a2 100644 --- a/src/App.web.tsx +++ b/src/App.web.tsx @@ -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() { + @@ -94,7 +96,9 @@ function App() { - + + + diff --git a/src/view/com/Dialog/index.tsx b/src/view/com/Dialog/index.tsx new file mode 100644 index 0000000000..fc3f784704 --- /dev/null +++ b/src/view/com/Dialog/index.tsx @@ -0,0 +1,7 @@ +import React from 'react' + +import {DialogProps} from '#/view/com/Dialog/types' + +export function Dialog(props: React.PropsWithChildren) { + return null +} diff --git a/src/view/com/Dialog/index.web.tsx b/src/view/com/Dialog/index.web.tsx new file mode 100644 index 0000000000..9c6f09f4f3 --- /dev/null +++ b/src/view/com/Dialog/index.web.tsx @@ -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) { + 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 && ( + + + + + + + + + + {children} + + + + + + + + )} + + ) +} diff --git a/src/view/com/Dialog/types.ts b/src/view/com/Dialog/types.ts new file mode 100644 index 0000000000..eb9dc56865 --- /dev/null +++ b/src/view/com/Dialog/types.ts @@ -0,0 +1,4 @@ +export type DialogProps = { + isOpen: boolean + onDismiss: () => void +} diff --git a/src/view/com/Portal.tsx b/src/view/com/Portal.tsx new file mode 100644 index 0000000000..47a216bd80 --- /dev/null +++ b/src/view/com/Portal.tsx @@ -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({ + outlet: null, + append: () => {}, + remove: () => {}, +}) + +export function Provider(props: React.PropsWithChildren<{}>) { + const map = React.useRef({}) + const [outlet, setOutlet] = React.useState(null) + + const append = React.useCallback((id, component) => { + if (map.current[id]) return + map.current[id] = {component} + setOutlet(<>{Object.values(map.current)}) + }, []) + + const remove = React.useCallback(id => { + delete map.current[id] + setOutlet(<>{Object.values(map.current)}) + }, []) + + console.log(outlet) + + return ( + + {props.children} + + ) +} + +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 +} diff --git a/src/view/screens/DebugNew.tsx b/src/view/screens/DebugNew.tsx index 444fe4f29a..ce7f5c20a8 100644 --- a/src/view/screens/DebugNew.tsx +++ b/src/view/screens/DebugNew.tsx @@ -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 ( + <> + + setIsOpen(false)}> + + + + + ) +} + export function DebugScreen() { const t = useTheme() @@ -325,6 +348,8 @@ export function DebugScreen() { + + diff --git a/yarn.lock b/yarn.lock index c1db264bc6..7d0ceecd38 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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"