diff --git a/package.json b/package.json index d694d26c31..0fc1e8bc95 100644 --- a/package.json +++ b/package.json @@ -58,6 +58,7 @@ "@lingui/react": "^4.5.0", "@mattermost/react-native-paste-input": "^0.6.4", "@miblanchard/react-native-slider": "^2.3.1", + "@radix-ui/react-dropdown-menu": "^2.0.6", "@react-native-async-storage/async-storage": "1.21.0", "@react-native-camera-roll/camera-roll": "^5.2.2", "@react-native-clipboard/clipboard": "^1.10.0", diff --git a/src/components/Menu/context.tsx b/src/components/Menu/context.tsx new file mode 100644 index 0000000000..9fc91f6815 --- /dev/null +++ b/src/components/Menu/context.tsx @@ -0,0 +1,8 @@ +import React from 'react' + +import type {ContextType} from '#/components/Menu/types' + +export const Context = React.createContext({ + // @ts-ignore + control: null, +}) diff --git a/src/components/Menu/index.tsx b/src/components/Menu/index.tsx new file mode 100644 index 0000000000..9885b2aead --- /dev/null +++ b/src/components/Menu/index.tsx @@ -0,0 +1,80 @@ +import React from 'react' + +import * as Dialog from '#/components/Dialog' +import {useInteractionState} from '#/components/hooks/useInteractionState' + +import {ContextType, TriggerChildProps} from '#/components/Menu/types' +import {Context} from '#/components/Menu/context' + +export {useDialogControl as useMenuControl} from '#/components/Dialog' + +export function Root({ + children, + control, +}: React.PropsWithChildren<{ + control?: Dialog.DialogOuterProps['control'] +}>) { + const defaultControl = Dialog.useDialogControl() + const context = React.useMemo( + () => ({ + control: control || defaultControl, + }), + [control, defaultControl], + ) + + return {children} +} + +export function Trigger({ + children, +}: { + children(props: TriggerChildProps): React.ReactNode +}) { + const {control} = React.useContext(Context) + const {state: focused, onIn: onFocus, onOut: onBlur} = useInteractionState() + const { + state: pressed, + onIn: onPressIn, + onOut: onPressOut, + } = useInteractionState() + + if (!control) { + throw new Error('Menu.Trigger must be used within a Menu.Root') + } + + return children({ + isNative: true, + control, + state: { + hovered: false, + focused, + pressed, + }, + handlers: { + onPress: control.open, + onFocus, + onBlur, + onPressIn, + onPressOut, + }, + }) +} + +export function Outer({children}: React.PropsWithChildren<{}>) { + const {control} = React.useContext(Context) + + if (!control) { + throw new Error('Menu.Outer must be used within a Menu.Root') + } + + return ( + + + + {children} + + + ) +} + +export function Button({}: {}) {} diff --git a/src/components/Menu/index.web.tsx b/src/components/Menu/index.web.tsx new file mode 100644 index 0000000000..d0387736b7 --- /dev/null +++ b/src/components/Menu/index.web.tsx @@ -0,0 +1,101 @@ +import React from 'react' +import {Pressable} from 'react-native' +import * as DropdownMenu from '@radix-ui/react-dropdown-menu' + +import * as Dialog from '#/components/Dialog' +import {useInteractionState} from '#/components/hooks/useInteractionState' +import {flatten, web} from '#/alf' + +import {ContextType, TriggerChildProps} from '#/components/Menu/types' +import {Context} from '#/components/Menu/context' + +export function useMenuControl(): Dialog.DialogControlProps { + return { + id: '', + // @ts-ignore + ref: null, + open: () => { + throw new Error(`Menu controls are only available on native platforms`) + }, + close: () => { + throw new Error(`Menu controls are only available on native platforms`) + }, + } +} + +export function Root({ + children, +}: React.PropsWithChildren<{ + control?: Dialog.DialogOuterProps['control'] +}>) { + const context = React.useMemo( + () => ({ + control: null, + }), + [], + ) + + return ( + + {children} + + ) +} + +export function Trigger({ + children, +}: { + children(props: TriggerChildProps): React.ReactNode +}) { + const { + state: hovered, + onIn: onMouseEnter, + onOut: onMouseLeave, + } = useInteractionState() + const {state: focused, onIn: onFocus, onOut: onBlur} = useInteractionState() + const { + state: pressed, + onIn: onPressIn, + onOut: onPressOut, + } = useInteractionState() + + return ( + + + {children({ + isNative: false, + control: null, + state: { + hovered, + focused, + pressed, + }, + handlers: {}, + })} + + + ) +} + +export function Outer(_props: React.PropsWithChildren<{}>) { + return ( + + + + New Tab
⌘+T
+
+
+
+ ) +} + +export function Button({}: {}) {} diff --git a/src/components/Menu/types.ts b/src/components/Menu/types.ts new file mode 100644 index 0000000000..da7436151f --- /dev/null +++ b/src/components/Menu/types.ts @@ -0,0 +1,33 @@ +import * as Dialog from '#/components/Dialog' + +export type ContextType = { + control: Dialog.DialogOuterProps['control'] | null +} + +export type TriggerChildProps = + | { + isNative: true + control: Dialog.DialogOuterProps['control'] + state: { + hovered: false + focused: boolean + pressed: boolean + } + handlers: { + onPress: () => void + onFocus: () => void + onBlur: () => void + onPressIn: () => void + onPressOut: () => void + } + } + | { + isNative: false + control: null + state: { + hovered: boolean + focused: boolean + pressed: boolean + } + handlers: {} + } diff --git a/src/view/screens/Storybook/Menus.tsx b/src/view/screens/Storybook/Menus.tsx new file mode 100644 index 0000000000..1e65390aa0 --- /dev/null +++ b/src/view/screens/Storybook/Menus.tsx @@ -0,0 +1,28 @@ +import React from 'react' +import {View} from 'react-native' + +import {atoms as a} from '#/alf' +import {Text} from '#/components/Typography' +import * as Menu from '#/components/Menu' +// import {useDialogStateControlContext} from '#/state/dialogs' + +export function Menus() { + // const {closeAllDialogs} = useDialogStateControlContext() + + return ( + + + + {({state, handlers}) => { + console.log(state) + return Open + }} + + + + Open + + + + ) +} diff --git a/src/view/screens/Storybook/index.tsx b/src/view/screens/Storybook/index.tsx index 40929555e5..67008181ed 100644 --- a/src/view/screens/Storybook/index.tsx +++ b/src/view/screens/Storybook/index.tsx @@ -16,6 +16,7 @@ import {Dialogs} from './Dialogs' import {Breakpoints} from './Breakpoints' import {Shadows} from './Shadows' import {Icons} from './Icons' +import {Menus} from './Menus' export function Storybook() { const t = useTheme() @@ -65,6 +66,7 @@ export function Storybook() { Dark + diff --git a/yarn.lock b/yarn.lock index ceb712ce27..e63d3af12d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4467,6 +4467,18 @@ "@radix-ui/react-use-callback-ref" "1.0.1" "@radix-ui/react-use-escape-keydown" "1.0.3" +"@radix-ui/react-dismissable-layer@1.0.5": + version "1.0.5" + resolved "https://registry.yarnpkg.com/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.0.5.tgz#3f98425b82b9068dfbab5db5fff3df6ebf48b9d4" + integrity sha512-aJeDjQhywg9LBu2t/At58hCvr7pEm0o2Ke1x33B+MhjNmmZ17sy4KImo0KPLgsnc/zN7GPdce8Cnn0SWvwZO7g== + dependencies: + "@babel/runtime" "^7.13.10" + "@radix-ui/primitive" "1.0.1" + "@radix-ui/react-compose-refs" "1.0.1" + "@radix-ui/react-primitive" "1.0.3" + "@radix-ui/react-use-callback-ref" "1.0.1" + "@radix-ui/react-use-escape-keydown" "1.0.3" + "@radix-ui/react-dropdown-menu@^2.0.1": version "2.0.5" resolved "https://registry.yarnpkg.com/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.0.5.tgz#19bf4de8ffa348b4eb6a86842f14eff93d741170" @@ -4481,6 +4493,20 @@ "@radix-ui/react-primitive" "1.0.3" "@radix-ui/react-use-controllable-state" "1.0.1" +"@radix-ui/react-dropdown-menu@^2.0.6": + version "2.0.6" + resolved "https://registry.yarnpkg.com/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.0.6.tgz#cdf13c956c5e263afe4e5f3587b3071a25755b63" + integrity sha512-i6TuFOoWmLWq+M/eCLGd/bQ2HfAX1RJgvrBQ6AQLmzfvsLdefxbWu8G9zczcPFfcSPehz9GcpF6K9QYreFV8hA== + dependencies: + "@babel/runtime" "^7.13.10" + "@radix-ui/primitive" "1.0.1" + "@radix-ui/react-compose-refs" "1.0.1" + "@radix-ui/react-context" "1.0.1" + "@radix-ui/react-id" "1.0.1" + "@radix-ui/react-menu" "2.0.6" + "@radix-ui/react-primitive" "1.0.3" + "@radix-ui/react-use-controllable-state" "1.0.1" + "@radix-ui/react-focus-guards@1.0.1": version "1.0.1" resolved "https://registry.yarnpkg.com/@radix-ui/react-focus-guards/-/react-focus-guards-1.0.1.tgz#1ea7e32092216b946397866199d892f71f7f98ad" @@ -4498,6 +4524,16 @@ "@radix-ui/react-primitive" "1.0.3" "@radix-ui/react-use-callback-ref" "1.0.1" +"@radix-ui/react-focus-scope@1.0.4": + version "1.0.4" + resolved "https://registry.yarnpkg.com/@radix-ui/react-focus-scope/-/react-focus-scope-1.0.4.tgz#2ac45fce8c5bb33eb18419cdc1905ef4f1906525" + integrity sha512-sL04Mgvf+FmyvZeYfNu1EPAaaxD+aw7cYeIB9L9Fvq8+urhltTRaEo5ysKOpHuKPclsZcSUMKlN05x4u+CINpA== + dependencies: + "@babel/runtime" "^7.13.10" + "@radix-ui/react-compose-refs" "1.0.1" + "@radix-ui/react-primitive" "1.0.3" + "@radix-ui/react-use-callback-ref" "1.0.1" + "@radix-ui/react-id@1.0.1": version "1.0.1" resolved "https://registry.yarnpkg.com/@radix-ui/react-id/-/react-id-1.0.1.tgz#73cdc181f650e4df24f0b6a5b7aa426b912c88c0" @@ -4531,6 +4567,31 @@ aria-hidden "^1.1.1" react-remove-scroll "2.5.5" +"@radix-ui/react-menu@2.0.6": + version "2.0.6" + resolved "https://registry.yarnpkg.com/@radix-ui/react-menu/-/react-menu-2.0.6.tgz#2c9e093c1a5d5daa87304b2a2f884e32288ae79e" + integrity sha512-BVkFLS+bUC8HcImkRKPSiVumA1VPOOEC5WBMiT+QAVsPzW1FJzI9KnqgGxVDPBcql5xXrHkD3JOVoXWEXD8SYA== + dependencies: + "@babel/runtime" "^7.13.10" + "@radix-ui/primitive" "1.0.1" + "@radix-ui/react-collection" "1.0.3" + "@radix-ui/react-compose-refs" "1.0.1" + "@radix-ui/react-context" "1.0.1" + "@radix-ui/react-direction" "1.0.1" + "@radix-ui/react-dismissable-layer" "1.0.5" + "@radix-ui/react-focus-guards" "1.0.1" + "@radix-ui/react-focus-scope" "1.0.4" + "@radix-ui/react-id" "1.0.1" + "@radix-ui/react-popper" "1.1.3" + "@radix-ui/react-portal" "1.0.4" + "@radix-ui/react-presence" "1.0.1" + "@radix-ui/react-primitive" "1.0.3" + "@radix-ui/react-roving-focus" "1.0.4" + "@radix-ui/react-slot" "1.0.2" + "@radix-ui/react-use-callback-ref" "1.0.1" + aria-hidden "^1.1.1" + react-remove-scroll "2.5.5" + "@radix-ui/react-popper@1.1.2": version "1.1.2" resolved "https://registry.yarnpkg.com/@radix-ui/react-popper/-/react-popper-1.1.2.tgz#4c0b96fcd188dc1f334e02dba2d538973ad842e9" @@ -4548,6 +4609,23 @@ "@radix-ui/react-use-size" "1.0.1" "@radix-ui/rect" "1.0.1" +"@radix-ui/react-popper@1.1.3": + version "1.1.3" + resolved "https://registry.yarnpkg.com/@radix-ui/react-popper/-/react-popper-1.1.3.tgz#24c03f527e7ac348fabf18c89795d85d21b00b42" + integrity sha512-cKpopj/5RHZWjrbF2846jBNacjQVwkP068DfmgrNJXpvVWrOvlAmE9xSiy5OqeE+Gi8D9fP+oDhUnPqNMY8/5w== + dependencies: + "@babel/runtime" "^7.13.10" + "@floating-ui/react-dom" "^2.0.0" + "@radix-ui/react-arrow" "1.0.3" + "@radix-ui/react-compose-refs" "1.0.1" + "@radix-ui/react-context" "1.0.1" + "@radix-ui/react-primitive" "1.0.3" + "@radix-ui/react-use-callback-ref" "1.0.1" + "@radix-ui/react-use-layout-effect" "1.0.1" + "@radix-ui/react-use-rect" "1.0.1" + "@radix-ui/react-use-size" "1.0.1" + "@radix-ui/rect" "1.0.1" + "@radix-ui/react-portal@1.0.3": version "1.0.3" resolved "https://registry.yarnpkg.com/@radix-ui/react-portal/-/react-portal-1.0.3.tgz#ffb961244c8ed1b46f039e6c215a6c4d9989bda1" @@ -4556,6 +4634,14 @@ "@babel/runtime" "^7.13.10" "@radix-ui/react-primitive" "1.0.3" +"@radix-ui/react-portal@1.0.4": + version "1.0.4" + resolved "https://registry.yarnpkg.com/@radix-ui/react-portal/-/react-portal-1.0.4.tgz#df4bfd353db3b1e84e639e9c63a5f2565fb00e15" + integrity sha512-Qki+C/EuGUVCQTOTD5vzJzJuMUlewbzuKyUy+/iHM2uwGiru9gZeBJtHAPKAEkB5KWGi9mP/CHKcY0wt1aW45Q== + dependencies: + "@babel/runtime" "^7.13.10" + "@radix-ui/react-primitive" "1.0.3" + "@radix-ui/react-presence@1.0.1": version "1.0.1" resolved "https://registry.yarnpkg.com/@radix-ui/react-presence/-/react-presence-1.0.1.tgz#491990ba913b8e2a5db1b06b203cb24b5cdef9ba"