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

UX-436 Migrate Button 🐐 #39

Merged
merged 11 commits into from
Mar 14, 2019
Merged

UX-436 Migrate Button 🐐 #39

merged 11 commits into from
Mar 14, 2019

Conversation

mikrotron
Copy link
Contributor

@mikrotron mikrotron commented Feb 22, 2019

🛠 Purpose

Add a <Button> component that can be rendered as either a <button> or global CSS resilient <RawButton>.

✏️ Notes to Reviewer

  • Buttons with icons are not yet supported since an <Icon> component has not yet been added.
  • Added a very basic Storybook story for now.
  • Updated Tokens package with latest design tokens / script fix.
  • Added customPropTypes helpers, including a ShirtSizes enum.
  • No tests (yet).
  • Resolves Button Component #40

🖥 Screenshots

screen shot 2019-02-21 at 5 48 37 pm

@mikrotron mikrotron changed the title UX-436 Migrate utton UX-436 Migrate Button Component 🐐 Feb 22, 2019
@mikrotron mikrotron changed the title UX-436 Migrate Button Component 🐐 UX-436 Migrate Button 🐐 Feb 22, 2019
@mikrotron mikrotron added WIP 📦 New Package A new shiny Component labels Feb 22, 2019
@@ -14,3 +15,9 @@ export const CenteredStory = styled.div`
justify-content: center;
width: 100%;
`;

export const Rule = styled.hr`
border: 1px solid ${tokens.border.color};

Choose a reason for hiding this comment

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

Here you define 4 borders with 1px width just to define color. Then below you remove all borders except one. And you need to think a little bit to understand what border we eventually have.
Can we define it like this?

border-bottom: 1px solid ${tokens.border.color};

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Because this is applied to an <hr> element, which already gets four borders from the user agent styles, I need to also remove the other borders for a 1px line effect. An alternative would be to set the border to none, then define border-bottom, as you suggest, or maybe in one, pretty long line.

Choose a reason for hiding this comment

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

alternative would be to set the border to none, then define border-bottom

👍

Copy link
Contributor

Choose a reason for hiding this comment

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

FYI: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/hr

The HTML <hr> element represents a thematic break between paragraph-level elements: for example, a change of scene in a story, or a shift of topic within a section.
Historically, this has been presented as a horizontal rule or line. While it may still be displayed as a horizontal rule in visual browsers, this element is now defined in semantic terms, rather than presentational terms, so if you wish to draw a horizontal line, you should do so using appropriate CSS.

I think this means that if we just want a horizontal line, we should not be using <hr> (even though it is indeed separating<p>s in the stories. Those paragraphs are not really textual, so there can't be a "a change of scene in a story, or a shift of topic within a section". Yes, I know it's just for stories.)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

But this is a story. The story of a component. And especially for stories written for Screener, I think a "shift of topic within a section" is basically what we'd want this component for. A break between the description / illustration of a component's different props. At least, this is one way to think of it that doesn't violate the semantics of the <hr>.

I'm not crazy about the name <Rule>, but wasn't too concerned about it for this pr. But maybe a better name would be something like <SectionBreak> or <TopicShift> or something like that. At any rate, we'll need to give this some careful though when we get to setting up the Storybook in a clearer, more consistent way.

${props.isFullWidth ? fullWidthStyles : ""}
${props.isActive ? activeStyles : ""}
${props.isDisabled ? disabledStyles : ""}
${["minor", "link"].includes(props.type) && props.isDisabled ? disabledTextStyles : ""}

Choose a reason for hiding this comment

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

Since the disabledTextStyles are relevant only for these two types, i think that it should be conditionally inserted inside typeStyles for minor and links types.

const typeStyles = (props) => ({
  ...,
  minor: `
    ${textButtonStyles}
    ${props.isDisabled && disabledTextStyles}

    &:hover {
      text-decoration: underline;
    }
  `,
  ...
}[props.type]);

const composedStyles = props => `
  ...
   ${typeStyles(props)}
  ...
