RAC better example of splitting Menu into constituent parts for better decoupling #4906
-
Im trying to create a menu component that adheres to the following client facing api... <Menu.Root>
<Menu.Trigger>
<Button or IconButton />
</Menu.Trigger>
<Menu.Popover>
{(items) => ...section or items}
</Menu.Popover>
</Menu.Root> for the life of me i can't get my head around the advanced example provided at the bottom of the Menu RAC page here. The example sadly leaves me with more questions than answers. How do generics get passed? how do i know what states to pass where to ensure full functionality and accessibility? What typescript types do i need to pass where? the current examples on the page tightly couple the button to the trigger menu, ie. i have to inject custom button props through the standard MenuTrigger to my button, however the consumer should be able to decide on the children of the button instead of just a label but i can't achieve this in the example implementation because the Menu has reserved the children prop for its collection. Honestly id settle for just an example where, as above, the consumer as the opportunity to pass their own trigger and still adhere to an intuitive api like above....can anyone assist? |
Beta Was this translation helpful? Give feedback.
Replies: 1 comment 1 reply
-
Alright i think i figured this out. to be honest i was bloody surprised when this worked the first time. some slick adobe magic goin on behind the scenes with these contexts...if anyone has any objections to this particular implementation id absolutely love to hear it. function Root<T extends object>(props: MenuRootProps) {
const triggerRef = useRef<HTMLButtonElement>(null);
const state = useMenuTriggerState(props);
const { menuTriggerProps, menuProps } = useMenuTrigger<T>(
props,
state,
triggerRef,
);
return (
<Provider
values={[
[
ButtonContext,
{ ...menuTriggerProps, isPressed: state.isOpen, ref: triggerRef },
],
[PopoverContext, { state, triggerRef }],
[MenuContext, menuProps],
]}
>
{props.children}
</Provider>
);
}
function Trigger({ children }: MenuTriggerProps) {
const triggerRef = useRef<HTMLButtonElement>(null);
const [props] = useContextProps({}, triggerRef, ButtonContext);
return cloneElement(children, { ...props });
}
function MenuList<T extends object>(props: MenuProps<T>) {
const menuRef = useRef<HTMLDivElement>(null);
const popoverRef = useRef<HTMLDivElement>(null);
const [popoverProps] = useContextProps({}, menuRef, PopoverContext);
const [menuProps] = useContextProps(props, popoverRef, MenuContext);
return (
<Popover {...popoverProps}>
<Menu {...menuProps}>{props.children}</Menu>
</Popover>
);
}
export const MenuTrigger = {
Root,
Trigger,
Menu: MenuList,
MenuSection: Section,
MenuOption: Option,
}; and now this api works a treat... <MenuTrigger.Root>
<MenuTrigger.Trigger>
<Button>Open</Button>
</MenuTrigger.Trigger>
<MenuTrigger.Menu items={simpleData}>
{(item) => (
// custom item that wraps the Item component
<MenuTrigger.MenuOption
id={item.id}
description={item.description}
label={item.label}
/>
)}
</MenuTrigger.Menu>
</MenuTrigger.Root> |
Beta Was this translation helpful? Give feedback.
Looks like it should work, but you might be able to simplify it a little? I think the
Root
component is almost identical to the builtin MenuTrigger component, which also callsuseMenuTrigger
and provides the same contexts. TheMenuList
component probably doesn't need to consume from thePopoverContext
orMenuContext
, because thePopover
andMenu
components you're wrapping already do that. So that just leavesTrigger
. I can't tell based on the code example, but if<Button>
is using a React Aria ComponentsButton
internally, then that will also already consume from theButtonContext
and you shouldn't need the Trigger wrapper at all.