Skip to content

Commit 53fe270

Browse files
jpfrancomergify[bot]
authored andcommitted
feat(select-field): Allow rendering custom options (#1516)
* feat(select-field): Allow rendering custom options * fix(select-field): Now scrolling into view after state update
1 parent af49827 commit 53fe270

File tree

3 files changed

+136
-22
lines changed

3 files changed

+136
-22
lines changed

src/components/select-field/BaseSelectField.js

Lines changed: 25 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -43,13 +43,15 @@ type Props = {
4343
onChange: Function,
4444
/** Function will be called with the user selected option (even on deselect or when the option was previously selected) */
4545
onOptionSelect?: Function,
46+
/** Function that allows custom rendering of select field options. When not provided the component will only render the option's displayText by default */
47+
optionRenderer?: (option: SelectOptionProp) => React.Node,
4648
/** List of options (displayText, value) */
4749
options: Array<SelectOptionProp>,
4850
/** The select button text shown when no options are selected. */
4951
placeholder?: string | React.Element<any>,
5052
/** The currently selected option values (can be empty) */
5153
selectedValues: Array<SelectOptionValueProp>,
52-
/** Array of ordered indices indicating where to insert separators (ex. index 2 means insert a separator after option 2) */
54+
/** Array of ordered indices indicating where to insert separators (ex. index 2 means insert a separator after option 2) */
5355
separatorIndices: Array<number>,
5456
/** The select button text (by default, component will use comma separated list of all selected option displayText) */
5557
title?: string | React.Element<any>,
@@ -59,6 +61,7 @@ type State = {
5961
activeItemID: ?string,
6062
activeItemIndex: number,
6163
isOpen: boolean,
64+
shouldScrollIntoView: boolean,
6265
};
6366

6467
class BaseSelectField extends React.Component<Props, State> {
@@ -80,20 +83,26 @@ class BaseSelectField extends React.Component<Props, State> {
8083
activeItemID: null,
8184
activeItemIndex: -1,
8285
isOpen: false,
86+
shouldScrollIntoView: false,
8387
};
8488
}
8589

86-
setActiveItem = (index: number) => {
87-
this.setState({ activeItemIndex: index });
90+
setActiveItem = (index: number, shouldScrollIntoView?: boolean = true) => {
91+
this.setState({ activeItemIndex: index, shouldScrollIntoView });
8892
if (index === -1) {
8993
this.setActiveItemID(null);
9094
}
9195
};
9296

9397
setActiveItemID = (id: ?string) => {
98+
const { shouldScrollIntoView } = this.state;
9499
const itemEl = id ? document.getElementById(id) : null;
95-
this.setState({ activeItemID: id });
96-
scrollIntoView(itemEl);
100+
101+
this.setState({ activeItemID: id, shouldScrollIntoView: false }, () => {
102+
if (shouldScrollIntoView) {
103+
scrollIntoView(itemEl, { block: 'nearest' });
104+
}
105+
});
97106
};
98107

99108
selectFieldID: string;
@@ -304,7 +313,7 @@ class BaseSelectField extends React.Component<Props, State> {
304313
};
305314

306315
renderSelectOptions = () => {
307-
const { options, selectedValues, separatorIndices } = this.props;
316+
const { optionRenderer, options, selectedValues, separatorIndices } = this.props;
308317
const { activeItemIndex } = this.state;
309318

310319
const selectOptions = options.map<React.Element<typeof DatalistItem | 'li'>>((item, index) => {
@@ -321,12 +330,8 @@ class BaseSelectField extends React.Component<Props, State> {
321330

322331
this.selectOption(index);
323332
},
324-
/* preventDefault on mousedown so blur doesn't happen before click */
325-
onMouseDown: event => {
326-
event.preventDefault();
327-
},
328333
onMouseEnter: () => {
329-
this.setActiveItem(index);
334+
this.setActiveItem(index, false);
330335
},
331336
setActiveItemID: this.setActiveItemID,
332337
};
@@ -342,7 +347,7 @@ class BaseSelectField extends React.Component<Props, State> {
342347
<div className="select-option-check-icon">
343348
{isSelected ? <IconCheck height={16} width={16} /> : null}
344349
</div>
345-
{displayText}
350+
{optionRenderer ? optionRenderer(item) : displayText}
346351
</DatalistItem>
347352
);
348353
/* eslint-enable react/jsx-key */
@@ -385,7 +390,14 @@ class BaseSelectField extends React.Component<Props, State> {
385390
'is-visible': isOpen,
386391
})}
387392
>
388-
<ul className="overlay" id={this.selectFieldID} role="listbox" {...listboxProps}>
393+
<ul
394+
className="overlay"
395+
id={this.selectFieldID}
396+
role="listbox"
397+
// preventDefault on mousedown so blur doesn't happen before click
398+
onMouseDown={event => event.preventDefault()}
399+
{...listboxProps}
400+
>
389401
{this.renderSelectOptions()}
390402
</ul>
391403
</div>

src/components/select-field/__tests__/BaseSelectField-test.js

Lines changed: 43 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ jest.mock('../../../utils/dom', () => ({
1313
describe('components/select-field/BaseSelectField', () => {
1414
afterEach(() => {
1515
sandbox.verifyAndRestore();
16+
jest.clearAllMocks();
1617
});
1718

1819
const options = [
@@ -168,6 +169,19 @@ describe('components/select-field/BaseSelectField', () => {
168169
expect(overlayWrapper.childAt(1).prop('role')).toEqual('separator');
169170
expect(overlayWrapper.childAt(4).prop('role')).toEqual('separator');
170171
});
172+
173+
test('should render option content using optionRenderer when provided', () => {
174+
const optionRenderer = jest.fn().mockImplementation(({ displayText, value }) => (
175+
<span>
176+
{displayText}-{value}
177+
</span>
178+
));
179+
const wrapper = shallowRenderSelectField({ optionRenderer });
180+
const itemsWrapper = wrapper.find('DatalistItem');
181+
182+
expect(optionRenderer).toHaveBeenCalledTimes(options.length);
183+
expect(itemsWrapper).toMatchSnapshot();
184+
});
171185
});
172186

173187
describe('render()', () => {
@@ -572,14 +586,11 @@ describe('components/select-field/BaseSelectField', () => {
572586
});
573587

574588
describe('onOptionMouseDown', () => {
575-
test('should prevent default when mousedown on item occurs to prevent blur', () => {
589+
test('should prevent default when mousedown on overlay occurs to prevent blur', () => {
576590
const wrapper = shallowRenderSelectField();
577-
wrapper
578-
.find('DatalistItem')
579-
.at(0)
580-
.simulate('mouseDown', {
581-
preventDefault: sandbox.mock(),
582-
});
591+
wrapper.find('.overlay').simulate('mouseDown', {
592+
preventDefault: sandbox.mock(),
593+
});
583594
});
584595
});
585596

@@ -594,6 +605,18 @@ describe('components/select-field/BaseSelectField', () => {
594605

595606
expect(wrapper.state('activeItemIndex')).toEqual(2);
596607
});
608+
609+
test('should set shouldScrollIntoView to false when hovering over item', () => {
610+
const wrapper = shallowRenderSelectField();
611+
wrapper.setState({ shouldScrollIntoView: true });
612+
613+
wrapper
614+
.find('DatalistItem')
615+
.at(2)
616+
.simulate('mouseEnter');
617+
618+
expect(wrapper.state('shouldScrollIntoView')).toBe(false);
619+
});
597620
});
598621

599622
describe('setActiveItem()', () => {
@@ -617,6 +640,12 @@ describe('components/select-field/BaseSelectField', () => {
617640
.withArgs(null);
618641
instance.setActiveItem(-1);
619642
});
643+
644+
test('should set shouldScrollIntoView to true by default when called', () => {
645+
wrapper.setState({ shouldScrollIntoView: false });
646+
instance.setActiveItem(1);
647+
expect(wrapper.state('shouldScrollIntoView')).toBe(true);
648+
});
620649
});
621650

622651
describe('setActiveItemID()', () => {
@@ -629,9 +658,14 @@ describe('components/select-field/BaseSelectField', () => {
629658
expect(wrapper.state('activeItemID')).toEqual(id);
630659
});
631660

632-
test('should scroll into view when called', () => {
661+
test('should scroll into view when called and previously specified in state', () => {
662+
wrapper.setState({ shouldScrollIntoView: false });
663+
instance.setActiveItemID(id);
664+
expect(scrollIntoView).toHaveBeenCalledTimes(0);
665+
666+
wrapper.setState({ shouldScrollIntoView: true });
633667
instance.setActiveItemID(id);
634-
expect(scrollIntoView).toHaveBeenCalled();
668+
expect(scrollIntoView).toHaveBeenCalledTimes(1);
635669
});
636670
});
637671

src/components/select-field/__tests__/__snapshots__/BaseSelectField-test.js.snap

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,71 @@ exports[`components/select-field/BaseSelectField renderSelectButton() should sen
1414
title=""
1515
/>
1616
`;
17+
18+
exports[`components/select-field/BaseSelectField renderSelectOptions() should render option content using optionRenderer when provided 1`] = `
19+
Array [
20+
<DatalistItem
21+
className="select-option"
22+
key="0"
23+
onClick={[Function]}
24+
onMouseEnter={[Function]}
25+
setActiveItemID={[Function]}
26+
>
27+
<div
28+
className="select-option-check-icon"
29+
/>
30+
<span>
31+
Any Type
32+
-
33+
</span>
34+
</DatalistItem>,
35+
<DatalistItem
36+
className="select-option"
37+
key="1"
38+
onClick={[Function]}
39+
onMouseEnter={[Function]}
40+
setActiveItemID={[Function]}
41+
>
42+
<div
43+
className="select-option-check-icon"
44+
/>
45+
<span>
46+
Audio
47+
-
48+
audio
49+
</span>
50+
</DatalistItem>,
51+
<DatalistItem
52+
className="select-option"
53+
key="2"
54+
onClick={[Function]}
55+
onMouseEnter={[Function]}
56+
setActiveItemID={[Function]}
57+
>
58+
<div
59+
className="select-option-check-icon"
60+
/>
61+
<span>
62+
Documents
63+
-
64+
document
65+
</span>
66+
</DatalistItem>,
67+
<DatalistItem
68+
className="select-option"
69+
key="3"
70+
onClick={[Function]}
71+
onMouseEnter={[Function]}
72+
setActiveItemID={[Function]}
73+
>
74+
<div
75+
className="select-option-check-icon"
76+
/>
77+
<span>
78+
Videos
79+
-
80+
video
81+
</span>
82+
</DatalistItem>,
83+
]
84+
`;

0 commit comments

Comments
 (0)