Skip to content

Commit 5668742

Browse files
committed
Add option group support to Select and OptionMenuList
* OptionMenuListItem has a new prop, `type`, which can be set to either `option` (the default) or `heading` (an option group heading). The usual OptionMenuListItem event handlers and icons are ignored for option group headings. * OptionMenuList now accepts arrays of child options as option values; these values will be rendered as an option group. The parent item's label will be used as the option group heading. If the parent item is disabled, all children will also be disabled.
1 parent 1f5325c commit 5668742

File tree

9 files changed

+445
-129
lines changed

9 files changed

+445
-129
lines changed

packages/data-grid/src/__test__/__snapshots__/quickFitler.test.jsx.snap

+6
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,7 @@ exports[`Snapshot test Check component matches previous HTML snapshot 1`] = `
252252
onMouseEnter={[Function]}
253253
selected={false}
254254
svg={null}
255+
type="option"
255256
>
256257
<li
257258
aria-selected={false}
@@ -304,6 +305,7 @@ exports[`Snapshot test Check component matches previous HTML snapshot 1`] = `
304305
onMouseEnter={[Function]}
305306
selected={false}
306307
svg={null}
308+
type="option"
307309
>
308310
<li
309311
aria-selected={false}
@@ -356,6 +358,7 @@ exports[`Snapshot test Check component matches previous HTML snapshot 1`] = `
356358
onMouseEnter={[Function]}
357359
selected={false}
358360
svg={null}
361+
type="option"
359362
>
360363
<li
361364
aria-selected={false}
@@ -600,6 +603,7 @@ exports[`Snapshot test Check component matches previous HTML snapshot 1`] = `
600603
onMouseEnter={[Function]}
601604
selected={false}
602605
svg={null}
606+
type="option"
603607
>
604608
<li
605609
aria-selected={false}
@@ -652,6 +656,7 @@ exports[`Snapshot test Check component matches previous HTML snapshot 1`] = `
652656
onMouseEnter={[Function]}
653657
selected={false}
654658
svg={null}
659+
type="option"
655660
>
656661
<li
657662
aria-selected={false}
@@ -704,6 +709,7 @@ exports[`Snapshot test Check component matches previous HTML snapshot 1`] = `
704709
onMouseEnter={[Function]}
705710
selected={false}
706711
svg={null}
712+
type="option"
707713
>
708714
<li
709715
aria-selected={false}

packages/react-components/source/react/helpers/customPropTypes.js

+14
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import PropTypes from 'prop-types';
2+
import Icon from '../library/icon';
23

34
/**
45
* Design system available element elevations
@@ -81,3 +82,16 @@ export const deprecated = message => typeChecker => {
8182
return typeChecker(props, key, componentName, location, propFullName);
8283
};
8384
};
85+
86+
export const optionMenuItemShape = {
87+
/** Value of the option */
88+
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
89+
/** The label to show */
90+
label: PropTypes.node.isRequired,
91+
/** Optional icon associated with this option */
92+
icon: PropTypes.oneOf(Icon.AVAILABLE_ICONS),
93+
/** Optional custom icon associated with this option */
94+
svg: PropTypes.element,
95+
/** Whether this option is disabled */
96+
disabled: PropTypes.bool,
97+
};

packages/react-components/source/react/internal/option-menu-list/OptionMenuList.js

+116-46
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
33
import classNames from 'classnames';
44
import scrollIntoView from 'scroll-into-view-if-needed';
55
import { isNil, focus, cancelEvent } from '../../helpers/statics';
6+
import { optionMenuItemShape } from '../../helpers/customPropTypes';
67

