Skip to content

Commit fcbdbd2

Browse files
Merge pull request #792 from glints-dev/feature/combobox-option-list-scrollable
feat: combobox option list scrollable
2 parents d7078da + 7523979 commit fcbdbd2

16 files changed

+200
-107
lines changed
Lines changed: 27 additions & 102 deletions
Original file line numberDiff line numberDiff line change
@@ -1,125 +1,40 @@
11
import { Meta, Story } from '@storybook/react';
2-
import React, { useCallback, useEffect, useState } from 'react';
3-
import { withGlintsPortalContainer } from '../../helpers/storybook/Decorators';
4-
5-
import { Blue } from '../utilities/colors';
6-
import { space8 } from '../utilities/spacing';
2+
import React from 'react';
73

4+
import { withGlintsPortalContainer } from '../../helpers/storybook/Decorators';
5+
import { Option, OptionList, TextInput } from './components';
86
import { Combobox, ComboboxProps } from './Combobox';
9-
import { StyledTag } from './comboboxStoryHelper/TagStyle';
7+
import { ComboboxMultiSelect } from './comboboxStoryHelper/ComboboxMultuSelect';
108

119
(Combobox as React.FunctionComponent<ComboboxProps>).displayName = 'Combobox';
1210

1311
export default {
1412
title: '@next/Combobox',
1513
component: Combobox,
14+
subcomponents: {
15+
'Combobox.Option': Option,
16+
'Combobox.OptionList': OptionList,
17+
'Combobox.TextInput': TextInput,
18+
},
1619
decorators: [withGlintsPortalContainer],
1720
} as Meta;
1821

1922
const countries = [
23+
{ label: 'Cambodia', value: 'Cambodia' },
2024
{ label: 'Indonesia', value: 'Indonesia' },
2125
{ label: 'Malaysia', value: 'Malaysia' },
26+
{ label: 'Philippines', value: 'Philippines' },
2227
{ label: 'Singapore', value: 'Singapore' },
2328
{ label: 'Taiwan', value: 'Taiwan' },
29+
{ label: 'Thailand', value: 'Thailand' },
2430
{ label: 'Vietnam', value: 'Vietnam' },
2531
];
2632

27-
const MultiSelectTemplate: Story<ComboboxProps> = args => {
28-
const [inputValue, setInputValue] = useState('');
29-
const [selectedOptions, setSelectedOptions] = useState([]);
30-
const [isSearchEmpty, setIsSearchEmpty] = useState(false);
31-
32-
const [options, setOptions] = useState(countries);
33-
34-
const handleInputChange = (value: string) => {
35-
setInputValue(value);
33+
const slicedCountries = countries.slice(0, 5);
3634

37-
if (value === '') {
38-
setOptions(countries);
39-
return;
40-
}
41-
42-
const filterRegex = new RegExp(value, 'i');
43-
const filterOptions = options.filter(country =>
44-
country.label.match(filterRegex)
45-
);
46-
setOptions(filterOptions);
47-
};
48-
49-
const handleSelect = (selected: string) => {
50-
if (selectedOptions.includes(selected)) {
51-
setSelectedOptions(selectedOptions.filter(option => option !== selected));
52-
} else {
53-
setSelectedOptions([...selectedOptions, selected]);
54-
}
55-
};
56-
57-
const optionsMarkup =
58-
options?.length > 0
59-
? options.map(option => {
60-
const { label, value } = option;
61-
62-
return (
63-
<Combobox.Option
64-
key={value}
65-
label={label}
66-
value={value}
67-
selected={selectedOptions.includes(value)}
68-
/>
69-
);
70-
})
71-
: null;
72-
73-
const removeTag = useCallback(
74-
tag => () => {
75-
const options = [...selectedOptions];
76-
options.splice(options.indexOf(tag), 1);
77-
setSelectedOptions(options);
78-
},
79-
[selectedOptions]
80-
);
81-
82-
const tagsMarkup = selectedOptions.map(option => (
83-
<StyledTag
84-
key={`option-${option}`}
85-
onRemove={removeTag(option)}
86-
textColor={Blue.S99}
87-
>
88-
{option}
89-
</StyledTag>
90-
));
91-
92-
useEffect(() => {
93-
if (options.length === 0) {
94-
setIsSearchEmpty(true);
95-
}
96-
97-
if (options.length > 0 && isSearchEmpty === true) {
98-
setIsSearchEmpty(false);
99-
}
100-
}, [isSearchEmpty, options]);
101-
102-
return (
103-
<div style={{ maxWidth: '500px' }}>
104-
<Combobox.Label>Label</Combobox.Label>
105-
<Combobox
106-
{...args}
107-
activator={
108-
<Combobox.TextInput
109-
value={inputValue}
110-
onChange={(value: string) => handleInputChange(value)}
111-
placeholder="Search"
112-
/>
113-
}
114-
>
115-
<Combobox.OptionList onSelect={handleSelect} isEmpty={isSearchEmpty}>
116-
{optionsMarkup}
117-
</Combobox.OptionList>
118-
</Combobox>
119-
<div style={{ paddingTop: space8 }}>{tagsMarkup}</div>
120-
</div>
121-
);
122-
};
35+
const MultiSelectTemplate: Story<ComboboxProps> = args => (
36+
<ComboboxMultiSelect {...args} countries={slicedCountries} />
37+
);
12338

