Skip to content

Commit

Permalink
feat(Button): convert functional Button to class
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
MichaelRoytman committed Nov 16, 2017
1 parent 82a2129 commit 9690fda
Show file tree
Hide file tree
Showing 5 changed files with 156 additions and 36 deletions.
12 changes: 12 additions & 0 deletions .storybook/__snapshots__/Storyshots.test.js.snap
Original file line number Diff line number Diff line change
@@ -1,5 +1,17 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`Storyshots Button basic usage 1`] = `
<button
className="btn"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[Function]}
type="button"
>
Click me and check the console!
</button>
`;

exports[`Storyshots CheckBox basic usage 1`] = `
<div
className="form-group"
Expand Down
15 changes: 15 additions & 0 deletions src/Button/Button.stories.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/* eslint-disable import/no-extraneous-dependencies */
/* eslint-disable no-console */
import React from 'react';
import { storiesOf } from '@storybook/react';
import { action } from '@storybook/addon-actions';

import Button from './index';

storiesOf('Button', module)
.add('basic usage', () => (
<Button
label="Click me and check the console!"
onClick={action('button-click')}
/>
));
39 changes: 39 additions & 0 deletions src/Button/Button.test.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/* eslint-disable import/no-extraneous-dependencies */
import React from 'react';
import { mount } from 'enzyme';
import Button from './index';

const defaultProps = {
label: 'Click me!',
};

describe('<Button />', () => {
let wrapper;
let button;

beforeEach(() => {
wrapper = mount(
<Button
{...defaultProps}
/>,
);

button = wrapper.find('button');
});
it('renders', () => {
expect(button).toHaveLength(1);
});
it('puts focus on button on click', () => {
expect(button.matchesElement(document.activeElement)).toEqual(false);
button.simulate('click');
expect(button.at(0).matchesElement(document.activeElement)).toEqual(true);
});
it('calls onClick prop on click', () => {
const onClickSpy = jest.fn();
wrapper.setProps({ onClick: onClickSpy });

expect(onClickSpy).toHaveBeenCalledTimes(0);
button.simulate('click');
expect(onClickSpy).toHaveBeenCalledTimes(1);
});
});
111 changes: 77 additions & 34 deletions src/Button/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,40 +4,83 @@ import PropTypes from 'prop-types';

import styles from './Button.scss';

function Button(props) {
const {
buttonType,
className,
label,
inputRef,
isClose,
onBlur,
onClick,
onKeyDown,
type,
...other
} = props;
class Button extends React.Component {
constructor(props) {
super(props);

return (
<button
className={classNames([
...className,
styles.btn,
], {
[styles[`btn-${buttonType}`]]: buttonType !== undefined,
}, {
[styles.close]: isClose,
})}
onBlur={onBlur}
onClick={onClick}
onKeyDown={onKeyDown}
type={type}
ref={inputRef}
{...other}
>
{label}
</button>
);
const {
onBlur,
onKeyDown,
} = props;

this.onBlur = onBlur.bind(this);
this.onKeyDown = onKeyDown.bind(this);
this.onClick = this.onClick.bind(this);
this.setRefs = this.setRefs.bind(this);
}

/*
The button component is given focus explicitly in its onClick to account
for the fact that an HTML <button> element in Firefox and Safari does not get
focus on onClick.
See https://developer.mozilla.org/en-US/docs/Web/HTML/Element/button.
*/
onClick(e) {
this.buttonRef.focus();
this.props.onClick(e);
}

/*
The button component needs a ref to itself to be able to force
focus in its onClick function (buttonRef). It also needs to accept
a callback function from parent components to give those parents
a reference to their child button (e.g. for the modal component).
Therefore, both have been wrapped in a function bound on the class,
since one cannot set two ref attributes on a component.
*/
setRefs(input) {
this.buttonRef = input;
this.props.inputRef(input);
}

render() {
const {
buttonType,
className,
label,
isClose,
type,
/* inputRef is not used directly in the render, but it needs to be assigned
here to prevent it from being passed to the HTML button component as part
of other.
*/
inputRef,
...other
} = this.props;

return (
<button
{...other}
className={classNames([
...className,
styles.btn,
], {
[styles[`btn-${buttonType}`]]: buttonType !== undefined,
}, {
[styles.close]: isClose,
})}
onBlur={this.onBlur}
onClick={this.onClick}
onKeyDown={this.onKeyDown}
type={type}
ref={this.setRefs}

>
{this.props.label}
</button>
);
}
}

export const buttonPropTypes = {
Expand All @@ -60,8 +103,8 @@ Button.defaultProps = {
inputRef: () => {},
isClose: false,
onBlur: () => {},
onClick: () => {},
onKeyDown: () => {},
onClick: () => {},
type: 'button',
};

Expand Down
15 changes: 13 additions & 2 deletions src/Modal/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ class Modal extends React.Component {

this.close = this.close.bind(this);
this.handleKeyDown = this.handleKeyDown.bind(this);
this.setXButton = this.setXButton.bind(this);
this.setCloseButton = this.setCloseButton.bind(this);

this.headerId = newId();

this.state = {
Expand All @@ -37,6 +40,14 @@ class Modal extends React.Component {
}
}

setXButton(input) {
this.xButton = input;
}

setCloseButton(input) {
this.closeButton = input;
}

close() {
this.setState({ open: false });
this.props.onClose();
Expand Down Expand Up @@ -100,7 +111,7 @@ class Modal extends React.Component {
aria-label={this.props.closeText}
buttonType="light"
onClick={this.close}
inputRef={(input) => { this.xButton = input; }}
inputRef={this.setXButton}
onKeyDown={this.handleKeyDown}
/>
</div>
Expand All @@ -113,7 +124,7 @@ class Modal extends React.Component {
label={this.props.closeText}
buttonType="secondary"
onClick={this.close}
inputRef={(input) => { this.closeButton = input; }}
inputRef={this.setCloseButton}
onKeyDown={this.handleKeyDown}
/>
</div>
Expand Down

0 comments on commit 9690fda

Please sign in to comment.