Page MenuHomePhabricator

[web] Separate color in Button
ClosedPublic

Authored by michal on Oct 10 2022, 1:59 AM.
Tags
None
Referenced Files
Unknown Object (File)
Thu, Nov 14, 4:49 PM
Unknown Object (File)
Thu, Nov 14, 4:49 PM
Unknown Object (File)
Thu, Nov 14, 4:49 PM
Unknown Object (File)
Thu, Nov 14, 4:49 PM
Unknown Object (File)
Thu, Nov 14, 4:49 PM
Unknown Object (File)
Thu, Nov 14, 4:49 PM
Unknown Object (File)
Thu, Nov 14, 4:49 PM
Unknown Object (File)
Thu, Nov 14, 4:48 PM
Subscribers

Details

Summary

Part of ENG-843
Separates color in Button so it can be used somewhere where the color is dynamic (depends on a color of a chat).

Test Plan
  1. Run flow
  2. Check if all buttons look correct

Diff Detail

Repository
rCOMM Comm
Lint
No Lint Coverage
Unit
No Test Coverage

Event Timeline

There are a very large number of changes, so older changes are hidden. Show Older Changes

Looks good, some minor questions/suggestions inline.

Do you think it would be clearer if we renamed primary to filled and secondary to outline to help distinguish the two variants?

web/components/button.react.js
21

Maybe name this prop buttonColor so it's clear that it's a style object and not value for color property?

32

Should we set a default color the way we set a default variant?

Should we make the color prop required instead of optional?

This revision is now accepted and ready to land.Oct 10 2022, 8:30 AM
In D5321#157310, @atul wrote:

Do you think it would be clearer if we renamed primary to filled and secondary to outline to help distinguish the two variants?

I think it makes sense. We might want to have success and danger buttons side by side in the future, and having them both be filled instead of primary makes more sense. Also, we now use this component in more places and not just as "Cancel", or "Continue" buttons.

web/components/button.react.js
21

Makes sense, I will change it

32

The preset color schemes (success, danger) really only apply to the primary variant, and different variants have different "defaults". Also in a future diff (that I haven't yet submitted), the default variant is changed to a plain variant, which has only minimal CSS properties, and the color may not apply to it. So I don't think it makes sense to make it required or set a default.

web/components/button.react.js
32

That makes sense, thanks for explaining!

tomek requested changes to this revision.Oct 11 2022, 4:00 AM
tomek added inline comments.
web/components/button.react.js
10–15

Can we avoid mixing the types here? Maybe we can expose some objects with predefined colors instead?

This revision now requires changes to proceed.Oct 11 2022, 4:00 AM

Rename variants and buttonColor prop.

web/components/button.react.js
10–15

The problem is that success and danger colors have :hover and :disabled properties in css. I don't think it's possible to specify them in inline styles. We could try to mimic them with onMouseEnter/onMouseLeave/disabled prop but I don't know if that's any better.

tomek added inline comments.
web/components/button.react.js
50 ↗(On Diff #17485)

One thing to note is that when buttonColor is undefined, this condition is met and style={undefined} - which is ok, but probably not obvious

This revision is now accepted and ready to land.Oct 12 2022, 5:56 AM

After talking with @ashoat it seems like the previous changes are not the best solution. In particular they don't support hover animation for all colors and only for the predefined success and danger.

The problem with implementing it all for all colors is that we can't specify :hover and :disabled in inline styles in javascript. There are a few solutions:

  • Use onMouseEnter and similar. It's less debuggable than css :hover and to me it's more verbose and less readable. We also lose the animation.
  • Use opacity or transform: brightness() or similar in :hover. There are two problems with this solution:
    • Our outline (secondary) button brightens instead of darkening on hover
    • The children are also transformed (so the text becomes darker). There are ways to handle this with :before or :after but they are kind of complicated
  • Setting css variables in javascript (scoped to the button) and then using them inside .css files in :hover

The third solution is a bit unconventional but I think it't the cleanest and simplest and it's implemented in this revision.

ashoat requested changes to this revision.Oct 14 2022, 8:43 AM

Back to you with questions :)

The problem with implementing it all for all colors is that we can't specify :hover and :disabled in inline styles in javascript.

Are you sure about this? Can you perhaps link some StackOverflow or GitHub discussions that talk about this? I wonder what would happen if you dynamically (in JS) render a <style> tag

  • Use onMouseEnter and similar. It's less debuggable than css :hover and to me it's more verbose and less readable. We also lose the animation.

I agree this is not great...

  • Use opacity or transform: brightness() or similar in :hover. There are two problems with this solution:
    • Our outline (secondary) button brightens instead of darkening on hover
    • The children are also transformed (so the text becomes darker). There are ways to handle this with :before or :after but they are kind of complicated

Hmm... for the first, I strongly suspect there is a way to darken instead of brighten via a transform, no? For the second, I'm curious to know more about how complicated these are.

  • Setting css variables in javascript (scoped to the button) and then using them inside .css files in :hover

The third solution is a bit unconventional but I think it't the cleanest and simplest and it's implemented in this revision.

This is cool!! I think the transform solution would be ideal, but this is a good fallback.

This revision now requires changes to proceed.Oct 14 2022, 8:43 AM

Are you sure about this? Can you perhaps link some StackOverflow or GitHub discussions that talk about this? I wonder what would happen if you dynamically (in JS) render a <style> tag

Here and here. I couldn't find anything that explicitly states that it's impossible but it seems like it. I tried just including ':hover': {color: 'red'} in the inline styles but it didn't work for me.
I think rendering <style> is one of the workarounds mentioned in the linked posts but it doesn't seem that great to me.

Hmm... for the first, I strongly suspect there is a way to darken instead of brighten via a transform, no? For the second, I'm curious to know more about how complicated these are.

Yeah, the first point was a minor complication. You can just use values higher than 1.

For the second, I couldn't figure out how to implement it before, but I tried it once again after your comment and I have something that works (although I haven't tested it for all cases yet). It would look something like this (button.css):