12439
export const MultiSelect = MultiSelectTemplate.bind({});
12540

@@ -136,7 +51,6 @@ MultiSelect.parameters = {
13651
{ label: 'Malaysia', value: 'Malaysia' },
13752
{ label: 'Singapore', value: 'Singapore' },
13853
{ label: 'Taiwan', value: 'Taiwan' },
139-
{ label: 'Vietnam', value: 'Vietnam' },
14054
];
14155
14256
const [inputValue, setInputValue] = useState('');
@@ -237,3 +151,14 @@ MultiSelect.parameters = {
237151
},
238152
},
239153
};
154+
155+
const MultiSelectScrollableTemplate: Story<ComboboxProps> = args => (
156+
<ComboboxMultiSelect {...args} countries={countries} />
157+
);
158+
159+
export const MultiSelectScrollable = MultiSelectScrollableTemplate.bind({});
160+
161+
MultiSelectScrollable.args = {
162+
allowMultiple: true,
163+
scrollable: true,
164+
};

src/@next/Combobox/Combobox.tsx

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { useCallback, useState } from 'react';
1+
import React, { useCallback, useEffect, useState } from 'react';
22
import { Popover } from '../Popover';
33
import { Typography } from '../Typography';
44
import { Neutral } from '../utilities/colors';
@@ -12,16 +12,37 @@ export interface ComboboxProps {
1212
children?: React.ReactNode;
1313
label?: React.ReactNode;
1414
onClose?: () => void;
15+
/** Margin Top = 8 ; Option height = 48 ; optionListHeight = (n options * option height) + margin top; */
16+
listHeight?: number;
17+
/** true = Allow vertical scroll, default by 6 options. */
18+
scrollable?: boolean;
1519
}
1620

