From 2a26d0613a170af159cfe2f2acc88c51346cd5f2 Mon Sep 17 00:00:00 2001 From: Laura Cabrera Date: Tue, 28 Nov 2017 16:14:48 -0800 Subject: [PATCH] feat(checkbox): Add interactivity and focusing/blurring --- src/components/Checkbox/Checkbox.jsx | 80 +++++++-- src/components/Checkbox/Checkbox.modules.scss | 76 +++++--- .../Checkbox/__tests__/Checkbox.spec.jsx | 163 +++++++++++++++--- .../__snapshots__/Checkbox.spec.jsx.snap | 22 --- 4 files changed, 266 insertions(+), 75 deletions(-) delete mode 100644 src/components/Checkbox/__tests__/__snapshots__/Checkbox.spec.jsx.snap diff --git a/src/components/Checkbox/Checkbox.jsx b/src/components/Checkbox/Checkbox.jsx index b1d2b4f4a5..c9b0a72d0c 100644 --- a/src/components/Checkbox/Checkbox.jsx +++ b/src/components/Checkbox/Checkbox.jsx @@ -2,27 +2,87 @@ import React from 'react' import PropTypes from 'prop-types' import safeRest from '../../utils/safeRest' +import joinClassNames from '../../utils/joinClassNames' import generateId from '../../utils/generateId' import Text from '../Typography/Text/Text' +import Box from '../Box/Box' +import DecorativeIcon from '../Icons/DecorativeIcon/DecorativeIcon' -// import styles from './Checkbox.modules.scss' +import styles from './Checkbox.modules.scss' +import displayStyles from '../Display.modules.scss' -const Checkbox = ({ label, ...rest }) => { - const checkboxId = generateId(rest.id, rest.name, label) +class Checkbox extends React.Component { + state = { + checked: this.props.checked, + focused: false, + } - return ( - - ) + onChange = event => { + const { onChange } = this.props + + this.setState(({ checked }) => ({ + checked: !checked, + })) + + if (onChange) { + onChange(event) + } + } + + onFocus = () => { + this.setState({ focused: true }) + } + + onBlur = () => { + this.setState({ focused: false }) + } + + render() { + const { label, checked, ...rest } = this.props + const checkboxId = generateId(rest.id, rest.name, label) + + return ( + + ) + } } Checkbox.propTypes = { label: PropTypes.string.isRequired, + checked: PropTypes.bool, + onChange: PropTypes.func, } -Checkbox.defaultProps = {} +Checkbox.defaultProps = { + checked: false, + onChange: undefined, +} export default Checkbox diff --git a/src/components/Checkbox/Checkbox.modules.scss b/src/components/Checkbox/Checkbox.modules.scss index 76d04bae2a..56faedbc97 100644 --- a/src/components/Checkbox/Checkbox.modules.scss +++ b/src/components/Checkbox/Checkbox.modules.scss @@ -1,28 +1,64 @@ +@import '../../scss/settings/colours'; @import '../../scss/settings/variables'; @import '../../scss/utility/mixins'; -/* FIXME: resetting globally scoped styles in form.scss - because of input:not(type='radio') -*/ -input[data-no-global-styles] { +.base { + cursor: pointer; + margin: 0; +} + +.fakeCheckbox { + display: block; + height: 20px; + width: 20px; + border-radius: 4px; + cursor: pointer; + outline: 0; +} + +.unchecked { + composes: fakeCheckbox; + border: solid 1px $color-shuttle-grey; + background-color: transparent; +} + +.fakeCheckboxError { + composes: fakeCheckbox; + border: solid 1px $color-cardinal; + background-color: transparent; +} + +.fakeCheckboxDisabled { + composes: fakeCheckbox; + background-color: $color-gainsboro; +} + +.checked { + composes: fakeCheckbox; border: none; - border-radius: 0; - box-shadow: none; - color: transparent; - font-size: inherit; - line-height: inherit; - outline: inherit; - padding: 0; - width: auto; - transition: none; - font-family: inherit; - - &:focus { - box-shadow: none; - border-color: inherit; + background-color: $color-accessible-green; + position: relative; + + i { + position: absolute; + top: 3px; + left: 3px; } +} + +.focused { + box-shadow: 0 0 4px 1px $color-shuttle-grey; +} + +.fakeCheckboxCheckedDisabled { + composes: fakeCheckbox; + border: none; + background-color: $color-gainsboro; + position: relative; - @include from-breakpoint(medium) { - letter-spacing: inherit; + i { + position: absolute; + top: 3px; + left: 3px; } } diff --git a/src/components/Checkbox/__tests__/Checkbox.spec.jsx b/src/components/Checkbox/__tests__/Checkbox.spec.jsx index 69b4b406c9..8268c7e1ee 100644 --- a/src/components/Checkbox/__tests__/Checkbox.spec.jsx +++ b/src/components/Checkbox/__tests__/Checkbox.spec.jsx @@ -1,43 +1,160 @@ import React from 'react' -import { shallow } from 'enzyme' +import { mount } from 'enzyme' +import Text from '../../Typography/Text/Text' +import DecorativeIcon from '../../Icons/DecorativeIcon/DecorativeIcon' import Checkbox from '../Checkbox' describe('Checkbox', () => { const defaultProps = { - label: 'The input', + label: 'The checkbox', } - const doShallow = (overrides = {}) => shallow() - const findInputElement = input => input.find('input') + const doMount = (overrides = {}) => { + const checkbox = mount() - it('renders', () => { - const checkbox = doShallow() + const findCheckboxElement = () => checkbox.find('input') - expect(checkbox).toMatchSnapshot() - }) + return { + checkbox, + label: checkbox.find('label'), + findCheckboxElement, + findFakeCheckbox: () => checkbox.find('[data-testid="fake-checkbox"]'), + check: () => findCheckboxElement().simulate('change', { target: { checked: true } }), + uncheck: () => findCheckboxElement().simulate('change', { target: { checked: false } }), + focus: () => findCheckboxElement().simulate('focus'), + blur: () => findCheckboxElement().simulate('blur'), + } + } - it('does other things', () => { - const checkbox = doShallow() + // it('renders', () => { + // const checkbox = doShallow() + // + // expect(checkbox).toMatchSnapshot() + // }) + // - expect(checkbox).toBePresent() + it('must have a label', () => { + const { label } = doMount({ label: 'Some label' }) + + expect(label).toContainReact(Some label) }) - it('passes additional attributes to the element', () => { - const checkbox = doShallow({ - disabled: 'true', - 'data-some-attr': 'some value', + describe('connecting the label to the checkbox', () => { + it('connects the label to the checkbox', () => { + const { label, findCheckboxElement } = doMount() + + expect(label.prop('htmlFor')).toEqual(findCheckboxElement().prop('id')) + }) + + it('uses the id when provided', () => { + const { label, findCheckboxElement } = doMount({ + id: 'the-id', + name: 'the-name', + label: 'The label', + }) + + expect(label).toHaveProp('htmlFor', 'the-id') + expect(findCheckboxElement()).toHaveProp('id', 'the-id') + }) + + it('uses the name when no id is provided', () => { + const { label, findCheckboxElement } = doMount({ name: 'the-name', label: 'The label' }) + + expect(label).toHaveProp('htmlFor', 'the-name') + expect(findCheckboxElement()).toHaveProp('id', 'the-name') + }) + + it('generates an id from the label when no id or name is provided', () => { + const { label, findCheckboxElement } = doMount({ label: 'The label' }) + + expect(label).toHaveProp('htmlFor', 'the-label') + expect(findCheckboxElement()).toHaveProp('id', 'the-label') }) - expect(findInputElement(checkbox)).toHaveProp('disabled', 'true') - expect(findInputElement(checkbox)).toHaveProp('data-some-attr', 'some value') }) - it('does not allow custom CSS', () => { - const checkbox = doShallow({ - className: 'my-custom-class', - style: { color: 'hotpink' }, + describe('interactivity', () => { + it('can be unchecked', () => { + const { findCheckboxElement, findFakeCheckbox } = doMount({ checked: false }) + + expect(findCheckboxElement()).toHaveProp('checked', false) + expect(findFakeCheckbox()).toHaveClassName('unchecked') + expect(findFakeCheckbox().find(DecorativeIcon)).toBeEmpty() + }) + + it('can be checked', () => { + const { findCheckboxElement, findFakeCheckbox } = doMount({ checked: true }) + + expect(findCheckboxElement()).toHaveProp('checked', true) + expect(findFakeCheckbox()).toHaveClassName('checked') + expect(findFakeCheckbox()).toContainReact( + + ) + }) + + it('checks and unchecks when clicking', () => { + const { findCheckboxElement, findFakeCheckbox, check, uncheck } = doMount() + + check() + + expect(findCheckboxElement()).toHaveProp('checked', true) + expect(findFakeCheckbox()).toHaveClassName('checked') + expect(findFakeCheckbox()).toContainReact( + + ) + + uncheck() + + expect(findCheckboxElement()).toHaveProp('checked', false) + expect(findFakeCheckbox()).toHaveClassName('unchecked') + expect(findFakeCheckbox().find(DecorativeIcon)).toBeEmpty() + }) + + it('triggers a change handler when checked or unchecked', () => { + const onChangeSpy = jest.fn() + const { check, uncheck } = doMount({ onChange: onChangeSpy }) + + check() + expect(onChangeSpy).toHaveBeenCalledWith( + expect.objectContaining({ target: { checked: true } }) + ) + + uncheck() + expect(onChangeSpy).toHaveBeenCalledWith( + expect.objectContaining({ target: { checked: false } }) + ) }) + }) - expect(findInputElement(checkbox)).not.toHaveProp('className', 'my-custom-class') - expect(findInputElement(checkbox)).not.toHaveProp('style') + describe('focusing', () => { + it('can be focused and unfocused', () => { + const { findFakeCheckbox, focus, blur } = doMount() + + focus() + expect(findFakeCheckbox()).toHaveClassName('focused unchecked') + + blur() + expect(findFakeCheckbox()).not.toHaveClassName('focused') + expect(findFakeCheckbox()).toHaveClassName('unchecked') + }) }) + + // + // it('passes additional attributes to the element', () => { + // const checkbox = doShallow({ + // disabled: 'true', + // 'data-some-attr': 'some value', + // }) + // expect(findInputElement(checkbox)).toHaveProp('disabled', 'true') + // expect(findInputElement(checkbox)).toHaveProp('data-some-attr', 'some value') + // }) + // + // it('does not allow custom CSS', () => { + // const checkbox = doShallow({ + // className: 'my-custom-class', + // style: { color: 'hotpink' }, + // }) + // + // expect(findInputElement(checkbox)).not.toHaveProp('className', 'my-custom-class') + // expect(findInputElement(checkbox)).not.toHaveProp('style') + // }) }) diff --git a/src/components/Checkbox/__tests__/__snapshots__/Checkbox.spec.jsx.snap b/src/components/Checkbox/__tests__/__snapshots__/Checkbox.spec.jsx.snap deleted file mode 100644 index 41c10b14b6..0000000000 --- a/src/components/Checkbox/__tests__/__snapshots__/Checkbox.spec.jsx.snap +++ /dev/null @@ -1,22 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Checkbox renders 1`] = ` - -`;