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

Is there a better alternative to cloneElement? #12921

Closed
2 tasks done
casank25 opened this issue Sep 18, 2018 · 23 comments · Fixed by #40220
Closed
2 tasks done

Is there a better alternative to cloneElement? #12921

casank25 opened this issue Sep 18, 2018 · 23 comments · Fixed by #40220
Labels
component: toggle button This is the name of the generic UI component, not the React module! enhancement This is not a bug, nor a new feature

Comments

@casank25
Copy link

When using ToggleButton from lab package and wrapping it in tooltip it raises a warning:
Warning: Failed prop type: The following properties are not supported: selected, onChange. Please remove them.

  • This is not a v1.x issue.
  • I have searched the issues of this repository and believe that this is not a duplicate.

Expected Behavior

It should be possible to add a tooltip to ToggleButton

Current Behavior

<Tooltip title="left" placement="top">
  <ToggleButton value="left">
    <FormatAlignLeftIcon />
  </ToggleButton>
</Tooltip>

Steps to Reproduce

Link: https://codesandbox.io/s/q7m82k4024

  1. Wrap the ToggleButton inside a Tooltip
  2. Run the code
  3. See the console for warning

Context

Since ToggleButtons are IconButton (at least that's how we intend to use it) it would be helpful to a user to see what that button will do and the best way for that is Tooltip

Your Environment

Tech Version
Material-UI v3.1.0
React v16.4
Browser any
@oliviertassinari oliviertassinari added the support: question Community support but can be turned into an improvement label Sep 18, 2018
@oliviertassinari
Copy link
Member

oliviertassinari commented Sep 18, 2018

@casank25 Either you need to reverse the order:

              <ToggleButton value="left">
                <Tooltip title="left" placement="top">
                  <FormatAlignLeftIcon />
                </Tooltip>
              </ToggleButton>

or to write a wrapping component to forward the properties where it's needed (jumping the tooltip).

@oliviertassinari oliviertassinari removed the support: question Community support but can be turned into an improvement label Sep 18, 2018
@oliviertassinari
Copy link
Member

Actually, we could forward the unused properties to the children to make it work out of the box. Do you want to work on that?

@oliviertassinari oliviertassinari added new feature New feature or request component: tooltip This is the name of the generic UI component, not the React module! good first issue Great for first contributions. Enable to learn the contribution process. labels Sep 18, 2018
@RobertPurcea
Copy link
Contributor

Working on it, I'll submit a PR today

@casank25
Copy link
Author

Ha, I guess I came too late to check back on this issue :)

@RobertPurcea
Copy link
Contributor

Actually, this might need a little more consideration.

The problem stated by the OP is caused by ToggleButtonGroup.
This component assumes it's immediate children will be buttons and passes 'selected' and 'onChange' props.
Now, this can be solved if Tooltip would simply pass all unused props but there is a second issue.

ToggleButtonGroup uses the 'value' prop of it's children to make toggle functionality work.
Without it, toggling does not work.

    const children = React.Children.map(childrenProp, child => {
      if (!React.isValidElement(child)) {
        return null;
      }
      // value comes from children. Having an empty tooltip is breaking this
      const { selected: buttonSelected, value: buttonValue } = child.props;

      const selected =
        buttonSelected === undefined ? isValueSelected(buttonValue, value) : buttonSelected;

      return React.cloneElement(child, {
        selected,
        onChange: exclusive ? this.handleExclusiveChange : this.handleChange,
      });
    });

@oliviertassinari let me know if you still want to progress with this, I am not sure there's a good way to support what OP is asking and whether or not it's worth it

@eps1lon
Copy link
Member

eps1lon commented Sep 19, 2018

Just a random thought but wouldn't it be better to create a context in ToggleButtonGroup that the ToggleButtons can listen to? That way we could get away from the cloneElement pattern.

@oliviertassinari
Copy link
Member

oliviertassinari commented Sep 23, 2018

This issue is really interesting it's at the reunion of many pain points:

we could forward the unused properties to the children to make it work out of the box

Just a random thought but wouldn't it be better to create a context in ToggleButtonGroup that the ToggleButtons can listen to?

@oliviertassinari oliviertassinari removed the good first issue Great for first contributions. Enable to learn the contribution process. label Sep 23, 2018
@oliviertassinari
Copy link
Member

