Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

React components as Tooltip children? #171

Closed
chasemccoy opened this issue May 10, 2019 · 21 comments
Closed

React components as Tooltip children? #171

chasemccoy opened this issue May 10, 2019 · 21 comments

Comments

@chasemccoy
Copy link

Using @reach/tooltip, and getting a lot of errors about getBoundingClientRect being undefined whenever I nest a React component within the tooltip:

<Tooltip label='Hello!'>
  <Box>foo</Box>
</Tooltip>

Screen Shot 2019-05-10 at 1 03 34 PM

If I pass just a regular HTML element, it works just fine. But it seems like components should be supported as children, no?

@ericchernuka
Copy link

I ran into this today. The underlying component must be able to the ref that Tooltip is cloning onto the child component. I ended up just modifying my functional component to use forwardRef.

If not, you might have to go a low-level so that you can use the useTooltip hook and pass the ref using another convention such as innerRef that has become common.

For more info on refs check the React docs here: https://reactjs.org/docs/refs-and-the-dom.html#adding-a-ref-to-a-class-component

@chasemccoy
Copy link
Author

@ericchernuka Hmm, this makes sense but I am not sure exactly what I need to do to solve this.

Here's the code for my tooltip component:

const Tooltip = (props: TypeProps) => {
  const {
    label,
    position = "bottom",
    ariaLabel,
    children,
    zIndex = 1,
    qa,
    ...rest
  } = props;

  const [trigger, tooltip] = useTooltip({ DEBUG_STYLE: false });

  return (
    <React.Fragment>
      {React.cloneElement(children, trigger)}

      <ReachTooltip
        {...tooltip}
        label={label}
        ariaLabel={ariaLabel || label}
        position={positions[position]}
        zIndex={zIndex}
        {...rest}
      />
    </React.Fragment>
  );
};

I am using the hook, but I am not sure what ref I am supposed to pass where.

@ericchernuka
Copy link

The trigger from useTooltip owns the ref. It can either be passed in my the ref key or you can let useTooltip initialize one for you. It then gets cloned into the child component. So the child component, <Box /> in your case needs to be able to forward the ref to a DOM node.

@chasemccoy
Copy link
Author

Ah, I see. So, no matter what I do, this will require source changes to the child component (i.e. this will never work with a component that doesn't forward refs?)

@ericchernuka
Copy link

Pretty sure that’s correct. My implementation was broken today until I added ref forwarding.

@chasemccoy
Copy link
Author

Gotcha. Thanks for the help! This is kind of a bummer because we can't control what the consumer of the component passes in, and if they don't pass a component that forwards refs, it will break in a really weird way.

@chasemccoy
Copy link
Author

chasemccoy commented May 11, 2019

I ended up solving this by doing something like this:

return (
    <React.Fragment>
      <Box {...trigger}>
        {children}
      </Box>

      <TooltipContainer
        {...tooltip}
        label={label}
        ariaLabel={ariaLabel || label}
        position={positions[position]}
        zIndex={zIndex}
        {...rest}
      />
    </React.Fragment>
  );

Basically wrapping the children of the tooltip in a container that forwards refs (I updated Box to forward refs). Don't love wrapping the children in an unnecessary element, but it works!

@raunofreiberg
Copy link
Contributor

This is a major shortcoming, imo. While building a design system, I cannot guarantee that the children will be components that forward refs.

@ryanflorence
Copy link
Member

const Box = forwardRef((props, ref) => {
  return <div ref={ref}/>
})

Tooltip needs a ref so it can manage focus, so you need to forward it.

If you don’t like that (seems weird, but okay) drop down to the useTooltip hook and spread the return values wherever you need it.

But there’s no need to start wrapping for one off situations. Forward Ref is what you want.

@rajjejosefsson
Copy link

@ryanflorence is there anything in the docs stating that forwardRefs needs to be used? I see a lot of people having the same issue.

@raunofreiberg
Copy link
Contributor

@ryanflorence I've tried doing what you suggested, but it doesn't seem to work on the second example with <Span />

https://codesandbox.io/s/naughty-sun-7q7bo

@ericchernuka
Copy link

ericchernuka commented Aug 10, 2019

@raunofreiberg Since the tooltip clones its children, you have to ensure you pass all the props onto the underlying component. So in your example, you need to spread the props from forwardRef onto the span so that all the on* handlers still work. I've forked your example with a working one found here: https://codesandbox.io/s/optimistic-benz-ldg49?fontsize=14

@brookback
Copy link

I just got here after some time of "Why isn't the tooltip showing at my button?!". Turns out it doesn't work on function components at all.

I think the docs should totally mention this, as it's pretty severe.

@ericchernuka
Copy link

@brookback Not sure why you're saying it doesn't work, but you may need to wrap your functional component in forwardRef and pass that ref to the child component so that the tooltip has something to attach to.

As an aside, I can make a PR that indicates that the underlying component requires a forwardRef if people are ok with that?

@brookback
Copy link

@ericchernuka Sorry – it worked after I found my way here and read instructions on forwardRef :) Just saying the docs could need some love around this.

