Skip to content

Commit cc7d9ee

Browse files
willb335levithomason
authored andcommitted
fix(Dropdown): stop onOpen from firing twice on icon click (Semantic-Org#2744)
* fix(Dropdown): stop onOpen from firing twice on icon click * test(Dropdown): test onOpen and check it fires once with search prop
1 parent f7f0cce commit cc7d9ee

File tree

2 files changed

+333
-549
lines changed

2 files changed

+333
-549
lines changed

src/modules/Dropdown/Dropdown.js

+65-121
Original file line numberDiff line numberDiff line change
@@ -43,10 +43,7 @@ export default class Dropdown extends Component {
4343
as: customPropTypes.as,
4444

4545
/** Label prefixed to an option added by a user. */
46-
additionLabel: PropTypes.oneOfType([
47-
PropTypes.element,
48-
PropTypes.string,
49-
]),
46+
additionLabel: PropTypes.oneOfType([PropTypes.element, PropTypes.string]),
5047

5148
/** Position of the `Add: ...` option in the dropdown list ('top' or 'bottom'). */
5249
additionPosition: PropTypes.oneOf(['top', 'bottom']),
@@ -55,10 +52,7 @@ export default class Dropdown extends Component {
5552
* Allow user additions to the list of options (boolean).
5653
* Requires the use of `selection`, `options` and `search`.
5754
*/
58-
allowAdditions: customPropTypes.every([
59-
customPropTypes.demand(['options', 'selection', 'search']),
60-
PropTypes.bool,
61-
]),
55+
allowAdditions: customPropTypes.every([customPropTypes.demand(['options', 'selection', 'search']), PropTypes.bool]),
6256

6357
/** A Dropdown can reduce its complexity. */
6458
basic: PropTypes.bool,
@@ -69,10 +63,7 @@ export default class Dropdown extends Component {
6963
/** Primary content. */
7064
children: customPropTypes.every([
7165
customPropTypes.disallow(['options', 'selection']),
72-
customPropTypes.givenProps(
73-
{ children: PropTypes.any.isRequired },
74-
PropTypes.element.isRequired,
75-
),
66+
customPropTypes.givenProps({ children: PropTypes.any.isRequired }, PropTypes.element.isRequired),
7667
]),
7768

7869
/** Additional classes. */
@@ -103,20 +94,14 @@ export default class Dropdown extends Component {
10394
/** Currently selected label in multi-select. */
10495
defaultSelectedLabel: customPropTypes.every([
10596
customPropTypes.demand(['multiple']),
106-
PropTypes.oneOfType([
107-
PropTypes.number,
108-
PropTypes.string,
109-
]),
97+
PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
11098
]),
11199

112100
/** Initial value or value array if multiple. */
113101
defaultValue: PropTypes.oneOfType([
114102
PropTypes.number,
115103
PropTypes.string,
116-
PropTypes.arrayOf(PropTypes.oneOfType([
117-
PropTypes.string,
118-
PropTypes.number,
119-
])),
104+
PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string, PropTypes.number])),
120105
]),
121106

122107
/** A dropdown menu can open to the left or to the right. */
@@ -138,10 +123,7 @@ export default class Dropdown extends Component {
138123
header: PropTypes.node,
139124

140125
/** Shorthand for Icon. */
141-
icon: PropTypes.oneOfType([
142-
PropTypes.node,
143-
PropTypes.object,
144-
]),
126+
icon: PropTypes.oneOfType([PropTypes.node, PropTypes.object]),
145127

146128
/** A dropdown can be formatted to appear inline in other content. */
147129
inline: PropTypes.bool,
@@ -283,17 +265,10 @@ export default class Dropdown extends Component {
283265
* A selection dropdown can allow a user to search through a large list of choices.
284266
* Pass a function here to replace the default search.
285267
*/
286-
search: PropTypes.oneOfType([
287-
PropTypes.bool,
288-
PropTypes.func,
289-
]),
268+
search: PropTypes.oneOfType([PropTypes.bool, PropTypes.func]),
290269

291270
/** A shorthand for a search input. */
292-
searchInput: PropTypes.oneOfType([
293-
PropTypes.array,
294-
PropTypes.node,
295-
PropTypes.object,
296-
]),
271+
searchInput: PropTypes.oneOfType([PropTypes.array, PropTypes.node, PropTypes.object]),
297272

298273
/** Current value of searchQuery. Creates a controlled component. */
299274
searchQuery: PropTypes.string,
@@ -312,10 +287,7 @@ export default class Dropdown extends Component {
312287
/** Currently selected label in multi-select. */
313288
selectedLabel: customPropTypes.every([
314289
customPropTypes.demand(['multiple']),
315-
PropTypes.oneOfType([
316-
PropTypes.string,
317-
PropTypes.number,
318-
]),
290+
PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
319291
]),
320292

321293
/** A dropdown can be used to select between choices in a form. */
@@ -329,30 +301,20 @@ export default class Dropdown extends Component {
329301
simple: PropTypes.bool,
330302

331303
/** A dropdown can receive focus. */
332-
tabIndex: PropTypes.oneOfType([
333-
PropTypes.number,
334-
PropTypes.string,
335-
]),
304+
tabIndex: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
336305

337306
/** The text displayed in the dropdown, usually for the active item. */
338307
text: PropTypes.string,
339308

340309
/** Custom element to trigger the menu to become visible. Takes place of 'text'. */
341-
trigger: customPropTypes.every([
342-
customPropTypes.disallow(['selection', 'text']),
343-
PropTypes.node,
344-
]),
310+
trigger: customPropTypes.every([customPropTypes.disallow(['selection', 'text']), PropTypes.node]),
345311

346312
/** Current value or value array if multiple. Creates a controlled component. */
347313
value: PropTypes.oneOfType([
348314
PropTypes.bool,
349315
PropTypes.string,
350316
PropTypes.number,
351-
PropTypes.arrayOf(PropTypes.oneOfType([
352-
PropTypes.bool,
353-
PropTypes.string,
354-
PropTypes.number,
355-
])),
317+
PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.bool, PropTypes.string, PropTypes.number])),
356318
]),
357319

358320
/** A dropdown can open upward. */
@@ -381,12 +343,7 @@ export default class Dropdown extends Component {
381343
wrapSelection: true,
382344
}
383345

384-
static autoControlledProps = [
385-
'open',
386-
'searchQuery',
387-
'selectedLabel',
388-
'value',
389-
]
346+
static autoControlledProps = ['open', 'searchQuery', 'selectedLabel', 'value']
390347

391348
static _meta = {
392349
name: 'Dropdown',
@@ -430,12 +387,12 @@ export default class Dropdown extends Component {
430387
if (hasValue && nextProps.multiple && !isNextValueArray) {
431388
console.error(
432389
'Dropdown `value` must be an array when `multiple` is set.' +
433-
` Received type: \`${Object.prototype.toString.call(nextProps.value)}\`.`,
390+
` Received type: \`${Object.prototype.toString.call(nextProps.value)}\`.`,
434391
)
435392
} else if (hasValue && !nextProps.multiple && isNextValueArray) {
436393
console.error(
437394
'Dropdown `value` must not be an array when `multiple` is not set.' +
438-
' Either set `multiple={true}` or use a string or number value.',
395+
' Either set `multiple={true}` or use a string or number value.',
439396
)
440397
}
441398
}
@@ -456,7 +413,8 @@ export default class Dropdown extends Component {
456413
return !shallowEqual(nextProps, this.props) || !shallowEqual(nextState, this.state)
457414
}
458415

459-
componentDidUpdate(prevProps, prevState) { // eslint-disable-line complexity
416+
componentDidUpdate(prevProps, prevState) {
417+
// eslint-disable-line complexity
460418
debug('componentDidUpdate()')
461419
debug('to state:', objectDiff(prevState, this.state))
462420

@@ -465,7 +423,7 @@ export default class Dropdown extends Component {
465423
debug('dropdown focused')
466424
if (!this.isMouseDown) {
467425
const { minCharacters, openOnFocus, search } = this.props
468-
const openable = !search || (search && minCharacters === 1)
426+
const openable = !search || (search && minCharacters === 1 && !this.state.open)
469427

470428
debug('mouse is not down, opening')
471429
if (openOnFocus && openable) this.open()
@@ -500,11 +458,7 @@ export default class Dropdown extends Component {
500458
} else if (prevState.open && !this.state.open) {
501459
debug('dropdown closed')
502460
this.handleClose()
503-
eventStack.unsub('keydown', [
504-
this.closeOnEscape,
505-
this.moveSelectionOnKeyDown,
506-
this.selectItemOnEnter,
507-
])
461+
eventStack.unsub('keydown', [this.closeOnEscape, this.moveSelectionOnKeyDown, this.selectItemOnEnter])
508462
eventStack.unsub('click', this.closeOnDocumentClick)
509463
if (!this.state.focus) {
510464
eventStack.unsub('keydown', this.removeItemOnBackspace)
@@ -539,9 +493,7 @@ export default class Dropdown extends Component {
539493

540494
closeOnChange = (e) => {
541495
const { closeOnChange, multiple } = this.props
542-
const shouldClose = _.isUndefined(closeOnChange)
543-
? !multiple
544-
: closeOnChange
496+
const shouldClose = _.isUndefined(closeOnChange) ? !multiple : closeOnChange
545497

546498
if (shouldClose) this.close(e)
547499
}
@@ -831,8 +783,7 @@ export default class Dropdown extends Component {
831783

832784
const re = new RegExp(_.escapeRegExp(strippedQuery), 'i')
833785

834-
filteredOptions = _.filter(filteredOptions, opt =>
835-
re.test(deburr ? _.deburr(opt.text) : opt.text))
786+
filteredOptions = _.filter(filteredOptions, opt => re.test(deburr ? _.deburr(opt.text) : opt.text))
836787
}
837788
}
838789

@@ -846,10 +797,7 @@ export default class Dropdown extends Component {
846797
key: 'addition',
847798
// by using an array, we can pass multiple elements, but when doing so
848799
// we must specify a `key` for React to know which one is which
849-
text: [
850-
additionLabelElement,
851-
<b key='addition-query'>{searchQuery}</b>,
852-
],
800+
text: [additionLabelElement, <b key='addition-query'>{searchQuery}</b>],
853801
value: searchQuery,
854802
className: 'addition',
855803
'data-additional': true,
@@ -871,10 +819,14 @@ export default class Dropdown extends Component {
871819
getEnabledIndices = (givenOptions) => {
872820
const options = givenOptions || this.getMenuOptions()
873821

874-
return _.reduce(options, (memo, item, index) => {
875-
if (!item.disabled) memo.push(index)
876-
return memo
877-
}, [])
822+
return _.reduce(
823+
options,
824+
(memo, item, index) => {
825+
if (!item.disabled) memo.push(index)
826+
return memo
827+
},
828+
[],
829+
)
878830
}
879831

880832
getItemByValue = (value) => {
@@ -944,9 +896,7 @@ export default class Dropdown extends Component {
944896
// Select the currently active item, if none, use the first item.
945897
// Multiple selects remove active items from the list,
946898
// their initial selected index should be 0.
947-
newSelectedIndex = multiple
948-
? firstIndex
949-
: this.getMenuItemIndexByValue(value, options) || enabledIndicies[0]
899+
newSelectedIndex = multiple ? firstIndex : this.getMenuItemIndexByValue(value, options) || enabledIndicies[0]
950900
} else if (multiple) {
951901
// multiple selects remove options from the menu as they are made active
952902
// keep the selected index within range of the remaining items
@@ -1096,12 +1046,13 @@ export default class Dropdown extends Component {
10961046
debug(`menu: ${menu}`)
10971047
debug(`item: ${item}`)
10981048
const isOutOfUpperView = item.offsetTop < menu.scrollTop
1099-
const isOutOfLowerView = (item.offsetTop + item.clientHeight) > menu.scrollTop + menu.clientHeight
1049+
const isOutOfLowerView = item.offsetTop + item.clientHeight > menu.scrollTop + menu.clientHeight
11001050

11011051
if (isOutOfUpperView) {
11021052
menu.scrollTop = item.offsetTop
11031053
} else if (isOutOfLowerView) {
1104-
menu.scrollTop = (item.offsetTop + item.clientHeight) - menu.clientHeight
1054+
// eslint-disable-next-line no-mixed-operators
1055+
menu.scrollTop = item.offsetTop + item.clientHeight - menu.clientHeight
11051056
}
11061057
}
11071058

@@ -1152,15 +1103,9 @@ export default class Dropdown extends Component {
11521103
renderText = () => {
11531104
const { multiple, placeholder, search, text } = this.props
11541105
const { searchQuery, value, open } = this.state
1155-
const hasValue = multiple
1156-
? !_.isEmpty(value)
1157-
: !_.isNil(value) && value !== ''
1106+
const hasValue = multiple ? !_.isEmpty(value) : !_.isNil(value) && value !== ''
11581107

1159-
const classes = cx(
1160-
placeholder && !hasValue && 'default',
1161-
'text',
1162-
search && searchQuery && 'filtered',
1163-
)
1108+
const classes = cx(placeholder && !hasValue && 'default', 'text', search && searchQuery && 'filtered')
11641109
let _text = placeholder
11651110
if (searchQuery) {
11661111
_text = null
@@ -1172,21 +1117,27 @@ export default class Dropdown extends Component {
11721117
_text = _.get(this.getItemByValue(value), 'text')
11731118
}
11741119

1175-
return <div className={classes} role='alert' aria-live='polite'>{_text}</div>
1120+
return (
1121+
<div className={classes} role='alert' aria-live='polite'>
1122+
{_text}
1123+
</div>
1124+
)
11761125
}
11771126

11781127
renderSearchInput = () => {
11791128
const { search, searchInput } = this.props
11801129
const { searchQuery } = this.state
11811130

11821131
if (!search) return null
1183-
return DropdownSearchInput.create(searchInput, { defaultProps: {
1184-
inputRef: this.handleSearchRef,
1185-
onChange: this.handleSearchChange,
1186-
style: { width: this.computeSearchInputWidth() },
1187-
tabIndex: this.computeSearchInputTabIndex(),
1188-
value: searchQuery,
1189-
} })
1132+
return DropdownSearchInput.create(searchInput, {
1133+
defaultProps: {
1134+
inputRef: this.handleSearchRef,
1135+
onChange: this.handleSearchChange,
1136+
style: { width: this.computeSearchInputWidth() },
1137+
tabIndex: this.computeSearchInputTabIndex(),
1138+
value: searchQuery,
1139+
},
1140+
})
11901141
}
11911142

11921143
renderSearchSizer = () => {
@@ -1218,10 +1169,7 @@ export default class Dropdown extends Component {
12181169
value: item.value,
12191170
}
12201171

1221-
return Label.create(
1222-
renderLabel(item, index, defaultProps),
1223-
{ defaultProps },
1224-
)
1172+
return Label.create(renderLabel(item, index, defaultProps), { defaultProps })
12251173
})
12261174
}
12271175

@@ -1234,19 +1182,19 @@ export default class Dropdown extends Component {
12341182
return <div className='message'>{noResultsMessage}</div>
12351183
}
12361184

1237-
const isActive = multiple
1238-
? optValue => _.includes(value, optValue)
1239-
: optValue => optValue === value
1240-
1241-
return _.map(options, (opt, i) => DropdownItem.create({
1242-
active: isActive(opt.value),
1243-
onClick: this.handleItemClick,
1244-
selected: selectedIndex === i,
1245-
...opt,
1246-
key: getKeyOrValue(opt.key, opt.value),
1247-
// Needed for handling click events on disabled items
1248-
style: { ...opt.style, pointerEvents: 'all' },
1249-
}))
1185+
const isActive = multiple ? optValue => _.includes(value, optValue) : optValue => optValue === value
1186+
1187+
return _.map(options, (opt, i) =>
1188+
DropdownItem.create({
1189+
active: isActive(opt.value),
1190+
onClick: this.handleItemClick,
1191+
selected: selectedIndex === i,
1192+
...opt,
1193+
key: getKeyOrValue(opt.key, opt.value),
1194+
// Needed for handling click events on disabled items
1195+
style: { ...opt.style, pointerEvents: 'all' },
1196+
}),
1197+
)
12501198
}
12511199

12521200
renderMenu = () => {
@@ -1257,11 +1205,7 @@ export default class Dropdown extends Component {
12571205
// single menu child
12581206
if (!childrenUtils.isNil(children)) {
12591207
const menuChild = Children.only(children)
1260-
const className = cx(
1261-
direction,
1262-
useKeyOnly(open, 'visible'),
1263-
menuChild.props.className,
1264-
)
1208+
const className = cx(direction, useKeyOnly(open, 'visible'), menuChild.props.className)
12651209

12661210
return cloneElement(menuChild, { className, ...ariaOptions })
12671211
}

0 commit comments

Comments
 (0)