@stack-spot/citric-react 0.37.1-beta.1 → 0.37.1-beta.2
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/dist/components/Select/MultiSelect.d.ts +23 -1
- package/dist/components/Select/MultiSelect.d.ts.map +1 -1
- package/dist/components/Select/MultiSelect.js +71 -7
- package/dist/components/Select/MultiSelect.js.map +1 -1
- package/package.json +1 -1
- package/src/components/Select/MultiSelect.tsx +28 -22
|
@@ -33,6 +33,28 @@ export interface BaseMultiSelectProps<T> extends Omit<RichSelectProps<T>, 'value
|
|
|
33
33
|
* @default false
|
|
34
34
|
*/
|
|
35
35
|
showSelectAll?: boolean;
|
|
36
|
+
/**
|
|
37
|
+
* Whether to render selected values as removable chips/tags.
|
|
38
|
+
*
|
|
39
|
+
* @default false
|
|
40
|
+
*/
|
|
41
|
+
showAsChips?: boolean;
|
|
42
|
+
/**
|
|
43
|
+
* Whether to allow adding custom values that don't exist in options.
|
|
44
|
+
* When enabled, typing in the search and pressing Enter will add the value.
|
|
45
|
+
* The value will be added as a string to the list.
|
|
46
|
+
*
|
|
47
|
+
* @default false
|
|
48
|
+
*/
|
|
49
|
+
allowCustomOptions?: boolean;
|
|
50
|
+
/**
|
|
51
|
+
* Function to create a new option from a string input.
|
|
52
|
+
* Required when `allowCustomOptions` is true.
|
|
53
|
+
*
|
|
54
|
+
* @param input the string input from the user
|
|
55
|
+
* @returns the new option of type T
|
|
56
|
+
*/
|
|
57
|
+
createOption?: (inputValue: string) => T;
|
|
36
58
|
}
|
|
37
59
|
export type MultiSelectProps<T> = Omit<React.JSX.IntrinsicElements['div'], 'ref' | 'onChange' | 'onFocus' | 'onBlur'> & BaseMultiSelectProps<T>;
|
|
38
60
|
/**
|
|
@@ -56,5 +78,5 @@ export type MultiSelectProps<T> = Omit<React.JSX.IntrinsicElements['div'], 'ref'
|
|
|
56
78
|
* return <MultiSelect options={options} renderLabel={o => o.name} renderKey={o => o.id} value={value} setValue={setValue} />
|
|
57
79
|
* ```
|
|
58
80
|
*/
|
|
59
|
-
export declare const MultiSelect: <T>({ ref, options, value, onChange, renderLabel, renderKey, disabled, loading, renderOption, renderHeader, searchable, maxHeight, style, className, showArrow, placeholder, showSelectAll, ...props }: MultiSelectProps<T>) => import("react/jsx-runtime.js").JSX.Element;
|
|
81
|
+
export declare const MultiSelect: <T>({ ref, options, value, onChange, renderLabel, renderKey, disabled, loading, renderOption, renderHeader, searchable, maxHeight, style, className, showArrow, placeholder, showSelectAll, showAsChips, allowCustomOptions, createOption, ...props }: MultiSelectProps<T>) => import("react/jsx-runtime.js").JSX.Element;
|
|
60
82
|
//# sourceMappingURL=MultiSelect.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"MultiSelect.d.ts","sourceRoot":"","sources":["../../../src/components/Select/MultiSelect.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"MultiSelect.d.ts","sourceRoot":"","sources":["../../../src/components/Select/MultiSelect.tsx"],"names":[],"mappings":"AAgBA,OAAO,EAAE,eAAe,EAAE,MAAM,SAAS,CAAA;AAEzC,MAAM,WAAW,oBAAoB,CAAC,CAAC,CAAE,SACvC,IAAI,CAAC,eAAe,CAAC,CAAC,CAAC,EAAE,OAAO,GAAG,UAAU,GAAG,cAAc,GAAG,aAAa,GAAG,cAAc,GAAG,UAAU,GAAG,SAAS,GAAG,QAAQ,CAAC;IACpI,KAAK,EAAE,CAAC,EAAE,CAAC;IACX,QAAQ,EAAE,CAAC,KAAK,EAAE,CAAC,EAAE,KAAK,IAAI,CAAC;IAC/B;;;;;;OAMG;IACH,YAAY,CAAC,EAAE,CAAC,MAAM,EAAE,CAAC,KAAK,KAAK,CAAC,SAAS,CAAC;IAC9C;;;;;;OAMG;IACH,YAAY,CAAC,EAAE,CAAC,KAAK,EAAE,CAAC,EAAE,KAAK,KAAK,CAAC,SAAS,CAAC;IAC/C;;;;;;;OAOG;IACH,WAAW,CAAC,EAAE,CAAC,MAAM,EAAE,CAAC,KAAK,MAAM,CAAC;IACpC;;;;OAIG;IACH,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB;;;;OAIG;IACH,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB;;;;;;OAMG;IACH,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B;;;;;;OAMG;IACH,YAAY,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,CAAC,CAAC;CAC1C;AAED,MAAM,MAAM,gBAAgB,CAAC,CAAC,IAAI,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,iBAAiB,CAAC,KAAK,CAAC,EAAE,KAAK,GAAG,UAAU,GAAG,SAAS,GAAG,QAAQ,CAAC,GACnH,oBAAoB,CAAC,CAAC,CAAC,CAAA;AAEzB;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,eAAO,MAAM,WAAW,GACD,CAAC,qPAsBnB,gBAAgB,CAAC,CAAC,CAAC,4CA8MvB,CAAA"}
|
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime.js";
|
|
2
2
|
import { listToClass } from '@stack-spot/portal-theme';
|
|
3
3
|
import { useTranslate } from '@stack-spot/portal-translate';
|
|
4
|
-
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
4
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
5
5
|
import { useCheckboxGroupControls } from '../../utils/checkbox.js';
|
|
6
6
|
import { applyCSSVariable } from '../../utils/css.js';
|
|
7
7
|
import { defaultRenderKey, defaultRenderLabel } from '../../utils/options.js';
|
|
8
8
|
import { withRef } from '../../utils/react.js';
|
|
9
|
+
import { Badge } from '../Badge.js';
|
|
9
10
|
import { Checkbox } from '../Checkbox.js';
|
|
10
11
|
import { CheckboxGroup } from '../CheckboxGroup.js';
|
|
11
12
|
import { CitricComponent } from '../CitricComponent.js';
|
|
13
|
+
import { IconButton } from '../IconBox.js';
|
|
12
14
|
import { Input } from '../Input.js';
|
|
13
15
|
import { Row } from '../layout.js';
|
|
14
16
|
import { ProgressCircular } from '../ProgressCircular.js';
|
|
@@ -34,18 +36,27 @@ import { useDisabledEffect, useFocusEffect, useOpenPanelEffect } from './hooks.j
|
|
|
34
36
|
* return <MultiSelect options={options} renderLabel={o => o.name} renderKey={o => o.id} value={value} setValue={setValue} />
|
|
35
37
|
* ```
|
|
36
38
|
*/
|
|
37
|
-
export const MultiSelect = withRef(function MultiSelect({ ref, options, value = [], onChange, renderLabel = defaultRenderLabel, renderKey = defaultRenderKey, disabled, loading, renderOption, renderHeader, searchable, maxHeight, style, className, showArrow, placeholder, showSelectAll, ...props }) {
|
|
39
|
+
export const MultiSelect = withRef(function MultiSelect({ ref, options, value = [], onChange, renderLabel = defaultRenderLabel, renderKey = defaultRenderKey, disabled, loading, renderOption, renderHeader, searchable, maxHeight, style, className, showArrow, placeholder, showSelectAll, showAsChips = false, allowCustomOptions = false, createOption, ...props }) {
|
|
38
40
|
const t = useTranslate(dictionary);
|
|
39
41
|
const _element = useRef(null);
|
|
40
42
|
const element = ref ?? _element;
|
|
41
43
|
const [open, setOpen] = useState(false);
|
|
42
44
|
const [focused, setFocused] = useState(false);
|
|
45
|
+
// Merge options with selected values that are not in the original options
|
|
46
|
+
const mergedOptions = useMemo(() => {
|
|
47
|
+
const optionKeys = new Set(options.map(renderKey));
|
|
48
|
+
const extraValues = value.filter(v => !optionKeys.has(renderKey(v)));
|
|
49
|
+
return [...options, ...extraValues];
|
|
50
|
+
}, [options, value, renderKey]);
|
|
43
51
|
const controls = useCheckboxGroupControls({
|
|
44
|
-
options,
|
|
52
|
+
options: mergedOptions,
|
|
45
53
|
renderKey,
|
|
46
54
|
initialValue: value,
|
|
47
55
|
onChange,
|
|
48
|
-
applyFilter: (filter, option) =>
|
|
56
|
+
applyFilter: (filter, option) => {
|
|
57
|
+
const label = renderLabel(option);
|
|
58
|
+
return label.toLocaleLowerCase().includes(filter.toLocaleLowerCase());
|
|
59
|
+
},
|
|
49
60
|
});
|
|
50
61
|
useOpenPanelEffect({ open, setOpen, setSearch: controls.setFilter, element, searchable });
|
|
51
62
|
useFocusEffect({ element, focused, setFocused, setOpen });
|
|
@@ -54,27 +65,74 @@ export const MultiSelect = withRef(function MultiSelect({ ref, options, value =
|
|
|
54
65
|
if (value !== controls.value)
|
|
55
66
|
controls.setValue(value);
|
|
56
67
|
}, [value.map(renderKey).join(',')]);
|
|
68
|
+
const handleRemoveChip = useCallback((option) => {
|
|
69
|
+
const newValue = value.filter(v => renderKey(v) !== renderKey(option));
|
|
70
|
+
controls.setValue(newValue);
|
|
71
|
+
}, [controls, renderKey, value]);
|
|
72
|
+
const handleAddCustomValue = useCallback((e) => {
|
|
73
|
+
if (!allowCustomOptions || !createOption || !controls.filter)
|
|
74
|
+
return;
|
|
75
|
+
const filterValue = String(controls.filter).trim();
|
|
76
|
+
if (e.key === 'Enter' && filterValue && filterValue.length > 0) {
|
|
77
|
+
e.preventDefault();
|
|
78
|
+
const newOption = createOption(filterValue);
|
|
79
|
+
const exists = value.some(v => {
|
|
80
|
+
const key1 = renderKey(v);
|
|
81
|
+
const key2 = renderKey(newOption);
|
|
82
|
+
if (typeof key1 === 'string' && typeof key2 === 'string') {
|
|
83
|
+
return key1?.toLowerCase() === key2?.toLowerCase();
|
|
84
|
+
}
|
|
85
|
+
return JSON.stringify(key1) === JSON.stringify(key2);
|
|
86
|
+
});
|
|
87
|
+
if (!exists) {
|
|
88
|
+
controls.setValue([...value, newOption]);
|
|
89
|
+
}
|
|
90
|
+
controls.setFilter('');
|
|
91
|
+
}
|
|
92
|
+
}, [allowCustomOptions, controls.filter, mergedOptions, renderLabel]);
|
|
93
|
+
// Check if the current filter does not match any existing option
|
|
94
|
+
const hasTemporaryOption = useMemo(() => {
|
|
95
|
+
if (!allowCustomOptions || !controls.filter)
|
|
96
|
+
return false;
|
|
97
|
+
const filterValue = String(controls.filter).trim();
|
|
98
|
+
if (!filterValue || filterValue.length === 0)
|
|
99
|
+
return false;
|
|
100
|
+
const matchesExisting = mergedOptions.some(option => {
|
|
101
|
+
const label = renderLabel(option);
|
|
102
|
+
if (!label)
|
|
103
|
+
return false;
|
|
104
|
+
return label.toLowerCase() === filterValue.toLowerCase();
|
|
105
|
+
});
|
|
106
|
+
return !matchesExisting;
|
|
107
|
+
}, [allowCustomOptions, controls.filter, mergedOptions, renderLabel]);
|
|
57
108
|
const header = useMemo(() => {
|
|
58
109
|
if (value.length === 0)
|
|
59
110
|
return _jsx("span", { className: "placeholder header-text", children: placeholder });
|
|
60
111
|
const reversed = [...value].reverse();
|
|
112
|
+
if (showAsChips) {
|
|
113
|
+
return (_jsx(Row, { className: "header-chips", gap: "4px", flex: "1", style: { maxWidth: '100%', flexWrap: 'wrap' }, children: reversed.map(option => (_jsxs(Badge, { className: "chip", tag: "div", onClick: (e) => { e.stopPropagation(); }, children: [_jsx("span", { children: renderLabel(option) }), !disabled && (_jsx(IconButton, { icon: "Times", type: "button", className: "remove-button", size: "xs", disabled: disabled, onClick: (e) => {
|
|
114
|
+
e.stopPropagation();
|
|
115
|
+
handleRemoveChip(option);
|
|
116
|
+
}, "aria-label": `${t.remove} ${renderLabel(option)}` }))] }, renderKey(option)))) }));
|
|
117
|
+
}
|
|
61
118
|
return ((renderHeader?.(reversed)
|
|
62
119
|
?? (renderOption
|
|
63
|
-
? _jsx(Row, { className: "header-text", children: reversed.map(renderOption) })
|
|
120
|
+
? _jsx(Row, { className: "header-text", children: reversed.map((option, index) => (_jsx("span", { children: renderOption(option) }, renderKey(option) ?? index))) })
|
|
64
121
|
: _jsx("span", { className: "header-text", children: reversed.map(renderLabel).join(', ') }))) || _jsx("span", {}));
|
|
65
|
-
}, [value, placeholder]);
|
|
122
|
+
}, [value, placeholder, showAsChips, disabled, renderKey, renderLabel, handleRemoveChip, t, renderHeader, renderOption]);
|
|
66
123
|
return (_jsxs(CitricComponent, { tag: "div", component: "multi-select", style: maxHeight ? applyCSSVariable(style, 'max-height', `${maxHeight}px`) : style, className: listToClass([
|
|
67
124
|
className,
|
|
68
125
|
showArrow === false && 'hide-arrow',
|
|
69
126
|
open && 'open',
|
|
70
127
|
focused && 'focused',
|
|
71
128
|
disabled && 'disabled',
|
|
129
|
+
showAsChips && 'with-chips',
|
|
72
130
|
]), ref: element, "aria-busy": loading, ...props, children: [_jsxs("header", { onClick: () => {
|
|
73
131
|
if (disabled)
|
|
74
132
|
return;
|
|
75
133
|
setFocused(true);
|
|
76
134
|
setOpen(true);
|
|
77
|
-
}, onFocus: () => setFocused(true), "aria-label": t.accessibilityHelp, tabIndex: disabled ? undefined : 0, className: renderHeader ? 'custom' : undefined, children: [header, loading && _jsx(ProgressCircular, { size: "xs", className: "loader" })] }), _jsxs("div", { className: "selection-panel", "aria-hidden": !open, ...(open ? {} : { inert: 'true' }), children: [searchable && _jsx("div", { className: "search-bar", children: _jsxs("div", { "data-citric": "field-group", className: "auto", children: [_jsx("i", { "data-citric": "icon-box", className: "citric-icon outline Search" }), _jsx(Input, { type: "search", value: controls.filter, onChange: controls.setFilter, "aria-label": t.searchAccessibility })] }) }), showSelectAll && (_jsx(Checkbox, { className: "select-all", onChange: checked => checked ? controls.selectAll() : controls.removeSelection(), value: controls.isAllSelected, children: controls.isAllSelected ? t.removeSelection : t.selectAll })), _jsx(CheckboxGroup, { className: "options", gap: "0", options: controls.options, onChange: controls.setValue, value: controls.value, renderKey: controls.renderKey, focusable: false, renderItem: (checkbox, option) => (_jsxs(CitricComponent, { component: "checkbox-row", tag: "label", className: listToClass(['option', controls.isUnfilteredButChecked(option) && 'unfiltered']), children: [checkbox, renderOption?.(option) ?? renderLabel(option)] })) })] })] }));
|
|
135
|
+
}, onFocus: () => setFocused(true), "aria-label": t.accessibilityHelp, tabIndex: disabled ? undefined : 0, className: renderHeader ? 'custom' : undefined, children: [header, loading && _jsx(ProgressCircular, { size: "xs", className: "loader" })] }), _jsxs("div", { className: "selection-panel", "aria-hidden": !open, ...(open ? {} : { inert: 'true' }), children: [searchable && _jsx("div", { className: "search-bar", children: _jsxs("div", { "data-citric": "field-group", className: "auto", children: [_jsx("i", { "data-citric": "icon-box", className: "citric-icon outline Search" }), _jsx(Input, { type: "search", value: controls.filter, onChange: controls.setFilter, onKeyUp: handleAddCustomValue, "aria-label": t.searchAccessibility, placeholder: allowCustomOptions ? t.searchOrAddPlaceholder : undefined })] }) }), showSelectAll && (_jsx(Checkbox, { className: "select-all", onChange: checked => checked ? controls.selectAll() : controls.removeSelection(), value: controls.isAllSelected, children: controls.isAllSelected ? t.removeSelection : t.selectAll })), _jsx(CheckboxGroup, { className: "options", gap: "0", options: controls.options, onChange: controls.setValue, value: controls.value, renderKey: controls.renderKey, focusable: false, renderItem: (checkbox, option) => (_jsxs(CitricComponent, { component: "checkbox-row", tag: "label", className: listToClass(['option', controls.isUnfilteredButChecked(option) && 'unfiltered']), children: [checkbox, renderOption?.(option) ?? renderLabel(option)] })) }), hasTemporaryOption && (_jsxs("div", { className: "temporary-option", style: { fontStyle: 'italic', padding: '8px 16px', opacity: 0.7 }, children: [String(controls.filter).trim(), " (", t.pressEnterToAdd, ")"] }))] })] }));
|
|
78
136
|
});
|
|
79
137
|
const dictionary = {
|
|
80
138
|
en: {
|
|
@@ -82,12 +140,18 @@ const dictionary = {
|
|
|
82
140
|
searchAccessibility: 'Filter the options',
|
|
83
141
|
removeSelection: 'Remove selection',
|
|
84
142
|
selectAll: 'Select all',
|
|
143
|
+
remove: 'Remove',
|
|
144
|
+
searchOrAddPlaceholder: 'Search or press Enter to add',
|
|
145
|
+
pressEnterToAdd: 'press Enter to add',
|
|
85
146
|
},
|
|
86
147
|
pt: {
|
|
87
148
|
accessibilityHelp: 'Pressione a seta para baixo para selecionar múltiplas opções',
|
|
88
149
|
searchAccessibility: 'Filtre as opções',
|
|
89
150
|
removeSelection: 'Remover seleção',
|
|
90
151
|
selectAll: 'Selecionar todos',
|
|
152
|
+
remove: 'Remover',
|
|
153
|
+
searchOrAddPlaceholder: 'Busque ou pressione Enter para adicionar',
|
|
154
|
+
pressEnterToAdd: 'pressione Enter para adicionar',
|
|
91
155
|
},
|
|
92
156
|
};
|
|
93
157
|
//# sourceMappingURL=MultiSelect.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"MultiSelect.js","sourceRoot":"","sources":["../../../src/components/Select/MultiSelect.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAA;AACtD,OAAO,EAAE,YAAY,EAAE,MAAM,8BAA8B,CAAA;AAC3D,OAAO,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAA;
|
|
1
|
+
{"version":3,"file":"MultiSelect.js","sourceRoot":"","sources":["../../../src/components/Select/MultiSelect.tsx"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { listToClass } from '@stack-spot/portal-theme'
|
|
2
2
|
import { useTranslate } from '@stack-spot/portal-translate'
|
|
3
|
-
import { useEffect, useMemo, useRef, useState } from 'react'
|
|
3
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
|
4
4
|
import { useCheckboxGroupControls } from '../../utils/checkbox'
|
|
5
5
|
import { applyCSSVariable } from '../../utils/css'
|
|
6
6
|
import { defaultRenderKey, defaultRenderLabel } from '../../utils/options'
|
|
7
7
|
import { withRef } from '../../utils/react'
|
|
8
|
+
import { Badge } from '../Badge'
|
|
8
9
|
import { Checkbox } from '../Checkbox'
|
|
9
10
|
import { CheckboxGroup } from '../CheckboxGroup'
|
|
10
11
|
import { CitricComponent } from '../CitricComponent'
|
|
@@ -143,7 +144,6 @@ export const MultiSelect = withRef(
|
|
|
143
144
|
onChange,
|
|
144
145
|
applyFilter: (filter, option) => {
|
|
145
146
|
const label = renderLabel(option)
|
|
146
|
-
if (!label) return false
|
|
147
147
|
return label.toLocaleLowerCase().includes(filter.toLocaleLowerCase())
|
|
148
148
|
},
|
|
149
149
|
})
|
|
@@ -156,12 +156,12 @@ export const MultiSelect = withRef(
|
|
|
156
156
|
if (value !== controls.value) controls.setValue(value)
|
|
157
157
|
}, [value.map(renderKey).join(',')])
|
|
158
158
|
|
|
159
|
-
const handleRemoveChip = (option: T) => {
|
|
159
|
+
const handleRemoveChip = useCallback((option: T) => {
|
|
160
160
|
const newValue = value.filter(v => renderKey(v) !== renderKey(option))
|
|
161
161
|
controls.setValue(newValue)
|
|
162
|
-
}
|
|
162
|
+
}, [controls, renderKey, value])
|
|
163
163
|
|
|
164
|
-
const handleAddCustomValue = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
|
164
|
+
const handleAddCustomValue = useCallback((e: React.KeyboardEvent<HTMLInputElement>) => {
|
|
165
165
|
if (!allowCustomOptions || !createOption || !controls.filter) return
|
|
166
166
|
const filterValue = String(controls.filter).trim()
|
|
167
167
|
if (e.key === 'Enter' && filterValue && filterValue.length > 0) {
|
|
@@ -171,44 +171,45 @@ export const MultiSelect = withRef(
|
|
|
171
171
|
const key1 = renderKey(v)
|
|
172
172
|
const key2 = renderKey(newOption)
|
|
173
173
|
if (typeof key1 === 'string' && typeof key2 === 'string') {
|
|
174
|
-
return key1
|
|
174
|
+
return key1?.toLowerCase() === key2?.toLowerCase()
|
|
175
175
|
}
|
|
176
|
-
return key1 === key2
|
|
176
|
+
return JSON.stringify(key1) === JSON.stringify(key2)
|
|
177
177
|
})
|
|
178
178
|
if (!exists) {
|
|
179
179
|
controls.setValue([...value, newOption])
|
|
180
180
|
}
|
|
181
181
|
controls.setFilter('' as any)
|
|
182
182
|
}
|
|
183
|
-
}
|
|
183
|
+
}, [allowCustomOptions, controls.filter, mergedOptions, renderLabel])
|
|
184
184
|
|
|
185
185
|
// Check if the current filter does not match any existing option
|
|
186
186
|
const hasTemporaryOption = useMemo(() => {
|
|
187
187
|
if (!allowCustomOptions || !controls.filter) return false
|
|
188
188
|
const filterValue = String(controls.filter).trim()
|
|
189
189
|
if (!filterValue || filterValue.length === 0) return false
|
|
190
|
-
|
|
190
|
+
|
|
191
191
|
const matchesExisting = mergedOptions.some(option => {
|
|
192
192
|
const label = renderLabel(option)
|
|
193
193
|
if (!label) return false
|
|
194
194
|
return label.toLowerCase() === filterValue.toLowerCase()
|
|
195
195
|
})
|
|
196
|
-
|
|
196
|
+
|
|
197
197
|
return !matchesExisting
|
|
198
198
|
}, [allowCustomOptions, controls.filter, mergedOptions, renderLabel])
|
|
199
199
|
|
|
200
200
|
const header = useMemo(() => {
|
|
201
201
|
if (value.length === 0) return <span className="placeholder header-text">{placeholder}</span>
|
|
202
202
|
const reversed = [...value].reverse()
|
|
203
|
-
|
|
203
|
+
|
|
204
204
|
if (showAsChips) {
|
|
205
205
|
return (
|
|
206
|
-
<Row className="header-chips" gap="4px">
|
|
206
|
+
<Row className="header-chips" gap="4px" flex="1" style={{ maxWidth: '100%', flexWrap: 'wrap' }}>
|
|
207
207
|
{reversed.map(option => (
|
|
208
|
-
<
|
|
208
|
+
<Badge
|
|
209
209
|
key={renderKey(option)}
|
|
210
|
-
data-citric="badge"
|
|
211
210
|
className="chip"
|
|
211
|
+
tag="div"
|
|
212
|
+
onClick={(e) => { e.stopPropagation() }}
|
|
212
213
|
>
|
|
213
214
|
<span>{renderLabel(option)}</span>
|
|
214
215
|
{!disabled && (
|
|
@@ -225,7 +226,7 @@ export const MultiSelect = withRef(
|
|
|
225
226
|
aria-label={`${t.remove} ${renderLabel(option)}`}
|
|
226
227
|
/>
|
|
227
228
|
)}
|
|
228
|
-
</
|
|
229
|
+
</Badge>
|
|
229
230
|
))}
|
|
230
231
|
</Row>
|
|
231
232
|
)
|
|
@@ -234,11 +235,16 @@ export const MultiSelect = withRef(
|
|
|
234
235
|
return (
|
|
235
236
|
(renderHeader?.(reversed)
|
|
236
237
|
?? (renderOption
|
|
237
|
-
? <Row className="header-text">
|
|
238
|
+
? <Row className="header-text">
|
|
239
|
+
{reversed.map((option, index) => (
|
|
240
|
+
<span key={renderKey(option) ?? index}>{renderOption(option)}</span>
|
|
241
|
+
))}
|
|
242
|
+
</Row>
|
|
238
243
|
: <span className="header-text">{reversed.map(renderLabel).join(', ')}</span>
|
|
239
244
|
)
|
|
240
245
|
) || <span></span>
|
|
241
|
-
)
|
|
246
|
+
)
|
|
247
|
+
}, [value, placeholder, showAsChips, disabled, renderKey, renderLabel, handleRemoveChip, t, renderHeader, renderOption])
|
|
242
248
|
|
|
243
249
|
return (
|
|
244
250
|
<CitricComponent
|
|
@@ -275,11 +281,11 @@ export const MultiSelect = withRef(
|
|
|
275
281
|
{searchable && <div className="search-bar">
|
|
276
282
|
<div data-citric="field-group" className="auto">
|
|
277
283
|
<i data-citric="icon-box" className="citric-icon outline Search"></i>
|
|
278
|
-
<Input
|
|
279
|
-
type="search"
|
|
280
|
-
value={controls.filter}
|
|
281
|
-
onChange={controls.setFilter}
|
|
282
|
-
|
|
284
|
+
<Input
|
|
285
|
+
type="search"
|
|
286
|
+
value={controls.filter}
|
|
287
|
+
onChange={controls.setFilter}
|
|
288
|
+
onKeyUp={handleAddCustomValue}
|
|
283
289
|
aria-label={t.searchAccessibility}
|
|
284
290
|
placeholder={allowCustomOptions ? t.searchOrAddPlaceholder : undefined}
|
|
285
291
|
/>
|