This shows up in the console at Codesandbox (although not for me locally 🤔):

image

So it looks like it's taken care of already.

@NickyMeuleman
Copy link

NickyMeuleman commented Feb 8, 2021

@ryanflorence I've tried doing what you suggested, but it doesn't seem to work on the second example with <Span />

codesandbox.io/s/naughty-sun-7q7bo

What's the proper way to do this?
I ran in to the same situation.
Ended up wrapping the Span component in a <span> (and deleted the one rendered as wrapping element in the component and replaced it by a fragment).
This works, and results in what I thought to be functionally identical code, but isn't.

@ryanodd
Copy link

ryanodd commented Apr 26, 2022

@ryanflorence I've tried doing what you suggested, but it doesn't seem to work on the second example with <Span />
codesandbox.io/s/naughty-sun-7q7bo

What's the proper way to do this? I ran in to the same situation. Ended up wrapping the Span component in a <span> (and deleted the one rendered as wrapping element in the component and replaced it by a fragment). This works, and results in what I thought to be functionally identical code, but isn't.

@NickyMeuleman I found a way to make this case work, by passing all props to the underlying span element like this: https://codesandbox.io/s/red-forest-5plimb

The need for ref forwarding is major usability issue with this library IMO. We'd need to remember this fact every time we want to use a tooltip. I think the docs should definitely mention this

@ryanodd
Copy link

ryanodd commented Apr 26, 2022

I haven't been able to find a way to use a class component as a tooltip child yet. Does anyone know a way?

Here's a simple demo with a tooltip wrapping a class component, functional component, and a basic HTML element. As you can see, wrapping a class component causes an unhandled exception from within reach-ui.

@karlhorky
Copy link
Contributor

karlhorky commented Jun 12, 2022

Edit: ‼️ Don't use this, it seems it doesn't work without React.forwardRef after all:


Ohh interesting, looks like you don't even need React.forwardRef anymore with the latest @reach/tooltip (as long as you pass the props along):

function Span(props) {
  return <span {...props}>{props.children}</span>;
}

function App() {
  return (
    <>
      <div>
        <Tooltip label="Bar">
          <Span>Hey Span</Span>
        </Tooltip>
      </div>
    </>
  );
}

Screen Shot 2022-06-12 at 12 45 28

https://codesandbox.io/s/optimistic-mclaren-9vrs05?file=/src/index.js

I am guessing this is because ref is included in the props which are passed to the element:

let trigger: TriggerParams<ElementType> = {
// The element that triggers the tooltip references the tooltip element with
// `aria-describedby`.
// https://www.w3.org/TR/wai-aria-practices-1.2/#tooltip
"aria-describedby": isVisible ? makeId("tooltip", id) : undefined,
"data-state": isVisible ? "tooltip-visible" : "tooltip-hidden",
"data-reach-tooltip-trigger": "",
ref,
onPointerEnter: composeEventHandlers(
onPointerEnter,
wrapPointerEventHandler(handleMouseEnter)
),
onPointerMove: composeEventHandlers(
onPointerMove,
wrapPointerEventHandler(handleMouseMove)
),
onPointerLeave: composeEventHandlers(
onPointerLeave,
wrapPointerEventHandler(handleMouseLeave)
),
onPointerDown: composeEventHandlers(
onPointerDown,
wrapPointerEventHandler(handleMouseDown)
),
onMouseEnter: wrapMouseEvent(onMouseEnter, handleMouseEnter),
onMouseMove: wrapMouseEvent(onMouseMove, handleMouseMove),
onMouseLeave: wrapMouseEvent(onMouseLeave, handleMouseLeave),
onMouseDown: wrapMouseEvent(onMouseDown, handleMouseDown),
onFocus: composeEventHandlers(onFocus, handleFocus),
onBlur: composeEventHandlers(onBlur, handleBlur),
onKeyDown: composeEventHandlers(onKeyDown, handleKeyDown),
};

@karlhorky
Copy link
Contributor

Ah, this appears to work at first, but then fails without React.forwardRef - same "You need to place the ref" warnings in the console:

Screen Shot 2022-06-12 at 13 05 36

Screen Shot 2022-06-12 at 13 05 56

@CodingDive
Copy link
Contributor

This now seems to be an issue with any HTML element or even when trying to use the fix described above.
https://codesandbox.io/s/thirsty-waterfall-1f8gjh?file=/src/index.js

Upon removing <StrictMode /> the warning disappears.

Relates to #916

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

10 participants