`;

@@ -9,19 +9,22 @@ color:
black-lighten-70: desaturate(lighten($color--black, 70), 5)
black-lighten-80: '#f7f7f7'
black-lighten-90: '#fcfcfc'
black-disabled: $color--black-lighten-40

Choose a reason for hiding this comment

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

Just curious: what's the reason of using .yaml extension instead of .yml?

Copy link
Contributor

Choose a reason for hiding this comment

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

there is no reason at all, I think this is the extension assigned when the script was created. We can totally rename if we want is a simple character https://github.com/acl-services/paprika/blob/master/packages/Tokens/script.js#L7


const sizes = ["xsmall", "small", "medium", "large", "xlarge"];

export const shirtSize = (props, propName, component) => {

Choose a reason for hiding this comment

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

Can we reuse PropTypes.oneOf here?

import PropTypes from 'prop-types';

const sizes = ["xsmall", "small", "medium", "large", "xlarge"];

export const shirtSize = PropTypes.oneOf(sizes);

Copy link
Contributor

Choose a reason for hiding this comment

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

An idea wouldn't be good to have an Enum definition for our library, something like enumType.xsmall, enumType.small, etc.

so we could do.

PropTypes.oneOf([EnumType.xsmall, EnumType.small]) this way is more descriptive when the developer reads it, and as well we can reuse the Enum across the library?

Copy link
Contributor

Choose a reason for hiding this comment

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

I'd say if we do use some form of enum, its ergonomics should be good enough that we don't have to list all the possible options explicitly inside oneOf(...)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

So if I define the enum as:

ShirtSize = {
  SMALL: 'small',
  MEDIUM: 'medium',
  LARGE: 'large',
}

Then I could also add a line like this:

ShirtSize.ALL = Object.values(ShirtSize);

So that we could use it like:

PropTypes.oneOf(ShirtSize.ALL);

Does that seem like a good pattern?
Do you think ANY would be better than ALL? Do you think ShirtSizes would read better than ShirtSize? Do we really need an Enum prefix/postfix?

}

setRef(props) {
this.$button = props.buttonRef || React.createRef();

Choose a reason for hiding this comment

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

Could you please clarify when we indeed need a reference to a Button? I see that it's used now in the Storybook to print info about clicked button but you can get all required data from event object inside onClick callback.

I believe that everything should be done on purpose, so if there is currently no case when we need this, I'd suggest getting rid of it.

Choose a reason for hiding this comment

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

Also, is it a good practice to call React.createRef() on every button re-render if buttonRef is not passed through props?

Copy link
Contributor

Choose a reason for hiding this comment

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

This should work.

const Button = React.forwardRef((props, ref) => {
  const handleClick = event => {
    if (!props.canPropagate) event.stopPropagation();
    if (!props.isDisabled) props.onClick(event);
  };

  const { ariaText, buttonRef, canPropagate, children, isDisabled, isSemantic, tabIndex, ...moreProps } = props;
  if (ariaText) moreProps["aria-label"] = ariaText;

  const buttonProps = {
    isDisabled,
    onClick: handleClick,
    ref,
    tabIndex,
    ...moreProps,
  };

  if (isSemantic) {
    buttonProps.disabled = isDisabled;
  } else {
    buttonProps.tabIndex = isDisabled ? -1 : tabIndex;
  }

  const ButtonTag = isSemantic ? ButtonStyled : RawButtonStyled;

  return <ButtonTag {...buttonProps}>{children}</ButtonTag>;
});

Copy link
Contributor

Choose a reason for hiding this comment

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

and using it:

function MyComponent() {
   const $ref = $ref.createRef()
   return <Button ref={$ref} />
}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think we need to address this for the <RawButton> first, so I've opened a separate PR for that purpose: #42


const ButtonTag = isSemantic ? ButtonStyled : RawButtonStyled;

return <ButtonTag {...buttonProps}>{children}</ButtonTag>;

Choose a reason for hiding this comment

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

children can be left in moreProps.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Seems odd to write {moreProps.children} here, and a bit cleaner the way it is (vs this.props.children).


render() {
const { ariaText, buttonRef, canPropagate, children, isDisabled, isSemantic, tabIndex, ...moreProps } = this.props;
if (ariaText) moreProps["aria-label"] = ariaText;

Choose a reason for hiding this comment

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

Since ariaText is used only to set value for aria-label, why do we need this prop?

Copy link
Contributor

Choose a reason for hiding this comment

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

^ also, couldn't we detect if the children are typeof String and in that case just assigned it to the aria-text otherwise, if includes a React Node, We could trigger a warning asking the developer to add a meaningful aria-text?

We could even transform this into a propType validation?

Choose a reason for hiding this comment

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

@nahumzs, I don't think that it's a good idea to assign aria-label automatically. If children is String then it's not really necessary to specify aria-label, the button already has text, so e.g. VoiceOver can pronounce what the button is. The same as children is a text + icon.
But if children is only an icon (no text at all) then it'd be great to force a consumer to specify aria-label.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

You're right, @KirillKayumov, we don't really need an explicit prop, we can just rely on the ...moreProps mechanism to forward aria-label on to the <ButtonTag>.

However, I have a suggestion that to insulate app developers from the implementation details of the a11y of our components, we could consistently use a "proprietary" ariaText prop for all of our Paprika components, and that could be used for the component in the best way. Sometimes that's simply an aria-label on the root node, but in some cases it might be on a specific element that is not the root node, or use a different attribute, like aria-labelledby, or even a different technique, like a <caption> or <legend>. Do you think this kind of API actually makes sense and makes the app developer's experience more delightful?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

After a bit of discussion with Nahum, we decided to stray even further in the naming to avoid confusion, and call this prop a11yText. The goal is to reduce the work (cognitive load) for the app developers.

buttonRef: PropTypes.shape({ current: PropTypes.instanceOf(Element) }),
canPropagate: PropTypes.bool,
children: PropTypes.node.isRequired,
className: PropTypes.string,

Choose a reason for hiding this comment

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

Do we really need to specify className here?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Nope, ✂️

raw small link
</Button>
</p>
{[

Choose a reason for hiding this comment

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

I think it's worth to render a "not semantic" button in this story as well.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Not semantic buttons are <RawButtons> (or whatever we decide to rename them to, that is still open for debate 🙂), and the three examples above here are, as specified by isSemantic={false}.

This component's stories will be revisited though in a subsequent task/PR that is concerned specifically with Storybook design patterns.

...moreProps,
};

if (isSemantic) {

Choose a reason for hiding this comment

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

I think this condition is redundant because you can safely specify both disabled and tabIndex props for both ButtonStyled and RawButtonStyled components. Can we re-write the render function like so?

buttonProps() {
  const { isDisabled, tabIndex } = this.props;

  return {
    ...this.props,
    disabled: isDisabled,
    onClick: this.handleClick,
    ref: this.$button,
    tabIndex: (isDisabled ? -1 : tabIndex),
  };
}

render() {
  const ButtonTag = this.props.isSemantic ? ButtonStyled : RawButtonStyled;

  return <ButtonTag {...this.buttonProps()} />;
}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is simpler, and I'm tempted to take this suggestion, despite the concerns I have that made me choose this more convoluted approach:

  1. disabled is going to end up passed along by a "raw" <Button> to the underlying <span> and it will be invalid (may throw warnings, I'll try and see).
  2. The tabIndex logic will be duplicated in a "raw" <Button>, which may not be a big deal, it's just a bit unnecessary.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Add to that:
3. type (submit or button) is being added to isSemantic buttons, but not <RawButtons>
And I think I have teetered back over to the other side again and prefer the branching logic.

if (ariaText) moreProps["aria-label"] = ariaText;

const buttonProps = {
isDisabled,
Copy link
Contributor

Choose a reason for hiding this comment

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

you should add type="button" to avoid the default value "submit" https://github.com/yannickcr/eslint-plugin-react/blob/master/docs/rules/button-has-type.md

Copy link
Contributor

Choose a reason for hiding this comment

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

What if we do want a submit button? I'm thinking that our type prop clashes with the HTML type attribute, so there's no way to make a submit button...

Copy link
Contributor

Choose a reason for hiding this comment

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

so what we need is a default prop be type='button', but the type is need it if not all button will be submit buttons.

Copy link
Contributor

Choose a reason for hiding this comment

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

I think what we need is two separate props like type (= submit | button | reset) and... kind? (= primary | link | ...)

Copy link
Contributor

Choose a reason for hiding this comment

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

^ yeah that makes sense.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

While I mostly agree, two points:

  1. I don't believe in <button type='reset'> so really the only options are submit | button. Most of the time we want button. So we could utilize a boolean prop, like isSubmit which is potentially a bit simpler.
  2. The above would allow us to keep the exising API of using type as the prop for this component... but I don't like that either, since type is a valid attribute in this case and it makes it a bit confusing... so I still like the idea of using a different name, like kind. Or theme? Or, taking inspiration from BluePrintJS, intent?

Aside: I don't think we should ever use props that are valid attributes in our new props APIs. title, for example, should be avoided.

Copy link
Contributor

@alexzherdev alexzherdev Mar 6, 2019

Choose a reason for hiding this comment

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

👍 on isSubmit
👍 on kind, 😐 on intent (seems like their notion is more narrow than what we need, e.g. doesn't include things like minor or link), 👎 on theme since it's a loaded word in the component library world

Aside: I don't think we should ever use props that are valid attributes in our new props APIs. title, for example, should be avoided.

To clarify, does that mean we don't want for there to be an intersection between our prop names and DOM attribute names? seems like too strict and sounds like you would e.g. prefer to use type prop for the type attribute?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

To move forward, how about I go with kind?
But, like the name of the, RawButton, we should revisit and get a bit wider feedback on this before we decide definitively (before we release).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Also, I think if the prop is controlling the same attribute, we can keep the same name, as you suggest at the end of your last comment. But when it would refer to two unrelated things, like type would for this component, let's avoid it.

@@ -14,3 +15,9 @@ export const CenteredStory = styled.div`
justify-content: center;
width: 100%;
`;

export const Rule = styled.hr`
border: 1px solid ${tokens.border.color};
Copy link
Contributor

