Skip to content

Commit aaee3b1

Browse files
Fabianopblayershifter
authored andcommitted
fix(Checkbox): prevent click propagation from the input element (#3435)
* fix(Checkbox): Prevents click propagation from the input (#3433) * fix(Checkbox) Create tests for click on input and label with and without id (#3433) * fix(Checkbox): Create handler for input click (#3433) * fix(Checkbox): Improve tests readability (#3433) * updates * updates * improve coverage * remove target
1 parent 5c7766f commit aaee3b1

File tree

4 files changed

+142
-83
lines changed

4 files changed

+142
-83
lines changed

src/modules/Checkbox/Checkbox.js

+49-24
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import cx from 'classnames'
22
import _ from 'lodash'
33
import PropTypes from 'prop-types'
4-
import React from 'react'
4+
import React, { createRef } from 'react'
55

66
import {
77
AutoControlledComponent as Component,
@@ -117,6 +117,9 @@ export default class Checkbox extends Component {
117117

118118
static autoControlledProps = ['checked', 'indeterminate']
119119

120+
inputRef = createRef()
121+
labelRef = createRef()
122+
120123
componentDidMount() {
121124
this.setIndeterminate()
122125
}
@@ -139,29 +142,50 @@ export default class Checkbox extends Component {
139142
return disabled ? -1 : 0
140143
}
141144

142-
handleInputRef = c => (this.inputRef = c)
143-
144-
handleChange = (e, fromMouseUp) => {
145-
debug('handleChange()')
145+
handleClick = (e) => {
146+
debug('handleClick()', _.get(e, 'target.tagName'))
146147
const { id } = this.props
147148
const { checked, indeterminate } = this.state
148149

150+
const hasId = !_.isNil(id)
151+
const isLabelClick = e.target === this.labelRef.current
152+
const isLabelClickAndForwardedToInput = isLabelClick && hasId
153+
154+
// https://github.com/Semantic-Org/Semantic-UI-React/pull/3351
155+
if (!isLabelClickAndForwardedToInput) {
156+
_.invoke(this.props, 'onClick', e, {
157+
...this.props,
158+
checked: !checked,
159+
indeterminate: !!indeterminate,
160+
})
161+
}
162+
163+
if (this.isClickFromMouse) {
164+
this.isClickFromMouse = false
165+
166+
if (isLabelClick && !hasId) {
167+
this.handleChange(e)
168+
}
169+
170+
if (hasId) {
171+
// To prevent two clicks from being fired from the component we have to stop the propagation
172+
// from the "input" click: https://github.com/Semantic-Org/Semantic-UI-React/issues/3433
173+
e.stopPropagation()
174+
}
175+
}
176+
}
177+
178+
handleChange = (e) => {
179+
const { checked } = this.state
180+
149181
if (!this.canToggle()) return
150-
if (fromMouseUp && !_.isNil(id)) return
182+
debug('handleChange()', _.get(e, 'target.tagName'))
151183

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.
154-
_.invoke(this.props, 'onClick', e, {
155-
...this.props,
156-
checked: !checked,
157-
indeterminate: !!indeterminate,
158-
})
159184
_.invoke(this.props, 'onChange', e, {
160185
...this.props,
161186
checked: !checked,
162187
indeterminate: false,
163188
})
164-
165189
this.trySetState({ checked: !checked, indeterminate: false })
166190
}
167191

@@ -174,24 +198,23 @@ export default class Checkbox extends Component {
174198
checked: !!checked,
175199
indeterminate: !!indeterminate,
176200
})
177-
_.invoke(this.inputRef, 'focus')
178201

202+
_.invoke(this.inputRef.current, 'focus')
203+
// Heads up!
204+
// We need to call "preventDefault" to keep element focused.
179205
e.preventDefault()
180206
}
181207

182208
handleMouseUp = (e) => {
183209
debug('handleMouseUp()')
184210
const { checked, indeterminate } = this.state
185211

212+
this.isClickFromMouse = true
186213
_.invoke(this.props, 'onMouseUp', e, {
187214
...this.props,
188215
checked: !!checked,
189216
indeterminate: !!indeterminate,
190217
})
191-
192-
// Handle mouseUp only on the left mouse button.
193-
// https://github.com/Semantic-Org/Semantic-UI-React/issues/3419
194-
if (e.button === 0) this.handleChange(e, true)
195218
}
196219

197220
// Note: You can't directly set the indeterminate prop on the input, so we
@@ -200,7 +223,7 @@ export default class Checkbox extends Component {
200223
setIndeterminate = () => {
201224
const { indeterminate } = this.state
202225

203-
if (this.inputRef) this.inputRef.indeterminate = !!indeterminate
226+
_.set(this.inputRef, 'current.indeterminate', !!indeterminate)
204227
}
205228

206229
render() {
@@ -242,6 +265,7 @@ export default class Checkbox extends Component {
242265
<ElementType
243266
{...rest}
244267
className={classes}
268+
onClick={this.handleClick}
245269
onChange={this.handleChange}
246270
onMouseDown={this.handleMouseDown}
247271
onMouseUp={this.handleMouseUp}
@@ -254,7 +278,7 @@ export default class Checkbox extends Component {
254278
id={id}
255279
name={name}
256280
readOnly
257-
ref={this.handleInputRef}
281+
ref={this.inputRef}
258282
tabIndex={this.computeTabIndex()}
259283
type={type}
260284
value={value}
@@ -263,9 +287,10 @@ export default class Checkbox extends Component {
263287
Heads Up!
264288
Do not remove empty labels, they are required by SUI CSS
265289
*/}
266-
{createHTMLLabel(label, { defaultProps: { htmlFor: id }, autoGenerateKey: false }) || (
267-
<label htmlFor={id} />
268-
)}
290+
{createHTMLLabel(label, {
291+
defaultProps: { htmlFor: id, ref: this.labelRef },
292+
autoGenerateKey: false,
293+
}) || <label htmlFor={id} ref={this.labelRef} />}
269294
</ElementType>
270295
)
271296
}

test/specs/commonTests/isConformant.js

+1-3
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ 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.
1716
* @param {Object} [options.eventTargets={}] Map of events and the child component to target.
1817
* @param {Number} [options.nestingLevel=0] The nesting level of the component.
1918
* @param {boolean} [options.rendersChildren=false] Does this component render any children?
@@ -23,7 +22,6 @@ import hasValidTypings from './hasValidTypings'
2322
*/
2423
export default (Component, options = {}) => {
2524
const {
26-
disabledHandlers = [],
2725
eventTargets = {},
2826
nestingLevel = 0,
2927
requiredProps = {},
@@ -220,7 +218,7 @@ export default (Component, options = {}) => {
220218
// This test catches the case where a developer forgot to call the event prop
221219
// after handling it internally. It also catch cases where the synthetic event was not passed back.
222220
_.each(syntheticEvent.types, ({ eventShape, listeners }) => {
223-
_.each(_.without(listeners, ...disabledHandlers), (listenerName) => {
221+
_.each(listeners, (listenerName) => {
224222
// onKeyDown => keyDown
225223
const eventName = _.camelCase(listenerName.replace('on', ''))
226224

0 commit comments

Comments
 (0)