oliviertassinari commented Sep 23, 2018

Regarding the cloneElement usage. @eps1lon is right, it's a broader issue. We have many components defaulting to this strategy.

  • Using cloneElement has multiple drawbacks:
  1. The behavior is hidden to the users. It can feel like magic. It's making wrapping component harder to write correctly. You need to do stuff like setting muiName and drilling properties down multiple components.
  2. It's breaking virtualization potentials.
  • Using the context API could be a way out. It's solving the virtualization issue. However, it's a double edge sword for the wrapping component story. If you don't need to access the injected properties it's perfect. If you do, it's a nightmare. Also, I'm unsure about the UX (bundle size and runtime performance overhead) and DX (enzyme support of the next context API, Apollo support, preact support etc.). Also, it can be problematic when we need to access a child property, like a value.
  • A third option would be to support React elements & render props. For instance:
<ToggleButtonGroup
  value={alignment}
  exclusive
  onChange={this.handleAlignment}
>
  {childProps => (
    <Tooltip value="left" title="left" placement="top">
      <ToggleButton {...childProps}>
        <FormatAlignLeftIcon />
      </ToggleButton>
    </Tooltip>
  )}
  <ToggleButton value="left">
    <FormatAlignLeftIcon />
  </ToggleButton>
</ToggleButtonGroup>

It's solving the wrapping issue quite elegantly. However, it doesn't solve the virtualization problem. Notice that I had to move the value property on the Tooltip.

What do you think? I like option 3.


I have been benchmark a large number of competitive tooltip implementations. Most of them don't forward the properties. For consistency with all our components, I think that it's best that we forward the properties to the children, it's what will be the first node the DOM structure. This way, our component is more flexible. I'm gonna work on it.

@eps1lon
Copy link
Member

eps1lon commented Sep 23, 2018

I'm not a big fan of the context API either since it's so verbose. But it makes composing the controlled children with other components much easier. Moving the value property up looks really bad. It would be really nice if people could just wrap whatever they want around the controlled child without worrying that they overwrite something important by accident.

It's really tempting to "just do it"™, see what problems we encounter and then benchmark against master.

react-bootstrap is also using cloneElement for ToggleButtonGroup.

@oliviertassinari oliviertassinari changed the title Tooltip raises Failed prop type warning when placed on ToggleButton Is there a better alternative to cloneElement? Sep 23, 2018
@oliviertassinari
Copy link
Member

@eps1lon How do you think we should solve the problem?

@eps1lon
Copy link
Member

eps1lon commented Sep 23, 2018

Maybe there isn't even a problem. Concerning this issue it might be sufficient to wrap the icon inside the ToggleButton in the Tooltip.

Maybe it's only a perf issue. But this would require a benchmark against another implementation.

The easiest thing is probably to look into other libraries and check out how they solved the problem and why.

When I watched https://www.youtube.com/watch?v=AiJ8tRRH0f8 I immediately thought about mui and I think there is a great solution in there.

It's just not an immediate problem in my opinion and I'm really unsure which way to go first.

@casank25
Copy link
Author

@eps1lon By wrapping icon inside ToggleButton instead of wrapping ToggleButton itself, wouldn't that present an inconsistency?

Based on docs, we wrap Button in Tooltip and we wrap IconButton in Tooltip.

Also, what if ToggleButton needs bigger padding? The user would have to hover over icon in order to see the tooltip.

@eps1lon
Copy link
Member

eps1lon commented Sep 24, 2018

@casank25 Oh I completely agree. This was just a meant as a temporary workaround.

@melissachang
Copy link

Actually, if I put title directly on ToggleButton, things work. No Tooltip needed. I updated https://codesandbox.io/s/38w8xry8rq

@JCQuintas
Copy link
Member

Couldn't we solve this with hooks now?

@eps1lon
Copy link
Member

eps1lon commented May 21, 2019

Couldn't we solve this with hooks now?

Hooks would just improve DX ergonomics. What actually solves this is the context API.

@JCQuintas
Copy link
Member

Hooks would just improve DX ergonomics. What actually solves this is the context API.

Sorry, I meant to answer this the problem bellow. I should have been more clear.

I'm not a big fan of the context API either since it's so verbose. But it makes composing the controlled children with other components much easier.

The hook API has an useContext which makes the ContextAPI a bit less verbose for the consumer, although it stays the same for the provider.

