Skip to content

Commit 6e00d84

Browse files
montezumekodiakhq[bot]
authored andcommitted
feat(primary-button): support as (#1170)
1 parent e88dce5 commit 6e00d84

File tree

5 files changed

+246
-85
lines changed

5 files changed

+246
-85
lines changed

src/components/buttons/primary-button/README.md

+15-14
Original file line numberDiff line numberDiff line change
@@ -23,20 +23,21 @@ label for accessibility reasons.
2323

2424
#### Properties
2525

26-
| Props | Type | Required | Values | Default | Description |
27-
| ------------------ | -------- | :------: | --------------------------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
28-
| `type` | `oneOf` | - | `submit`, `reset`, `button` | `button` | Used as the HTML `type` attribute. |
29-
| `label` | `string` || - | - | Should describe what the button does, for accessibility purposes (screen-reader users) |
30-
| `buttonAttributes` | `object` | - | - | - | Allows setting custom attributes on the underlying button html element |
31-
| `iconLeft` | `node` || - | - | The left icon displayed within the button |
32-
| `isToggleButton` | `bool` || - | `false` | If this is active, it means the button will persist in an "active" state when toggled (see `isToggled`), and back to normal state when untoggled |
33-
| `isToggled` | `bool` | - | - | - | Tells when the button should present a toggled state. It does not have any effect when `isToggleButton` is false |
34-
| `isDisabled` | `bool` | - | - | - | Tells when the button should present a disabled state |
35-
| `onClick` | `func` || - | - | What the button will trigger when clicked |
36-
| `size` | `oneOf` | - | `big`, `small` | `big` | - |
37-
| `tone` | `oneOf` | - | `urgent`, `primary` | `primary` | The component may have a theme only if `isToggleButton` is true |
38-
39-
The component further forwards all `data-` and `aria-` attributes to the underlying `button` component.
26+
| Props | Type | Required | Values | Default | Description |
27+
| ------------------ | --------------------- | :------: | --------------------------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
28+
| `type` | `oneOf` | - | `submit`, `reset`, `button` | `button` | Used as the HTML `type` attribute. |
29+
| `label` | `string` || - | - | Should describe what the button does, for accessibility purposes (screen-reader users) |
30+
| `buttonAttributes` | `object` | - | - | - | Allows setting custom attributes on the underlying button html element |
31+
| `iconLeft` | `node` || - | - | The left icon displayed within the button |
32+
| `isToggleButton` | `bool` || - | `false` | If this is active, it means the button will persist in an "active" state when toggled (see `isToggled`), and back to normal state when untoggled |
33+
| `isToggled` | `bool` | - | - | - | Tells when the button should present a toggled state. It does not have any effect when `isToggleButton` is false |
34+
| `isDisabled` | `bool` | - | - | - | Tells when the button should present a disabled state |
35+
| `onClick` | `func` || - | - | What the button will trigger when clicked |
36+
| `size` | `oneOf` | - | `big`, `small` | `big` | - |
37+
| `tone` | `oneOf` | - | `urgent`, `primary` | `primary` | The component may have a theme only if `isToggleButton` is true |
38+
| `as` | `string` or `element` | - | - | - | You may pass in a string like "a" to have the button render as an anchor tag instead. Or you could pass in a React Component, like a `Link`. |
39+
40+
The component further forwards all valid HTML attributes to the underlying `button` component.
4041

4142
Main Functions and use cases are:
4243

src/components/buttons/primary-button/primary-button.js

+40-39
Original file line numberDiff line numberDiff line change
@@ -1,61 +1,62 @@
11
import PropTypes from 'prop-types';
22
import React from 'react';
33
import isNil from 'lodash/isNil';
4+
import omit from 'lodash/omit';
45
import { css } from '@emotion/core';
56
import vars from '../../../../materials/custom-properties';
6-
import filterAriaAttributes from '../../../utils/filter-aria-attributes';
7-
import filterDataAttributes from '../../../utils/filter-data-attributes';
7+
import filterInvalidAttributes from '../../../utils/filter-invalid-attributes';
88
import Spacings from '../../spacings';
99
import AccessibleButton from '../accessible-button';
10-
import {
11-
getButtonLayoutStyles,
12-
getButtonStyles,
13-
} from './primary-button.styles';
10+
import { getButtonStyles } from './primary-button.styles';
11+
12+
const propsToOmit = ['type'];
1413

1514
const PrimaryButton = props => {
1615
const dataProps = {
1716
'data-track-component': 'PrimaryButton',
18-
...filterAriaAttributes(props),
19-
...filterDataAttributes(props),
17+
...filterInvalidAttributes(omit(props, propsToOmit)),
18+
// if there is a divergence between `isDisabled` and `disabled`,
19+
// we fall back to `isDisabled`
20+
disabled: props.isDisabled,
2021
};
2122

2223
const isActive = props.isToggleButton && props.isToggled;
2324
return (
24-
<div css={getButtonLayoutStyles(props.size)}>
25-
<AccessibleButton
26-
type={props.type}
27-
buttonAttributes={dataProps}
28-
label={props.label}
29-
onClick={props.onClick}
30-
isToggleButton={props.isToggleButton}
31-
isToggled={props.isToggled}
32-
isDisabled={props.isDisabled}
33-
css={getButtonStyles(props.isDisabled, isActive, props.tone)}
34-
>
35-
<Spacings.Inline alignItems="center" scale="xs">
36-
{Boolean(props.iconLeft) && (
37-
<span
38-
css={css`
39-
margin: 0 ${vars.spacingXs} 0 0;
40-
display: flex;
41-
align-items: center;
42-
justify-content: center;
43-
`}
44-
>
45-
{React.cloneElement(props.iconLeft, {
46-
color: props.isDisabled ? 'neutral60' : 'surface',
47-
size: props.size === 'small' ? 'medium' : 'big',
48-
})}
49-
</span>
50-
)}
51-
<span>{props.label}</span>
52-
</Spacings.Inline>
53-
</AccessibleButton>
54-
</div>
25+
<AccessibleButton
26+
as={props.as}
27+
type={props.type}
28+
buttonAttributes={dataProps}
29+
label={props.label}
30+
onClick={props.onClick}
31+
isToggleButton={props.isToggleButton}
32+
isToggled={props.isToggled}
33+
isDisabled={props.isDisabled}
34+
css={getButtonStyles(props.isDisabled, isActive, props.tone, props.size)}
35+
>
36+
<Spacings.Inline alignItems="center" scale="xs">
37+
{Boolean(props.iconLeft) && (
38+
<span
39+
css={css`
40+
margin: 0 ${vars.spacingXs} 0 0;
41+
display: flex;
42+
align-items: center;
43+
justify-content: center;
44+
`}
45+
>
46+
{React.cloneElement(props.iconLeft, {
47+
color: props.isDisabled ? 'neutral60' : 'surface',
48+
size: props.size === 'small' ? 'medium' : 'big',
49+
})}
50+
</span>
51+
)}
52+
<span>{props.label}</span>
53+
</Spacings.Inline>
54+
</AccessibleButton>
5555
);
5656
};
5757

5858
PrimaryButton.propTypes = {
59+
as: PropTypes.oneOfType([PropTypes.string, PropTypes.elementType]),
5960
type: PropTypes.oneOf(['submit', 'reset', 'button']),
6061
label: PropTypes.string.isRequired,
6162
buttonAttributes: PropTypes.object,

src/components/buttons/primary-button/primary-button.spec.js

+34
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import React from 'react';
2+
import { Link } from 'react-router-dom';
23
import { render } from '../../../test-utils';
34
import { PlusBoldIcon } from '../../icons';
45
import PrimaryButton from './primary-button';
@@ -70,4 +71,37 @@ describe('rendering', () => {
7071
expect(getByLabelText('Add')).toHaveAttribute('type', 'reset');
7172
});
7273
});
74+
describe('when used with `as`', () => {
75+
describe('when as is a valid HTML element', () => {
76+
it('should render as that HTML element', () => {
77+
const { getByLabelText } = render(
78+
<PrimaryButton
79+
{...props}
80+
as="a"
81+
href="https://www.kanyetothe.com"
82+
target="_BLANK"
83+
/>
84+
);
85+
const linkButton = getByLabelText('Add');
86+
expect(linkButton).toHaveAttribute(
87+
'href',
88+
'https://www.kanyetothe.com'
89+
);
90+
expect(linkButton).not.toHaveAttribute('type', 'button');
91+
expect(linkButton).toHaveAttribute('target', '_BLANK');
92+
});
93+
});
94+
describe('when as is a React component', () => {
95+
it('should render as that component', () => {
96+
const { getByLabelText } = render(
97+
<PrimaryButton {...props} as={Link} to="foo/bar" target="_BLANK" />
98+
);
99+
100+
const linkButton = getByLabelText('Add');
101+
expect(linkButton).toHaveAttribute('href', '/foo/bar');
102+
expect(linkButton).toHaveAttribute('target', '_BLANK');
103+
expect(linkButton).not.toHaveAttribute('type', 'button');
104+
});
105+
});
106+
});
73107
});

