@leafygreen-ui/combobox 1.2.1 → 2.0.1
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.
- package/CHANGELOG.md +32 -0
- package/dist/Chip.d.ts.map +1 -1
- package/dist/Combobox.d.ts.map +1 -1
- package/dist/Combobox.styles.d.ts +39 -41
- package/dist/Combobox.styles.d.ts.map +1 -1
- package/dist/Combobox.types.d.ts +5 -0
- package/dist/Combobox.types.d.ts.map +1 -1
- package/dist/ComboboxContext.d.ts +5 -1
- package/dist/ComboboxContext.d.ts.map +1 -1
- package/dist/ComboboxGroup.d.ts.map +1 -1
- package/dist/ComboboxMenu/ComboboxMenu.d.ts +10 -0
- package/dist/ComboboxMenu/ComboboxMenu.d.ts.map +1 -0
- package/dist/ComboboxMenu/Menu.styles.d.ts +25 -0
- package/dist/ComboboxMenu/Menu.styles.d.ts.map +1 -0
- package/dist/ComboboxOption.d.ts.map +1 -1
- package/dist/ComboboxTestUtils.d.ts.map +1 -1
- package/dist/Menu.styles.d.ts +21 -0
- package/dist/Menu.styles.d.ts.map +1 -0
- package/dist/esm/index.js +1 -1
- package/dist/esm/index.js.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/package.json +9 -9
- package/src/Chip.tsx +125 -91
- package/src/Combobox.spec.tsx +3 -1
- package/src/Combobox.story.tsx +4 -2
- package/src/Combobox.styles.ts +250 -300
- package/src/Combobox.tsx +161 -181
- package/src/Combobox.types.ts +7 -0
- package/src/ComboboxContext.tsx +16 -1
- package/src/ComboboxGroup.tsx +27 -17
- package/src/ComboboxMenu/ComboboxMenu.tsx +161 -0
- package/src/ComboboxMenu/Menu.styles.ts +131 -0
- package/src/ComboboxOption.tsx +120 -32
- package/src/ComboboxTestUtils.tsx +3 -2
- package/src/Menu.styles.ts +110 -0
- package/tsconfig.json +0 -3
- package/tsconfig.tsbuildinfo +1 -1
package/src/Combobox.tsx
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
import clone from 'lodash/clone';
|
|
2
|
+
import isArray from 'lodash/isArray';
|
|
3
|
+
import isEqual from 'lodash/isEqual';
|
|
4
|
+
import isNull from 'lodash/isNull';
|
|
5
|
+
import isString from 'lodash/isString';
|
|
6
|
+
import isUndefined from 'lodash/isUndefined';
|
|
1
7
|
import React, {
|
|
2
8
|
useCallback,
|
|
3
9
|
useEffect,
|
|
@@ -5,11 +11,8 @@ import React, {
|
|
|
5
11
|
useRef,
|
|
6
12
|
useState,
|
|
7
13
|
} from 'react';
|
|
8
|
-
import { clone, isArray, isEqual, isNull, isString, isUndefined } from 'lodash';
|
|
9
14
|
import { Description, Label } from '@leafygreen-ui/typography';
|
|
10
|
-
import Popover from '@leafygreen-ui/popover';
|
|
11
15
|
import {
|
|
12
|
-
useAvailableSpace,
|
|
13
16
|
useDynamicRefs,
|
|
14
17
|
useEventListener,
|
|
15
18
|
useIdAllocator,
|
|
@@ -18,7 +21,7 @@ import {
|
|
|
18
21
|
import Icon from '@leafygreen-ui/icon';
|
|
19
22
|
import IconButton from '@leafygreen-ui/icon-button';
|
|
20
23
|
import { cx } from '@leafygreen-ui/emotion';
|
|
21
|
-
import {
|
|
24
|
+
import { palette } from '@leafygreen-ui/palette';
|
|
22
25
|
import { consoleOnce, isComponentType, keyMap } from '@leafygreen-ui/lib';
|
|
23
26
|
import {
|
|
24
27
|
ComboboxProps,
|
|
@@ -28,28 +31,8 @@ import {
|
|
|
28
31
|
OptionObject,
|
|
29
32
|
ComboboxElement,
|
|
30
33
|
ComboboxSize,
|
|
34
|
+
State,
|
|
31
35
|
} from './Combobox.types';
|
|
32
|
-
import { ComboboxContext } from './ComboboxContext';
|
|
33
|
-
import { InternalComboboxOption } from './ComboboxOption';
|
|
34
|
-
import { Chip } from './Chip';
|
|
35
|
-
import {
|
|
36
|
-
clearButtonStyle,
|
|
37
|
-
clearButtonFocusOverrideStyles,
|
|
38
|
-
comboboxFocusStyle,
|
|
39
|
-
comboboxParentStyle,
|
|
40
|
-
comboboxStyle,
|
|
41
|
-
endIcon,
|
|
42
|
-
errorMessageStyle,
|
|
43
|
-
inputElementStyle,
|
|
44
|
-
inputWrapperStyle,
|
|
45
|
-
loadingIconStyle,
|
|
46
|
-
menuList,
|
|
47
|
-
menuMessage,
|
|
48
|
-
menuStyle,
|
|
49
|
-
menuWrapperStyle,
|
|
50
|
-
_tempLabelDescriptionOverrideStyle,
|
|
51
|
-
} from './Combobox.styles';
|
|
52
|
-
import { InternalComboboxGroup } from './ComboboxGroup';
|
|
53
36
|
import {
|
|
54
37
|
flattenChildren,
|
|
55
38
|
getOptionObjectFromValue,
|
|
@@ -57,6 +40,32 @@ import {
|
|
|
57
40
|
getValueForDisplayName,
|
|
58
41
|
getNameAndValue,
|
|
59
42
|
} from './utils';
|
|
43
|
+
import { ComboboxContext, useDarkMode } from './ComboboxContext';
|
|
44
|
+
import { InternalComboboxGroup } from './ComboboxGroup';
|
|
45
|
+
import { InternalComboboxOption } from './ComboboxOption';
|
|
46
|
+
import { Chip } from './Chip';
|
|
47
|
+
import {
|
|
48
|
+
comboboxFocusStyle,
|
|
49
|
+
inputWrapperStyle,
|
|
50
|
+
baseComboboxStyles,
|
|
51
|
+
comboboxThemeStyles,
|
|
52
|
+
comboboxSizeStyles,
|
|
53
|
+
comboboxDisabledStyles,
|
|
54
|
+
comboboxErrorStyles,
|
|
55
|
+
comboboxParentStyle,
|
|
56
|
+
baseInputElementStyle,
|
|
57
|
+
inputElementSizeStyle,
|
|
58
|
+
inputElementTransitionStyles,
|
|
59
|
+
multiselectInputElementStyle,
|
|
60
|
+
clearButtonStyle,
|
|
61
|
+
endIconStyle,
|
|
62
|
+
errorMessageThemeStyle,
|
|
63
|
+
errorMessageSizeStyle,
|
|
64
|
+
multiselectInputElementPadding,
|
|
65
|
+
labelDescriptionContainerStyle,
|
|
66
|
+
inputElementThemeStyle,
|
|
67
|
+
} from './Combobox.styles';
|
|
68
|
+
import { ComboboxMenu } from './ComboboxMenu/ComboboxMenu';
|
|
60
69
|
|
|
61
70
|
/**
|
|
62
71
|
* Combobox is a combination of a Select and TextInput,
|
|
@@ -97,6 +106,7 @@ export default function Combobox<M extends boolean>({
|
|
|
97
106
|
popoverZIndex,
|
|
98
107
|
...rest
|
|
99
108
|
}: ComboboxProps<M>) {
|
|
109
|
+
const theme = useDarkMode(darkMode);
|
|
100
110
|
const getOptionRef = useDynamicRefs<HTMLLIElement>({ prefix: 'option' });
|
|
101
111
|
const getChipRef = useDynamicRefs<HTMLSpanElement>({ prefix: 'chip' });
|
|
102
112
|
|
|
@@ -357,6 +367,8 @@ export default function Combobox<M extends boolean>({
|
|
|
357
367
|
const [focusedElementName, trackFocusedElement] = useState<
|
|
358
368
|
ComboboxElement | undefined
|
|
359
369
|
>();
|
|
370
|
+
const isElementFocused = (elementName: ComboboxElement) =>
|
|
371
|
+
elementName === focusedElementName;
|
|
360
372
|
|
|
361
373
|
type Direction = 'next' | 'prev' | 'first' | 'last';
|
|
362
374
|
|
|
@@ -595,74 +607,75 @@ export default function Combobox<M extends boolean>({
|
|
|
595
607
|
*/
|
|
596
608
|
|
|
597
609
|
/**
|
|
598
|
-
* Callback to render
|
|
610
|
+
* Callback to render a child as an <InternalComboboxOption> element
|
|
599
611
|
*/
|
|
600
|
-
const
|
|
601
|
-
(
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
);
|
|
641
|
-
}
|
|
642
|
-
} else if (isComponentType(child, 'ComboboxGroup')) {
|
|
643
|
-
const nestedChildren = renderInternalOptions(child.props.children);
|
|
644
|
-
|
|
645
|
-
if (nestedChildren && nestedChildren?.length > 0) {
|
|
646
|
-
return (
|
|
647
|
-
<InternalComboboxGroup
|
|
648
|
-
label={child.props.label}
|
|
649
|
-
className={child.props.className}
|
|
650
|
-
>
|
|
651
|
-
{renderInternalOptions(nestedChildren)}
|
|
652
|
-
</InternalComboboxGroup>
|
|
653
|
-
);
|
|
654
|
-
}
|
|
612
|
+
const renderOption = useCallback(
|
|
613
|
+
(child: React.ReactNode) => {
|
|
614
|
+
if (isComponentType(child, 'ComboboxOption')) {
|
|
615
|
+
const { value, displayName } = getNameAndValue(child.props);
|
|
616
|
+
|
|
617
|
+
if (shouldOptionBeVisible(value)) {
|
|
618
|
+
const { className, glyph, disabled } = child.props;
|
|
619
|
+
const index = allOptions.findIndex(opt => opt.value === value);
|
|
620
|
+
|
|
621
|
+
const isFocused = highlightedOption === value;
|
|
622
|
+
const isSelected = isMultiselect(selection)
|
|
623
|
+
? selection.includes(value)
|
|
624
|
+
: selection === value;
|
|
625
|
+
|
|
626
|
+
const setSelected = () => {
|
|
627
|
+
sethighlightedOption(value);
|
|
628
|
+
updateSelection(value);
|
|
629
|
+
setInputFocus();
|
|
630
|
+
|
|
631
|
+
if (value === selection) {
|
|
632
|
+
closeMenu();
|
|
633
|
+
}
|
|
634
|
+
};
|
|
635
|
+
|
|
636
|
+
const optionRef = getOptionRef(value);
|
|
637
|
+
|
|
638
|
+
return (
|
|
639
|
+
<InternalComboboxOption
|
|
640
|
+
value={value}
|
|
641
|
+
displayName={displayName}
|
|
642
|
+
isFocused={isFocused}
|
|
643
|
+
isSelected={isSelected}
|
|
644
|
+
disabled={disabled}
|
|
645
|
+
setSelected={setSelected}
|
|
646
|
+
glyph={glyph}
|
|
647
|
+
className={className}
|
|
648
|
+
index={index}
|
|
649
|
+
ref={optionRef}
|
|
650
|
+
/>
|
|
651
|
+
);
|
|
655
652
|
}
|
|
656
|
-
})
|
|
653
|
+
} else if (isComponentType(child, 'ComboboxGroup')) {
|
|
654
|
+
const nestedChildren = React.Children.map(
|
|
655
|
+
child.props.children,
|
|
656
|
+
renderOption,
|
|
657
|
+
);
|
|
658
|
+
|
|
659
|
+
if (nestedChildren && nestedChildren?.length > 0) {
|
|
660
|
+
return (
|
|
661
|
+
<InternalComboboxGroup
|
|
662
|
+
label={child.props.label}
|
|
663
|
+
className={child.props.className}
|
|
664
|
+
>
|
|
665
|
+
{React.Children.map(nestedChildren, renderOption)}
|
|
666
|
+
</InternalComboboxGroup>
|
|
667
|
+
);
|
|
668
|
+
}
|
|
669
|
+
}
|
|
657
670
|
},
|
|
658
671
|
[
|
|
659
672
|
allOptions,
|
|
660
|
-
highlightedOption,
|
|
661
673
|
getOptionRef,
|
|
674
|
+
highlightedOption,
|
|
662
675
|
isMultiselect,
|
|
663
|
-
shouldOptionBeVisible,
|
|
664
676
|
selection,
|
|
665
677
|
setInputFocus,
|
|
678
|
+
shouldOptionBeVisible,
|
|
666
679
|
updateSelection,
|
|
667
680
|
],
|
|
668
681
|
);
|
|
@@ -671,8 +684,8 @@ export default function Combobox<M extends boolean>({
|
|
|
671
684
|
* The rendered JSX elements for the options
|
|
672
685
|
*/
|
|
673
686
|
const renderedOptionsJSX = useMemo(
|
|
674
|
-
() =>
|
|
675
|
-
[children,
|
|
687
|
+
() => React.Children.map(children, renderOption),
|
|
688
|
+
[children, renderOption],
|
|
676
689
|
);
|
|
677
690
|
|
|
678
691
|
/**
|
|
@@ -752,15 +765,20 @@ export default function Combobox<M extends boolean>({
|
|
|
752
765
|
ref={clearButtonRef}
|
|
753
766
|
onClick={handleClearButtonClick}
|
|
754
767
|
onFocus={handleClearButtonFocus}
|
|
755
|
-
className={cx(clearButtonStyle
|
|
768
|
+
className={cx(clearButtonStyle)}
|
|
769
|
+
darkMode={darkMode}
|
|
756
770
|
>
|
|
757
771
|
<Icon glyph="XWithCircle" />
|
|
758
772
|
</IconButton>
|
|
759
773
|
)}
|
|
760
774
|
{state === 'error' ? (
|
|
761
|
-
<Icon
|
|
775
|
+
<Icon
|
|
776
|
+
glyph="Warning"
|
|
777
|
+
color={darkMode ? palette.red.light1 : palette.red.base}
|
|
778
|
+
className={endIconStyle(size)}
|
|
779
|
+
/>
|
|
762
780
|
) : (
|
|
763
|
-
<Icon glyph="CaretDown" className={
|
|
781
|
+
<Icon glyph="CaretDown" className={endIconStyle(size)} />
|
|
764
782
|
)}
|
|
765
783
|
</>
|
|
766
784
|
);
|
|
@@ -769,6 +787,8 @@ export default function Combobox<M extends boolean>({
|
|
|
769
787
|
doesSelectionExist,
|
|
770
788
|
disabled,
|
|
771
789
|
state,
|
|
790
|
+
darkMode,
|
|
791
|
+
size,
|
|
772
792
|
updateSelection,
|
|
773
793
|
onClear,
|
|
774
794
|
onFilter,
|
|
@@ -906,59 +926,11 @@ export default function Combobox<M extends boolean>({
|
|
|
906
926
|
setMenuWidth(comboboxRef.current?.clientWidth ?? 0);
|
|
907
927
|
}, [comboboxRef, isOpen, highlightedOption, selection]);
|
|
908
928
|
|
|
909
|
-
// Handler fired when the
|
|
929
|
+
// Handler fired when the menu has finished transitioning in/out
|
|
910
930
|
const handleTransitionEnd = () => {
|
|
911
931
|
setMenuWidth(comboboxRef.current?.clientWidth ?? 0);
|
|
912
932
|
};
|
|
913
933
|
|
|
914
|
-
/**
|
|
915
|
-
* The rendered menu JSX contents
|
|
916
|
-
* Includes error, empty, search and default states
|
|
917
|
-
*/
|
|
918
|
-
const renderedMenuContents = useMemo((): JSX.Element => {
|
|
919
|
-
switch (searchState) {
|
|
920
|
-
case 'loading': {
|
|
921
|
-
return (
|
|
922
|
-
<span className={menuMessage}>
|
|
923
|
-
<Icon
|
|
924
|
-
glyph="Refresh"
|
|
925
|
-
color={uiColors.blue.base}
|
|
926
|
-
className={loadingIconStyle}
|
|
927
|
-
/>
|
|
928
|
-
{searchLoadingMessage}
|
|
929
|
-
</span>
|
|
930
|
-
);
|
|
931
|
-
}
|
|
932
|
-
|
|
933
|
-
case 'error': {
|
|
934
|
-
return (
|
|
935
|
-
<span className={menuMessage}>
|
|
936
|
-
<Icon glyph="Warning" color={uiColors.red.base} />
|
|
937
|
-
{searchErrorMessage}
|
|
938
|
-
</span>
|
|
939
|
-
);
|
|
940
|
-
}
|
|
941
|
-
|
|
942
|
-
case 'unset':
|
|
943
|
-
default: {
|
|
944
|
-
if (renderedOptionsJSX && renderedOptionsJSX.length > 0) {
|
|
945
|
-
return <ul className={menuList}>{renderedOptionsJSX}</ul>;
|
|
946
|
-
}
|
|
947
|
-
|
|
948
|
-
return <span className={menuMessage}>{searchEmptyMessage}</span>;
|
|
949
|
-
}
|
|
950
|
-
}
|
|
951
|
-
}, [
|
|
952
|
-
renderedOptionsJSX,
|
|
953
|
-
searchEmptyMessage,
|
|
954
|
-
searchErrorMessage,
|
|
955
|
-
searchLoadingMessage,
|
|
956
|
-
searchState,
|
|
957
|
-
]);
|
|
958
|
-
|
|
959
|
-
/** The max height of the menu element */
|
|
960
|
-
const maxHeight = Math.min(256, useAvailableSpace(comboboxRef));
|
|
961
|
-
|
|
962
934
|
/**
|
|
963
935
|
*
|
|
964
936
|
* Event Handlers
|
|
@@ -1211,32 +1183,26 @@ export default function Combobox<M extends boolean>({
|
|
|
1211
1183
|
size,
|
|
1212
1184
|
withIcons,
|
|
1213
1185
|
disabled,
|
|
1186
|
+
isOpen,
|
|
1187
|
+
state,
|
|
1188
|
+
searchState,
|
|
1214
1189
|
chipTruncationLocation,
|
|
1215
1190
|
chipCharacterLimit,
|
|
1216
1191
|
inputValue,
|
|
1217
1192
|
}}
|
|
1218
1193
|
>
|
|
1219
1194
|
<div
|
|
1220
|
-
className={cx(
|
|
1221
|
-
comboboxParentStyle({ darkMode, size, overflow }),
|
|
1222
|
-
className,
|
|
1223
|
-
)}
|
|
1195
|
+
className={cx(comboboxParentStyle(size, overflow), className)}
|
|
1224
1196
|
{...rest}
|
|
1225
1197
|
>
|
|
1226
|
-
<div>
|
|
1198
|
+
<div className={labelDescriptionContainerStyle}>
|
|
1227
1199
|
{label && (
|
|
1228
|
-
<Label
|
|
1229
|
-
id={labelId}
|
|
1230
|
-
htmlFor={inputId}
|
|
1231
|
-
className={_tempLabelDescriptionOverrideStyle}
|
|
1232
|
-
>
|
|
1200
|
+
<Label id={labelId} htmlFor={inputId} darkMode={darkMode}>
|
|
1233
1201
|
{label}
|
|
1234
1202
|
</Label>
|
|
1235
1203
|
)}
|
|
1236
1204
|
{description && (
|
|
1237
|
-
<Description
|
|
1238
|
-
{description}
|
|
1239
|
-
</Description>
|
|
1205
|
+
<Description darkMode={darkMode}>{description}</Description>
|
|
1240
1206
|
)}
|
|
1241
1207
|
</div>
|
|
1242
1208
|
|
|
@@ -1249,25 +1215,29 @@ export default function Combobox<M extends boolean>({
|
|
|
1249
1215
|
aria-controls={menuId}
|
|
1250
1216
|
aria-owns={menuId}
|
|
1251
1217
|
tabIndex={-1}
|
|
1252
|
-
className={cx(comboboxStyle, {
|
|
1253
|
-
[comboboxFocusStyle]: focusedElementName === ComboboxElement.Input,
|
|
1254
|
-
})}
|
|
1255
1218
|
onMouseDown={handleInputWrapperMousedown}
|
|
1256
1219
|
onClick={handleComboboxClick}
|
|
1257
1220
|
onFocus={handleComboboxFocus}
|
|
1258
1221
|
onKeyDown={handleKeyDown}
|
|
1259
1222
|
onTransitionEnd={handleTransitionEnd}
|
|
1260
|
-
|
|
1261
|
-
|
|
1223
|
+
className={cx(
|
|
1224
|
+
baseComboboxStyles,
|
|
1225
|
+
comboboxThemeStyles[theme],
|
|
1226
|
+
comboboxSizeStyles(size),
|
|
1227
|
+
{
|
|
1228
|
+
[comboboxDisabledStyles[theme]]: disabled,
|
|
1229
|
+
[comboboxErrorStyles[theme]]: state === State.error,
|
|
1230
|
+
[comboboxFocusStyle[theme]]: isElementFocused(
|
|
1231
|
+
ComboboxElement.Input,
|
|
1232
|
+
),
|
|
1233
|
+
},
|
|
1234
|
+
)}
|
|
1262
1235
|
>
|
|
1263
1236
|
<div
|
|
1264
1237
|
ref={inputWrapperRef}
|
|
1265
1238
|
className={inputWrapperStyle({
|
|
1266
|
-
overflow,
|
|
1267
|
-
isOpen,
|
|
1268
|
-
selection,
|
|
1269
1239
|
size,
|
|
1270
|
-
|
|
1240
|
+
overflow,
|
|
1271
1241
|
})}
|
|
1272
1242
|
>
|
|
1273
1243
|
{renderedChips}
|
|
@@ -1278,7 +1248,18 @@ export default function Combobox<M extends boolean>({
|
|
|
1278
1248
|
aria-labelledby={labelId}
|
|
1279
1249
|
ref={inputRef}
|
|
1280
1250
|
id={inputId}
|
|
1281
|
-
className={
|
|
1251
|
+
className={cx(
|
|
1252
|
+
baseInputElementStyle,
|
|
1253
|
+
inputElementSizeStyle[size],
|
|
1254
|
+
inputElementThemeStyle[theme],
|
|
1255
|
+
inputElementTransitionStyles(isOpen, overflow),
|
|
1256
|
+
{
|
|
1257
|
+
[multiselectInputElementStyle(size, inputValue)]:
|
|
1258
|
+
isMultiselect(selection),
|
|
1259
|
+
[multiselectInputElementPadding(selection)]:
|
|
1260
|
+
isMultiselect(selection),
|
|
1261
|
+
},
|
|
1262
|
+
)}
|
|
1282
1263
|
placeholder={placeholderValue}
|
|
1283
1264
|
disabled={disabled ?? undefined}
|
|
1284
1265
|
onChange={handleInputChange}
|
|
@@ -1290,34 +1271,33 @@ export default function Combobox<M extends boolean>({
|
|
|
1290
1271
|
</div>
|
|
1291
1272
|
|
|
1292
1273
|
{state === 'error' && errorMessage && (
|
|
1293
|
-
<div
|
|
1274
|
+
<div
|
|
1275
|
+
className={cx(
|
|
1276
|
+
errorMessageThemeStyle[theme],
|
|
1277
|
+
errorMessageSizeStyle[size],
|
|
1278
|
+
)}
|
|
1279
|
+
>
|
|
1280
|
+
{errorMessage}
|
|
1281
|
+
</div>
|
|
1294
1282
|
)}
|
|
1295
1283
|
|
|
1296
1284
|
{/******* /
|
|
1297
1285
|
* Menu *
|
|
1298
1286
|
/ *******/}
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
justify="middle"
|
|
1287
|
+
|
|
1288
|
+
<ComboboxMenu
|
|
1289
|
+
id={menuId}
|
|
1290
|
+
labelId={labelId}
|
|
1304
1291
|
refEl={comboboxRef}
|
|
1305
|
-
|
|
1306
|
-
|
|
1292
|
+
ref={menuRef}
|
|
1293
|
+
menuWidth={menuWidth}
|
|
1294
|
+
searchLoadingMessage={searchLoadingMessage}
|
|
1295
|
+
searchErrorMessage={searchErrorMessage}
|
|
1296
|
+
searchEmptyMessage={searchEmptyMessage}
|
|
1307
1297
|
{...popoverProps}
|
|
1308
1298
|
>
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
role="listbox"
|
|
1312
|
-
aria-labelledby={labelId}
|
|
1313
|
-
aria-expanded={isOpen}
|
|
1314
|
-
ref={menuRef}
|
|
1315
|
-
className={menuStyle({ maxHeight })}
|
|
1316
|
-
onMouseDownCapture={e => e.preventDefault()}
|
|
1317
|
-
>
|
|
1318
|
-
{renderedMenuContents}
|
|
1319
|
-
</div>
|
|
1320
|
-
</Popover>
|
|
1299
|
+
{renderedOptionsJSX}
|
|
1300
|
+
</ComboboxMenu>
|
|
1321
1301
|
</div>
|
|
1322
1302
|
</ComboboxContext.Provider>
|
|
1323
1303
|
);
|
package/src/Combobox.types.ts
CHANGED
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
import { ReactElement, ReactNode } from 'react';
|
|
2
2
|
import { Either } from '@leafygreen-ui/lib';
|
|
3
3
|
|
|
4
|
+
export const Theme = {
|
|
5
|
+
Dark: 'dark',
|
|
6
|
+
Light: 'light',
|
|
7
|
+
} as const;
|
|
8
|
+
|
|
9
|
+
export type Theme = typeof Theme[keyof typeof Theme];
|
|
10
|
+
|
|
4
11
|
/**
|
|
5
12
|
* Prop Enums & Types
|
|
6
13
|
*/
|
package/src/ComboboxContext.tsx
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import { createContext } from 'react';
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
ComboboxSize,
|
|
4
|
+
SearchState,
|
|
5
|
+
State,
|
|
6
|
+
Theme,
|
|
7
|
+
TrunctationLocation,
|
|
8
|
+
} from './Combobox.types';
|
|
3
9
|
|
|
4
10
|
interface ComboboxData {
|
|
5
11
|
multiselect: boolean;
|
|
@@ -7,6 +13,9 @@ interface ComboboxData {
|
|
|
7
13
|
size: ComboboxSize;
|
|
8
14
|
withIcons: boolean;
|
|
9
15
|
disabled: boolean;
|
|
16
|
+
isOpen: boolean;
|
|
17
|
+
state: State;
|
|
18
|
+
searchState: SearchState;
|
|
10
19
|
chipTruncationLocation?: TrunctationLocation;
|
|
11
20
|
chipCharacterLimit?: number;
|
|
12
21
|
inputValue?: string;
|
|
@@ -18,4 +27,10 @@ export const ComboboxContext = createContext<ComboboxData>({
|
|
|
18
27
|
size: ComboboxSize.Default,
|
|
19
28
|
withIcons: false,
|
|
20
29
|
disabled: false,
|
|
30
|
+
isOpen: false,
|
|
31
|
+
state: State.none,
|
|
32
|
+
searchState: SearchState.unset,
|
|
21
33
|
});
|
|
34
|
+
|
|
35
|
+
export const useDarkMode = (darkMode: boolean) =>
|
|
36
|
+
darkMode ? Theme.Dark : Theme.Light;
|
package/src/ComboboxGroup.tsx
CHANGED
|
@@ -1,20 +1,18 @@
|
|
|
1
1
|
import { css, cx } from '@leafygreen-ui/emotion';
|
|
2
2
|
import { useIdAllocator } from '@leafygreen-ui/hooks';
|
|
3
|
-
import {
|
|
3
|
+
import { palette } from '@leafygreen-ui/palette';
|
|
4
4
|
import React, { useContext } from 'react';
|
|
5
|
-
import { ComboboxGroupProps } from './Combobox.types';
|
|
6
|
-
import { ComboboxContext } from './ComboboxContext';
|
|
7
|
-
|
|
8
|
-
const comboboxGroupStyle
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
border-bottom: 1px solid var(--lg-combobox-group-border-color);
|
|
17
|
-
`;
|
|
5
|
+
import { ComboboxGroupProps, Theme } from './Combobox.types';
|
|
6
|
+
import { ComboboxContext, useDarkMode } from './ComboboxContext';
|
|
7
|
+
|
|
8
|
+
const comboboxGroupStyle: Record<Theme, string> = {
|
|
9
|
+
[Theme.Light]: css`
|
|
10
|
+
padding-top: 8px;
|
|
11
|
+
`,
|
|
12
|
+
[Theme.Dark]: css`
|
|
13
|
+
padding-top: 8px;
|
|
14
|
+
`,
|
|
15
|
+
};
|
|
18
16
|
|
|
19
17
|
const comboboxGroupLabel = css`
|
|
20
18
|
cursor: default;
|
|
@@ -27,22 +25,34 @@ const comboboxGroupLabel = css`
|
|
|
27
25
|
font-weight: bold;
|
|
28
26
|
text-transform: uppercase;
|
|
29
27
|
letter-spacing: 0.4px;
|
|
30
|
-
color: var(--lg-combobox-group-label-color);
|
|
31
28
|
`;
|
|
32
29
|
|
|
30
|
+
const comboboxGroupLabelThemeStyle: Record<Theme, string> = {
|
|
31
|
+
[Theme.Light]: css`
|
|
32
|
+
color: ${palette.gray.dark1};
|
|
33
|
+
`,
|
|
34
|
+
[Theme.Dark]: css`
|
|
35
|
+
color: ${palette.gray.light1};
|
|
36
|
+
`,
|
|
37
|
+
};
|
|
38
|
+
|
|
33
39
|
export function InternalComboboxGroup({
|
|
34
40
|
label,
|
|
35
41
|
className,
|
|
36
42
|
children,
|
|
37
43
|
}: ComboboxGroupProps): JSX.Element {
|
|
38
44
|
const { darkMode } = useContext(ComboboxContext);
|
|
45
|
+
const theme = useDarkMode(darkMode);
|
|
39
46
|
|
|
40
47
|
const groupId = useIdAllocator({ prefix: 'combobox-group' });
|
|
41
48
|
const childCount = React.Children.count(children);
|
|
42
49
|
|
|
43
50
|
return childCount > 0 ? (
|
|
44
|
-
<div className={cx(comboboxGroupStyle
|
|
45
|
-
<div
|
|
51
|
+
<div className={cx(comboboxGroupStyle[theme], className)}>
|
|
52
|
+
<div
|
|
53
|
+
className={cx(comboboxGroupLabel, comboboxGroupLabelThemeStyle[theme])}
|
|
54
|
+
id={groupId}
|
|
55
|
+
>
|
|
46
56
|
{label}
|
|
47
57
|
</div>
|
|
48
58
|
<div role="group" aria-labelledby={groupId}>
|