@leafygreen-ui/combobox 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,248 @@
1
+ import React from 'react';
2
+ import { boolean, select } from '@storybook/addon-knobs';
3
+ import { storiesOf } from '@storybook/react';
4
+ import LeafygreenProvider from '@leafygreen-ui/leafygreen-provider';
5
+ import Icon from '@leafygreen-ui/icon';
6
+ import Button from '@leafygreen-ui/button';
7
+ import { css } from '@leafygreen-ui/emotion';
8
+ import { Combobox, ComboboxOption, ComboboxGroup } from '.';
9
+ import { useState } from '@storybook/client-api';
10
+
11
+ const Wrapper = ({ children }: any) => (
12
+ <div
13
+ className={css`
14
+ width: 384px;
15
+ height: 100vh;
16
+ `}
17
+ >
18
+ <LeafygreenProvider>{children}</LeafygreenProvider>
19
+ </div>
20
+ );
21
+
22
+ storiesOf('Combobox', module)
23
+ .add('Single Select', () => {
24
+ const [isError, setIsError] = useState(false);
25
+
26
+ const handleChange = (value: string | null) => {
27
+ if (value === 'pomegranate') {
28
+ setIsError(true);
29
+ } else {
30
+ setIsError(false);
31
+ }
32
+ };
33
+
34
+ return (
35
+ <Wrapper>
36
+ <Combobox
37
+ label="Choose a fruit"
38
+ description="Please pick one"
39
+ placeholder="Select fruit"
40
+ searchState={select(
41
+ 'Seach state',
42
+ ['unset', 'error', 'loading'],
43
+ 'unset',
44
+ )}
45
+ state={isError ? 'error' : 'none'}
46
+ disabled={boolean('Disabled', false)}
47
+ errorMessage="No Pomegranates!"
48
+ onChange={handleChange}
49
+ >
50
+ <ComboboxOption value="apple" displayName="Apple" />
51
+ <ComboboxOption value="banana" displayName="Banana" />
52
+ <ComboboxOption value="carrot" displayName="Carrot" />
53
+ <ComboboxOption value="dragonfruit" displayName="Dragonfruit" />
54
+ <ComboboxOption value="eggplant" displayName="Eggplant" />
55
+ <ComboboxOption value="fig" displayName="Fig" />
56
+ <ComboboxOption value="grape" displayName="Grape" />
57
+ <ComboboxOption value="honeydew" displayName="Honeydew" />
58
+ <ComboboxOption
59
+ value="iceberg-lettuce"
60
+ displayName="Iceberg lettuce"
61
+ />
62
+ <ComboboxOption
63
+ value="pomegranate"
64
+ displayName="Pomegranate"
65
+ glyph={<Icon glyph="Warning" />}
66
+ />
67
+ <ComboboxGroup label="Peppers">
68
+ <ComboboxOption value="cayenne" displayName="Cayenne" />
69
+ <ComboboxOption value="ghost-pepper" displayName="Ghost pepper" />
70
+ <ComboboxOption value="habanero" displayName="Habanero" />
71
+ <ComboboxOption value="jalapeno" displayName="Jalapeño" />
72
+ <ComboboxOption value="red-pepper" displayName="Red pepper" />
73
+ <ComboboxOption value="scotch-bonnet" displayName="Scotch bonnet" />
74
+ </ComboboxGroup>
75
+ </Combobox>
76
+ </Wrapper>
77
+ );
78
+ })
79
+ .add('Multi Select', () => {
80
+ const [isError, setIsError] = useState(false);
81
+
82
+ const handleChange = (value: Array<string>) => {
83
+ if (value.includes('pomegranate')) {
84
+ setIsError(true);
85
+ } else {
86
+ setIsError(false);
87
+ }
88
+ };
89
+
90
+ return (
91
+ <Wrapper>
92
+ <Combobox
93
+ label="Choose some fruit"
94
+ description="Pick as many as you want!"
95
+ placeholder="Select fruit"
96
+ initialValue={['apple', 'carrot', 'fig']}
97
+ multiselect={true}
98
+ overflow={select(
99
+ 'Overflow',
100
+ ['expand-y', 'expand-x', 'scroll-x'],
101
+ 'expand-y',
102
+ )}
103
+ state={isError ? 'error' : 'none'}
104
+ errorMessage="No Pomegranates!"
105
+ onChange={handleChange}
106
+ chipTruncationLocation={select(
107
+ 'Chip Truncation Location',
108
+ ['start', 'middle', 'end', 'none'],
109
+ 'middle',
110
+ )}
111
+ >
112
+ <ComboboxOption value="apple" displayName="Apple" />
113
+ <ComboboxOption value="banana" displayName="Banana" />
114
+ <ComboboxOption value="carrot" displayName="Carrot" />
115
+ <ComboboxOption value="dragonfruit" displayName="Dragonfruit" />
116
+ <ComboboxOption value="eggplant" displayName="Eggplant" />
117
+ <ComboboxOption value="fig" displayName="Fig" />
118
+ <ComboboxOption value="grape" displayName="Grape" />
119
+ <ComboboxOption value="honeydew" displayName="Honeydew" />
120
+ <ComboboxOption
121
+ value="iceberg-lettuce"
122
+ displayName="Iceberg lettuce"
123
+ />
124
+ <ComboboxOption
125
+ value="pomegranate"
126
+ displayName="Pomegranate"
127
+ glyph={<Icon glyph="Warning" />}
128
+ />
129
+ <ComboboxGroup label="Peppers">
130
+ <ComboboxOption value="cayenne" displayName="Cayenne" />
131
+ <ComboboxOption value="ghost-pepper" displayName="Ghost pepper" />
132
+ <ComboboxOption value="habanero" displayName="Habanero" />
133
+ <ComboboxOption value="jalapeno" displayName="Jalapeño" />
134
+ <ComboboxOption value="red-pepper" displayName="Red pepper" />
135
+ <ComboboxOption value="scotch-bonnet" displayName="Scotch bonnet" />
136
+ </ComboboxGroup>
137
+ </Combobox>
138
+ </Wrapper>
139
+ );
140
+ })
141
+ .add('External filter', () => {
142
+ const allOptions = [
143
+ 'apple',
144
+ 'banana',
145
+ 'carrot',
146
+ 'dragonfruit',
147
+ 'eggplant',
148
+ 'fig',
149
+ 'grape',
150
+ 'honeydew',
151
+ 'iceberg-lettuce',
152
+ 'jalapeño',
153
+ ];
154
+
155
+ const [filteredOptions, setOptions] = useState(['carrot', 'grape']);
156
+
157
+ const handleFilter = (input: string) => {
158
+ setOptions(allOptions.filter(option => option.includes(input)));
159
+ };
160
+
161
+ return (
162
+ <Wrapper>
163
+ <Combobox
164
+ label="Choose some fruit"
165
+ placeholder="Select fruit"
166
+ initialValue={['apple', 'fig', 'raspberry']}
167
+ multiselect={true}
168
+ overflow={'expand-y'}
169
+ onFilter={handleFilter}
170
+ filteredOptions={filteredOptions}
171
+ >
172
+ {allOptions.map(option => (
173
+ <ComboboxOption key={option} value={option} />
174
+ ))}
175
+ </Combobox>
176
+ </Wrapper>
177
+ );
178
+ })
179
+ .add('Empty', () => {
180
+ return (
181
+ <Wrapper>
182
+ <Combobox
183
+ multiselect={false}
184
+ label="Choose a fruit"
185
+ description="Please pick one"
186
+ placeholder="Select fruit"
187
+ ></Combobox>
188
+ </Wrapper>
189
+ );
190
+ })
191
+ .add('Controlled single select', () => {
192
+ const [selection, setSelection] = useState<string | null>('apple');
193
+
194
+ const handleChange = (value: string | null) => {
195
+ console.log({ value });
196
+ setSelection(value);
197
+ };
198
+
199
+ return (
200
+ <Wrapper>
201
+ <Combobox
202
+ multiselect={false}
203
+ label="Choose a fruit"
204
+ description="Please pick one"
205
+ placeholder="Select fruit"
206
+ onChange={handleChange}
207
+ value={selection}
208
+ >
209
+ <ComboboxOption value="apple" />
210
+ <ComboboxOption value="banana" />
211
+ <ComboboxOption value="carrot" />
212
+ </Combobox>
213
+ <Button onClick={() => setSelection('carrot')}>Select Carrot</Button>
214
+ </Wrapper>
215
+ );
216
+ })
217
+ .add('Controlled multiselect', () => {
218
+ const [selection, setSelection] = useState(['apple']);
219
+
220
+ const handleChange = (value: Array<string>) => {
221
+ console.log({ value });
222
+ setSelection(value);
223
+ };
224
+
225
+ return (
226
+ <Wrapper>
227
+ <Combobox
228
+ multiselect={true}
229
+ label="Choose a fruit"
230
+ description="Please pick one"
231
+ placeholder="Select fruit"
232
+ onChange={handleChange}
233
+ value={selection}
234
+ >
235
+ <ComboboxOption value="apple" />
236
+ <ComboboxOption value="banana" />
237
+ <ComboboxOption value="carrot" />
238
+ </Combobox>
239
+ <Button
240
+ onClick={() =>
241
+ setSelection(['apple', 'banana', 'carrot', 'raspberry'])
242
+ }
243
+ >
244
+ Select all
245
+ </Button>
246
+ </Wrapper>
247
+ );
248
+ });
@@ -0,0 +1,354 @@
1
+ /**
2
+ * Styles
3
+ */
4
+
5
+ import { css, cx, keyframes } from '@leafygreen-ui/emotion';
6
+ import { uiColors } from '@leafygreen-ui/palette';
7
+ import { fontFamilies } from '@leafygreen-ui/tokens';
8
+ import { isArray } from 'lodash';
9
+ import { ComboboxSize, Overflow, State } from './Combobox.types';
10
+
11
+ export const comboboxParentStyle = ({
12
+ darkMode,
13
+ size,
14
+ overflow,
15
+ }: {
16
+ darkMode: boolean;
17
+ size: ComboboxSize;
18
+ overflow: Overflow;
19
+ }) => {
20
+ const modeStyle = (darkMode: boolean) => {
21
+ if (darkMode) {
22
+ return css``;
23
+ } else {
24
+ return css`
25
+ --lg-combobox-color-error: ${uiColors.red.base};
26
+ --lg-combobox-text-color: ${uiColors.gray.dark3};
27
+ --lg-combobox-text-color-disabled: ${uiColors.gray.dark1};
28
+
29
+ --lg-combobox-background-color: ${uiColors.gray.light3};
30
+ --lg-combobox-background-color-focus: ${uiColors.white};
31
+ --lg-combobox-background-color-disabled: ${uiColors.gray.light2};
32
+
33
+ --lg-combobox-border-color: ${uiColors.gray.base};
34
+ --lg-combobox-border-color-disabled: ${uiColors.gray.light1};
35
+ --lg-combobox-border-color-error: ${uiColors.red.base};
36
+
37
+ --lg-combobox-shadow: 0px 1px 2px rgba(6, 22, 33, 0.3);
38
+ --lg-combobox-shadow-focus: 0px 4px 4px rgba(6, 22, 33, 0.3);
39
+ `;
40
+ }
41
+ };
42
+
43
+ const sizeStyle = (size: ComboboxSize) => {
44
+ switch (size) {
45
+ case 'default':
46
+ return css`
47
+ --lg-combobox-padding-y: 5px;
48
+ --lg-combobox-padding-x: 7px;
49
+ --lg-combobox-height: 24px;
50
+ --lg-combobox-font-size: 14px;
51
+ --lg-combobox-line-height: 20px;
52
+ --lg-combobox-border-radius: 3px;
53
+ --lg-combobox-input-default-width: 12ch;
54
+ --lg-combobox-icon-height: 16px;
55
+ `;
56
+ }
57
+ };
58
+
59
+ return cx(
60
+ modeStyle(darkMode),
61
+ sizeStyle(size),
62
+ css`
63
+ --lg-combobox-width: ${overflow === 'expand-x' ? 'unset' : '100%'};
64
+ --lg-combobox-padding: var(--lg-combobox-padding-y)
65
+ var(--lg-combobox-padding-x) var(--lg-combobox-padding-y)
66
+ ${overflow === 'scroll-x' ? '0' : 'var(--lg-combobox-padding-x)'};
67
+ width: var(--lg-combobox-width);
68
+ `,
69
+ );
70
+ };
71
+
72
+ export const comboboxStyle = css`
73
+ display: flex;
74
+ flex-wrap: nowrap;
75
+ align-items: center;
76
+ padding: var(--lg-combobox-padding);
77
+ color: var(--lg-combobox-text-color);
78
+ background-color: var(--lg-combobox-background-color);
79
+ box-shadow: var(--lg-combobox-shadow);
80
+ border: 1px solid var(--lg-combobox-border-color);
81
+ border-radius: var(--lg-combobox-border-radius);
82
+ width: var(--lg-combobox-width);
83
+ cursor: text;
84
+ transition: 150ms ease-in-out;
85
+ transition-property: background-color, box-shadow;
86
+ min-width: 256px;
87
+
88
+ &:focus-within {
89
+ background-color: var(--lg-combobox-background-color-focus);
90
+ box-shadow: var(--lg-combobox-shadow-focus);
91
+ }
92
+
93
+ &[data-disabled='true'] {
94
+ color: var(--lg-combobox-text-color-disabled);
95
+ background-color: var(--lg-combobox-background-color-disabled);
96
+ border-color: var(--lg-combobox-border-color-disabled);
97
+ box-shadow: unset;
98
+ cursor: not-allowed;
99
+ }
100
+
101
+ &[data-state='error'] {
102
+ border-color: var(--lg-combobox-border-color-error);
103
+ }
104
+ `;
105
+
106
+ export const interactionRingStyle = css`
107
+ width: var(--lg-combobox-width);
108
+ `;
109
+
110
+ export const interactionRingColor = ({
111
+ state,
112
+ darkMode,
113
+ }: {
114
+ state: State;
115
+ darkMode: boolean;
116
+ }) => {
117
+ if (darkMode) {
118
+ return {
119
+ hovered: state === 'error' ? uiColors.red.dark2 : undefined,
120
+ };
121
+ } else {
122
+ return {
123
+ hovered: state === 'error' ? uiColors.red.light3 : undefined,
124
+ };
125
+ }
126
+ };
127
+
128
+ export const inputWrapperStyle = ({
129
+ overflow,
130
+ isOpen,
131
+ selection,
132
+ value,
133
+ }: {
134
+ overflow: Overflow;
135
+ isOpen: boolean;
136
+ selection: string | Array<string> | null;
137
+ value?: string;
138
+ }) => {
139
+ const isMultiselect = isArray(selection) && selection.length > 0;
140
+ const inputLength = value?.length ?? 0;
141
+
142
+ // The input should be hidden when there are elements selected in a multiselect
143
+ // We don't set \`display: none\` since we need to be able to set .focus() on the element
144
+ const inputWidth = isMultiselect
145
+ ? isOpen || inputLength > 0
146
+ ? `${inputLength + 1}ch`
147
+ : '0'
148
+ : 'var(--lg-combobox-input-default-width)';
149
+
150
+ const baseWrapperStyle = css`
151
+ flex-grow: 1;
152
+ width: var(--lg-combobox-width);
153
+
154
+ --lg-combobox-input-width: ${inputWidth};
155
+ `;
156
+
157
+ switch (overflow) {
158
+ case 'scroll-x': {
159
+ return css`
160
+ ${baseWrapperStyle}
161
+ display: block;
162
+ height: var(--lg-combobox-height);
163
+ white-space: nowrap;
164
+ overflow-x: scroll;
165
+ scroll-behavior: smooth;
166
+ scrollbar-width: none;
167
+ /*
168
+ * Immediate transition in, slow transition out.
169
+ * '-in' transition is handled by \`scroll-behavior\`
170
+ */
171
+ --lg-combobox-input-transition: width ease-in-out
172
+ ${isOpen ? '0' : '100ms'};
173
+
174
+ &::-webkit-scrollbar {
175
+ display: none;
176
+ }
177
+
178
+ & > * {
179
+ margin-inline: 2px;
180
+
181
+ &:first-child {
182
+ margin-inline-start: var(--lg-combobox-padding-x);
183
+ }
184
+
185
+ &:last-child {
186
+ margin-inline-end: var(--lg-combobox-padding-x);
187
+ }
188
+ }
189
+ `;
190
+ }
191
+
192
+ case 'expand-x': {
193
+ return css`
194
+ ${baseWrapperStyle}
195
+ display: flex;
196
+ gap: 4px;
197
+ flex-wrap: nowrap;
198
+ white-space: nowrap;
199
+ --lg-combobox-input-transition: width 150ms ease-in-out;
200
+ `;
201
+ }
202
+
203
+ // TODO - look into animating input element height on wrap
204
+ case 'expand-y': {
205
+ return css`
206
+ ${baseWrapperStyle}
207
+ display: flex;
208
+ flex-wrap: wrap;
209
+ gap: 4px;
210
+ overflow-x: visible;
211
+ `;
212
+ }
213
+ }
214
+ };
215
+
216
+ export const inputElementStyle = css`
217
+ border: none;
218
+ cursor: inherit;
219
+ background-color: inherit;
220
+ box-sizing: content-box;
221
+ padding-block: calc(
222
+ (var(--lg-combobox-height) - var(--lg-combobox-line-height)) / 2
223
+ );
224
+ padding-inline: 0;
225
+ height: var(--lg-combobox-line-height);
226
+ width: var(
227
+ --lg-combobox-input-width,
228
+ var(--lg-combobox-input-default-width, auto)
229
+ );
230
+ transition: var(--lg-combobox-input-transition);
231
+
232
+ &:focus {
233
+ outline: none;
234
+ }
235
+ `;
236
+
237
+ export const clearButton = css`
238
+ // Add a negative margin so the button takes up the same space as the regular icons
239
+ margin-block: calc(var(--lg-combobox-icon-height) / 2 - 100%);
240
+ `;
241
+
242
+ export const errorMessageStyle = css`
243
+ font-size: var(--lg-combobox-font-size);
244
+ line-height: var(--lg-combobox-line-height);
245
+ color: var(--lg-combobox-color-error);
246
+ padding-top: var(--lg-combobox-padding-y);
247
+ `;
248
+
249
+ export const endIcon = css`
250
+ margin-inline-end: calc(var(--lg-combobox-padding-x) / 2);
251
+ `;
252
+
253
+ const loadingIconAnimation = keyframes`
254
+ 0% {
255
+ transform: rotate(0deg);
256
+ }
257
+ 100% {
258
+ transform: rotate(360deg);
259
+ }
260
+ `;
261
+ export const loadingIconStyle = css`
262
+ animation: ${loadingIconAnimation} 1.5s linear infinite;
263
+ `;
264
+
265
+ /**
266
+ * Menu styles
267
+ */
268
+ export const menuWrapperStyle = ({
269
+ darkMode,
270
+ size,
271
+ width = 384,
272
+ }: {
273
+ darkMode: boolean;
274
+ size: ComboboxSize;
275
+ width?: number;
276
+ }) => {
277
+ let menuModeStyle, menuSizeStyle;
278
+
279
+ if (darkMode) {
280
+ menuModeStyle = css``;
281
+ } else {
282
+ menuModeStyle = css`
283
+ --lg-combobox-menu-color: ${uiColors.gray.dark3};
284
+ --lg-combobox-menu-message-color: ${uiColors.gray.dark1};
285
+ --lg-combobox-menu-background-color: ${uiColors.white};
286
+ --lg-combobox-menu-shadow: 0px 3px 7px rgba(0, 0, 0, 0.25);
287
+ --lg-combobox-item-hover-color: ${uiColors.gray.light2};
288
+ --lg-combobox-item-active-color: ${uiColors.blue.light3};
289
+ --lg-combobox-item-wedge-color: ${uiColors.blue.base};
290
+ `;
291
+ }
292
+
293
+ switch (size) {
294
+ case 'default':
295
+ menuSizeStyle = css`
296
+ --lg-combobox-menu-border-radius: 4px;
297
+ --lg-combobox-item-height: 36px;
298
+ --lg-combobox-item-padding-y: 8px;
299
+ --lg-combobox-item-padding-x: 12px;
300
+ --lg-combobox-item-font-size: 14px;
301
+ --lg-combobox-item-line-height: 21px;
302
+ --lg-combobox-item-wedge-height: 22px;
303
+ `;
304
+ }
305
+
306
+ return cx(
307
+ menuModeStyle,
308
+ menuSizeStyle,
309
+ css`
310
+ width: ${width}px;
311
+ border-radius: var(--lg-combobox-menu-border-radius);
312
+
313
+ & > * {
314
+ border-radius: inherit;
315
+ }
316
+ `,
317
+ );
318
+ };
319
+
320
+ export const menuStyle = ({ maxHeight }: { maxHeight: number }) => css`
321
+ position: relative;
322
+ width: 100%;
323
+ margin: 0;
324
+ padding: 0;
325
+ font-family: ${fontFamilies.default};
326
+ color: var(--lg-combobox-menu-color);
327
+ background-color: var(--lg-combobox-menu-background-color);
328
+ box-shadow: var(--lg-combobox-menu-shadow);
329
+ border-radius: inherit;
330
+ overflow: auto;
331
+ min-height: var(--lg-combobox-item-height);
332
+ max-height: ${maxHeight}px;
333
+ scroll-behavior: smooth;
334
+ `;
335
+
336
+ export const menuList = css`
337
+ position: relative;
338
+ margin: 0;
339
+ padding: 0;
340
+ `;
341
+
342
+ export const menuMessage = css`
343
+ display: inline-flex;
344
+ align-items: center;
345
+ gap: 8px;
346
+ font-size: var(--lg-combobox-item-font-size);
347
+ color: var(--lg-combobox-menu-message-color);
348
+ padding: var(--lg-combobox-item-padding-y) var(--lg-combobox-item-padding-x);
349
+
350
+ & > svg {
351
+ width: 1em;
352
+ height: 1em;
353
+ }
354
+ `;