Skip to content

Commit 9690fda

Browse files
feat(Button): convert functional Button to class
* convert functional Button component to class to handle focus on click * add Button tests * add Button story to storybook * replace inline render functions from Modal with bound class functions
1 parent 82a2129 commit 9690fda

File tree

5 files changed

+156
-36
lines changed

5 files changed

+156
-36
lines changed

.storybook/__snapshots__/Storyshots.test.js.snap

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,17 @@
11
// Jest Snapshot v1, https://goo.gl/fbAQLP
22

3+
exports[`Storyshots Button basic usage 1`] = `
4+
<button
5+
className="btn"
6+
onBlur={[Function]}
7+
onClick={[Function]}
8+
onKeyDown={[Function]}
9+
type="button"
10+
>
11+
Click me and check the console!
12+
</button>
13+
`;
14+
315
exports[`Storyshots CheckBox basic usage 1`] = `
416
<div
517
className="form-group"

src/Button/Button.stories.jsx

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
/* eslint-disable import/no-extraneous-dependencies */
2+
/* eslint-disable no-console */
3+
import React from 'react';
4+
import { storiesOf } from '@storybook/react';
5+
import { action } from '@storybook/addon-actions';
6+
7+
import Button from './index';
8+
9+
storiesOf('Button', module)
10+
.add('basic usage', () => (
11+
<Button
12+
label="Click me and check the console!"
13+
onClick={action('button-click')}
14+
/>
15+
));

src/Button/Button.test.jsx

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/* eslint-disable import/no-extraneous-dependencies */
2+
import React from 'react';
3+
import { mount } from 'enzyme';
4+
import Button from './index';
5+
6+
const defaultProps = {
7+
label: 'Click me!',
8+
};
9+
10+
describe('<Button />', () => {
11+
let wrapper;
12+
let button;
13+
14+
beforeEach(() => {
15+
wrapper = mount(
16+
<Button
17+
{...defaultProps}
18+
/>,
19+
);
20+
21+
button = wrapper.find('button');
22+
});
23+
it('renders', () => {
24+
expect(button).toHaveLength(1);
25+
});
26+
it('puts focus on button on click', () => {
27+
expect(button.matchesElement(document.activeElement)).toEqual(false);
28+
button.simulate('click');
29+
expect(button.at(0).matchesElement(document.activeElement)).toEqual(true);
30+
});
31+
it('calls onClick prop on click', () => {
32+
const onClickSpy = jest.fn();
33+
wrapper.setProps({ onClick: onClickSpy });
34+
35+
expect(onClickSpy).toHaveBeenCalledTimes(0);
36+
button.simulate('click');
37+
expect(onClickSpy).toHaveBeenCalledTimes(1);
38+
});
39+
});

src/Button/index.jsx

Lines changed: 77 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -4,40 +4,83 @@ import PropTypes from 'prop-types';
44

55
import styles from './Button.scss';
66

7-
function Button(props) {
8-
const {
9-
buttonType,
10-
className,
11-
label,
12-
inputRef,
13-
isClose,
14-
onBlur,
15-
onClick,
16-
onKeyDown,
17-
type,
18-
...other
19-
} = props;
7+
class Button extends React.Component {
8+
constructor(props) {
9+
super(props);
2010

21-
return (
22-
<button
23-
className={classNames([
24-
...className,
25-
styles.btn,
26-
], {
27-
[styles[`btn-${buttonType}`]]: buttonType !== undefined,
28-
}, {
29-
[styles.close]: isClose,
30-
})}
31-
onBlur={onBlur}
32-
onClick={onClick}
33-
onKeyDown={onKeyDown}
34-
type={type}
35-
ref={inputRef}
36-
{...other}
37-
>
38-
{label}
39-
</button>
40-
);
11+
const {
12+
onBlur,
13+
onKeyDown,
14+
} = props;
15+
16+
this.onBlur = onBlur.bind(this);
17+
this.onKeyDown = onKeyDown.bind(this);
18+
this.onClick = this.onClick.bind(this);
19+
this.setRefs = this.setRefs.bind(this);
20+
}
21+
22+
/*
23+
The button component is given focus explicitly in its onClick to account
24+
for the fact that an HTML <button> element in Firefox and Safari does not get
25+
focus on onClick.
26+
27+
See https://developer.mozilla.org/en-US/docs/Web/HTML/Element/button.
28+
*/
29+
onClick(e) {
30+
this.buttonRef.focus();
31+
this.props.onClick(e);
32+
}
33+
34+
/*
35+
The button component needs a ref to itself to be able to force
36+
focus in its onClick function (buttonRef). It also needs to accept
37+
a callback function from parent components to give those parents
38+
a reference to their child button (e.g. for the modal component).
39+
Therefore, both have been wrapped in a function bound on the class,
40+
since one cannot set two ref attributes on a component.
41+
*/
42+
setRefs(input) {
43+
this.buttonRef = input;
44+
this.props.inputRef(input);
45+
}
46+
47+
render() {
48+
const {
49+
buttonType,
50+
className,
51+
label,
52+
isClose,
53+
type,
54+
/* inputRef is not used directly in the render, but it needs to be assigned
55+
here to prevent it from being passed to the HTML button component as part
56+
of other.
57+
*/
58+
inputRef,
59+
...other
60+
} = this.props;
61+
62+
return (
63+
<button
64+
{...other}
65+
className={classNames([
66+
...className,
67+
styles.btn,
68+
], {
69+
[styles[`btn-${buttonType}`]]: buttonType !== undefined,
70+
}, {
71+
[styles.close]: isClose,
72+
})}
73+
onBlur={this.onBlur}
74+
onClick={this.onClick}
75+
onKeyDown={this.onKeyDown}
76+
type={type}
77+
ref={this.setRefs}
78+
79+
>
80+
{this.props.label}
81+
</button>
82+
);
83+
}
4184
}
4285

4386
export const buttonPropTypes = {
@@ -60,8 +103,8 @@ Button.defaultProps = {
60103
inputRef: () => {},
61104
isClose: false,
62105
onBlur: () => {},
63-
onClick: () => {},
64106
onKeyDown: () => {},
107+
onClick: () => {},
65108
type: 'button',
66109
};
67110

src/Modal/index.jsx

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ class Modal extends React.Component {
1212

1313
this.close = this.close.bind(this);
1414
this.handleKeyDown = this.handleKeyDown.bind(this);
15+
this.setXButton = this.setXButton.bind(this);
16+
this.setCloseButton = this.setCloseButton.bind(this);
17+
1518
this.headerId = newId();
1619

1720
this.state = {
@@ -37,6 +40,14 @@ class Modal extends React.Component {
3740
}
3841
}
3942

43+
setXButton(input) {
44+
this.xButton = input;
45+
}
46+
47+
setCloseButton(input) {
48+
this.closeButton = input;
49+
}
50+
4051
close() {
4152
this.setState({ open: false });
4253
this.props.onClose();
@@ -100,7 +111,7 @@ class Modal extends React.Component {
100111
aria-label={this.props.closeText}
101112
buttonType="light"
102113
onClick={this.close}
103-
inputRef={(input) => { this.xButton = input; }}
114+
inputRef={this.setXButton}
104115
onKeyDown={this.handleKeyDown}
105116
/>
106117
</div>
@@ -113,7 +124,7 @@ class Modal extends React.Component {
113124
label={this.props.closeText}
114125
buttonType="secondary"
115126
onClick={this.close}
116-
inputRef={(input) => { this.closeButton = input; }}
127+
inputRef={this.setCloseButton}
117128
onKeyDown={this.handleKeyDown}
118129
/>
119130
</div>

0 commit comments

Comments
 (0)