.btn {
  position: relative;
  border-radius: 4px;
  font-size: var(--m-font-16);
  padding: 12px 24px;
  color: var(--fg);
  cursor: pointer;
}

.btn > * {
  z-index: 2;
}

.btn::before {
  content: "";
  border-radius: inherit;
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background: inherit;
  z-index: 1;
}

/* === These classes would be exclusive */
.btn-light:hover::before {
  filter: brightness(0.85);
}

.btn-dark:hover::before {
  filter: brightness(1.15);
}
/* === */

There is at least one problem with this. You can't use plain text in buttons (<Button> Cancel </Button>) because of the z-index manipulation. You have to first wrap the text in <div> or something else which might be unexpected for someone using this component. Also, I wonder if changing z-index could break some other thing, I can see we are changing it in a few other places in the code but I'm not familiar with that property very much.

For the second, I couldn't figure out how to implement it before, but I tried it once again after your comment and I have something that works (although I haven't tested it for all cases yet). It would look something like this (button.css):

.btn {
  position: relative;
  border-radius: 4px;
  font-size: var(--m-font-16);
  padding: 12px 24px;
  color: var(--fg);
  cursor: pointer;
}

.btn > * {
  z-index: 2;
}

.btn::before {
  content: "";
  border-radius: inherit;
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background: inherit;
  z-index: 1;
}

/* === These classes would be exclusive */
.btn-light:hover::before {
  filter: brightness(0.85);
}

.btn-dark:hover::before {
  filter: brightness(1.15);
}
/* === */

Can you explain more why the z-index is necessary? What are you trying to achieve with it?

There is at least one problem with this. You can't use plain text in buttons (<Button> Cancel </Button>) because of the z-index manipulation. You have to first wrap the text in <div> or something else which might be unexpected for someone using this component. Also, I wonder if changing z-index could break some other thing, I can see we are changing it in a few other places in the code but I'm not familiar with that property very much.

I think it would be pretty easy to check in the Button component if the children prop is a string, and if so to wrap it with a div. However I do agree with you that z-index is probably best to avoid if possible... still confused why it's necessary

ashoat requested changes to this revision.Oct 14 2022, 10:36 AM

Back to you with question

This revision now requires changes to proceed.Oct 14 2022, 10:36 AM

