ycs77 / headlessui-float

Easily use Headless UI with Floating UI to position floating elements.
https://headlessui-float.vercel.app
MIT License
344 stars 12 forks source link

How to use Float when Menu.Button is a Fragment? #14

Closed jarcoal closed 1 year ago

jarcoal commented 1 year ago

Use Version Use version when question appear:

Describe the question

I'm trying to make a reusable wrapper around the Headless UI's Menu.Button component, but for some reason when I use it, Float doesn't seem able to position the Menu.Items correctly and it just goes to the top left of the screen instead of next to the button itself.

Here's what I'm using:

import { Menu as HeadlessMenu } from '@headlessui/react'

// ....

const Opener: React.FC<OpenerProps> = React.forwardRef<
  HTMLDivElement,
  OpenerProps
>(({ children }, ref) => (
  <HeadlessMenu.Button as={React.Fragment}>
    <children.type {...children.props} ref={ref} />
  </HeadlessMenu.Button>
))

If I change:

<HeadlessMenu.Button as={React.Fragment}>

to:

<HeadlessMenu.Button as="div">

it works, but for my use-case I really want to use a Fragment there instead.

Any thoughts on what I could do to use a Fragment but still get the correct positioning? I thought by passing the ref down into the children that would do it, but apparently not.

In case it helps, here's what I have for my Float component (again this is inside a custom reusable component):

const Wrapper: React.FC<WrapperProps> = ({ children, placement }) => (
  <HeadlessMenu>
    <Float
      offset={10}
      placement={placement}
      portal
      enter="tw-transition tw-ease-out tw-duration-100"
      enterFrom="tw-transform tw-opacity-0 tw-scale-95"
      enterTo="tw-transform tw-opacity-100 tw-scale-100"
      leave="tw-transition tw-ease-in tw-duration-75"
      leaveFrom="tw-transform tw-opacity-100 tw-scale-100"
      leaveTo="tw-transform tw-opacity-0 tw-scale-95"
    >
      {children}
    </Float>
  </HeadlessMenu>
)
ycs77 commented 1 year ago

Hi @jarcoal, the ref must be defined on the <Menu.Button>:

const Opener: React.FC<OpenerProps> = React.forwardRef<
  HTMLDivElement,
  OpenerProps
>(({ children }, ref) => (
  <HeadlessMenu.Button as={React.Fragment} ref={ref}>
    <children.type {...children.props} />
  </HeadlessMenu.Button>
))
jarcoal commented 1 year ago

Thanks for the reply @ycs77 . I should've mentioned that I tried that and received this Typescript error related to the ref prop:

Type '{ children: ReactElement<any, string | JSXElementConstructor<any>> & ReactNode; as: ExoticComponent<{ children?: ReactNode; }>; ref: ForwardedRef<...>; }' is not assignable to type 'IntrinsicAttributes & Props<ExoticComponent<{ children?: ReactNode; }>, ButtonRenderPropArg, ButtonPropsWeControl>'.
  Property 'ref' does not exist on type 'IntrinsicAttributes & Props<ExoticComponent<{ children?: ReactNode; }>, ButtonRenderPropArg, ButtonPropsWeControl>'.ts(2322)

I'm assuming the type HTMLDivElement needs to be changed to something else, I'm just not sure what it should be...

ycs77 commented 1 year ago

The button DOM type is HTMLButtonElement, this will be clear the error:

const Opener: React.FC<OpenerProps> = React.forwardRef<
  HTMLButtonElement,
  OpenerProps
>(({ children }, ref) => (
  <HeadlessMenu.Button as={React.Fragment} ref={ref}>
    <children.type {...children.props} />
  </HeadlessMenu.Button>
))
jarcoal commented 1 year ago

Same error:

Type '{ children: ReactElement<any, string | JSXElementConstructor<any>> & ReactNode; as: ExoticComponent<{ children?: ReactNode; }>; ref: ForwardedRef<...>; }' is not assignable to type 'IntrinsicAttributes & Props<ExoticComponent<{ children?: ReactNode; }>, ButtonRenderPropArg, ButtonPropsWeControl>'.
  Property 'ref' does not exist on type 'IntrinsicAttributes & Props<ExoticComponent<{ children?: ReactNode; }>, ButtonRenderPropArg, ButtonPropsWeControl>'.ts(2322)

Maybe it's an issue with Headless-UI?

For now I've created a work-around, so I'm all set, just wanted to see if there was a quick-fix that I was missing. Thanks again for your help.