Skip to content

Commit 9ca7bc3

Browse files
Fabianopblayershifter
authored andcommitted
fix(Checkbox): prevent onClick from being called twice (#3351)
* fix(Checkbox): Let click handler call onClick to avoid duplicate calls (#3348) * fix(Checkbox): Fix test to call onClick from a click event (#3348) * fix(Checkbox): Move onClick call completely to handleChange (#3348). * fix(Checkbox): Create tests for DOM Comparisons (#3348). * revert change * Update Checkbox-test.js * Update Checkbox-test.js * Update isConformant.js * fix(Checkbox): Fix typo in handleClick comment (#3348). * fix(Checkbox): Add tests for controlled component with setState as function (#3348). * fix(Checkbox): ensure onClick is called * fix(Checkbox): Fire DOM event on controlled component tests to emulate the real behavior (#3348). * fix(Checkbox): Completely remove click handler (#3348). * small cleanup
1 parent cda2a2f commit 9ca7bc3

File tree

3 files changed

+84
-16
lines changed

3 files changed

+84
-16
lines changed

src/modules/Checkbox/Checkbox.js

+7-12
Original file line numberDiff line numberDiff line change
@@ -149,26 +149,22 @@ export default class Checkbox extends Component {
149149
if (!this.canToggle()) return
150150
if (fromMouseUp && !_.isNil(id)) return
151151

152+
// We don't have a separate click handler as it's already called in here,
153+
// and also to avoid duplicate calls, matching all DOM Checkbox comparisons.
152154
_.invoke(this.props, 'onClick', e, {
153155
...this.props,
154156
checked: !checked,
155157
indeterminate: !!indeterminate,
156158
})
157-
_.invoke(this.props, 'onChange', e, { ...this.props, checked: !checked, indeterminate: false })
159+
_.invoke(this.props, 'onChange', e, {
160+
...this.props,
161+
checked: !checked,
162+
indeterminate: false,
163+
})
158164

159165
this.trySetState({ checked: !checked, indeterminate: false })
160166
}
161167

162-
handleClick = (e) => {
163-
// We handle onClick in onChange if it is provided, to preserve proper call order.
164-
// Don't call onClick twice if their is already an onChange handler, it calls onClick.
165-
// https://github.com/Semantic-Org/Semantic-UI-React/pull/2748
166-
const { onChange, onClick } = this.props
167-
if (onChange || !onClick) return
168-
169-
onClick(e, this.props)
170-
}
171-
172168
handleMouseDown = (e) => {
173169
debug('handleMouseDown()')
174170
const { checked, indeterminate } = this.state
@@ -244,7 +240,6 @@ export default class Checkbox extends Component {
244240
{...rest}
245241
className={classes}
246242
onChange={this.handleChange}
247-
onClick={this.handleClick}
248243
onMouseDown={this.handleMouseDown}
249244
onMouseUp={this.handleMouseUp}
250245
>

test/specs/commonTests/isConformant.js

+3-1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import hasValidTypings from './hasValidTypings'
1313
* Assert Component conforms to guidelines that are applicable to all components.
1414
* @param {React.Component|Function} Component A component that should conform.
1515
* @param {Object} [options={}]
16+
* @param {String[]} [options.disabledHandlers=[]] An array of listeners that are disabled.
1617
* @param {Object} [options.eventTargets={}] Map of events and the child component to target.
1718
* @param {Number} [options.nestingLevel=0] The nesting level of the component.
1819
* @param {boolean} [options.rendersChildren=false] Does this component render any children?
@@ -22,6 +23,7 @@ import hasValidTypings from './hasValidTypings'
2223
*/
2324
export default (Component, options = {}) => {
2425
const {
26+
disabledHandlers = [],
2527
eventTargets = {},
2628
nestingLevel = 0,
2729
requiredProps = {},
@@ -218,7 +220,7 @@ export default (Component, options = {}) => {
218220
// This test catches the case where a developer forgot to call the event prop
219221
// after handling it internally. It also catch cases where the synthetic event was not passed back.
220222
_.each(syntheticEvent.types, ({ eventShape, listeners }) => {
221-
_.each(listeners, (listenerName) => {
223+
_.each(_.without(listeners, ...disabledHandlers), (listenerName) => {
222224
// onKeyDown => keyDown
223225
const eventName = _.camelCase(listenerName.replace('on', ''))
224226

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

+74-3
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,9 @@ const wrapperMount = (element, opts) => {
2323
const wrapperShallow = (...args) => (wrapper = shallow(...args))
2424

2525
describe('Checkbox', () => {
26-
common.isConformant(Checkbox)
26+
common.isConformant(Checkbox, {
27+
disabledHandlers: ['onClick'],
28+
})
2729
common.hasUIClassName(Checkbox)
2830

2931
common.propKeyOnlyToClassName(Checkbox, 'checked')
@@ -239,7 +241,7 @@ describe('Checkbox', () => {
239241
})
240242

241243
describe('onClick', () => {
242-
it('is called with (event, data) on mouse up', () => {
244+
it('is called with (event, data) on mouseup', () => {
243245
const onClick = sandbox.spy()
244246
const props = { name: 'foo', value: 'bar', checked: false, indeterminate: true }
245247
mount(<Checkbox onClick={onClick} {...props} />).simulate('mouseup')
@@ -254,7 +256,7 @@ describe('Checkbox', () => {
254256
)
255257
})
256258

257-
it('is not called when on change when "id" is passed', () => {
259+
it('is not called when "id" is passed', () => {
258260
const onClick = sandbox.spy()
259261
mount(<Checkbox id='foo' onClick={onClick} />).simulate('mouseup')
260262

@@ -352,4 +354,73 @@ describe('Checkbox', () => {
352354
.should.have.prop('type', 'radio')
353355
})
354356
})
357+
358+
describe('comparisons with native DOM', () => {
359+
const assertMatrix = [
360+
{
361+
description: 'click on label: fires on mouse up',
362+
event: 'mouseup',
363+
target: 'label',
364+
},
365+
{
366+
description: 'key on input: fires on space key',
367+
event: 'click',
368+
target: 'input',
369+
},
370+
371+
{
372+
description: 'click on label: fires on mouse click',
373+
event: 'click',
374+
target: 'label',
375+
id: 'foo',
376+
},
377+
]
378+
379+
assertMatrix.forEach(({ description, event, target, ...props }) => {
380+
it(description, () => {
381+
const dataId = _.uniqueId('checkbox')
382+
const selector = `[data-id=${dataId}] ${target}`
383+
384+
const onClick = sandbox.spy()
385+
const onChange = sandbox.spy()
386+
387+
wrapperMount(
388+
<Checkbox {...props} data-id={dataId} onClick={onClick} onChange={onChange} />,
389+
{ attachTo },
390+
)
391+
domEvent.fire(selector, event)
392+
393+
onClick.should.have.been.calledOnce()
394+
onChange.should.have.been.calledOnce()
395+
396+
onChange.should.have.been.calledAfter(onClick)
397+
})
398+
})
399+
})
400+
401+
describe('Controlled component', () => {
402+
const getControlledCheckbox = isOnClick =>
403+
class ControlledCheckbox extends React.Component {
404+
state = { checked: false }
405+
toggle = () => this.setState(prevState => ({ checked: !prevState.checked }))
406+
render() {
407+
const handler = isOnClick ? { onClick: this.toggle } : { onChange: this.toggle }
408+
return <Checkbox label='Check this box' checked={this.state.checked} {...handler} />
409+
}
410+
}
411+
412+
it('toggles state on "change" with "setState" as function', () => {
413+
const ControlledCheckbox = getControlledCheckbox(false)
414+
wrapperMount(<ControlledCheckbox />)
415+
domEvent.fire(document.querySelector('input'), 'click')
416+
wrapper.state().should.eql({ checked: true })
417+
})
418+
419+
it('toggles state on "click" with "setState" as function', () => {
420+
const ControlledCheckbox = getControlledCheckbox(true)
421+
wrapperMount(<ControlledCheckbox />)
422+
domEvent.fire(document.querySelector('input'), 'click')
423+
wrapper.state().should.eql({ checked: true })
424+
})
425+
})
355426
})

0 commit comments

Comments
 (0)