Skip to content

Commit e9c075f

Browse files
jeffcarbslevithomason
authored andcommitted
feat(Checkbox): Indeterminate state (#1043)
* feat(Checkbox): Indeterminate state * fix(checkbox): fix lifecycle method signatures
1 parent ded36f4 commit e9c075f

File tree

4 files changed

+112
-16
lines changed

4 files changed

+112
-16
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import React from 'react'
2+
import { Checkbox } from 'semantic-ui-react'
3+
4+
const CheckboxExampleIndeterminate = () => (
5+
<Checkbox label='This checkbox is indeterminate' defaultIndeterminate />
6+
)
7+
8+
export default CheckboxExampleIndeterminate

docs/app/Examples/modules/Checkbox/States/index.js

+12-7
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,14 @@ import { Message } from 'semantic-ui-react'
55

66
const CheckboxStatesExamples = () => (
77
<ExampleSection title='States'>
8+
<ComponentExample
9+
title='Read Only'
10+
description='A checkbox can be read-only and unable to change states.'
11+
examplePath='modules/Checkbox/States/CheckboxExampleReadOnly'
12+
/>
813
<ComponentExample
914
title='Checked'
10-
description='A checkbox can come pre-checked.'
15+
description='A checkbox can be checked.'
1116
examplePath='modules/Checkbox/States/CheckboxExampleChecked'
1217
>
1318
<Message>
@@ -19,14 +24,14 @@ const CheckboxStatesExamples = () => (
1924
</Message>
2025
</ComponentExample>
2126
<ComponentExample
22-
title='Disabled'
23-
description='Checkboxes can be disabled.'
24-
examplePath='modules/Checkbox/States/CheckboxExampleDisabled'
27+
title='Indeterminate'
28+
description='A checkbox can be indeterminate.'
29+
examplePath='modules/Checkbox/States/CheckboxExampleIndeterminate'
2530
/>
2631
<ComponentExample
27-
title='Read Only'
28-
description='Make the checkbox unable to be edited by the user.'
29-
examplePath='modules/Checkbox/States/CheckboxExampleReadOnly'
32+
title='Disabled'
33+
description='A checkbox can be read-only and unable to change states.'
34+
examplePath='modules/Checkbox/States/CheckboxExampleDisabled'
3035
/>
3136
<ComponentExample
3237
title='Remote Control'

src/modules/Checkbox/Checkbox.js

+36-7
Original file line numberDiff line numberDiff line change
@@ -44,12 +44,18 @@ export default class Checkbox extends Component {
4444
/** The initial value of checked. */
4545
defaultChecked: PropTypes.bool,
4646

47+
/** Whether or not checkbox is indeterminate. */
48+
defaultIndeterminate: PropTypes.bool,
49+
4750
/** A checkbox can appear disabled and be unable to change states */
4851
disabled: PropTypes.bool,
4952

5053
/** Removes padding for a label. Auto applied when there is no label. */
5154
fitted: PropTypes.bool,
5255

56+
/** Whether or not checkbox is indeterminate. */
57+
indeterminate: PropTypes.bool,
58+
5359
/** The text of the associated label element. */
5460
label: customPropTypes.itemShorthand,
5561

@@ -60,15 +66,15 @@ export default class Checkbox extends Component {
6066
* Called when the user attempts to change the checked state.
6167
*
6268
* @param {SyntheticEvent} event - React's original SyntheticEvent.
63-
* @param {object} data - All props and proposed checked state.
69+
* @param {object} data - All props and proposed checked/indeterminate state.
6470
*/
6571
onChange: PropTypes.func,
6672

6773
/**
6874
* Called when the checkbox or label is clicked.
6975
*
7076
* @param {SyntheticEvent} event - React's original SyntheticEvent.
71-
* @param {object} data - All props and current checked state.
77+
* @param {object} data - All props and current checked/indeterminate state.
7278
*/
7379
onClick: PropTypes.func,
7480

@@ -106,32 +112,53 @@ export default class Checkbox extends Component {
106112

107113
static autoControlledProps = [
108114
'checked',
115+
'indeterminate',
109116
]
110117

111118
static _meta = _meta
112119

113120
state = {}
114121

122+
componentDidMount() {
123+
this.setIndeterminate()
124+
}
125+
126+
componentDidUpdate() {
127+
this.setIndeterminate()
128+
}
129+
115130
canToggle = () => {
116131
const { disabled, radio, readOnly } = this.props
117132
const { checked } = this.state
118133

119134
return !disabled && !readOnly && !(radio && checked)
120135
}
121136

137+
handleRef = (c) => {
138+
this.checkboxRef = c
139+
}
140+
122141
handleClick = (e) => {
123142
debug('handleClick()')
124143
const { onChange, onClick } = this.props
125-
const { checked } = this.state
144+
const { checked, indeterminate } = this.state
126145

127146
if (this.canToggle()) {
128-
if (onClick) onClick(e, { ...this.props, checked: !!checked })
129-
if (onChange) onChange(e, { ...this.props, checked: !checked })
147+
if (onClick) onClick(e, { ...this.props, checked: !!checked, indeterminate: !!indeterminate })
148+
if (onChange) onChange(e, { ...this.props, checked: !checked, indeterminate: false })
130149

131-
this.trySetState({ checked: !checked })
150+
this.trySetState({ checked: !checked, indeterminate: false })
132151
}
133152
}
134153

154+
// Note: You can't directly set the indeterminate prop on the input, so we
155+
// need to maintain a ref to the input and set it manually whenever the
156+
// component updates.
157+
setIndeterminate = () => {
158+
const { indeterminate } = this.state
159+
if (this.checkboxRef) this.checkboxRef.indeterminate = !!indeterminate
160+
}
161+
135162
render() {
136163
const {
137164
className,
@@ -145,12 +172,13 @@ export default class Checkbox extends Component {
145172
type,
146173
value,
147174
} = this.props
148-
const { checked } = this.state
175+
const { checked, indeterminate } = this.state
149176

150177
const classes = cx(
151178
'ui',
152179
useKeyOnly(checked, 'checked'),
153180
useKeyOnly(disabled, 'disabled'),
181+
useKeyOnly(indeterminate, 'indeterminate'),
154182
// auto apply fitted class to compact white space when there is no label
155183
// http://semantic-ui.com/modules/checkbox.html#fitted
156184
useKeyOnly(!label, 'fitted'),
@@ -171,6 +199,7 @@ export default class Checkbox extends Component {
171199
className='hidden'
172200
name={name}
173201
readOnly
202+
ref={this.handleRef}
174203
tabIndex={0}
175204
type={type}
176205
value={value}

test/specs/modules/Checkbox/Checkbox-test.js

+56-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import React from 'react'
2+
import { findDOMNode } from 'react-dom'
23

34
import Checkbox from 'src/modules/Checkbox/Checkbox'
45
import * as common from 'test/specs/commonTests'
@@ -45,6 +46,57 @@ describe('Checkbox', () => {
4546
})
4647
})
4748

49+
describe('indeterminate', () => {
50+
it('can be indeterminate', () => {
51+
const wrapper = mount(<Checkbox indeterminate />)
52+
53+
const checkboxNode = findDOMNode(wrapper.instance())
54+
const input = checkboxNode.querySelector('input')
55+
56+
input.indeterminate.should.be.true()
57+
58+
wrapper.simulate('click').find('input')
59+
input.indeterminate.should.be.true()
60+
})
61+
it('can not be indeterminate', () => {
62+
const wrapper = mount(<Checkbox indeterminate={false} />)
63+
64+
const checkboxNode = findDOMNode(wrapper.instance())
65+
const input = checkboxNode.querySelector('input')
66+
67+
input.indeterminate.should.be.false()
68+
69+
wrapper.simulate('click').find('input')
70+
input.indeterminate.should.be.false()
71+
})
72+
})
73+
74+
describe('defaultIndeterminate', () => {
75+
it('sets the initial indeterminate state', () => {
76+
const wrapper = mount(<Checkbox defaultIndeterminate />)
77+
78+
const checkboxNode = findDOMNode(wrapper.instance())
79+
const input = checkboxNode.querySelector('input')
80+
81+
input.indeterminate.should.be.true()
82+
})
83+
84+
it('unsets indeterminate state on any click', () => {
85+
const wrapper = mount(<Checkbox defaultIndeterminate />)
86+
87+
const checkboxNode = findDOMNode(wrapper.instance())
88+
const input = findDOMNode(checkboxNode.querySelector('input'))
89+
90+
input.indeterminate.should.be.true()
91+
92+
wrapper.simulate('click').find('input')
93+
input.indeterminate.should.be.false()
94+
95+
wrapper.simulate('click').find('input')
96+
input.indeterminate.should.be.false()
97+
})
98+
})
99+
48100
describe('disabled', () => {
49101
it('cannot be checked', () => {
50102
shallow(<Checkbox disabled />)
@@ -70,14 +122,15 @@ describe('Checkbox', () => {
70122
describe('onChange', () => {
71123
it('is called with (event { name, value, !checked }) on click', () => {
72124
const spy = sandbox.spy()
73-
const expectProps = { name: 'foo', value: 'bar', checked: false }
125+
const expectProps = { name: 'foo', value: 'bar', checked: false, indeterminate: true }
74126
mount(<Checkbox onChange={spy} {...expectProps} />)
75127
.simulate('click')
76128

77129
spy.should.have.been.calledOnce()
78130
spy.should.have.been.calledWithMatch({}, {
79131
...expectProps,
80132
checked: !expectProps.checked,
133+
indeterminate: false,
81134
})
82135
})
83136
it('is not called when the checkbox has the disabled prop set', () => {
@@ -90,14 +143,15 @@ describe('Checkbox', () => {
90143
describe('onClick', () => {
91144
it('is called with (event { name, value, checked }) on label click', () => {
92145
const spy = sandbox.spy()
93-
const expectProps = { name: 'foo', value: 'bar', checked: false }
146+
const expectProps = { name: 'foo', value: 'bar', checked: false, indeterminate: true }
94147
mount(<Checkbox onClick={spy} {...expectProps} />)
95148
.simulate('click')
96149

97150
spy.should.have.been.calledOnce()
98151
spy.should.have.been.calledWithMatch({}, {
99152
...expectProps,
100153
checked: expectProps.checked,
154+
indeterminate: expectProps.indeterminate,
101155
})
102156
})
103157
it('is not called when the checkbox has the disabled prop set', () => {

0 commit comments

Comments
 (0)