Add controlled dropdown

This commit is contained in:
Eric Bailey
2024-03-05 10:52:23 -06:00
parent 0f3bf15729
commit 4cdb72f944
6 changed files with 102 additions and 42 deletions
+16 -11
View File
@@ -21,7 +21,8 @@ export function useDialogControl(): DialogOuterProps['control'] {
open: () => {}, open: () => {},
close: () => {}, close: () => {},
}) })
const {activeDialogs} = useDialogStateContext() const {activeDialogs, openDialogs} = useDialogStateContext()
const isOpen = openDialogs.includes(id)
React.useEffect(() => { React.useEffect(() => {
activeDialogs.current.set(id, control) activeDialogs.current.set(id, control)
@@ -31,14 +32,18 @@ export function useDialogControl(): DialogOuterProps['control'] {
} }
}, [id, activeDialogs]) }, [id, activeDialogs])
return { return React.useMemo<DialogOuterProps['control']>(
id, () => ({
ref: control, id,
open: () => { ref: control,
control.current.open() isOpen,
}, open: () => {
close: cb => { control.current.open()
control.current.close(cb) },
}, close: cb => {
} control.current.close(cb)
},
}),
[id, control, isOpen],
)
} }
+1
View File
@@ -22,6 +22,7 @@ export type DialogControlRefProps = {
export type DialogControlProps = DialogControlRefProps & { export type DialogControlProps = DialogControlRefProps & {
id: string id: string
ref: React.RefObject<DialogControlRefProps> ref: React.RefObject<DialogControlRefProps>
isOpen: boolean
} }
export type DialogContextProps = { export type DialogContextProps = {
+5 -1
View File
@@ -18,6 +18,10 @@ import {
export {useDialogControl as useMenuControl} from '#/components/Dialog' export {useDialogControl as useMenuControl} from '#/components/Dialog'
export function useMemoControlContext() {
return React.useContext(Context)
}
export function Root({ export function Root({
children, children,
control, control,
@@ -157,7 +161,7 @@ export function Group({children, style}: GroupProps) {
{React.Children.toArray(children).map((child, i) => { {React.Children.toArray(children).map((child, i) => {
// ignore null children, like Dividers // ignore null children, like Dividers
return React.isValidElement(child) && child.props.children ? ( return React.isValidElement(child) && child.props.children ? (
<React.Fragment> <React.Fragment key={i}>
{i > 0 ? ( {i > 0 ? (
<View style={[a.border_b, t.atoms.border_contrast_low]} /> <View style={[a.border_b, t.atoms.border_contrast_low]} />
) : null} ) : null}
+55 -23
View File
@@ -18,34 +18,60 @@ import {
import {Context} from '#/components/Menu/context' import {Context} from '#/components/Menu/context'
export function useMenuControl(): Dialog.DialogControlProps { export function useMenuControl(): Dialog.DialogControlProps {
return { const id = React.useId()
id: '', const [isOpen, setIsOpen] = React.useState(false)
// @ts-ignore
ref: null, return React.useMemo(
open: () => { () => ({
throw new Error(`Menu controls are only available on native platforms`) id,
}, ref: {current: null},
close: () => { isOpen,
throw new Error(`Menu controls are only available on native platforms`) open() {
}, setIsOpen(true)
} },
close() {
setIsOpen(false)
},
}),
[id, isOpen, setIsOpen],
)
}
export function useMemoControlContext() {
return React.useContext(Context)
} }
export function Root({ export function Root({
children, children,
control,
}: React.PropsWithChildren<{ }: React.PropsWithChildren<{
control?: Dialog.DialogOuterProps['control'] control?: Dialog.DialogOuterProps['control']
}>) { }>) {
const defaultControl = useMenuControl()
const context = React.useMemo<ContextType>( const context = React.useMemo<ContextType>(
() => ({ () => ({
control: null, control: control || defaultControl,
}), }),
[], [control, defaultControl],
)
const onOpenChange = React.useCallback(
(open: boolean) => {
if (context.control.isOpen && !open) {
context.control.close()
} else if (!context.control.isOpen && open) {
context.control.open()
}
},
[context.control],
) )
return ( return (
<Context.Provider value={context}> <Context.Provider value={context}>
<DropdownMenu.Root>{children}</DropdownMenu.Root> <DropdownMenu.Root
open={context.control.isOpen}
onOpenChange={onOpenChange}>
{children}
</DropdownMenu.Root>
</Context.Provider> </Context.Provider>
) )
} }
@@ -56,6 +82,7 @@ export function Trigger({
}: ViewStyleProp & { }: ViewStyleProp & {
children(props: TriggerChildProps): React.ReactNode children(props: TriggerChildProps): React.ReactNode
}) { }) {
const {control} = React.useContext(Context)
const { const {
state: hovered, state: hovered,
onIn: onMouseEnter, onIn: onMouseEnter,
@@ -69,13 +96,16 @@ export function Trigger({
onFocus={onFocus} onFocus={onFocus}
onBlur={onBlur} onBlur={onBlur}
style={flatten([style, web({outline: 0})])} style={flatten([style, web({outline: 0})])}
onPointerDown={() => {
control.open()
}}
{...web({ {...web({
onMouseEnter, onMouseEnter,
onMouseLeave, onMouseLeave,
})}> })}>
{children({ {children({
isNative: false, isNative: false,
control: null, control,
state: { state: {
hovered, hovered,
focused, focused,
@@ -94,7 +124,7 @@ export function Outer({children}: React.PropsWithChildren<{}>) {
return ( return (
<DropdownMenu.Portal> <DropdownMenu.Portal>
<DropdownMenu.Content sideOffset={5} loop> <DropdownMenu.Content sideOffset={5} loop>
<View style={[a.rounded_sm, t.atoms.bg_contrast_50, {padding: 6}]}> <View style={[a.rounded_sm, a.p_xs, t.atoms.bg_contrast_50]}>
{children} {children}
</View> </View>
@@ -122,6 +152,7 @@ export function Item({children, label, onPress}: ItemProps) {
className="radix-dropdown-item" className="radix-dropdown-item"
accessibilityHint="" accessibilityHint=""
accessibilityLabel={label} accessibilityLabel={label}
onPress={onPress}
onFocus={onFocus} onFocus={onFocus}
onBlur={onBlur} onBlur={onBlur}
style={flatten([ style={flatten([
@@ -129,7 +160,7 @@ export function Item({children, label, onPress}: ItemProps) {
a.align_center, a.align_center,
a.gap_sm, a.gap_sm,
a.py_sm, a.py_sm,
a.px_md, a.px_sm,
a.rounded_xs, a.rounded_xs,
{minHeight: 36}, {minHeight: 36},
web({outline: 0}), web({outline: 0}),
@@ -181,12 +212,13 @@ export function Divider() {
const t = useTheme() const t = useTheme()
return ( return (
<DropdownMenu.Separator <DropdownMenu.Separator
style={{ style={flatten([
height: 1, a.my_xs,
backgroundColor: t.atoms.bg_contrast_100.backgroundColor, t.atoms.bg_contrast_100,
marginTop: 6, {
marginBottom: 6, height: 1,
}} },
])}
/> />
) )
} }
+2 -2
View File
@@ -5,7 +5,7 @@ import * as Dialog from '#/components/Dialog'
import {TextStyleProp, ViewStyleProp} from '#/alf' import {TextStyleProp, ViewStyleProp} from '#/alf'
export type ContextType = { export type ContextType = {
control: Dialog.DialogOuterProps['control'] | null control: Dialog.DialogOuterProps['control']
} }
export type TriggerChildProps = export type TriggerChildProps =
@@ -30,7 +30,7 @@ export type TriggerChildProps =
} }
| { | {
isNative: false isNative: false
control: null control: Dialog.DialogOuterProps['control']
state: { state: {
hovered: boolean hovered: boolean
focused: boolean focused: boolean
+23 -5
View File
@@ -1,22 +1,38 @@
import React from 'react' import React from 'react'
import {View} from 'react-native' import {View} from 'react-native'
import {atoms as a} from '#/alf' import {atoms as a, useTheme} from '#/alf'
import {Text} from '#/components/Typography' import {Text} from '#/components/Typography'
import * as Menu from '#/components/Menu' import * as Menu from '#/components/Menu'
import {MagnifyingGlass2_Stroke2_Corner0_Rounded as Search} from '#/components/icons/MagnifyingGlass2' import {MagnifyingGlass2_Stroke2_Corner0_Rounded as Search} from '#/components/icons/MagnifyingGlass2'
// import {useDialogStateControlContext} from '#/state/dialogs' // import {useDialogStateControlContext} from '#/state/dialogs'
export function Menus() { export function Menus() {
const t = useTheme()
const menuControl = Menu.useMenuControl()
// const {closeAllDialogs} = useDialogStateControlContext() // const {closeAllDialogs} = useDialogStateControlContext()
return ( return (
<View style={[a.gap_md]}> <View style={[a.gap_md]}>
<View style={[a.flex_row, a.align_start]}> <View style={[a.flex_row, a.align_start]}>
<Menu.Root> <Menu.Root control={menuControl}>
<Menu.Trigger style={[a.flex_1]}> <Menu.Trigger style={[a.flex_1]}>
{({handlers}) => { {({state, handlers}) => {
return <Text {...handlers}>Open</Text> return (
<Text
{...handlers}
style={[
a.py_sm,
a.px_md,
a.rounded_sm,
t.atoms.bg_contrast_50,
(state.hovered || state.focused || state.pressed) && [
t.atoms.bg_contrast_200,
],
]}>
Open
</Text>
)
}} }}
</Menu.Trigger> </Menu.Trigger>
@@ -29,7 +45,9 @@ export function Menus() {
<Menu.Divider /> <Menu.Divider />
<Menu.Item label="Another item" onPress={() => {}}> <Menu.Item
label="Another item"
onPress={() => menuControl.close()}>
<Menu.ItemText>Another item</Menu.ItemText> <Menu.ItemText>Another item</Menu.ItemText>
</Menu.Item> </Menu.Item>
</Menu.Group> </Menu.Group>