1721
export const Combobox = ({
1822
activator,
1923
allowMultiple = false,
2024
children,
2125
onClose,
26+
listHeight,
27+
scrollable,
2228
}: ComboboxProps) => {
2329
const [popoverActive, setPopoverActive] = useState(false);
2430
const [textInputWidth, setTextInputWidth] = useState();
31+
const [optionListHeight, setOptionListHeight] = useState('');
32+
33+
useEffect(() => {
34+
if (listHeight) {
35+
setOptionListHeight(`${listHeight + 24}px`);
36+
37+
return;
38+
}
39+
40+
if (scrollable) {
41+
setOptionListHeight(`${296 + 24}px`);
42+
43+
return;
44+
}
45+
}, [listHeight, scrollable]);
2546

2647
const handleClose = useCallback(() => {
2748
setPopoverActive(false);
@@ -62,7 +83,7 @@ export const Combobox = ({
6283
preventFocusOnClose
6384
fullWidth
6485
>
65-
<Popover.Pane>
86+
<Popover.Pane height={optionListHeight}>
6687
<ComboboxOptionContext.Provider value={optionContextValue}>
6788
<ComboboxTextInputContext.Provider value={textInputContextValue}>
6889
{children}
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import React, { useCallback, useEffect, useState } from 'react';
2+
import { Blue } from '../../utilities/colors';
3+
import { space8 } from '../../utilities/spacing';
4+
import { Combobox } from '../Combobox';
5+
import { OptionType } from '../components/OptionList/Option';
6+
import { StyledTag } from './TagStyle';
7+
8+
interface ComboboxMultiSelectProps {
9+
countries: OptionType[];
10+
}
11+
export const ComboboxMultiSelect = ({
12+
countries,
13+
...args
14+
}: ComboboxMultiSelectProps) => {
15+
const [inputValue, setInputValue] = useState('');
16+
const [selectedOptions, setSelectedOptions] = useState([]);
17+
const [isSearchEmpty, setIsSearchEmpty] = useState(false);
18+
19+
const [options, setOptions] = useState(countries);
20+
21+
const handleInputChange = (value: string) => {
22+
setInputValue(value);
23+
24+
if (value === '') {
25+
setOptions(countries);
26+
return;
27+
}
28+
29+
const filterRegex = new RegExp(value, 'i');
30+
const filterOptions = options.filter(country =>
31+
country.label.match(filterRegex)
32+
);
33+
setOptions(filterOptions);
34+
};
35+
36+
const handleSelect = (selected: string) => {
37+
if (selectedOptions.includes(selected)) {
38+
setSelectedOptions(selectedOptions.filter(option => option !== selected));
39+
} else {
40+
setSelectedOptions([...selectedOptions, selected]);
41+
}
42+
};
43+
44+
const optionsMarkup =
45+
options?.length > 0
46+
? options.map(option => {
47+
const { label, value } = option;
48+
49+
return (
50+
<Combobox.Option
51+
key={value}
52+
label={label}
53+
value={value}
54+
selected={selectedOptions.includes(value)}
55+
/>
56+
);
57+
})
58+
: null;
59+
60+
const removeTag = useCallback(
61+
tag => () => {
62+
const options = [...selectedOptions];
63+
options.splice(options.indexOf(tag), 1);
64+
setSelectedOptions(options);
65+
},
66+
[selectedOptions]
67+
);
68+
69+
const tagsMarkup = selectedOptions.map(option => (
70+
<StyledTag
71+
key={`option-${option}`}
72+
onRemove={removeTag(option)}
73+
textColor={Blue.S99}
74+
>
75+
{option}
76+
</StyledTag>
77+
));
78+
79+
useEffect(() => {
80+
if (options.length === 0) {
81+
setIsSearchEmpty(true);
82+
}
83+
84+
if (options.length > 0 && isSearchEmpty === true) {
85+
setIsSearchEmpty(false);
86+
}
87+
}, [isSearchEmpty, options]);
88+
89+
return (
90+
<div style={{ maxWidth: '500px' }}>
91+
<Combobox.Label>Label</Combobox.Label>
92+
<Combobox
93+
{...args}
94+
activator={
95+
<Combobox.TextInput
96+
value={inputValue}
97+
onChange={(value: string) => handleInputChange(value)}
98+
placeholder="Search"
99+
/>
100+
}
101+
>
102+
<Combobox.OptionList onSelect={handleSelect} isEmpty={isSearchEmpty}>
103+
{optionsMarkup}
104+
</Combobox.OptionList>
105+
</Combobox>
106+
<div style={{ paddingTop: space8 }}>{tagsMarkup}</div>
107+
</div>
108+
);
109+
};

src/@next/Combobox/components/OptionList/OptionList.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { OptionListContainer, StyledOptionList } from './OptionListStyle';
77
export interface OptionListProps extends NoOptionListProps {
88
children: React.ReactNode;
99
isEmpty?: boolean;
10+
height?: string;
1011
onSelect?(value: string): void;
1112
}
1213

src/@next/Combobox/components/OptionList/OptionListStyle.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ export const OptionListContainer = styled.div<OptionListContainerProps>`
1111
width: ${props => props.textInputWidth}px;
1212
padding: ${space8} 0;
1313
`;
14+
1415
export const StyledOptionList = styled.ul`
1516
list-style: none;
1617
margin: 0 ${space8};

src/@next/Popover/Popover.stories.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ const PopoverExample = (props: PopoverProps) => {
9999
activator={example2Activator}
100100
onClose={togglePopoverActive2}
101101
>
102-
<Popover.Pane>
102+
<Popover.Pane fixed>
103103
<Popover.Section>
104104
<Typography as="span" variant="subtitle2">
105105
AVAILABLE SALES CHANNEL

src/@next/Popover/PopoverStyle.ts

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { createGlobalStyle } from 'styled-components';
22
import { borderRadius8 } from '../utilities/borderRadius';
3-
import { Neutral } from '../utilities/colors';
3+
import { Blue, Neutral } from '../utilities/colors';
44
import { space16, space4, space8 } from '../utilities/spacing';
55

66
// we need to use global style here because popover is created outside the root element for react app
@@ -155,4 +155,24 @@ export const StyledPopover: any = createGlobalStyle`
155155
.Polaris-PositionedOverlay--preventInteraction {
156156
pointer-events: none;
157157
}
158+
159+
.Polaris-Scrollable {
160+
position: relative;
161+
max-height: none;
162+
overflow-x: hidden;
163+
overflow-y: hidden;
164+
165+
:focus {
166+
outline: 0.125rem solid ${Blue.S54};
167+
outline-offset: 0.125rem;
168+
}
169+
}
170+
171+
.Polaris-Scrollable--horizontal {
172+
overflow-x: auto;
173+
}
174+
175+
.Polaris-Scrollable--vertical {
176+
overflow-y: auto;
177+
}
158178
`;

0 commit comments

Comments
 (0)