@buerokratt-ria/common-gui-components 0.0.59 → 0.0.61
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 +12 -0
- package/package.json +1 -1
- package/templates/history-page/src/History.scss +29 -1
- package/templates/history-page/src/components/HeaderCombobox/index.tsx +6 -0
- package/templates/history-page/src/constants.ts +1 -0
- package/templates/history-page/src/index.tsx +112 -106
- package/translations/en/common.json +1 -0
- package/translations/et/common.json +1 -0
- package/ui-components/DataTable/DataTable.scss +8 -0
- package/ui-components/DataTable/index.tsx +61 -56
- package/ui-components/FormElements/FormCombobox/FormCombobox.scss +44 -2
- package/ui-components/FormElements/FormCombobox/index.tsx +204 -58
- package/ui-components/FormElements/FormSelect/FormMultiselect.tsx +108 -36
- package/ui-components/FormElements/FormSelect/FormSelect.scss +32 -0
|
@@ -49,6 +49,10 @@
|
|
|
49
49
|
display: block;
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
+
+ #{$self}__menu--combobox {
|
|
53
|
+
display: flex;
|
|
54
|
+
}
|
|
55
|
+
|
|
52
56
|
+#{$self}__menu_up {
|
|
53
57
|
display: block;
|
|
54
58
|
}
|
|
@@ -124,7 +128,8 @@
|
|
|
124
128
|
}
|
|
125
129
|
|
|
126
130
|
#{$self}__search,
|
|
127
|
-
#{$self}__options
|
|
131
|
+
#{$self}__options,
|
|
132
|
+
#{$self}__actions {
|
|
128
133
|
background-color: get-color(white);
|
|
129
134
|
}
|
|
130
135
|
}
|
|
@@ -152,21 +157,56 @@
|
|
|
152
157
|
}
|
|
153
158
|
|
|
154
159
|
&__menu--combobox {
|
|
160
|
+
flex-direction: column;
|
|
155
161
|
top: auto;
|
|
156
162
|
bottom: 100%;
|
|
163
|
+
max-height: none;
|
|
157
164
|
overflow: hidden;
|
|
158
165
|
margin-top: 0;
|
|
159
166
|
margin-bottom: 3px;
|
|
160
167
|
}
|
|
161
168
|
|
|
169
|
+
&__menu--down {
|
|
170
|
+
top: 100%;
|
|
171
|
+
bottom: auto;
|
|
172
|
+
margin-top: 3px;
|
|
173
|
+
margin-bottom: 0;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
&__menu--portal {
|
|
177
|
+
display: flex;
|
|
178
|
+
position: fixed;
|
|
179
|
+
top: 100%;
|
|
180
|
+
bottom: auto;
|
|
181
|
+
right: auto;
|
|
182
|
+
margin-top: 3px;
|
|
183
|
+
margin-bottom: 0;
|
|
184
|
+
z-index: 10000;
|
|
185
|
+
}
|
|
186
|
+
|
|
162
187
|
&__options {
|
|
163
|
-
|
|
188
|
+
flex: 1 1 auto;
|
|
189
|
+
max-height: 220px;
|
|
190
|
+
min-height: 0;
|
|
164
191
|
overflow: auto;
|
|
165
192
|
list-style: none;
|
|
166
193
|
margin: 0;
|
|
167
194
|
padding: 0;
|
|
168
195
|
}
|
|
169
196
|
|
|
197
|
+
&__actions {
|
|
198
|
+
display: flex;
|
|
199
|
+
flex: 0 0 auto;
|
|
200
|
+
justify-content: center;
|
|
201
|
+
padding: 12px 16px 16px;
|
|
202
|
+
|
|
203
|
+
> .btn.btn--s {
|
|
204
|
+
justify-content: center;
|
|
205
|
+
min-width: 106px;
|
|
206
|
+
padding: 4px 16px;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
170
210
|
&__search {
|
|
171
211
|
padding: get-spacing(paldiski);
|
|
172
212
|
position: relative;
|
|
@@ -227,7 +267,9 @@
|
|
|
227
267
|
}
|
|
228
268
|
|
|
229
269
|
&__option--combobox {
|
|
270
|
+
box-sizing: border-box;
|
|
230
271
|
cursor: pointer;
|
|
272
|
+
min-height: 40px;
|
|
231
273
|
|
|
232
274
|
&[aria-selected=true] {
|
|
233
275
|
background-color: get-color(white);
|
|
@@ -5,15 +5,17 @@ import {
|
|
|
5
5
|
ReactNode,
|
|
6
6
|
useEffect,
|
|
7
7
|
useId,
|
|
8
|
+
useLayoutEffect,
|
|
8
9
|
useMemo,
|
|
9
10
|
useRef,
|
|
10
11
|
useState
|
|
11
12
|
} from 'react';
|
|
13
|
+
import { createPortal } from 'react-dom';
|
|
12
14
|
import clsx from 'clsx';
|
|
13
15
|
import { useTranslation } from 'react-i18next';
|
|
14
16
|
import { MdArrowDropDown, MdExpandMore, MdSearch } from 'react-icons/md';
|
|
15
17
|
|
|
16
|
-
import { Icon } from '../..';
|
|
18
|
+
import { Button, Icon } from '../..';
|
|
17
19
|
import './FormCombobox.scss';
|
|
18
20
|
|
|
19
21
|
type FormComboboxOption = {
|
|
@@ -32,6 +34,10 @@ type FormComboboxBaseProps = {
|
|
|
32
34
|
readonly style?: CSSProperties;
|
|
33
35
|
readonly isSearchEnabled?: boolean;
|
|
34
36
|
readonly hideInputStyle?: boolean;
|
|
37
|
+
readonly isMenuPortaled?: boolean;
|
|
38
|
+
readonly allOptionValue?: string;
|
|
39
|
+
readonly direction?: 'down' | 'up';
|
|
40
|
+
readonly selectedOptionsCount?: number;
|
|
35
41
|
};
|
|
36
42
|
|
|
37
43
|
type FormComboboxSingleProps = FormComboboxBaseProps & {
|
|
@@ -48,6 +54,7 @@ type FormComboboxMultipleProps = FormComboboxBaseProps & {
|
|
|
48
54
|
readonly defaultValue?: string[];
|
|
49
55
|
readonly onChange?: (value: string[]) => void;
|
|
50
56
|
readonly onSelectionChange?: (selection: FormComboboxOption[] | null) => void;
|
|
57
|
+
readonly isApplyBtnVisible?: boolean;
|
|
51
58
|
};
|
|
52
59
|
|
|
53
60
|
type FormComboboxProps = FormComboboxSingleProps | FormComboboxMultipleProps;
|
|
@@ -122,6 +129,39 @@ const orderSelectedOptionsFirst = (
|
|
|
122
129
|
return [...selectedOptions, ...unselectedOptions];
|
|
123
130
|
};
|
|
124
131
|
|
|
132
|
+
const getNextMultipleValues = (
|
|
133
|
+
option: FormComboboxOption,
|
|
134
|
+
options: FormComboboxOption[],
|
|
135
|
+
selectedValues: string[],
|
|
136
|
+
allOptionValue?: string
|
|
137
|
+
): string[] => {
|
|
138
|
+
if (!allOptionValue) {
|
|
139
|
+
return selectedValues.includes(option.value)
|
|
140
|
+
? selectedValues.filter((value) => value !== option.value)
|
|
141
|
+
: [...selectedValues, option.value];
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const optionValues = options.map((item) => item.value);
|
|
145
|
+
const realOptionValues = optionValues.filter((value) => value !== allOptionValue);
|
|
146
|
+
|
|
147
|
+
if (option.value === allOptionValue) {
|
|
148
|
+
return selectedValues.includes(allOptionValue) ? [] : [allOptionValue, ...realOptionValues];
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const nextRealValues = selectedValues.includes(option.value)
|
|
152
|
+
? selectedValues.filter((value) => value !== option.value && value !== allOptionValue)
|
|
153
|
+
: [...selectedValues.filter((value) => value !== allOptionValue), option.value];
|
|
154
|
+
|
|
155
|
+
if (
|
|
156
|
+
realOptionValues.length > 0 &&
|
|
157
|
+
realOptionValues.every((value) => nextRealValues.includes(value))
|
|
158
|
+
) {
|
|
159
|
+
return [allOptionValue, ...realOptionValues];
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return nextRealValues;
|
|
163
|
+
};
|
|
164
|
+
|
|
125
165
|
export const FormCombobox: FC<FormComboboxProps> = ({
|
|
126
166
|
label,
|
|
127
167
|
hideLabel,
|
|
@@ -132,6 +172,9 @@ export const FormCombobox: FC<FormComboboxProps> = ({
|
|
|
132
172
|
style,
|
|
133
173
|
isSearchEnabled = false,
|
|
134
174
|
hideInputStyle = false,
|
|
175
|
+
isMenuPortaled = false,
|
|
176
|
+
allOptionValue,
|
|
177
|
+
direction = 'up',
|
|
135
178
|
...props
|
|
136
179
|
}) => {
|
|
137
180
|
const id = useId();
|
|
@@ -144,15 +187,23 @@ export const FormCombobox: FC<FormComboboxProps> = ({
|
|
|
144
187
|
const [internalMultipleValue, setInternalMultipleValue] = useState<string[]>(
|
|
145
188
|
getInitialMultipleValue(props)
|
|
146
189
|
);
|
|
190
|
+
const [menuStyle, setMenuStyle] = useState<CSSProperties>({});
|
|
147
191
|
const wrapperRef = useRef<HTMLDivElement | null>(null);
|
|
192
|
+
const triggerRef = useRef<HTMLButtonElement | null>(null);
|
|
193
|
+
const menuRef = useRef<HTMLDivElement | null>(null);
|
|
148
194
|
const searchInputRef = useRef<HTMLInputElement | null>(null);
|
|
149
195
|
|
|
150
196
|
const isMultiple = props.multiple === true;
|
|
151
|
-
const
|
|
197
|
+
const isApplyBtnVisible = props.multiple === true ? props.isApplyBtnVisible : false;
|
|
198
|
+
const selectedValues = useMemo(() => (
|
|
199
|
+
getSelectedValues(props, internalSingleValue, internalMultipleValue)
|
|
200
|
+
), [props.multiple, props.value, internalSingleValue, internalMultipleValue]);
|
|
201
|
+
const [draftMultipleValue, setDraftMultipleValue] = useState<string[]>(selectedValues);
|
|
202
|
+
const menuSelectedValues = isApplyBtnVisible ? draftMultipleValue : selectedValues;
|
|
152
203
|
|
|
153
204
|
const orderedOptions = useMemo(() => (
|
|
154
|
-
orderSelectedOptionsFirst(options,
|
|
155
|
-
), [options,
|
|
205
|
+
orderSelectedOptionsFirst(options, menuSelectedValues)
|
|
206
|
+
), [options, menuSelectedValues]);
|
|
156
207
|
|
|
157
208
|
const filteredOptions = useMemo(() => {
|
|
158
209
|
const normalizedQuery = query.trim().toLowerCase();
|
|
@@ -167,15 +218,54 @@ export const FormCombobox: FC<FormComboboxProps> = ({
|
|
|
167
218
|
options.filter((option) => selectedValues.includes(option.value))
|
|
168
219
|
), [options, selectedValues]);
|
|
169
220
|
|
|
221
|
+
const updateMenuPosition = () => {
|
|
222
|
+
const triggerRect = triggerRef.current?.getBoundingClientRect();
|
|
223
|
+
|
|
224
|
+
if (!triggerRect) return;
|
|
225
|
+
|
|
226
|
+
const offset = 10;
|
|
227
|
+
|
|
228
|
+
setMenuStyle({
|
|
229
|
+
left: triggerRect.left,
|
|
230
|
+
minWidth: hideInputStyle ? 296 : triggerRect.width,
|
|
231
|
+
top: triggerRect.bottom + offset,
|
|
232
|
+
});
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
useLayoutEffect(() => {
|
|
236
|
+
if (!isOpen || !isMenuPortaled) return;
|
|
237
|
+
|
|
238
|
+
updateMenuPosition();
|
|
239
|
+
|
|
240
|
+
window.addEventListener('resize', updateMenuPosition);
|
|
241
|
+
document.addEventListener('scroll', updateMenuPosition, true);
|
|
242
|
+
|
|
243
|
+
return () => {
|
|
244
|
+
window.removeEventListener('resize', updateMenuPosition);
|
|
245
|
+
document.removeEventListener('scroll', updateMenuPosition, true);
|
|
246
|
+
};
|
|
247
|
+
}, [hideInputStyle, isMenuPortaled, isOpen]);
|
|
248
|
+
|
|
170
249
|
useEffect(() => {
|
|
171
250
|
if (isOpen) {
|
|
172
251
|
searchInputRef.current?.focus();
|
|
173
252
|
}
|
|
174
253
|
}, [isOpen]);
|
|
175
254
|
|
|
255
|
+
useEffect(() => {
|
|
256
|
+
if (isOpen && isApplyBtnVisible) {
|
|
257
|
+
setDraftMultipleValue(selectedValues);
|
|
258
|
+
}
|
|
259
|
+
}, [isOpen, isApplyBtnVisible]);
|
|
260
|
+
|
|
176
261
|
useEffect(() => {
|
|
177
262
|
const handleDocumentClick = (event: MouseEvent) => {
|
|
178
|
-
|
|
263
|
+
const target = event.target as Node;
|
|
264
|
+
|
|
265
|
+
if (
|
|
266
|
+
!wrapperRef.current?.contains(target) &&
|
|
267
|
+
!menuRef.current?.contains(target)
|
|
268
|
+
) {
|
|
179
269
|
setIsOpen(false);
|
|
180
270
|
setQuery('');
|
|
181
271
|
}
|
|
@@ -189,7 +279,12 @@ export const FormCombobox: FC<FormComboboxProps> = ({
|
|
|
189
279
|
}, []);
|
|
190
280
|
|
|
191
281
|
const closeOnFocusOutside = (event: ReactFocusEvent<HTMLDivElement>) => {
|
|
192
|
-
|
|
282
|
+
const relatedTarget = event.relatedTarget as Node | null;
|
|
283
|
+
|
|
284
|
+
if (
|
|
285
|
+
!event.currentTarget.contains(relatedTarget) &&
|
|
286
|
+
!menuRef.current?.contains(relatedTarget)
|
|
287
|
+
) {
|
|
193
288
|
setIsOpen(false);
|
|
194
289
|
setQuery('');
|
|
195
290
|
}
|
|
@@ -215,11 +310,15 @@ export const FormCombobox: FC<FormComboboxProps> = ({
|
|
|
215
310
|
if (!props.multiple) return;
|
|
216
311
|
const multipleProps = props as FormComboboxMultipleProps;
|
|
217
312
|
|
|
218
|
-
const
|
|
219
|
-
|
|
220
|
-
: [...selectedValues, option.value];
|
|
313
|
+
const currentValues = isApplyBtnVisible ? draftMultipleValue : selectedValues;
|
|
314
|
+
const nextValues = getNextMultipleValues(option, options, currentValues, allOptionValue);
|
|
221
315
|
const nextSelection = options.filter((item) => nextValues.includes(item.value));
|
|
222
316
|
|
|
317
|
+
if (isApplyBtnVisible) {
|
|
318
|
+
setDraftMultipleValue(nextValues);
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
|
|
223
322
|
if (multipleProps.value === undefined) {
|
|
224
323
|
setInternalMultipleValue(nextValues);
|
|
225
324
|
}
|
|
@@ -239,10 +338,29 @@ export const FormCombobox: FC<FormComboboxProps> = ({
|
|
|
239
338
|
setSingleValue(option);
|
|
240
339
|
};
|
|
241
340
|
|
|
341
|
+
const applyMultipleValue = () => {
|
|
342
|
+
if (!props.multiple) return;
|
|
343
|
+
|
|
344
|
+
const multipleProps = props as FormComboboxMultipleProps;
|
|
345
|
+
|
|
346
|
+
if (multipleProps.value === undefined) {
|
|
347
|
+
setInternalMultipleValue(draftMultipleValue);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
multipleProps.onChange?.(draftMultipleValue);
|
|
351
|
+
const nextSelection = options.filter((item) => draftMultipleValue.includes(item.value));
|
|
352
|
+
|
|
353
|
+
multipleProps.onSelectionChange?.(nextSelection.length ? nextSelection : null);
|
|
354
|
+
setIsOpen(false);
|
|
355
|
+
setQuery('');
|
|
356
|
+
};
|
|
357
|
+
|
|
242
358
|
const placeholderValue = placeholder || t('global.choose');
|
|
243
359
|
const triggerLabel = isMultiple
|
|
244
360
|
? selectedOptions.length > 0
|
|
245
|
-
?
|
|
361
|
+
? props.selectedOptionsCount !== undefined
|
|
362
|
+
? `${placeholder ?? t('global.chosen')} (${props.selectedOptionsCount})`
|
|
363
|
+
: selectedOptions.map((option) => option.label).join(', ')
|
|
246
364
|
: placeholderValue
|
|
247
365
|
: selectedOptions[0]?.label ?? placeholderValue;
|
|
248
366
|
const triggerContent = hideInputStyle ? label ?? triggerLabel : triggerLabel;
|
|
@@ -254,11 +372,84 @@ export const FormCombobox: FC<FormComboboxProps> = ({
|
|
|
254
372
|
hideInputStyle && 'select--plain',
|
|
255
373
|
);
|
|
256
374
|
|
|
375
|
+
const menu = (
|
|
376
|
+
<div
|
|
377
|
+
ref={menuRef}
|
|
378
|
+
className={clsx(
|
|
379
|
+
'select__menu select__menu--combobox',
|
|
380
|
+
`select__menu--${direction}`,
|
|
381
|
+
isMenuPortaled && 'select__menu--portal'
|
|
382
|
+
)}
|
|
383
|
+
style={isMenuPortaled ? menuStyle : undefined}
|
|
384
|
+
>
|
|
385
|
+
{
|
|
386
|
+
isSearchEnabled && <div className='select__search'>
|
|
387
|
+
<Icon
|
|
388
|
+
label='Search icon'
|
|
389
|
+
size='medium'
|
|
390
|
+
className='select__search-icon'
|
|
391
|
+
icon={<MdSearch className='search__icon-size' color='#5D6071' />}
|
|
392
|
+
/>
|
|
393
|
+
<input
|
|
394
|
+
ref={searchInputRef}
|
|
395
|
+
className='select__search-input'
|
|
396
|
+
value={query}
|
|
397
|
+
onChange={(event) => setQuery(event.target.value)}
|
|
398
|
+
placeholder={searchPlaceholder ?? t('global.search')}
|
|
399
|
+
disabled={disabled}
|
|
400
|
+
/>
|
|
401
|
+
</div>
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
<ul className='select__options' role='listbox' aria-label={searchPlaceholder ?? t('global.search')}>
|
|
405
|
+
{filteredOptions.length > 0 ? (
|
|
406
|
+
filteredOptions.map((option) => {
|
|
407
|
+
const isSelected = menuSelectedValues.includes(option.value);
|
|
408
|
+
|
|
409
|
+
return (
|
|
410
|
+
<li
|
|
411
|
+
key={option.value}
|
|
412
|
+
role='option'
|
|
413
|
+
aria-selected={isSelected}
|
|
414
|
+
className='select__option select__option--combobox'
|
|
415
|
+
onMouseDown={(event) => event.preventDefault()}
|
|
416
|
+
onClick={() => selectOption(option)}
|
|
417
|
+
>
|
|
418
|
+
<input
|
|
419
|
+
type={isMultiple ? 'checkbox' : 'radio'}
|
|
420
|
+
checked={isSelected}
|
|
421
|
+
value={option.value}
|
|
422
|
+
onChange={() => null}
|
|
423
|
+
onClick={(event) => event.preventDefault()}
|
|
424
|
+
/>
|
|
425
|
+
<span>{option.label}</span>
|
|
426
|
+
</li>
|
|
427
|
+
);
|
|
428
|
+
})
|
|
429
|
+
) : null}
|
|
430
|
+
</ul>
|
|
431
|
+
|
|
432
|
+
{isApplyBtnVisible && (
|
|
433
|
+
<div className='select__actions'>
|
|
434
|
+
<Button
|
|
435
|
+
size='s'
|
|
436
|
+
type='button'
|
|
437
|
+
onClick={applyMultipleValue}
|
|
438
|
+
disabled={disabled}
|
|
439
|
+
>
|
|
440
|
+
{t('global.apply')}
|
|
441
|
+
</Button>
|
|
442
|
+
</div>
|
|
443
|
+
)}
|
|
444
|
+
</div>
|
|
445
|
+
);
|
|
446
|
+
|
|
257
447
|
return (
|
|
258
448
|
<div ref={wrapperRef} className={selectClasses} style={style} onBlur={closeOnFocusOutside}>
|
|
259
449
|
{label && !hideLabel && <label htmlFor={id} className='select__label'>{label}</label>}
|
|
260
450
|
<div className='select__wrapper'>
|
|
261
451
|
<button
|
|
452
|
+
ref={triggerRef}
|
|
262
453
|
id={id}
|
|
263
454
|
type='button'
|
|
264
455
|
className='select__trigger'
|
|
@@ -277,54 +468,9 @@ export const FormCombobox: FC<FormComboboxProps> = ({
|
|
|
277
468
|
</button>
|
|
278
469
|
|
|
279
470
|
{isOpen && (
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
<Icon
|
|
284
|
-
label='Search icon'
|
|
285
|
-
size='medium'
|
|
286
|
-
className='select__search-icon'
|
|
287
|
-
icon={<MdSearch className='search__icon-size' color='#5D6071' />}
|
|
288
|
-
/>
|
|
289
|
-
<input
|
|
290
|
-
ref={searchInputRef}
|
|
291
|
-
className='select__search-input'
|
|
292
|
-
value={query}
|
|
293
|
-
onChange={(event) => setQuery(event.target.value)}
|
|
294
|
-
placeholder={searchPlaceholder ?? t('global.search')}
|
|
295
|
-
disabled={disabled}
|
|
296
|
-
/>
|
|
297
|
-
</div>
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
<ul className='select__options' role='listbox' aria-label={searchPlaceholder ?? t('global.search')}>
|
|
301
|
-
{filteredOptions.length > 0 ? (
|
|
302
|
-
filteredOptions.map((option) => {
|
|
303
|
-
const isSelected = selectedValues.includes(option.value);
|
|
304
|
-
|
|
305
|
-
return (
|
|
306
|
-
<li
|
|
307
|
-
key={option.value}
|
|
308
|
-
role='option'
|
|
309
|
-
aria-selected={isSelected}
|
|
310
|
-
className='select__option select__option--combobox'
|
|
311
|
-
onMouseDown={(event) => event.preventDefault()}
|
|
312
|
-
onClick={() => selectOption(option)}
|
|
313
|
-
>
|
|
314
|
-
<input
|
|
315
|
-
type={isMultiple ? 'checkbox' : 'radio'}
|
|
316
|
-
checked={isSelected}
|
|
317
|
-
value={option.value}
|
|
318
|
-
onChange={() => null}
|
|
319
|
-
onClick={(event) => event.preventDefault()}
|
|
320
|
-
/>
|
|
321
|
-
<span>{option.label}</span>
|
|
322
|
-
</li>
|
|
323
|
-
);
|
|
324
|
-
})
|
|
325
|
-
) : null}
|
|
326
|
-
</ul>
|
|
327
|
-
</div>
|
|
471
|
+
isMenuPortaled && typeof document !== 'undefined'
|
|
472
|
+
? createPortal(menu, document.body)
|
|
473
|
+
: menu
|
|
328
474
|
)}
|
|
329
475
|
</div>
|
|
330
476
|
</div>
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import React, { FC, ReactNode, SelectHTMLAttributes, useId, useState } from 'react';
|
|
1
|
+
import React, { FC, ReactNode, SelectHTMLAttributes, useEffect, useId, useState } from 'react';
|
|
2
2
|
import { useSelect } from 'downshift';
|
|
3
3
|
import clsx from 'clsx';
|
|
4
4
|
import { useTranslation } from 'react-i18next';
|
|
5
5
|
import { MdArrowDropDown } from 'react-icons/md';
|
|
6
6
|
|
|
7
|
-
import { Icon } from '../..';
|
|
7
|
+
import { Button, Icon } from '../..';
|
|
8
8
|
import './FormSelect.scss';
|
|
9
9
|
|
|
10
10
|
type SelectOption = { label: string, value: string };
|
|
@@ -18,6 +18,58 @@ type FormMultiselectProps = SelectHTMLAttributes<HTMLSelectElement> & {
|
|
|
18
18
|
selectedOptions?: SelectOption[];
|
|
19
19
|
selectedOptionsCount?: number;
|
|
20
20
|
onSelectionChange?: (selection: SelectOption[] | null) => void;
|
|
21
|
+
isApplyBtnVisible?: boolean;
|
|
22
|
+
allOptionValue?: string;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const getNextSelectedItems = (
|
|
26
|
+
selectedItem: SelectOption,
|
|
27
|
+
options: SelectOption[],
|
|
28
|
+
selectedItems: SelectOption[],
|
|
29
|
+
allOptionValue?: string
|
|
30
|
+
): SelectOption[] => {
|
|
31
|
+
if (!allOptionValue) {
|
|
32
|
+
const index = selectedItems.findIndex((item) => item.value === selectedItem.value);
|
|
33
|
+
const items: SelectOption[] = [];
|
|
34
|
+
|
|
35
|
+
if (index > 0) {
|
|
36
|
+
items.push(
|
|
37
|
+
...selectedItems.slice(0, index),
|
|
38
|
+
...selectedItems.slice(index + 1)
|
|
39
|
+
);
|
|
40
|
+
} else if (index === 0) {
|
|
41
|
+
items.push(...selectedItems.slice(1));
|
|
42
|
+
} else {
|
|
43
|
+
items.push(...selectedItems, selectedItem);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return items;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const realOptions = options.filter((option) => option.value !== allOptionValue);
|
|
50
|
+
const allOption = options.find((option) => option.value === allOptionValue);
|
|
51
|
+
|
|
52
|
+
if (selectedItem.value === allOptionValue) {
|
|
53
|
+
return selectedItems.some((item) => item.value === allOptionValue)
|
|
54
|
+
? []
|
|
55
|
+
: [...(allOption ? [allOption] : []), ...realOptions];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const selectedWithoutAll = selectedItems.filter((item) => item.value !== allOptionValue);
|
|
59
|
+
const isSelected = selectedWithoutAll.some((item) => item.value === selectedItem.value);
|
|
60
|
+
const nextRealItems = isSelected
|
|
61
|
+
? selectedWithoutAll.filter((item) => item.value !== selectedItem.value)
|
|
62
|
+
: [...selectedWithoutAll, selectedItem];
|
|
63
|
+
|
|
64
|
+
if (
|
|
65
|
+
allOption &&
|
|
66
|
+
realOptions.length > 0 &&
|
|
67
|
+
realOptions.every((option) => nextRealItems.some((item) => item.value === option.value))
|
|
68
|
+
) {
|
|
69
|
+
return [allOption, ...realOptions];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return nextRealItems;
|
|
21
73
|
};
|
|
22
74
|
|
|
23
75
|
const FormMultiselect: FC<FormMultiselectProps> = (
|
|
@@ -31,6 +83,8 @@ const FormMultiselect: FC<FormMultiselectProps> = (
|
|
|
31
83
|
selectedOptions,
|
|
32
84
|
selectedOptionsCount,
|
|
33
85
|
onSelectionChange,
|
|
86
|
+
isApplyBtnVisible = false,
|
|
87
|
+
allOptionValue,
|
|
34
88
|
...rest
|
|
35
89
|
},
|
|
36
90
|
) => {
|
|
@@ -63,30 +117,35 @@ const FormMultiselect: FC<FormMultiselectProps> = (
|
|
|
63
117
|
if (!selectedItem) {
|
|
64
118
|
return;
|
|
65
119
|
}
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
if (index > 0) {
|
|
69
|
-
items.push(
|
|
70
|
-
...selectedItems.slice(0, index),
|
|
71
|
-
...selectedItems.slice(index + 1)
|
|
72
|
-
);
|
|
73
|
-
} else if (index === 0) {
|
|
74
|
-
items.push(...selectedItems.slice(1));
|
|
75
|
-
} else {
|
|
76
|
-
items.push(...selectedItems, selectedItem);
|
|
77
|
-
}
|
|
120
|
+
const items = getNextSelectedItems(selectedItem, options, selectedItems, allOptionValue);
|
|
121
|
+
|
|
78
122
|
setSelectedItems(items);
|
|
79
|
-
if (
|
|
123
|
+
if (!isApplyBtnVisible) {
|
|
124
|
+
onSelectionChange?.(items.length ? items : null);
|
|
125
|
+
}
|
|
80
126
|
},
|
|
81
127
|
});
|
|
82
128
|
|
|
129
|
+
useEffect(() => {
|
|
130
|
+
setSelectedItems(selectedOptions ?? []);
|
|
131
|
+
}, [selectedOptions]);
|
|
132
|
+
|
|
133
|
+
const applySelection = () => {
|
|
134
|
+
onSelectionChange?.(selectedItems.length ? selectedItems : null);
|
|
135
|
+
};
|
|
136
|
+
|
|
83
137
|
const selectClasses = clsx(
|
|
84
138
|
'select',
|
|
85
139
|
disabled && 'select--disabled',
|
|
86
140
|
);
|
|
87
141
|
|
|
88
142
|
const placeholderValue = placeholder || t('global.choose');
|
|
89
|
-
const
|
|
143
|
+
const selectedItemsCount = allOptionValue
|
|
144
|
+
? selectedItems.filter((item) => item.value !== allOptionValue).length
|
|
145
|
+
: selectedItems.length;
|
|
146
|
+
const displaySelectedCount = isApplyBtnVisible
|
|
147
|
+
? selectedItemsCount
|
|
148
|
+
: selectedOptionsCount ?? selectedItems.length;
|
|
90
149
|
|
|
91
150
|
return (
|
|
92
151
|
<div className={selectClasses} style={rest.style}>
|
|
@@ -97,27 +156,40 @@ const FormMultiselect: FC<FormMultiselectProps> = (
|
|
|
97
156
|
<Icon label='Dropdown icon' size='medium' icon={<MdArrowDropDown color='#5D6071' />} />
|
|
98
157
|
</div>
|
|
99
158
|
|
|
100
|
-
<
|
|
101
|
-
{
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
159
|
+
<div className='select__menu select__menu--multiselect'>
|
|
160
|
+
<ul className='select__options' {...getMenuProps()}>
|
|
161
|
+
{isOpen &&
|
|
162
|
+
options.map((item, index) => (
|
|
163
|
+
<li
|
|
164
|
+
key={`${item.label}-${index}`}
|
|
165
|
+
className={clsx('select__option', { 'select__option--selected': highlightedIndex === index })}
|
|
166
|
+
{...getItemProps({
|
|
167
|
+
item,
|
|
168
|
+
index,
|
|
169
|
+
})}
|
|
170
|
+
>
|
|
171
|
+
<input
|
|
172
|
+
type='checkbox'
|
|
173
|
+
checked={selectedItems.map((s) => s.value).includes(item.value)}
|
|
174
|
+
value={item.value}
|
|
175
|
+
onChange={() => null}
|
|
176
|
+
/>
|
|
177
|
+
<span>{item.label}</span>
|
|
178
|
+
</li>
|
|
179
|
+
))}
|
|
180
|
+
</ul>
|
|
181
|
+
{isOpen && isApplyBtnVisible && (
|
|
182
|
+
<div className='select__actions'>
|
|
183
|
+
<Button
|
|
184
|
+
size='s'
|
|
185
|
+
type='button'
|
|
186
|
+
onClick={applySelection}
|
|
110
187
|
>
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
/>
|
|
117
|
-
<span>{item.label}</span>
|
|
118
|
-
</li>
|
|
119
|
-
))}
|
|
120
|
-
</ul>
|
|
188
|
+
{t('global.apply')}
|
|
189
|
+
</Button>
|
|
190
|
+
</div>
|
|
191
|
+
)}
|
|
192
|
+
</div>
|
|
121
193
|
</div>
|
|
122
194
|
</div>
|
|
123
195
|
);
|
|
@@ -48,6 +48,10 @@
|
|
|
48
48
|
display: block;
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
+
+ #{$self}__menu--multiselect {
|
|
52
|
+
display: flex;
|
|
53
|
+
}
|
|
54
|
+
|
|
51
55
|
+#{$self}__menu_up {
|
|
52
56
|
display: block;
|
|
53
57
|
}
|
|
@@ -74,6 +78,21 @@
|
|
|
74
78
|
margin-top: 3px;
|
|
75
79
|
}
|
|
76
80
|
|
|
81
|
+
&__menu--multiselect {
|
|
82
|
+
flex-direction: column;
|
|
83
|
+
overflow: hidden;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
&__options {
|
|
87
|
+
flex: 1 1 auto;
|
|
88
|
+
max-height: 224px;
|
|
89
|
+
min-height: 0;
|
|
90
|
+
overflow: auto;
|
|
91
|
+
list-style: none;
|
|
92
|
+
margin: 0;
|
|
93
|
+
padding: 0;
|
|
94
|
+
}
|
|
95
|
+
|
|
77
96
|
&__menu_up {
|
|
78
97
|
display: none;
|
|
79
98
|
position: absolute;
|
|
@@ -118,4 +137,17 @@
|
|
|
118
137
|
color: get-color(black-coral-20);
|
|
119
138
|
}
|
|
120
139
|
}
|
|
140
|
+
|
|
141
|
+
&__actions {
|
|
142
|
+
display: flex;
|
|
143
|
+
flex: 0 0 auto;
|
|
144
|
+
justify-content: center;
|
|
145
|
+
padding: 12px 16px 16px;
|
|
146
|
+
|
|
147
|
+
> .btn.btn--s {
|
|
148
|
+
justify-content: center;
|
|
149
|
+
min-width: 106px;
|
|
150
|
+
padding: 4px 16px;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
121
153
|
}
|