Add POC menu abstraction
This commit is contained in:
@@ -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,
|
||||
})
|
||||
@@ -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({}: {}) {}
|
||||
@@ -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({}: {}) {}
|
||||
@@ -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: {}
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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 />
|
||||
|
||||
Reference in New Issue
Block a user