Add controlled dropdown
This commit is contained in:
@@ -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,
|
id,
|
||||||
ref: control,
|
ref: control,
|
||||||
|
isOpen,
|
||||||
open: () => {
|
open: () => {
|
||||||
control.current.open()
|
control.current.open()
|
||||||
},
|
},
|
||||||
close: cb => {
|
close: cb => {
|
||||||
control.current.close(cb)
|
control.current.close(cb)
|
||||||
},
|
},
|
||||||
}
|
}),
|
||||||
|
[id, control, isOpen],
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 = {
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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},
|
||||||
|
isOpen,
|
||||||
|
open() {
|
||||||
|
setIsOpen(true)
|
||||||
},
|
},
|
||||||
close: () => {
|
close() {
|
||||||
throw new Error(`Menu controls are only available on native platforms`)
|
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([
|
||||||
|
a.my_xs,
|
||||||
|
t.atoms.bg_contrast_100,
|
||||||
|
{
|
||||||
height: 1,
|
height: 1,
|
||||||
backgroundColor: t.atoms.bg_contrast_100.backgroundColor,
|
},
|
||||||
marginTop: 6,
|
])}
|
||||||
marginBottom: 6,
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user