From 95a70ee92c4b145c9a146dbc275947b603b9463f Mon Sep 17 00:00:00 2001 From: Penple03 Date: Wed, 22 Aug 2018 18:37:13 -0700 Subject: [PATCH 1/3] Checkboxes + Radio Buttons + Tests --- package.json | 1 + react/Checkbox.jsx | 68 +++++++++++++++++++++++++++++++ react/Radio.jsx | 65 ++++++++++++++++++++++++++++++ sass/_library.scss | 2 + sass/inc/_animation.scss | 3 +- sass/inc/_checkbox.scss | 75 +++++++++++++++++++++++++++++++++++ sass/inc/_radio.scss | 38 ++++++++++++++++++ test/Checkbox.test.js | 71 +++++++++++++++++++++++++++++++++ test/Radio.test.js | 70 ++++++++++++++++++++++++++++++++ todo/App.jsx | 6 ++- todo/Section/CheckboxDemo.jsx | 29 ++++++++++++++ todo/Section/RadioDemo.jsx | 30 ++++++++++++++ yarn.lock | 72 ++++++++++++++++++++++++++++++++- 13 files changed, 527 insertions(+), 3 deletions(-) create mode 100644 react/Checkbox.jsx create mode 100644 react/Radio.jsx create mode 100644 sass/inc/_checkbox.scss create mode 100644 sass/inc/_radio.scss create mode 100644 test/Checkbox.test.js create mode 100644 test/Radio.test.js create mode 100644 todo/Section/CheckboxDemo.jsx create mode 100644 todo/Section/RadioDemo.jsx diff --git a/package.json b/package.json index 44d78a3..d1b1c45 100644 --- a/package.json +++ b/package.json @@ -58,6 +58,7 @@ "node-sass": "^4.9.2", "prop-types": "^15.6.2", "sass-loader": "^7.0.3", + "sinon": "^6.1.5", "standard": "^11.0.1", "style-loader": "^0.21.0", "webpack": "^4.16.1", diff --git a/react/Checkbox.jsx b/react/Checkbox.jsx new file mode 100644 index 0000000..0d7815e --- /dev/null +++ b/react/Checkbox.jsx @@ -0,0 +1,68 @@ +import React from 'react' +import { pickRest } from '../lib/utils' +import PropTypes from 'prop-types' + +export default class Checkbox extends React.Component { + constructor (props) { + super(props) + this.state = { + checked: props.defaultChecked + } + this.handleClick = this.handleClick.bind(this) + this.handleKeyPress = this.handleKeyPress.bind(this) + } + + canToggle () { + return !this.props.disabled && !this.isControlled() + } + + isControlled () { + return this.props.checked !== undefined + } + + handleClick (e) { + const controlled = this.isControlled() + const checked = controlled ? this.props.checked : this.state.checked + if (!controlled) { + if (this.canToggle()) { + this.setState({ + checked: !checked + }) + } + } + if (this.props.onChange) { + this.props.onChange(e, {...this.props, checked: !checked}) + } + } + + handleKeyPress (e) { + if (e.key === 'Enter' || e.key === ' ') { + this.handleClick(e) + } + } + + render () { + const [mods, { children, as, checked, onChange, value, ...rest }] = pickRest(this.props, ['primary', 'secondary', 'red', 'disabled', 'indeterminate', 'switch']) + mods.checked = this.isControlled() ? checked : this.state.checked + const As = as + return + } +} + +Checkbox.defaultProps = { + as: 'span', + defaultChecked: false +} + +Checkbox.propTypes = { + as: PropTypes.oneOfType([PropTypes.func, PropTypes.string]), + primary: PropTypes.bool, + secondary: PropTypes.bool, + red: PropTypes.bool, + disabled: PropTypes.bool, + indeterminate: PropTypes.bool, + defaultChecked: PropTypes.bool, + checked: PropTypes.bool, + value: PropTypes.any, + toggle: PropTypes.bool +} diff --git a/react/Radio.jsx b/react/Radio.jsx new file mode 100644 index 0000000..9e22d48 --- /dev/null +++ b/react/Radio.jsx @@ -0,0 +1,65 @@ +import React from 'react' +import { pickRest } from '../lib/utils' +import PropTypes from 'prop-types' + +export default class Radio extends React.Component { + constructor (props) { + super(props) + this.state = { + checked: props.defaultChecked || !!props.checked + } + this.handleClick = this.handleClick.bind(this) + this.handleKeyPress = this.handleKeyPress.bind(this) + } + + canToggle () { + return !this.props.disabled && !this.isControlled() && !this.state.checked + } + + isControlled () { + return this.props.checked !== undefined + } + + handleClick (e) { + const controlled = this.isControlled() + if (!controlled) { + if (this.canToggle()) { + this.setState({ + checked: true + }) + } + } + if (this.props.onChange) { + this.props.onChange(e, {...this.props, checked: true}) + } + } + + handleKeyPress (e) { + if (e.key === 'Enter' || e.key === ' ') { + this.handleClick(e) + } + } + + render () { + const [mods, { children, as, checked, onChange, value, ...rest }] = pickRest(this.props, ['primary', 'secondary', 'red', 'disabled', 'radio']) + mods.checked = this.isControlled() ? checked : this.state.checked + const As = as + return + } +} + +Radio.defaultProps = { + as: 'span', + defaultChecked: false +} + +Radio.propTypes = { + as: PropTypes.oneOfType([PropTypes.func, PropTypes.string]), + primary: PropTypes.bool, + secondary: PropTypes.bool, + red: PropTypes.bool, + disabled: PropTypes.bool, + defaultChecked: PropTypes.bool, + checked: PropTypes.bool, + value: PropTypes.any +} diff --git a/sass/_library.scss b/sass/_library.scss index 3501486..0258004 100644 --- a/sass/_library.scss +++ b/sass/_library.scss @@ -3,7 +3,9 @@ @import 'inc/animation'; @import 'inc/button'; @import 'inc/card'; +@import 'inc/checkbox'; @import 'inc/grid'; +@import 'inc/radio'; @import 'inc/reset'; @import 'inc/text'; @import 'inc/tooltip'; diff --git a/sass/inc/_animation.scss b/sass/inc/_animation.scss index 220bdcf..52034a6 100644 --- a/sass/inc/_animation.scss +++ b/sass/inc/_animation.scss @@ -1,2 +1,3 @@ $animation-button-hover: background-color .3s; -$ease-out-quad: cubic-bezier(0.250, 0.460, 0.450, 0.940); \ No newline at end of file +$ease-out-quad: cubic-bezier(0.250, 0.460, 0.450, 0.940); +$animation-checkbox-click: transform .3s; \ No newline at end of file diff --git a/sass/inc/_checkbox.scss b/sass/inc/_checkbox.scss new file mode 100644 index 0000000..b6227c9 --- /dev/null +++ b/sass/inc/_checkbox.scss @@ -0,0 +1,75 @@ +.checkbox { + display: inline-block; + height: 24px; + width: 24px; + border: 1px solid $black; + border-radius: 5px; + margin: 0 5px; + color: $white; + text-align: center; + vertical-align: top; + + &--primary { border-color: $primary } + &--secondary { border-color: $secondary } + &--red { border-color: $red } + &--disabled { + border-color: #C8C8C8; + cursor: not-allowed; + } + + $self: &; + + &--checked:not(#{$self}--switch) { + border: 0; + background-color: $black; + &#{$self}--primary { background-color: $primary } + &#{$self}--secondary { background-color: $secondary } + &#{$self}--red { background-color: $red } + &#{$self}--disabled { background-color: #C8C8C8; } + + &::after { + content: "\2713"; + } + &#{$self}--indeterminate::after { + content: "\2015"; + } + } + + &--switch { + background-color: #D8D8D8; + width: 48px; + border-radius: 12px; + border: none; + + &::after { + content: ""; + display: block; + width: 24px; + height: 24px; + border-radius: 50%; + background-color: $white; + box-shadow: 0 0 5px 0 rgba(0,0,0,0.25); + transition: $animation-checkbox-click; + box-sizing: content-box; + // background-clip: padding-box; + } + + &#{$self}--checked { + &::after { + background-color: $black; + transform: translate(24px); + } + &#{$self}--primary::after { background-color: $primary } + &#{$self}--secondary::after { background-color: $secondary } + &#{$self}--red::after { background-color: $red } + } + + // &#{$self}:focus-visible::after { + // margin-left: -10px; + // margin-top: -10px; + // border: 10px solid rgba(0,0,0,0.05); + // } + + &#{$self}--disabled::after { background-color: #C8C8C8; } + } +} \ No newline at end of file diff --git a/sass/inc/_radio.scss b/sass/inc/_radio.scss new file mode 100644 index 0000000..9656781 --- /dev/null +++ b/sass/inc/_radio.scss @@ -0,0 +1,38 @@ +.radio { + display: inline-block; + height: 24px; + width: 24px; + border: 1px solid $black; + border-radius: 50%; + margin: 0 5px; + color: $white; + text-align: center; + vertical-align: top; + + &--primary { border-color: $primary; } + &--secondary { border-color: $secondary; } + &--red { border-color: $red; } + &--disabled { + border-color: #C8C8C8; + cursor: not-allowed; + } + + $self: &; + + &--checked { + &::after { + content: ""; + border-radius: 50%; + display: block; + margin: 4px; + height: 14px; + width: 14px; + background-color: $black; + } + + &#{$self}--primary::after { background-color: $primary; } + &#{$self}--secondary::after { background-color: $secondary; } + &#{$self}--red::after { background-color: $red; } + &#{$self}--disabled::after { background-color: #C8C8C8; } + } +} \ No newline at end of file diff --git a/test/Checkbox.test.js b/test/Checkbox.test.js new file mode 100644 index 0000000..1972959 --- /dev/null +++ b/test/Checkbox.test.js @@ -0,0 +1,71 @@ +/* global describe, it */ +import React from 'react' +import expect from 'must' +import { shallow } from 'enzyme' +import { spy } from 'sinon' +import Checkbox from '../react/Checkbox' + +describe('', () => { + it('renders as a span', () => { + const wrapper = shallow() + expect(wrapper.is('span')).to.be.true() + }) + + it('augments itself', () => { + const wrapper = shallow() + expect(wrapper.is('a')).to.be.true() + }) + + it('renders classes', () => { + const wrapper = shallow() + expect(wrapper.hasClass('checkbox')).to.be.true() + expect(wrapper.hasClass('checkbox--primary')).to.be.true() + expect(wrapper.hasClass('checkbox--secondary')).to.be.true() + expect(wrapper.hasClass('checkbox--red')).to.be.true() + expect(wrapper.hasClass('checkbox--disabled')).to.be.true() + expect(wrapper.hasClass('checkbox--indeterminate')).to.be.true() + }) + + it('passes extra props down', () => { + const fakeFunc = () => {} + const wrapper = shallow() + expect(wrapper.find('span').prop('onClick')).to.equal(fakeFunc) + }) + + it('starts checked when defaultChecked passed', () => { + const wrapper = shallow() + expect(wrapper.state('checked')).to.be.true() + }) + + it('toggles state on click', () => { + const wrapper = shallow() + wrapper.find('span').simulate('click') + expect(wrapper.state('checked')).to.be.true() + }) + + it('doesn\'t toggle state on disabled click', () => { + const wrapper = shallow() + wrapper.find('span').simulate('click') + expect(wrapper.state('checked')).to.be.false() + }) + + it('onChange callback is called', () => { + const callback = spy() + const wrapper = shallow() + wrapper.find('span').simulate('click') + expect(callback.calledOnce).to.be.true() + }) + + describe('controlled components', () => { + it('can be controlled', () => { + const wrapper = shallow() + expect(wrapper.hasClass('checkbox--checked')).to.be.true() + }) + + it('don\'t toggle state when clicked', () => { + const wrapper = shallow() + wrapper.find('span').simulate('click') + expect(wrapper.state('checked')).to.be.false() + }) + }) +}) diff --git a/test/Radio.test.js b/test/Radio.test.js new file mode 100644 index 0000000..7a71ee2 --- /dev/null +++ b/test/Radio.test.js @@ -0,0 +1,70 @@ +/* global describe, it */ +import React from 'react' +import expect from 'must' +import { shallow } from 'enzyme' +import { spy } from 'sinon' +import Radio from '../react/Radio' + +describe('', () => { + it('renders as a span', () => { + const wrapper = shallow() + expect(wrapper.is('span')).to.be.true() + }) + + it('augments itself', () => { + const wrapper = shallow() + expect(wrapper.is('a')).to.be.true() + }) + + it('renders classes', () => { + const wrapper = shallow() + expect(wrapper.hasClass('radio')).to.be.true() + expect(wrapper.hasClass('radio--primary')).to.be.true() + expect(wrapper.hasClass('radio--secondary')).to.be.true() + expect(wrapper.hasClass('radio--red')).to.be.true() + expect(wrapper.hasClass('radio--disabled')).to.be.true() + }) + + it('passes extra props down', () => { + const fakeFunc = () => {} + const wrapper = shallow() + expect(wrapper.find('span').prop('onClick')).to.equal(fakeFunc) + }) + + it('starts checked when defaultChecked passed', () => { + const wrapper = shallow() + expect(wrapper.state('checked')).to.be.true() + }) + + it('toggles state on click', () => { + const wrapper = shallow() + wrapper.find('span').simulate('click') + expect(wrapper.state('checked')).to.be.true() + }) + + it('doesn\'t toggle state on disabled click', () => { + const wrapper = shallow() + wrapper.find('span').simulate('click') + expect(wrapper.state('checked')).to.be.false() + }) + + it('onChange callback is called', () => { + const callback = spy() + const wrapper = shallow() + wrapper.find('span').simulate('click') + expect(callback.calledOnce).to.be.true() + }) + + describe('controlled components', () => { + it('can be controlled', () => { + const wrapper = shallow() + expect(wrapper.hasClass('radio--checked')).to.be.true() + }) + + it('don\'t toggle state when clicked', () => { + const wrapper = shallow() + wrapper.find('span').simulate('click') + expect(wrapper.state('checked')).to.be.false() + }) + }) +}) diff --git a/todo/App.jsx b/todo/App.jsx index b66e67b..0318555 100644 --- a/todo/App.jsx +++ b/todo/App.jsx @@ -6,7 +6,9 @@ import ButtonDemo from './Section/ButtonDemo' import FontDemo from './Section/FontDemo' import GridDemo from './Section/GridDemo' import CardDemo from './Section/CardDemo' -import TooltipDemo from './Section/TooltipDemo'; +import TooltipDemo from './Section/TooltipDemo' +import CheckboxDemo from './Section/CheckboxDemo' +import RadioDemo from './Section/RadioDemo' require(`../sass/bulwark-${STYLE}.scss`) @@ -18,6 +20,8 @@ const App = () => ( + + ) diff --git a/todo/Section/CheckboxDemo.jsx b/todo/Section/CheckboxDemo.jsx new file mode 100644 index 0000000..2f0f402 --- /dev/null +++ b/todo/Section/CheckboxDemo.jsx @@ -0,0 +1,29 @@ +import React from 'react' + +import Checkbox from '../../react/Checkbox' + +const CheckboxDemo = () => ( +
+

Checkboxes

+ + + + + +
+
+ + + + + +

Switches

+ + + + + +
+) + +export default CheckboxDemo diff --git a/todo/Section/RadioDemo.jsx b/todo/Section/RadioDemo.jsx new file mode 100644 index 0000000..0df1037 --- /dev/null +++ b/todo/Section/RadioDemo.jsx @@ -0,0 +1,30 @@ +import React from 'react' + +import Radio from '../../react/Radio' + +export default class RadioDemo extends React.Component { + constructor (props) { + super(props) + this.state = { + value: 4 + } + this.handleChange = this.handleChange.bind(this) + } + + handleChange (e, {value}) { + this.setState({value}) + } + + render () { + return ( +
+

Radio Buttons

+ + + + + +
+ ) + } +} diff --git a/yarn.lock b/yarn.lock index 3db38a3..ee9e15f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -172,6 +172,22 @@ lodash "^4.17.5" to-fast-properties "^2.0.0" +"@sinonjs/commons@^1.0.1": + version "1.0.2" + resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-1.0.2.tgz#3e0ac737781627b8844257fadc3d803997d0526e" + dependencies: + type-detect "4.0.8" + +"@sinonjs/formatio@^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@sinonjs/formatio/-/formatio-2.0.0.tgz#84db7e9eb5531df18a8c5e0bfb6e449e55e654b2" + dependencies: + samsam "1.3.0" + +"@sinonjs/samsam@^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@sinonjs/samsam/-/samsam-2.0.0.tgz#9163742ac35c12d3602dece74317643b35db6a80" + "@types/node@*": version "10.7.1" resolved "https://registry.yarnpkg.com/@types/node/-/node-10.7.1.tgz#b704d7c259aa40ee052eec678758a68d07132a2e" @@ -2229,7 +2245,7 @@ detect-node@^2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/detect-node/-/detect-node-2.0.3.tgz#a2033c09cc8e158d37748fbde7507832bd6ce127" -diff@^3.2.0: +diff@^3.2.0, diff@^3.5.0: version "3.5.0" resolved "https://registry.yarnpkg.com/diff/-/diff-3.5.0.tgz#800c0dd1e0a8bfbc95835c202ad220fe317e5a12" @@ -4466,6 +4482,10 @@ jsx-ast-utils@^2.0.1: dependencies: array-includes "^3.0.3" +just-extend@^1.1.27: + version "1.1.27" + resolved "https://registry.yarnpkg.com/just-extend/-/just-extend-1.1.27.tgz#ec6e79410ff914e472652abfa0e603c03d60e905" + killable@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/killable/-/killable-1.0.0.tgz#da8b84bd47de5395878f95d64d02f2449fe05e6b" @@ -4613,6 +4633,10 @@ lodash.flattendeep@^4.4.0: version "4.4.0" resolved "https://registry.yarnpkg.com/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz#fb030917f86a3134e5bc9bec0d69e0013ddfedb2" +lodash.get@^4.4.2: + version "4.4.2" + resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99" + lodash.mergewith@^4.6.0: version "4.6.1" resolved "https://registry.yarnpkg.com/lodash.mergewith/-/lodash.mergewith-4.6.1.tgz#639057e726c3afbdb3e7d42741caa8d6e4335927" @@ -4656,6 +4680,10 @@ loglevelnext@^1.0.1: es6-symbol "^3.1.1" object.assign "^4.1.0" +lolex@^2.3.2, lolex@^2.7.1: + version "2.7.1" + resolved "https://registry.yarnpkg.com/lolex/-/lolex-2.7.1.tgz#e40a8c4d1f14b536aa03e42a537c7adbaf0c20be" + long@4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/long/-/long-4.0.0.tgz#9a7b71cfb7d361a194ea555241c92f7468d5bf28" @@ -5034,6 +5062,16 @@ nice-try@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.4.tgz#d93962f6c52f2c1558c0fbda6d512819f1efe1c4" +nise@^1.4.2: + version "1.4.3" + resolved "https://registry.yarnpkg.com/nise/-/nise-1.4.3.tgz#d1996e8d15256ceff1a0a1596e0c72bff370e37c" + dependencies: + "@sinonjs/formatio" "^2.0.0" + just-extend "^1.1.27" + lolex "^2.3.2" + path-to-regexp "^1.7.0" + text-encoding "^0.6.4" + no-case@^2.2.0: version "2.3.2" resolved "https://registry.yarnpkg.com/no-case/-/no-case-2.3.2.tgz#60b813396be39b3f1288a4c1ed5d1e7d28b464ac" @@ -5531,6 +5569,12 @@ path-to-regexp@0.1.7: version "0.1.7" resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" +path-to-regexp@^1.7.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-1.7.0.tgz#59fde0f435badacba103a84e9d3bc64e96b9937d" + dependencies: + isarray "0.0.1" + path-type@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/path-type/-/path-type-1.1.0.tgz#59c44f7ee491da704da415da5a4070ba4f8fe441" @@ -6347,6 +6391,10 @@ safe-regex@^1.1.0: version "2.1.2" resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" +samsam@1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/samsam/-/samsam-1.3.0.tgz#8d1d9350e25622da30de3e44ba692b5221ab7c50" + sane@^2.0.0: version "2.5.2" resolved "https://registry.yarnpkg.com/sane/-/sane-2.5.2.tgz#b4dc1861c21b427e929507a3e751e2a2cb8ab3fa" @@ -6535,6 +6583,20 @@ signal-exit@^3.0.0, signal-exit@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d" +sinon@^6.1.5: + version "6.1.5" + resolved "https://registry.yarnpkg.com/sinon/-/sinon-6.1.5.tgz#41451502d43cd5ffb9d051fbf507952400e81d09" + dependencies: + "@sinonjs/commons" "^1.0.1" + "@sinonjs/formatio" "^2.0.0" + "@sinonjs/samsam" "^2.0.0" + diff "^3.5.0" + lodash.get "^4.4.2" + lolex "^2.7.1" + nise "^1.4.2" + supports-color "^5.4.0" + type-detect "^4.0.8" + sisteransi@^0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/sisteransi/-/sisteransi-0.1.1.tgz#5431447d5f7d1675aac667ccd0b865a4994cb3ce" @@ -6944,6 +7006,10 @@ test-exclude@^4.2.1: read-pkg-up "^3.0.0" require-main-filename "^1.0.1" +text-encoding@^0.6.4: + version "0.6.4" + resolved "https://registry.yarnpkg.com/text-encoding/-/text-encoding-0.6.4.tgz#e399a982257a276dae428bb92845cb71bdc26d19" + text-table@~0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" @@ -7078,6 +7144,10 @@ type-check@~0.3.2: dependencies: prelude-ls "~1.1.2" +type-detect@4.0.8, type-detect@^4.0.8: + version "4.0.8" + resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c" + type-is@~1.6.15, type-is@~1.6.16: version "1.6.16" resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.16.tgz#f89ce341541c672b25ee7ae3c73dee3b2be50194" From 124bd74137e2fac1a37de3d8f4527fff95c3593f Mon Sep 17 00:00:00 2001 From: Unknown Date: Thu, 23 Aug 2018 16:31:27 -0700 Subject: [PATCH 2/3] 100% Test coverage --- .gitignore | 4 +++- react/Checkbox.jsx | 6 +++--- react/Radio.jsx | 6 +++--- test/Checkbox.test.js | 20 ++++++++++++++++++++ test/Radio.test.js | 20 ++++++++++++++++++++ 5 files changed, 49 insertions(+), 7 deletions(-) diff --git a/.gitignore b/.gitignore index 2ddec8a..73ec79a 100644 --- a/.gitignore +++ b/.gitignore @@ -9,4 +9,6 @@ node_modules/ \.DS_Store -.vscode \ No newline at end of file +.vscode + +coverage/ \ No newline at end of file diff --git a/react/Checkbox.jsx b/react/Checkbox.jsx index 0d7815e..a09867b 100644 --- a/react/Checkbox.jsx +++ b/react/Checkbox.jsx @@ -9,7 +9,7 @@ export default class Checkbox extends React.Component { checked: props.defaultChecked } this.handleClick = this.handleClick.bind(this) - this.handleKeyPress = this.handleKeyPress.bind(this) + this.handleKeyDown = this.handleKeyDown.bind(this) } canToggle () { @@ -35,7 +35,7 @@ export default class Checkbox extends React.Component { } } - handleKeyPress (e) { + handleKeyDown (e) { if (e.key === 'Enter' || e.key === ' ') { this.handleClick(e) } @@ -45,7 +45,7 @@ export default class Checkbox extends React.Component { const [mods, { children, as, checked, onChange, value, ...rest }] = pickRest(this.props, ['primary', 'secondary', 'red', 'disabled', 'indeterminate', 'switch']) mods.checked = this.isControlled() ? checked : this.state.checked const As = as - return + return } } diff --git a/react/Radio.jsx b/react/Radio.jsx index 9e22d48..0a42cbb 100644 --- a/react/Radio.jsx +++ b/react/Radio.jsx @@ -9,7 +9,7 @@ export default class Radio extends React.Component { checked: props.defaultChecked || !!props.checked } this.handleClick = this.handleClick.bind(this) - this.handleKeyPress = this.handleKeyPress.bind(this) + this.handleKeyDown = this.handleKeyDown.bind(this) } canToggle () { @@ -34,7 +34,7 @@ export default class Radio extends React.Component { } } - handleKeyPress (e) { + handleKeyDown (e) { if (e.key === 'Enter' || e.key === ' ') { this.handleClick(e) } @@ -44,7 +44,7 @@ export default class Radio extends React.Component { const [mods, { children, as, checked, onChange, value, ...rest }] = pickRest(this.props, ['primary', 'secondary', 'red', 'disabled', 'radio']) mods.checked = this.isControlled() ? checked : this.state.checked const As = as - return + return } } diff --git a/test/Checkbox.test.js b/test/Checkbox.test.js index 1972959..54754ac 100644 --- a/test/Checkbox.test.js +++ b/test/Checkbox.test.js @@ -68,4 +68,24 @@ describe('', () => { expect(wrapper.state('checked')).to.be.false() }) }) + + describe('keyboard', () => { + it('activates on enter', () => { + const wrapper = shallow() + wrapper.find('span').simulate('keyDown', {key: 'Enter'}) + expect(wrapper.state('checked')).to.be.true() + }) + + it('activates on spacebar', () => { + const wrapper = shallow() + wrapper.find('span').simulate('keyDown', {key: ' '}) + expect(wrapper.state('checked')).to.be.true() + }) + + it('doesn\'t activate on other', () => { + const wrapper = shallow() + wrapper.find('span').simulate('keyDown', {key: 'f'}) + expect(wrapper.state('checked')).to.be.false() + }) + }) }) diff --git a/test/Radio.test.js b/test/Radio.test.js index 7a71ee2..de27886 100644 --- a/test/Radio.test.js +++ b/test/Radio.test.js @@ -67,4 +67,24 @@ describe('', () => { expect(wrapper.state('checked')).to.be.false() }) }) + + describe('activates on keyboard', () => { + it('enter', () => { + const wrapper = shallow() + wrapper.find('span').simulate('keyDown', {key: 'Enter'}) + expect(wrapper.state('checked')).to.be.true() + }) + + it('spacebar', () => { + const wrapper = shallow() + wrapper.find('span').simulate('keyDown', {key: ' '}) + expect(wrapper.state('checked')).to.be.true() + }) + + it('doesn\'t activate on other', () => { + const wrapper = shallow() + wrapper.find('span').simulate('keyDown', {key: 'f'}) + expect(wrapper.state('checked')).to.be.false() + }) + }) }) From df15199ac7669dfc697a8f0429f7f3dd569dc5e0 Mon Sep 17 00:00:00 2001 From: dustinengle Date: Tue, 4 Sep 2018 13:40:57 -0700 Subject: [PATCH 3/3] combine with master and update for plugins --- package.json | 2 +- react/Checkbox.jsx | 67 +++++++++++++++++++++------------------------- react/Radio.jsx | 67 ++++++++++++++++++++++------------------------ yarn.lock | 42 ++++++++++++++++++----------- 4 files changed, 90 insertions(+), 88 deletions(-) diff --git a/package.json b/package.json index 28b6a0a..d125717 100644 --- a/package.json +++ b/package.json @@ -61,7 +61,7 @@ "node-sass": "^4.9.2", "prop-types": "^15.6.2", "sass-loader": "^7.0.3", - "sinon": "^6.1.5", + "sinon": "^6.1.6", "standard": "^11.0.1", "style-loader": "^0.21.0", "webpack": "^4.16.1", diff --git a/react/Checkbox.jsx b/react/Checkbox.jsx index a09867b..3df8df6 100644 --- a/react/Checkbox.jsx +++ b/react/Checkbox.jsx @@ -1,26 +1,39 @@ -import React from 'react' -import { pickRest } from '../lib/utils' import PropTypes from 'prop-types' +import React from 'react' +import {pickRest} from '../lib/utils' export default class Checkbox extends React.Component { - constructor (props) { - super(props) - this.state = { - checked: props.defaultChecked - } - this.handleClick = this.handleClick.bind(this) - this.handleKeyDown = this.handleKeyDown.bind(this) + static defaultProps = { + as: 'span', + defaultChecked: false } - canToggle () { - return !this.props.disabled && !this.isControlled() + static propTypes = { + as: PropTypes.oneOfType([PropTypes.func, PropTypes.string]), + primary: PropTypes.bool, + secondary: PropTypes.bool, + red: PropTypes.bool, + disabled: PropTypes.bool, + indeterminate: PropTypes.bool, + defaultChecked: PropTypes.bool, + checked: PropTypes.bool, + value: PropTypes.any, + toggle: PropTypes.bool } - isControlled () { - return this.props.checked !== undefined + state = { + checked: false } - handleClick (e) { + componentWillMount () { + if (this.props.defaultChecked) this.setState({checked: true}) + } + + canToggle = () => !this.props.disabled && !this.isControlled() + + isControlled = () => this.props.checked !== undefined + + handleClick = (ev) => { const controlled = this.isControlled() const checked = controlled ? this.props.checked : this.state.checked if (!controlled) { @@ -31,13 +44,13 @@ export default class Checkbox extends React.Component { } } if (this.props.onChange) { - this.props.onChange(e, {...this.props, checked: !checked}) + this.props.onChange(ev, {...this.props, checked: !checked}) } } - handleKeyDown (e) { - if (e.key === 'Enter' || e.key === ' ') { - this.handleClick(e) + handleKeyDown = (ev) => { + if (ev.key === 'Enter' || ev.key === ' ') { + this.handleClick(ev) } } @@ -48,21 +61,3 @@ export default class Checkbox extends React.Component { return } } - -Checkbox.defaultProps = { - as: 'span', - defaultChecked: false -} - -Checkbox.propTypes = { - as: PropTypes.oneOfType([PropTypes.func, PropTypes.string]), - primary: PropTypes.bool, - secondary: PropTypes.bool, - red: PropTypes.bool, - disabled: PropTypes.bool, - indeterminate: PropTypes.bool, - defaultChecked: PropTypes.bool, - checked: PropTypes.bool, - value: PropTypes.any, - toggle: PropTypes.bool -} diff --git a/react/Radio.jsx b/react/Radio.jsx index 0a42cbb..877d45c 100644 --- a/react/Radio.jsx +++ b/react/Radio.jsx @@ -1,26 +1,39 @@ -import React from 'react' -import { pickRest } from '../lib/utils' import PropTypes from 'prop-types' +import React from 'react' +import {pickRest} from '../lib/utils' export default class Radio extends React.Component { - constructor (props) { - super(props) - this.state = { - checked: props.defaultChecked || !!props.checked - } - this.handleClick = this.handleClick.bind(this) - this.handleKeyDown = this.handleKeyDown.bind(this) + static defaultProps = { + as: 'span', + defaultChecked: false } - canToggle () { - return !this.props.disabled && !this.isControlled() && !this.state.checked + static propTypes = { + as: PropTypes.oneOfType([PropTypes.func, PropTypes.string]), + primary: PropTypes.bool, + secondary: PropTypes.bool, + red: PropTypes.bool, + disabled: PropTypes.bool, + defaultChecked: PropTypes.bool, + checked: PropTypes.bool, + value: PropTypes.any } - isControlled () { - return this.props.checked !== undefined + state = { + checked: false + } + + componentWillMount() { + if (this.props.defaultChecked || this.props.checked) { + this.setState({checked: this.props.defaultChecked || this.props.checked}) + } } - handleClick (e) { + canToggle = () => !this.props.disabled && !this.isControlled() && !this.state.checked + + isControlled = () => this.props.checked !== undefined + + handleClick = (ev) => { const controlled = this.isControlled() if (!controlled) { if (this.canToggle()) { @@ -30,36 +43,20 @@ export default class Radio extends React.Component { } } if (this.props.onChange) { - this.props.onChange(e, {...this.props, checked: true}) + this.props.onChange(ev, {...this.props, checked: true}) } } - handleKeyDown (e) { - if (e.key === 'Enter' || e.key === ' ') { - this.handleClick(e) + handleKeyDown = (ev) => { + if (ev.key === 'Enter' || ev.key === ' ') { + this.handleClick(ev) } } render () { - const [mods, { children, as, checked, onChange, value, ...rest }] = pickRest(this.props, ['primary', 'secondary', 'red', 'disabled', 'radio']) + const [mods, {children, as, checked, onChange, value, ...rest}] = pickRest(this.props, ['primary', 'secondary', 'red', 'disabled', 'radio']) mods.checked = this.isControlled() ? checked : this.state.checked const As = as return } } - -Radio.defaultProps = { - as: 'span', - defaultChecked: false -} - -Radio.propTypes = { - as: PropTypes.oneOfType([PropTypes.func, PropTypes.string]), - primary: PropTypes.bool, - secondary: PropTypes.bool, - red: PropTypes.bool, - disabled: PropTypes.bool, - defaultChecked: PropTypes.bool, - checked: PropTypes.bool, - value: PropTypes.any -} diff --git a/yarn.lock b/yarn.lock index 03e2ff5..a63a74b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -172,7 +172,7 @@ lodash "^4.17.5" to-fast-properties "^2.0.0" -"@sinonjs/commons@^1.0.1": +"@sinonjs/commons@^1.0.2": version "1.0.2" resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-1.0.2.tgz#3e0ac737781627b8844257fadc3d803997d0526e" dependencies: @@ -4495,9 +4495,9 @@ jsx-ast-utils@^2.0.1: dependencies: array-includes "^3.0.3" -just-extend@^1.1.27: - version "1.1.27" - resolved "https://registry.yarnpkg.com/just-extend/-/just-extend-1.1.27.tgz#ec6e79410ff914e472652abfa0e603c03d60e905" +just-extend@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/just-extend/-/just-extend-3.0.0.tgz#cee004031eaabf6406da03a7b84e4fe9d78ef288" killable@^1.0.0: version "1.0.0" @@ -4693,10 +4693,14 @@ loglevelnext@^1.0.1: es6-symbol "^3.1.1" object.assign "^4.1.0" -lolex@^2.3.2, lolex@^2.7.1: +lolex@^2.3.2: version "2.7.1" resolved "https://registry.yarnpkg.com/lolex/-/lolex-2.7.1.tgz#e40a8c4d1f14b536aa03e42a537c7adbaf0c20be" +lolex@^2.7.2: + version "2.7.2" + resolved "https://registry.yarnpkg.com/lolex/-/lolex-2.7.2.tgz#ff11decab010cf37d6194ab44bb57970b5f4966b" + long@4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/long/-/long-4.0.0.tgz#9a7b71cfb7d361a194ea555241c92f7468d5bf28" @@ -5075,12 +5079,12 @@ nice-try@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.4.tgz#d93962f6c52f2c1558c0fbda6d512819f1efe1c4" -nise@^1.4.2: - version "1.4.3" - resolved "https://registry.yarnpkg.com/nise/-/nise-1.4.3.tgz#d1996e8d15256ceff1a0a1596e0c72bff370e37c" +nise@^1.4.4: + version "1.4.4" + resolved "https://registry.yarnpkg.com/nise/-/nise-1.4.4.tgz#b8d9dd35334c99e514b75e46fcc38e358caecdd0" dependencies: "@sinonjs/formatio" "^2.0.0" - just-extend "^1.1.27" + just-extend "^3.0.0" lolex "^2.3.2" path-to-regexp "^1.7.0" text-encoding "^0.6.4" @@ -6596,18 +6600,18 @@ signal-exit@^3.0.0, signal-exit@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d" -sinon@^6.1.5: - version "6.1.5" - resolved "https://registry.yarnpkg.com/sinon/-/sinon-6.1.5.tgz#41451502d43cd5ffb9d051fbf507952400e81d09" +sinon@^6.1.6: + version "6.1.6" + resolved "https://registry.yarnpkg.com/sinon/-/sinon-6.1.6.tgz#cae6f4693c5fe7a70c2129461ba569a5b995d646" dependencies: - "@sinonjs/commons" "^1.0.1" + "@sinonjs/commons" "^1.0.2" "@sinonjs/formatio" "^2.0.0" "@sinonjs/samsam" "^2.0.0" diff "^3.5.0" lodash.get "^4.4.2" - lolex "^2.7.1" - nise "^1.4.2" - supports-color "^5.4.0" + lolex "^2.7.2" + nise "^1.4.4" + supports-color "^5.5.0" type-detect "^4.0.8" sisteransi@^0.1.1: @@ -6971,6 +6975,12 @@ supports-color@^5.1.0, supports-color@^5.3.0, supports-color@^5.4.0: dependencies: has-flag "^3.0.0" +supports-color@^5.5.0: + version "5.5.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" + dependencies: + has-flag "^3.0.0" + symbol-tree@^3.2.2: version "3.2.2" resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.2.tgz#ae27db38f660a7ae2e1c3b7d1bc290819b8519e6"