Without the z-index the ::before pseudo-element covers the Button children and I couldn't find a way to make it work without it.

ashoat requested changes to this revision.Oct 14 2022, 11:59 AM

What's the ::before for? I think I need to grok the problem and solution much better before I can be an effective reviewer here, unfortunately... if you can provide a detailed explanation that would be super helpful. Alternately, I can resign from this revision and you can pair with somebody else on the team, and later they could explain to me what's going on here.

This revision now requires changes to proceed.Oct 14 2022, 11:59 AM

Sorry, I should have explained more and not just dropped the code in the comment.

The problem is that transform also applies to the Button children (text and icons) and it looks bad so we don’t want that. We need to create another element that will be the background of the button. We can do that in css with ::before pseudo-element that works as if there was an additional child of the <button>. It needs to fill the entire parent so that’s why I added the position, left, width etc. But because of that, it displays above the other siblings and that’s why we need to change the z-index.

I hope that makes it clearer.

I have talked with @tomek about this and we have figured out a solution without the z-index. I will update this diff shortly.

Updated to the new method. There's a slight visual change because now instead of using predefined colors for :hover colors we are using the filter property. I have tried to select the brightness values so that the changes are minimal (before/after):

{F202020}
{F202021}

image.png (116×732 px, 15 KB)

image.png (108×712 px, 13 KB)

Additionally, the filled buttons previously had a transparent 1px border which was a bit problematic with this method, but from what I could see they don't have it on figma so I have removed it.

michal attached a referenced file: F202029: image.png. (Show Details)
michal edited the test plan for this revision. (Show Details)
ashoat added 2 blocking reviewer(s): tomek, atul.

Great work!! I haven't looked super closely but I trust you've figured out a perfect solution. Will defer to @tomek and @atul for the final accept

tomek requested changes to this revision.Oct 18 2022, 4:00 AM
tomek added inline comments.
web/components/button.css
6 ↗(On Diff #17612)

This was an important property: it was added so that when filled and outlined buttons are rendered next to each other, they have the same height.

web/components/button.react.js
14 ↗(On Diff #17612)

Maybe we can avoid specifying value type for all the fields assigning a type to this variable

This revision now requires changes to proceed.Oct 18 2022, 4:00 AM

Update. Changed typing and re-added the transparent border. To keep all the properties dealing with border in sync I have added two css variables. Otherwise if someone wanted to change the border width or radius there would subtle visual bugs.

tomek requested changes to this revision.Oct 18 2022, 9:05 AM

We're really close - just a couple of questions inline.

web/components/button.css
18 ↗(On Diff #17635)

What happens when we have an element with .outline class but without .btn - what would be the value of var(--border-width)? Would modifying this selector to check if both classes are there will make it safer? .outline.btn

web/components/button.react.js
52–60 ↗(On Diff #17635)

I don't think it is beneficial to memoize it. Usually memoization makes sense when it avoids expensive recomputation or / and when it provides referential equality. In this case the computation isn't expensive and we already have referential equality.

64 ↗(On Diff #17635)

Is it safe to not provide keys when using Children.map?

This revision now requires changes to proceed.Oct 18 2022, 9:05 AM

Update

web/components/button.css
18 ↗(On Diff #17635)

If the css variable isn't defined the property will be skipped (if there isn't --border-width set higher in the hierarchy). We could also specify a fallback var(--border-width, 1px). But having a selector with both classes is probably the best idea because we are sure that we use the variable from the current button.

web/components/button.react.js
52–60 ↗(On Diff #17635)

Makes sense. In previous revisions of this diff, there was more code here and I forgot to change it.

64 ↗(On Diff #17635)

According to this yes. I have also checked the newer versions and I think that's still the case.

atul added inline comments.
web/components/button.css
31–32 ↗(On Diff #17654)

Can we flip this to avoid using calc()?

Something like:

bottom: var(--border-width);
right: var(--border-width);

?

This revision is now accepted and ready to land.Oct 20 2022, 1:41 AM
web/components/button.css
31–32 ↗(On Diff #17654)

Unfortunately, this doesn't look correct. We still have to use calc(-1*) even with bottom and right:

bottom: calc(-1 * var(--border-width));
right: calc(-1 * var(--border-width));