@ghost
Copy link

ghost commented Aug 26, 2019

Just FYI. As per closed issue #17079, I am unable to wrap my toggle buttons and get full functionality. Clicking the button does fire the event, but the background does not latch to the new toggle state. See https://codesandbox.io/s/create-react-app-jf5qe

I see that it is the same root design issue as described here, in that ToggleButtonGroup requires its buttons to be immediate children. Thanks.

@volodymyr-kushnir
Copy link

volodymyr-kushnir commented Apr 9, 2020

@leantide you'd just have to set value on your wrapping component and not in your wrapped component, i.e.

<ToggleButton value={"toggle1"}>Toggle 1</ToggleButton>
<Toggle2 value={"toggle2"} />
<ToggleButton value={"toggle3"}>Toggle 3</ToggleButton>
<Toggle4 value={"toggle4"} />

https://codesandbox.io/s/create-react-app-kuhkl
UPD: I assume you have fixed your issue by now, but I commented anyway, because I just had the same issue and thought that perhaps somebody else would also find this thread later. Your codesandbox really helped, thanks.

@xstable
Copy link

xstable commented Feb 14, 2022

Simplest answer to me was to add the propery "value" to Tooltip:

  function generateToggleButton(filter, brand, tooltip) {
    return (
      <Tooltip
        arrow
        key={filter}
        title={tooltip}
        value={filter}
        disableHoverListener={tooltip === `brands.${filter}.tooltip`}
        disableTouchListener={tooltip === `brands.${filter}.tooltip`}
      >
        <ToggleButton color="primary" value={filter}>
          {brand}
        </ToggleButton>
      </Tooltip>
    );
  }
     <ToggleButtonGroup
          size="small"
          value={brands}
          onClick={e => setBrandsInQuery(e.target?.value)}
          color="primary"
        >
          {Object.values(ProductBrands).map(
            brand => generateToggleButton(brand, t(`brands.${brand}.label`), t(`brands.${brand}.tooltip`)),
          )}
        </ToggleButtonGroup>

@totszwai
Copy link

totszwai commented Mar 28, 2022

Guys, Tooltip breaks ToggleButton.

The selected CSS class will not get set if you wraps the Tooltip around the button.

<ToggleButtonGroup>
  <Tooltip title="whatever1">
     <ToggleButton value="blah1">Blah1</ToggleButton>
  </Tooltip>
  <Tooltip title="whatever2">
     <ToggleButton value="blah2">Blah2</ToggleButton>
  </Tooltip>
</ToogleButtonGroup>

Without Tooltip you can see that the proper class is being added:
image

When wrapping the ToggleButton with a tooltip, it breaks:
image

@jmca
Copy link

jmca commented Sep 23, 2022

Simplest answer to me was to add the propery "value" to Tooltip:

  function generateToggleButton(filter, brand, tooltip) {
    return (
      <Tooltip
        arrow
        key={filter}
        title={tooltip}
        value={filter}
        disableHoverListener={tooltip === `brands.${filter}.tooltip`}
        disableTouchListener={tooltip === `brands.${filter}.tooltip`}
      >
        <ToggleButton color="primary" value={filter}>
          {brand}
        </ToggleButton>
      </Tooltip>
    );
  }
     <ToggleButtonGroup
          size="small"
          value={brands}
          onClick={e => setBrandsInQuery(e.target?.value)}
          color="primary"
        >
          {Object.values(ProductBrands).map(
            brand => generateToggleButton(brand, t(`brands.${brand}.label`), t(`brands.${brand}.tooltip`)),
          )}
        </ToggleButtonGroup>

Passing value also works for me so far, thanks for the tip. Extending TooltipProps interface keeps it valid TS too:

import '@material-ui/core';

declare module '@material-ui/core' {
  export interface TooltipProps {
    value?: any;
  }
}

I still need to test out the solution at #21026 (comment) which is way less code.

@ZeeshanTamboli ZeeshanTamboli added component: toggle button This is the name of the generic UI component, not the React module! enhancement This is not a bug, nor a new feature and removed core Infrastructure work going on behind the scenes new feature New feature or request labels Jan 5, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
component: toggle button This is the name of the generic UI component, not the React module! enhancement This is not a bug, nor a new feature
Projects
None yet
Development

Successfully merging a pull request may close this issue.