78
import {
89
UP_KEY_CODE,
@@ -15,21 +16,20 @@ import {
1516
} from '../../constants';
1617

1718
import OptionMenuListItem from './OptionMenuListItem';
18-
import Icon from '../../library/icon';
1919

2020
const propTypes = {
2121
id: PropTypes.string.isRequired,
2222
multiple: PropTypes.bool,
2323
autocomplete: PropTypes.bool,
2424
showCancel: PropTypes.bool,
2525
options: PropTypes.arrayOf(
26-
PropTypes.shape({
27-
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number])
28-
.isRequired,
29-
label: PropTypes.node.isRequired,
30-
icon: PropTypes.oneOf(Icon.AVAILABLE_ICONS),
31-
disabled: PropTypes.bool,
32-
}),
26+
PropTypes.oneOfType([
27+
PropTypes.shape(optionMenuItemShape),
28+
PropTypes.shape({
29+
...optionMenuItemShape,
30+
value: PropTypes.arrayOf(PropTypes.shape(optionMenuItemShape)),
31+
}),
32+
]),
3333
),
3434
selected: PropTypes.oneOfType([
3535
PropTypes.string,
@@ -72,10 +72,16 @@ const defaultProps = {
7272

7373
const getOptionId = (id, value) => `${id}-${value}`;
7474

75+
const getFocusableOptions = options =>
76+
options.map(opt => (Array.isArray(opt.value) ? opt.value : opt)).flat();
77+
7578
const getFocusedId = (focusedIndex, id, options) =>
76-
isNil(focusedIndex) || focusedIndex >= options.length
79+
typeof focusedIndex !== 'number' || focusedIndex >= options.length
7780
? undefined
78-
: getOptionId(id, options[focusedIndex].value);
81+
: getOptionId(
82+
id,
83+
getFocusableOptions(options)[Math.max(focusedIndex, 0)].value,
84+
);
7985

8086
const getSelectionSet = selection =>
8187
new Set(
@@ -134,7 +140,11 @@ class OptionMenuList extends Component {
134140
}
135141

136142
onMouseEnterItem(focusedIndex) {
137-
this.focusItem(focusedIndex);
143+
if (typeof focusedIndex === 'number') {
144+
this.focusItem(focusedIndex);
145+
} else {
146+
this.setState({ focusedIndex: null });
147+
}
138148
}
139149

140150
onCancel(e) {
@@ -161,7 +171,9 @@ class OptionMenuList extends Component {
161171
if (isNil(focusedIndex)) {
162172
this.focusFirst();
163173
} else {
164-
this.focusItem(Math.min(options.length - 1, focusedIndex + 1));
174+
this.focusItem(
175+
Math.min(getFocusableOptions(options).length - 1, focusedIndex + 1),
176+
);
165177
}
166178
}
167179

@@ -192,7 +204,7 @@ class OptionMenuList extends Component {
192204
}
193205
case SPACE_KEY_CODE:
194206
case ENTER_KEY_CODE: {
195-
const focused = options[focusedIndex];
207+
const focused = getFocusableOptions(options)[focusedIndex];
196208
if (focused && !focused.disabled) {
197209
this.selectFocusedItem();
198210
onClickItem();
@@ -251,7 +263,7 @@ class OptionMenuList extends Component {
251263
focusLast() {
252264
const { options } = this.props;
253265

254-
this.focusItem(options.length - 1);
266+
this.focusItem(getFocusableOptions(options).length - 1);
255267
}
256268

257269
focusItem(focusedIndex) {
@@ -296,25 +308,14 @@ class OptionMenuList extends Component {
296308
const { options } = this.props;
297309

298310
if (!isNil(focusedIndex)) {
299-
const { value } = options[focusedIndex];
311+
const { value } = getFocusableOptions(options)[focusedIndex];
300312

301313
this.select(value);
302314
}
303315
}
304316

305317
/* eslint-disable jsx-a11y/click-events-have-key-events */
306318
render() {
307-
const {
308-
onClickItem,
309-
onMouseEnterItem,
310-
onCancel,
311-
onKeyDown,
312-
onKeyDownInAction,
313-
onFocus,
314-
onMenuBlur,
315-
onActionBlur,
316-
} = this;
317-
const { focusedIndex } = this.state;
318319
const {
319320
id,
320321
options,
@@ -329,7 +330,6 @@ class OptionMenuList extends Component {
329330
className,
330331
style,
331332
onBlur,
332-
focusedIndex: focussed,
333333
onFocusItem,
334334
footer,
335335
onClickItem: onClick,
@@ -340,8 +340,95 @@ class OptionMenuList extends Component {
340340
return null;
341341
}
342342

343+
const {
344+
onClickItem,
345+
onMouseEnterItem,
346+
onCancel,
347+
onKeyDown,
348+
onKeyDownInAction,
349+
onFocus,
350+
onMenuBlur,
351+
onActionBlur,
352+
} = this;
353+
343354
const selectionSet = getSelectionSet(selected);
344-
const focusedId = getFocusedId(focusedIndex, id, options);
355+
356+
delete rest.focusedIndex;
357+
const { focusedIndex } = this.state;
358+
const focusedId = getFocusedId(
359+
focusedIndex,
360+
id,
361+
getFocusableOptions(options),
362+
);
363+
364+
const renderListItems = (items, offset = 0) => {
365+
const list = [];
366+
367+
items.forEach(item => {
368+
if (Array.isArray(item.value)) {
369+
const groupId = `group-${item.value
370+
.map(child => child.value)
371+
.join('-')}`;
372+
const labelId = `${groupId}-label`;
373+
374+
list.push(
375+
<ul
376+
role="group"
377+
aria-labelledby={labelId}
378+
className="rc-menu-list-group"
379+
id={groupId}
380+
key={groupId}
381+
>
382+
{item.label && (
383+
<OptionMenuListItem
384+
type="heading"
385+
disabled={item.disabled}
386+
id={labelId}
387+
key={labelId}
388+
onMouseEnter={() => onMouseEnterItem(null)}
389+
>
390+
{item.label}
391+
</OptionMenuListItem>
392+
)}
393+
{renderListItems(
394+
item.value.map(child =>
395+
Object.assign(child, {
396+
disabled: item.disabled || child.disabled,
397+
}),
398+
),
399+
list.length + offset,
400+
)}
401+
</ul>,
402+
);
403+
// eslint-disable-next-line no-param-reassign
404+
offset += item.value.length - 1;
405+
} else {
406+
const index = list.length + offset;
407+
list.push(
408+
<OptionMenuListItem
409+
id={getOptionId(id, item.value)}
410+
key={item.value}
411+
focused={index === focusedIndex}
412+
selected={selectionSet.has(item.value)}
413+
icon={item.icon}
414+
svg={item.svg}
415+
disabled={item.disabled}
416+
onClick={() =>
417+
item.disabled ? undefined : onClickItem(item.value)
418+
}
419+
onMouseEnter={() => onMouseEnterItem(index)}
420+
ref={option => {
421+
this.optionRefs[index] = option;
422+
}}
423+
>
424+
{item.label}
425+
</OptionMenuListItem>,
426+
);
427+
}
428+
});
429+
430+
return list;
431+
};
345432

346433
const list = (
347434
<ul
@@ -359,24 +446,7 @@ class OptionMenuList extends Component {
359446
}}
360447
{...rest}
361448
>
362-
{options.map(({ value, label, icon, svg, disabled }, index) => (
363-
<OptionMenuListItem
364-
id={getOptionId(id, value)}
365-
key={value}
366-
focused={index === focusedIndex}
367-
selected={selectionSet.has(value)}
368-
icon={icon}
369-
svg={svg}
370-
disabled={disabled}
371-
onClick={disabled ? undefined : () => onClickItem(value)}
372-
onMouseEnter={() => onMouseEnterItem(index)}
373-
ref={option => {
374-
this.optionRefs[index] = option;
375-
}}
376-
>
377-
{label}
378-
</OptionMenuListItem>
379-
))}
449+
{renderListItems(options)}
380450
</ul>
381451
);
382452

0 commit comments

Comments
 (0)