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

fix(Button): render disabled button as span #1540

Closed
wants to merge 1 commit into from

Conversation

s0ber
Copy link
Contributor

@s0ber s0ber commented Sep 1, 2020

SPT-880

Description

Right now we render disabled button as <button disabled /> with no pointer events. And this stands in our way when we want to render the tooltip (which is a very common case in Staff Portal). And we have to wrap the button in additional span just to render this tooltip. In many cases it breaks the styling (please take a look at this bug for example: https://toptal-core.atlassian.net/browse/SPT-880). There are also problems with button groups styling when one of the buttons is disabled.

The solution is simple. Let's not block all pointer events for disabled buttons and just render them as spans in the first place like we do in legacy platform.

Downsides: some tests may break in various projects and in general it's not always good when DOM element changes. But in the longterm it'll allow to avoid many hacks related to disabled buttons.

How to test

  • Open Button examples
  • Make sure disabled buttons are rendered as spans
  • Check out new Button example with even handlers

Review

  • Read CONTRIBUTING.md and Component API principles
  • Make sure you've converted all js/jsx file into ts/tsx in your PR
  • Annotate all props in component with documentation
  • Create examples for component
  • Ensure that deployed demo has expected results and good examples
  • Ensure that unit tests pass by running yarn test
  • Ensure that visuals tests pass by running yarn test:visual. If not - check the documentation how to fix visual tests
PR commands

List of available commands:

  • @toptal-bot run all - Run whole pipeline
  • @toptal-bot run danger - Danger checks
  • @toptal-bot run lint - Run linter
  • @toptal-bot run test - Run jest
  • @toptal-bot run build - Check build
  • @toptal-bot run test:visual or @toptal-bot run visual - Run visual tests
  • @toptal-bot run deploy:documentation - Deploy documentation

@s0ber s0ber requested a review from a team as a code owner September 1, 2020 20:34
@s0ber
Copy link
Contributor Author

s0ber commented Sep 1, 2020

@toptal-bot run package:alpha-release

@s0ber
Copy link
Contributor Author

s0ber commented Sep 1, 2020

@toptal-bot run all

@hweeTan
Copy link
Contributor

hweeTan commented Sep 2, 2020

@s0ber Or maybe we can adjust the Tooltip component so it has a prop to always wraps the children with a Container or something? Because to me, it makes sense for disabled button not to have pointer-events. The issue here maybe is with Tooltip relying on the children to show, so maybe we should make it work regardless what the children are? WDYT?

@@ -186,7 +187,7 @@ export const Button = forwardRef<HTMLButtonElement, Props>(function Button(
title={title}
value={value}
type={type}
component={as!}
component={disabled && as === 'button' ? 'span' : as!}
Copy link
Member

@ertrzyiks ertrzyiks Sep 2, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We would need aria-role='button' to render a button with a span tag

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will add it, thanks!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ertrzyiks It seems aria-role="button" is not valid, React complains to it. There is role="button" that is added by MUI, it seems that should be enough.

@s0ber
Copy link
Contributor Author

s0ber commented Sep 2, 2020

@hweeTan Additional markup coming from the tooltip is not good, IMHO. It'll again bring unexpected styling issues. It's a leaking abstraction — you'll now have to account to the fact that everything can be wrapped into additional span. Basically it'll affect every possible component.

title: 'Disabled',
description: 'The button shows that currently unable to be interacted with'
})
.addExample('Button/story/DisabledWithEvents.example.tsx', {
title: 'Disabled with mouse events',
description: 'The button is disabled, but event handlers can be added'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems counter-intuive. If the button is disabled so should event handlers. If events are happening then the button is not disabled.

IMO we should just integrate a tooltip in the button.

<Button 
  disabled
  disabledMessage="No bueno" // automatically renders a tooltip with the specified content
/>

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good to me, but the problem is that with disabled button there won't be a way to add the tooltip. If we add the tooltip then the button stops being disabled (and any other event handler can be added).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@s0ber What if we set the tooltip as a child of the button?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@borisyordanov I just checked if events are fired for child nodes and it seems they are, at least in Chrome. The only problem I see here is that we'll need to change styles a bit. I'll try playing with this and prepare a complete solution to see how good will it look like in the end.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@borisyordanov I decided not to examine the option of adding the tooltip prop inside the Button because Tooltip is much more than just a single prop. And it doesn't bring many benefits really — just couples Button and Tooltip, also brings a lot of complicated logic inside the Button, requires significant styling refactoring, pollutes the markup with additional span for every button and so on 🙂
copied from Slack for the historical reasons

@vedrani
Copy link
Contributor

vedrani commented Sep 2, 2020

Be aware it needs compatibility with v5 also. Check all variants here https://share.goabstract.com/f9b77f8f-544b-4ced-a6a3-76b5a32f2934?sha=2717bd241694d688a866dfab6dddc3f75edd671c

@s0ber
Copy link
Contributor Author

s0ber commented Sep 2, 2020

@vedrani I'll help merging it into v5 branch!

import { Button } from '@toptal/picasso'
import { useNotifications } from '@toptal/picasso/utils'

const Example = () => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand what value this example brings us. I'd rather see examples that cover

  • a disabled button that shows a tooltip on hover
  • a button group where one or more buttons are disabled and show a tooltip

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will add proper examples!

@@ -188,10 +188,14 @@ page
.addExample('Button/story/Basic.example.jsx', 'Basic')
.addExample('Button/story/Variants.example.jsx', 'Variants')
.addExample('Button/story/States.example.jsx', 'States')
.addExample('Button/story/Disabled.example.jsx', {
.addExample('Button/story/Disabled.example.tsx', {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❤️

@vedrani
Copy link
Contributor

vedrani commented Sep 3, 2020

@vedrani I'll help merging it into v5 branch!

Please check #1537

@s0ber s0ber force-pushed the disabled-button-as-span branch 3 times, most recently from 81d6c83 to 487e44c Compare September 3, 2020 22:09
@@ -84,7 +84,7 @@ export interface StaticProps {
}

const getVariantType = (variant: VariantType) => {
const [type] = variant!.split('-')
const [type] = variant!.split('-') // eslint-disable-line @typescript-eslint/no-non-null-assertion
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this eslint disable necessary?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For some reason precommit hook started complaining to me yesterday, maybe it started happening when Davinci dependency version was bumped recently, not sure TBH.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is pretty strange, can you please figure out why this error started to happen?

className={className}
style={style}
disabled={disabled}
title={title}
value={value}
type={type}
component={as!}
component={disabled && as === 'button' ? 'span' : as!} // eslint-disable-line @typescript-eslint/no-non-null-assertion
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's move these conditionals into variables?

</Tooltip>
</Container>

<Container top='small'>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add some text above each of these examples so it's more obvious that the top example is two buttons and the bottom is a button group

})
: configClasses
? React.cloneElement(childNode, { classes: configClasses })
: childNode
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@s0ber It'll take me a week and 100 espresso's to understand this code. Can we try to make it more readable? Maybe add some comments explaining what's going on

Copy link
Contributor Author

@s0ber s0ber Sep 4, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeap, it's tricky and I don't like it but I was not able to come up with something better. This code allows to inject classes and childrenClasses props into the chidren react nodes. Previously it was just classes. childrenClasses is a prop that you have to pass to Tooltip and this way they will be forwarded to the component that is wrapped by the Tooltip. We use this to pass classes from Button.Group to Tooltip to Button.

It technically may be useful in other cases where there are components that break the direct relationship between parent and children. E.g. previously there was just Button.Group -> Button relationship and Button.Group was passing some classes to Button for the proper styling. No when there is Tooltip between them things become more complicated.

I think we can proceed there and fix those things later because it is kind of internal technical issue, it only affects styling and it doesn't introduce anything in the public API.

@toptal-devbot
Copy link
Collaborator

🎉 Last commit is successfully deployed 🎉

Demo is available on:

Your davinci-bot 🚀

@denieler
Copy link
Collaborator

denieler commented Sep 7, 2020

isn't it a bad idea in terms of accessibility to render a button as span?

@borisyordanov
Copy link
Contributor

isn't it a bad idea in terms of accessibility to render a button as span?

I guess it is, but considering we have been living with bad accessibility for over a year I think we can let this slide. I haven't seen any numbers, but I'm guessing the number of disabled Picasso users is low. When we start using Picasso for public pages we'll definitely have to revisit this.

@denieler
Copy link
Collaborator

denieler commented Sep 7, 2020

I don't know the issue deep enough, but isn't there some better solution that using span instead of button? As I understand we have an issue that we need to wrap button with span to be able to show a Tooltip. But maybe we can fix Tooltip component instead to make it working with pure button tag

@s0ber
Copy link
Contributor Author

s0ber commented Sep 7, 2020

@denieler If you have specific ideas I'll be very happy if you'll provide them. Maybes are not enough. I've posted multiple times why using button doesn't work in various places. There was actually a way to use it suggested by @borisyordanov — to point tooltip to the inner element of the button tag, but it was a bad solution from multiple points of view and it's explained in the PR comments.

As for ARIA, we use role="button" (it is actually being done by MUI):
https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/button_role

@denieler
Copy link
Collaborator

denieler commented Sep 7, 2020

I can provide solutions after closely checking possible options. I don't have any now, but I see we are fixing the consequences instead of the root of the problem

@denieler
Copy link
Collaborator

denieler commented Sep 7, 2020

just an example, in MUI here they don't use any wrappers for Button component

Screenshot 2020-09-07 at 10 54 44

@s0ber
Copy link
Contributor Author

s0ber commented Sep 7, 2020

@denieler those buttons are not disabled

@denieler
Copy link
Collaborator

denieler commented Sep 7, 2020

Looks like this is a suggested solution - mui/material-ui#8416 (comment)

You can add an additional div or span always as a wrapper for children prop of Tooltip

@s0ber
Copy link
Contributor Author

s0ber commented Sep 7, 2020

@denieler please read the PR comments, this option was discussed

@denieler
Copy link
Collaborator

denieler commented Sep 7, 2020

@denieler please read the PR comments, this option was discussed

if you can give me a link - it would be great

@s0ber
Copy link
Contributor Author

s0ber commented Sep 7, 2020

@denieler
Copy link
Collaborator

denieler commented Sep 7, 2020

@hweeTan Additional markup coming from the tooltip is not good, IMHO. It'll again bring unexpected styling issues. It's a leaking abstraction — you'll now have to account to the fact that everything can be wrapped into additional span. Basically it'll affect every possible component.

I don't see an issue here. We can merge this as a breaking change into v5 branch. But instead, it will be a solid solution for a long time, instead of patching Button's disabled state.

@s0ber
Copy link
Contributor Author

s0ber commented Sep 7, 2020

@denieler I'm not sure how random div or span in the markup can be a good solution considering it already breaks the styling in multiple places. After this change it'll affect every place where the Tooltip is being used — flex items will be broken, sibling selectors will be broken, table cells will be broken, nth-child selectors will be broken, etc.

@denieler
Copy link
Collaborator

denieler commented Sep 7, 2020

@denieler I'm not sure how random div or span in the markup can be a good solution considering it already breaks the styling in multiple places. After this change it'll affect every place where the Tooltip is being used — flex items will be broken, sibling selectors will be broken, table cells will be broken, nth-child selectors will be broken, etc.

the breaking change is not an issue, as I said. It's just 1-time work. But we can ground a new way of using Tooltip if needed, a new behaviour of the Tooltip markdown and styles, if needed as well.

@s0ber
Copy link
Contributor Author

s0ber commented Sep 7, 2020

@denieler It's not just a breaking change. You'll have to test every possible component and a combination of components and all new components that will ever appear just to figure out that additional span in the markup won't break the styling. It'll be something that is very hard to test but very easy to break.

@denieler
Copy link
Collaborator

denieler commented Sep 7, 2020

@denieler It's not just a breaking change. You'll have to test every possible component and a combination of components and all new components that will ever appear just to figure out that additional span in the markup won't break the styling. It'll be something that is very hard to test but very easy to break.

yes, and this is true for every breaking change in Picasso. We shouldn't be afraid of making them still. Of course, they are unlikely, but sometimes we need to choose between 2 evils

@s0ber s0ber closed this Sep 7, 2020
@denieler denieler deleted the disabled-button-as-span branch December 18, 2020 09:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
7 participants