-
Notifications
You must be signed in to change notification settings - Fork 9
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
Conversation
@@ -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}; |
There was a problem hiding this comment.
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};
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 defineborder-bottom
👍
There was a problem hiding this comment.
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.)
There was a problem hiding this comment.
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.
packages/Button/Button.styles.js
Outdated
${props.isFullWidth ? fullWidthStyles : ""} | ||
${props.isActive ? activeStyles : ""} | ||
${props.isDisabled ? disabledStyles : ""} | ||
${["minor", "link"].includes(props.type) && props.isDisabled ? disabledTextStyles : ""} |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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
?
There was a problem hiding this comment.
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
packages/helpers/customPropTypes.js
Outdated
|
||
const sizes = ["xsmall", "small", "medium", "large", "xlarge"]; | ||
|
||
export const shirtSize = (props, propName, component) => { |
There was a problem hiding this comment.
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);
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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(...)
There was a problem hiding this comment.
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?
packages/Button/Button.js
Outdated
} | ||
|
||
setRef(props) { | ||
this.$button = props.buttonRef || React.createRef(); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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>;
});
There was a problem hiding this comment.
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} />
}
There was a problem hiding this comment.
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
packages/Button/Button.js
Outdated
|
||
const ButtonTag = isSemantic ? ButtonStyled : RawButtonStyled; | ||
|
||
return <ButtonTag {...buttonProps}>{children}</ButtonTag>; |
There was a problem hiding this comment.
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
.
There was a problem hiding this comment.
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
).
packages/Button/Button.js
Outdated
|
||
render() { | ||
const { ariaText, buttonRef, canPropagate, children, isDisabled, isSemantic, tabIndex, ...moreProps } = this.props; | ||
if (ariaText) moreProps["aria-label"] = ariaText; |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
packages/Button/Button.js
Outdated
buttonRef: PropTypes.shape({ current: PropTypes.instanceOf(Element) }), | ||
canPropagate: PropTypes.bool, | ||
children: PropTypes.node.isRequired, | ||
className: PropTypes.string, |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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> | ||
{[ |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
packages/Button/Button.js
Outdated
...moreProps, | ||
}; | ||
|
||
if (isSemantic) { |
There was a problem hiding this comment.
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()} />;
}
There was a problem hiding this comment.
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:
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).- The
tabIndex
logic will be duplicated in a "raw"<Button>
, which may not be a big deal, it's just a bit unnecessary.
There was a problem hiding this comment.
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.
packages/Button/Button.js
Outdated
if (ariaText) moreProps["aria-label"] = ariaText; | ||
|
||
const buttonProps = { | ||
isDisabled, |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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...
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
| ...)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
^ yeah that makes sense.
There was a problem hiding this comment.
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:
- I don't believe in
<button type='reset'>
so really the only options aresubmit
|button
. Most of the time we wantbutton
. So we could utilize a boolean prop, likeisSubmit
which is potentially a bit simpler. - 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, sincetype
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, likekind
. Ortheme
? 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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
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}; |
There was a problem hiding this comment.
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.)
packages/helpers/customPropTypes.js
Outdated
|
||
const sizes = ["xsmall", "small", "medium", "large", "xlarge"]; | ||
|
||
export const shirtSize = (props, propName, component) => { |
There was a problem hiding this comment.
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"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why not g
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
packages/Button/package.json
Outdated
"prop-types": "^15.7.2" | ||
}, | ||
"peerDependencies": { | ||
"react": "^16.4.0", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
16.8?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
16.8.2
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not 16.8.4
?
https://www.npmjs.com/package/react
packages/Button/Button.js
Outdated
|
||
const propTypes = { | ||
ariaText: PropTypes.string, | ||
buttonRef: PropTypes.shape({ current: PropTypes.instanceOf(Element) }), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
N.B.: facebook/prop-types#240
packages/Button/Button.styles.js
Outdated
} | ||
`; | ||
|
||
const commonPostStyles = ` |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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 = ` |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
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.
packages/Button/Button.styles.js
Outdated
`; | ||
|
||
export const ButtonStyled = styled.button` | ||
${props => composedStyles(props)} |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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", |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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" ?
There was a problem hiding this comment.
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.
packages/Button/Button.styles.js
Outdated
} | ||
|
||
&:active { | ||
box-shadow: ${tokens.highlight.active.noBorder.boxShadow}, inset 0 1px 1px 0 rgba(0,0,0, 0.1), |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
fc687b0
to
ab031b6
Compare
There was a problem hiding this 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.
🛠 Purpose
Add a
<Button>
component that can be rendered as either a<button>
or global CSS resilient<RawButton>
.✏️ Notes to Reviewer
<Icon>
component has not yet been added.Tokens
package with latest design tokens / script fix.ShirtSizes
enum.🖥 Screenshots