Add POC menu abstraction

This commit is contained in:
Eric Bailey
2024-03-04 17:06:27 -06:00
parent 6c9d6f5b05
commit a496a380d3
8 changed files with 339 additions and 0 deletions
+8
View File
@@ -0,0 +1,8 @@
import React from 'react'
import type {ContextType} from '#/components/Menu/types'
export const Context = React.createContext<ContextType>({
// @ts-ignore
control: null,
})
+80
View File
@@ -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<ContextType>(
() => ({
control: control || defaultControl,
}),
[control, defaultControl],
)
return <Context.Provider value={context}>{children}</Context.Provider>
}
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 (
<Dialog.Outer control={control}>
<Dialog.Handle />
<Dialog.ScrollableInner label="Menu TODO">
{children}
</Dialog.ScrollableInner>
</Dialog.Outer>
)
}
export function Button({}: {}) {}
+101
View File
@@ -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<ContextType>(
() => ({
control: null,
}),
[],
)
return (
<Context.Provider value={context}>
<DropdownMenu.Root>{children}</DropdownMenu.Root>
</Context.Provider>
)
}
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 (
<DropdownMenu.Trigger asChild>
<Pressable
onPressIn={onPressIn}
onPressOut={onPressOut}
onFocus={onFocus}
onBlur={onBlur}
style={flatten([web({outline: 0})])}
{...web({
onMouseEnter,
onMouseLeave,
})}>
{children({
isNative: false,
control: null,
state: {
hovered,
focused,
pressed,
},
handlers: {},
})}
</Pressable>
</DropdownMenu.Trigger>
)
}
export function Outer(_props: React.PropsWithChildren<{}>) {
return (
<DropdownMenu.Portal>
<DropdownMenu.Content sideOffset={5}>
<DropdownMenu.Item>
New Tab <div className="RightSlot">+T</div>
</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu.Portal>
)
}
export function Button({}: {}) {}
+33
View File
@@ -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: {}
}
+28
View File
@@ -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 (
<View style={[a.gap_md]}>
<Menu.Root>
<Menu.Trigger>
{({state, handlers}) => {
console.log(state)
return <Text {...handlers}>Open</Text>
}}
</Menu.Trigger>
<Menu.Outer>
<Text>Open</Text>
</Menu.Outer>
</Menu.Root>
</View>
)
}
+2
View File
@@ -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
</Button>
</View>
<Menus />
<ThemeProvider theme="light">
<Theming />