Skip to content

Commit

Permalink
feat(checkbox): Add interactivity and focusing/blurring
Browse files Browse the repository at this point in the history
  • Loading branch information
lzcabrera committed Nov 29, 2017
1 parent b4ec4d0 commit 2a26d06
Show file tree
Hide file tree
Showing 4 changed files with 266 additions and 75 deletions.
80 changes: 70 additions & 10 deletions src/components/Checkbox/Checkbox.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<label data-no-global-styles htmlFor={checkboxId.identity()}>
<input {...safeRest(rest)} type="checkbox" data-no-global-styles id={checkboxId.identity()} />
<Text size="medium">{label}</Text>
</label>
)
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 (
<label data-no-global-styles htmlFor={checkboxId.identity()}>
<Box inline between={3}>
<span
className={joinClassNames(
this.state.checked ? styles.checked : styles.unchecked,
this.state.focused && styles.focused
)}
data-testid="fake-checkbox"
>
<input
{...safeRest(rest)}
id={checkboxId.identity()}
type="checkbox"
checked={this.state.checked}
className={displayStyles.hide}
onChange={this.onChange}
onFocus={this.onFocus}
onBlur={this.onBlur}
/>
{this.state.checked && (
<DecorativeIcon symbol="checkmark" size={16} variant="inverted" />
)}
</span>

<Text size="medium">{label}</Text>
</Box>
</label>
)
}
}

Checkbox.propTypes = {
label: PropTypes.string.isRequired,
checked: PropTypes.bool,
onChange: PropTypes.func,
}

Checkbox.defaultProps = {}
Checkbox.defaultProps = {
checked: false,
onChange: undefined,
}

export default Checkbox
76 changes: 56 additions & 20 deletions src/components/Checkbox/Checkbox.modules.scss
Original file line number Diff line number Diff line change
@@ -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;
}
}
163 changes: 140 additions & 23 deletions src/components/Checkbox/__tests__/Checkbox.spec.jsx
Original file line number Diff line number Diff line change
@@ -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(<Checkbox {...defaultProps} {...overrides} />)
const findInputElement = input => input.find('input')
const doMount = (overrides = {}) => {
const checkbox = mount(<Checkbox {...defaultProps} {...overrides} />)

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(<Text size="medium">Some label</Text>)
})

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(
<DecorativeIcon symbol="checkmark" size={16} variant="inverted" />
)
})

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(
<DecorativeIcon symbol="checkmark" size={16} variant="inverted" />
)

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')
// })
})

This file was deleted.

0 comments on commit 2a26d06

Please sign in to comment.