src/components/buttons/primary-button/primary-button.styles.js

+20-32
Original file line numberDiff line numberDiff line change
@@ -1,47 +1,35 @@
1+
/* eslint-disable import/prefer-default-export */
12
import { css } from '@emotion/core';
23
import vars from '../../../../materials/custom-properties';
34

4-
const getButtonLayoutStyles = size => {
5-
const baseLayoutStyles = css`
6-
display: inline-flex;
7-
align-items: center;
8-
color: ${vars.colorSurface};
9-
transition: background-color ${vars.transitionLinear80Ms};
10-
`;
5+
const getSizeStyles = size => {
116
switch (size) {
127
case 'small':
13-
return [
14-
baseLayoutStyles,
15-
css`
16-
border-radius: ${vars.borderRadius4};
17-
> button {
18-
padding: 0 ${vars.spacingS} 0 ${vars.spacingS};
19-
height: ${vars.smallButtonHeight};
20-
border-radius: ${vars.borderRadius4};
21-
}
22-
`,
23-
];
8+
return css`
9+
border-radius: ${vars.borderRadius4};
10+
padding: 0 ${vars.spacingS} 0 ${vars.spacingS};
11+
height: ${vars.smallButtonHeight};
12+
`;
13+
2414
case 'big':
25-
return [
26-
baseLayoutStyles,
27-
css`
28-
border-radius: ${vars.borderRadius6};
29-
> button {
30-
padding: 0 ${vars.spacingM} 0 ${vars.spacingM};
31-
height: ${vars.bigButtonHeight};
32-
border-radius: ${vars.borderRadius6};
33-
}
34-
`,
35-
];
15+
return css`
16+
padding: 0 ${vars.spacingM} 0 ${vars.spacingM};
17+
height: ${vars.bigButtonHeight};
18+
border-radius: ${vars.borderRadius6};
19+
`;
20+
3621
default:
3722
return css``;
3823
}
3924
};
40-
const getButtonStyles = (isDisabled, isActive, tone) => {
25+
26+
const getButtonStyles = (isDisabled, isActive, tone, size) => {
4127
const baseStyles = css`
42-
display: flex;
4328
align-items: center;
29+
color: ${vars.colorSurface};
30+
transition: background-color ${vars.transitionLinear80Ms};
4431
font-size: ${vars.fontSizeDefault};
32+
${getSizeStyles(size)}
4533
`;
4634
// "disabled" takes precendece over "active"
4735
if (isDisabled) {
@@ -145,4 +133,4 @@ const getButtonStyles = (isDisabled, isActive, tone) => {
145133
}
146134
};
147135

148-
export { getButtonLayoutStyles, getButtonStyles };
136+
export { getButtonStyles };

src/components/buttons/primary-button/primary-button.visualroute.js

+137
Original file line numberDiff line numberDiff line change
@@ -98,5 +98,142 @@ export const component = () => (
9898
isDisabled={true}
9999
/>
100100
</Spec>
101+
<Spec label="with `as` as Link - regular">
102+
<PrimaryButton
103+
as="a"
104+
href="https://kanyetothe.com"
105+
label="A label text"
106+
onClick={() => {}}
107+
/>
108+
</Spec>
109+
110+
<Spec label="with `as` as Link - disabled">
111+
<PrimaryButton
112+
as="a"
113+
href="https://kanyetothe.com"
114+
label="A label text"
115+
onClick={() => {}}
116+
isDisabled={true}
117+
/>
118+
</Spec>
119+
120+
<Spec label="with `as` as Link - with icon left (default)">
121+
<PrimaryButton
122+
as="a"
123+
href="https://kanyetothe.com"
124+
label="A label text"
125+
onClick={() => {}}
126+
iconLeft={<InformationIcon />}
127+
/>
128+
</Spec>
129+
130+
<Spec label="with `as` as Link - as toggle button - when not toggled">
131+
<PrimaryButton
132+
as="a"
133+
href="https://kanyetothe.com"
134+
label="A label text"
135+
onClick={() => {}}
136+
isToggleButton={true}
137+
/>
138+
</Spec>
139+
140+
<Spec label="with `as` as Link - as toggle button - when toggled">
141+
<PrimaryButton
142+
as="a"
143+
href="https://kanyetothe.com"
144+
label="A label text"
145+
onClick={() => {}}
146+
isToggleButton={true}
147+
isToggled={true}
148+
/>
149+
</Spec>
150+
151+
<Spec label='size - when "big"'>
152+
<PrimaryButton
153+
as="a"
154+
href="https://kanyetothe.com"
155+
label="A label text"
156+
onClick={() => {}}
157+
size="big"
158+
/>
159+
</Spec>
160+
161+
<Spec label='size - when "small"'>
162+
<PrimaryButton
163+
as="a"
164+
href="https://kanyetothe.com"
165+
label="A label text"
166+
onClick={() => {}}
167+
size="small"
168+
/>
169+
</Spec>
170+
171+
<Spec label='tone - when "urgent"'>
172+
<PrimaryButton
173+
as="a"
174+
href="https://kanyetothe.com"
175+
label="A label text"
176+
onClick={() => {}}
177+
tone="urgent"
178+
/>
179+
</Spec>
180+
181+
<Spec label='tone - when "primary"'>
182+
<PrimaryButton
183+
as="a"
184+
href="https://kanyetothe.com"
185+
label="A label text"
186+
onClick={() => {}}
187+
tone="primary"
188+
/>
189+
</Spec>
190+
191+
<Spec label="with `as` as Link - as toggle button - when toggled and disabled">
192+
<PrimaryButton
193+
as="a"
194+
href="https://kanyetothe.com"
195+
label="A label text"
196+
onClick={() => {}}
197+
isToggleButton={true}
198+
isToggled={true}
199+
isDisabled={true}
200+
/>
201+
</Spec>
202+
203+
<Spec label="with `as` as Link - as toggle button (urgent tone) - when not toggled">
204+
<PrimaryButton
205+
as="a"
206+
href="https://kanyetothe.com"
207+
label="A label text"
208+
onClick={() => {}}
209+
tone="urgent"
210+
isToggleButton={true}
211+
/>
212+
</Spec>
213+
214+
<Spec label="with `as` as Link - as toggle button (urgent tone) - when toggled">
215+
<PrimaryButton
216+
as="a"
217+
href="https://kanyetothe.com"
218+
label="A label text"
219+
onClick={() => {}}
220+
tone="urgent"
221+
isToggleButton={true}
222+
isToggled={true}
223+
/>
224+
</Spec>
225+
226+
<Spec label="with `as` as Link - as toggle button (urgent tone) - when toggled and disabled">
227+
<PrimaryButton
228+
as="a"
229+
href="https://kanyetothe.com"
230+
label="A label text"
231+
onClick={() => {}}
232+
tone="urgent"
233+
isToggleButton={true}
234+
isToggled={true}
235+
isDisabled={true}
236+
/>
237+
</Spec>
101238
</Suite>
102239
);

0 commit comments

Comments
 (0)