Choose a reason for hiding this comment

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

FYI: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/hr

The HTML <hr> element represents a thematic break between paragraph-level elements: for example, a change of scene in a story, or a shift of topic within a section.
Historically, this has been presented as a horizontal rule or line. While it may still be displayed as a horizontal rule in visual browsers, this element is now defined in semantic terms, rather than presentational terms, so if you wish to draw a horizontal line, you should do so using appropriate CSS.

I think this means that if we just want a horizontal line, we should not be using <hr> (even though it is indeed separating<p>s in the stories. Those paragraphs are not really textual, so there can't be a "a change of scene in a story, or a shift of topic within a section". Yes, I know it's just for stories.)


const sizes = ["xsmall", "small", "medium", "large", "xlarge"];

export const shirtSize = (props, propName, component) => {
Copy link
Contributor

Choose a reason for hiding this comment

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

I'd say if we do use some form of enum, its ergonomics should be good enough that we don't have to list all the possible options explicitly inside oneOf(...)

@@ -121,7 +121,7 @@ import TokenSquare from "./TokenSquare"
const matches = str.match(/\$[\w'-]+/gi);
if (matches) {
matches.forEach(match => {
const re = new RegExp(match.replace("$", "\\$"), "gi");
const re = new RegExp(match.replace("$", "\\$"), "i");
Copy link
Contributor

Choose a reason for hiding this comment

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

Why not g?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

"prop-types": "^15.7.2"
},
"peerDependencies": {
"react": "^16.4.0",
Copy link
Contributor

Choose a reason for hiding this comment

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

16.8?

Copy link
Contributor

Choose a reason for hiding this comment

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

16.8.2

Copy link
Contributor Author

Choose a reason for hiding this comment

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


const propTypes = {
ariaText: PropTypes.string,
buttonRef: PropTypes.shape({ current: PropTypes.instanceOf(Element) }),
Copy link
Contributor

Choose a reason for hiding this comment

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

}
`;

const commonPostStyles = `
Copy link
Contributor

Choose a reason for hiding this comment

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

What does post mean here?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

They needed to come later, to override some styles above it... but it seems to no longer be necessary.


// Modifiers

const fullWidthStyles = `
Copy link
Contributor

Choose a reason for hiding this comment

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

Hmm it feels a bit weird to have a dedicated full width variant. Can we leave it up to the consumer to style?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

In general, fewer responsibilities for the consumer, the better. Especially CSS responsibilities... that way the rules of the design system can be enforced. And it makes the experience simpler for the app developer (in theory).

Copy link
Contributor

Choose a reason for hiding this comment

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

But we don't know what "full width" means in the consumer's context. Sometimes it's width: 100%, other times it's flex-grow: 1, and maybe even other times it could be display: block? I think this is starting to be layout territory where IMO we should give flexibility. I've definitely heard advice like making sure there's a way to use a component both inline and as a block (if it makes sense), I think it's a similar concern.

`;

export const ButtonStyled = styled.button`
${props => composedStyles(props)}
Copy link
Contributor

Choose a reason for hiding this comment

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

Normally I would prefer inlining but in styled components I guess it's more natural to see the props argument?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Nope, this is a good suggestion, and I like it better as simply ${composedStyles}.

{
"name": "@paprika/button",
"version": "0.0.1",
"description": "Button component",
Copy link
Contributor

Choose a reason for hiding this comment

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

This will be the package description published to npm. We should probably start using something more meaningful.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good point, but how descriptive do we need to be for a button? "Skeuomorphic, accessible button component" ?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We intend to address this for all of our components before a 1.0.0 release and hopefully we can recruit a proper wordsmith the help with it.

}

&:active {
box-shadow: ${tokens.highlight.active.noBorder.boxShadow}, inset 0 1px 1px 0 rgba(0,0,0, 0.1),
Copy link
Contributor

Choose a reason for hiding this comment

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

rgba(0,0,0, 0.1) is this irregular spacing on purpose?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

No, I'm surprised it wasn't fixed by prettier, but despite the fact I think it's actually easier to read, I don't think it should be irregular, as you put it. Will change.

@mikrotron mikrotron removed the WIP label Mar 7, 2019
@mikrotron mikrotron force-pushed the UX-436/migrate-button branch from fc687b0 to ab031b6 Compare March 14, 2019 23:01
Copy link
Contributor

@nahumzs nahumzs left a comment

Choose a reason for hiding this comment

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

Let's 🚢 this one, and address any other comment in another PR.

@mikrotron mikrotron merged commit 418d4d8 into master Mar 14, 2019
@mikrotron mikrotron deleted the UX-436/migrate-button branch March 14, 2019 23:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
📦 New Package A new shiny Component
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Button Component
4 participants