@vkontakte/vkui 6.7.0 → 6.7.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/cjs/components/AppRoot/AppRoot.d.ts.map +1 -1
- package/dist/cjs/components/AppRoot/AppRoot.js +9 -3
- package/dist/cjs/components/AppRoot/AppRoot.js.map +1 -1
- package/dist/cjs/components/BaseGallery/CarouselBase/CarouselBase.d.ts.map +1 -1
- package/dist/cjs/components/BaseGallery/CarouselBase/CarouselBase.js +9 -0
- package/dist/cjs/components/BaseGallery/CarouselBase/CarouselBase.js.map +1 -1
- package/dist/cjs/components/CustomSelect/CustomSelect.d.ts +12 -2
- package/dist/cjs/components/CustomSelect/CustomSelect.d.ts.map +1 -1
- package/dist/cjs/components/CustomSelect/CustomSelect.js +72 -52
- package/dist/cjs/components/CustomSelect/CustomSelect.js.map +1 -1
- package/dist/cjs/components/CustomSelect/CustomSelectInput.d.ts +1 -3
- package/dist/cjs/components/CustomSelect/CustomSelectInput.d.ts.map +1 -1
- package/dist/cjs/components/CustomSelect/CustomSelectInput.js +24 -19
- package/dist/cjs/components/CustomSelect/CustomSelectInput.js.map +1 -1
- package/dist/cjs/components/HorizontalScroll/HorizontalScroll.d.ts +0 -2
- package/dist/cjs/components/HorizontalScroll/HorizontalScroll.d.ts.map +1 -1
- package/dist/cjs/components/HorizontalScroll/HorizontalScroll.js.map +1 -1
- package/dist/cjs/components/Select/Select.js +2 -1
- package/dist/cjs/components/Select/Select.js.map +1 -1
- package/dist/cjs/components/SimpleCell/SimpleCell.d.ts +4 -2
- package/dist/cjs/components/SimpleCell/SimpleCell.d.ts.map +1 -1
- package/dist/cjs/components/SimpleCell/SimpleCell.js.map +1 -1
- package/dist/cjs/components/Spacing/Spacing.js +1 -1
- package/dist/cjs/components/Spacing/Spacing.js.map +1 -1
- package/dist/cjs/lib/floating/useFloatingWithInteractions/useFloatingWithInteractions.d.ts.map +1 -1
- package/dist/cjs/lib/floating/useFloatingWithInteractions/useFloatingWithInteractions.js +3 -0
- package/dist/cjs/lib/floating/useFloatingWithInteractions/useFloatingWithInteractions.js.map +1 -1
- package/dist/components/AppRoot/AppRoot.d.ts.map +1 -1
- package/dist/components/AppRoot/AppRoot.js +9 -3
- package/dist/components/AppRoot/AppRoot.js.map +1 -1
- package/dist/components/BaseGallery/CarouselBase/CarouselBase.d.ts.map +1 -1
- package/dist/components/BaseGallery/CarouselBase/CarouselBase.js +10 -1
- package/dist/components/BaseGallery/CarouselBase/CarouselBase.js.map +1 -1
- package/dist/components/CustomSelect/CustomSelect.d.ts +12 -2
- package/dist/components/CustomSelect/CustomSelect.d.ts.map +1 -1
- package/dist/components/CustomSelect/CustomSelect.js +64 -44
- package/dist/components/CustomSelect/CustomSelect.js.map +1 -1
- package/dist/components/CustomSelect/CustomSelectInput.d.ts +1 -3
- package/dist/components/CustomSelect/CustomSelectInput.d.ts.map +1 -1
- package/dist/components/CustomSelect/CustomSelectInput.js +24 -19
- package/dist/components/CustomSelect/CustomSelectInput.js.map +1 -1
- package/dist/components/HorizontalScroll/HorizontalScroll.d.ts +0 -2
- package/dist/components/HorizontalScroll/HorizontalScroll.d.ts.map +1 -1
- package/dist/components/HorizontalScroll/HorizontalScroll.js.map +1 -1
- package/dist/components/Select/Select.js +2 -1
- package/dist/components/Select/Select.js.map +1 -1
- package/dist/components/SimpleCell/SimpleCell.d.ts +4 -2
- package/dist/components/SimpleCell/SimpleCell.d.ts.map +1 -1
- package/dist/components/SimpleCell/SimpleCell.js.map +1 -1
- package/dist/components/Spacing/Spacing.js +1 -1
- package/dist/components/Spacing/Spacing.js.map +1 -1
- package/dist/components.css +3 -3
- package/dist/components.css.map +1 -1
- package/dist/components.js.tmp +128 -162
- package/dist/cssm/components/AppRoot/AppRoot.d.ts.map +1 -1
- package/dist/cssm/components/AppRoot/AppRoot.js +9 -3
- package/dist/cssm/components/AppRoot/AppRoot.js.map +1 -1
- package/dist/cssm/components/BaseGallery/CarouselBase/CarouselBase.d.ts.map +1 -1
- package/dist/cssm/components/BaseGallery/CarouselBase/CarouselBase.js +10 -1
- package/dist/cssm/components/BaseGallery/CarouselBase/CarouselBase.js.map +1 -1
- package/dist/cssm/components/CellButton/CellButton.module.css +9 -2
- package/dist/cssm/components/CustomSelect/CustomSelect.d.ts +12 -2
- package/dist/cssm/components/CustomSelect/CustomSelect.d.ts.map +1 -1
- package/dist/cssm/components/CustomSelect/CustomSelect.js +60 -41
- package/dist/cssm/components/CustomSelect/CustomSelect.js.map +1 -1
- package/dist/cssm/components/CustomSelect/CustomSelectInput.d.ts +1 -3
- package/dist/cssm/components/CustomSelect/CustomSelectInput.d.ts.map +1 -1
- package/dist/cssm/components/CustomSelect/CustomSelectInput.js +21 -16
- package/dist/cssm/components/CustomSelect/CustomSelectInput.js.map +1 -1
- package/dist/cssm/components/CustomSelect/CustomSelectInput.module.css +40 -74
- package/dist/cssm/components/HorizontalScroll/HorizontalScroll.d.ts +0 -2
- package/dist/cssm/components/HorizontalScroll/HorizontalScroll.d.ts.map +1 -1
- package/dist/cssm/components/HorizontalScroll/HorizontalScroll.js.map +1 -1
- package/dist/cssm/components/Select/Select.js +2 -1
- package/dist/cssm/components/Select/Select.js.map +1 -1
- package/dist/cssm/components/SimpleCell/SimpleCell.d.ts +4 -2
- package/dist/cssm/components/SimpleCell/SimpleCell.d.ts.map +1 -1
- package/dist/cssm/components/SimpleCell/SimpleCell.js.map +1 -1
- package/dist/cssm/components/SimpleCell/SimpleCell.module.css +4 -2
- package/dist/cssm/components/Spacing/Spacing.js +1 -1
- package/dist/cssm/components/Spacing/Spacing.js.map +1 -1
- package/dist/cssm/components/Spacing/Spacing.module.css +1 -2
- package/dist/cssm/components/TabsItem/TabsItem.module.css +1 -1
- package/dist/cssm/lib/floating/useFloatingWithInteractions/useFloatingWithInteractions.d.ts.map +1 -1
- package/dist/cssm/lib/floating/useFloatingWithInteractions/useFloatingWithInteractions.js +3 -0
- package/dist/cssm/lib/floating/useFloatingWithInteractions/useFloatingWithInteractions.js.map +1 -1
- package/dist/lib/floating/useFloatingWithInteractions/useFloatingWithInteractions.d.ts.map +1 -1
- package/dist/lib/floating/useFloatingWithInteractions/useFloatingWithInteractions.js +3 -0
- package/dist/lib/floating/useFloatingWithInteractions/useFloatingWithInteractions.js.map +1 -1
- package/dist/vkui.css +3 -3
- package/dist/vkui.css.map +1 -1
- package/dist/vkui.js.tmp +128 -162
- package/package.json +1 -1
- package/src/components/AppRoot/AppRoot.tsx +16 -13
- package/src/components/BaseGallery/CarouselBase/CarouselBase.tsx +16 -1
- package/src/components/CellButton/CellButton.module.css +5 -1
- package/src/components/CustomSelect/CustomSelect.tsx +101 -53
- package/src/components/CustomSelect/CustomSelectInput.module.css +35 -55
- package/src/components/CustomSelect/CustomSelectInput.tsx +35 -24
- package/src/components/HorizontalScroll/HorizontalScroll.tsx +0 -2
- package/src/components/Select/Select.tsx +2 -2
- package/src/components/SimpleCell/SimpleCell.module.css +3 -1
- package/src/components/SimpleCell/SimpleCell.tsx +4 -2
- package/src/components/Spacing/Spacing.module.css +1 -2
- package/src/components/Spacing/Spacing.tsx +1 -1
- package/src/components/TabsItem/TabsItem.module.css +1 -1
- package/src/lib/floating/useFloatingWithInteractions/useFloatingWithInteractions.ts +3 -0
- package/dist/cjs/components/CustomSelect/helpers.d.ts +0 -8
- package/dist/cjs/components/CustomSelect/helpers.d.ts.map +0 -1
- package/dist/cjs/components/CustomSelect/helpers.js +0 -76
- package/dist/cjs/components/CustomSelect/helpers.js.map +0 -1
- package/dist/cjs/components/CustomSelect/types.d.ts +0 -12
- package/dist/cjs/components/CustomSelect/types.d.ts.map +0 -1
- package/dist/cjs/components/CustomSelect/types.js +0 -6
- package/dist/cjs/components/CustomSelect/types.js.map +0 -1
- package/dist/components/CustomSelect/helpers.d.ts +0 -8
- package/dist/components/CustomSelect/helpers.d.ts.map +0 -1
- package/dist/components/CustomSelect/helpers.js +0 -48
- package/dist/components/CustomSelect/helpers.js.map +0 -1
- package/dist/components/CustomSelect/types.d.ts +0 -12
- package/dist/components/CustomSelect/types.d.ts.map +0 -1
- package/dist/components/CustomSelect/types.js +0 -3
- package/dist/components/CustomSelect/types.js.map +0 -1
- package/dist/cssm/components/CustomSelect/helpers.d.ts +0 -8
- package/dist/cssm/components/CustomSelect/helpers.d.ts.map +0 -1
- package/dist/cssm/components/CustomSelect/helpers.js +0 -44
- package/dist/cssm/components/CustomSelect/helpers.js.map +0 -1
- package/dist/cssm/components/CustomSelect/types.d.ts +0 -12
- package/dist/cssm/components/CustomSelect/types.d.ts.map +0 -1
- package/dist/cssm/components/CustomSelect/types.js +0 -3
- package/dist/cssm/components/CustomSelect/types.js.map +0 -1
- package/src/components/CustomSelect/helpers.tsx +0 -61
- package/src/components/CustomSelect/types.ts +0 -15
|
@@ -2,6 +2,7 @@ import * as React from 'react';
|
|
|
2
2
|
import { classNames, debounce } from '@vkontakte/vkjs';
|
|
3
3
|
import { useAdaptivity } from '../../hooks/useAdaptivity';
|
|
4
4
|
import { useExternRef } from '../../hooks/useExternRef';
|
|
5
|
+
import { useFocusWithin } from '../../hooks/useFocusWithin';
|
|
5
6
|
import { useDOM } from '../../lib/dom';
|
|
6
7
|
import type { Placement } from '../../lib/floating';
|
|
7
8
|
import { defaultFilterFn, type FilterFn } from '../../lib/select';
|
|
@@ -12,24 +13,21 @@ import {
|
|
|
12
13
|
CustomSelectDropdown,
|
|
13
14
|
type CustomSelectDropdownProps,
|
|
14
15
|
} from '../CustomSelectDropdown/CustomSelectDropdown';
|
|
16
|
+
import {
|
|
17
|
+
CustomSelectOption,
|
|
18
|
+
type CustomSelectOptionProps,
|
|
19
|
+
} from '../CustomSelectOption/CustomSelectOption';
|
|
15
20
|
import { DropdownIcon } from '../DropdownIcon/DropdownIcon';
|
|
16
21
|
import type { FormFieldProps } from '../FormField/FormField';
|
|
17
22
|
import type { NativeSelectProps } from '../NativeSelect/NativeSelect';
|
|
18
23
|
import type { SelectType } from '../Select/Select';
|
|
19
24
|
import { Footnote } from '../Typography/Footnote/Footnote';
|
|
25
|
+
import { VisuallyHidden } from '../VisuallyHidden/VisuallyHidden';
|
|
20
26
|
import {
|
|
21
27
|
CustomSelectClearButton,
|
|
22
28
|
type CustomSelectClearButtonProps,
|
|
23
29
|
} from './CustomSelectClearButton';
|
|
24
30
|
import { CustomSelectInput, type CustomSelectInputProps } from './CustomSelectInput';
|
|
25
|
-
import {
|
|
26
|
-
calculateInputValueFromOptions,
|
|
27
|
-
defaultRenderOptionFn,
|
|
28
|
-
findIndexAfter,
|
|
29
|
-
findIndexBefore,
|
|
30
|
-
findSelectedIndex,
|
|
31
|
-
} from './helpers';
|
|
32
|
-
import type { CustomSelectOptionInterface, CustomSelectRenderOption } from './types';
|
|
33
31
|
import styles from './CustomSelect.module.css';
|
|
34
32
|
|
|
35
33
|
const sizeYClassNames = {
|
|
@@ -37,6 +35,32 @@ const sizeYClassNames = {
|
|
|
37
35
|
compact: styles['CustomSelect--sizeY-compact'],
|
|
38
36
|
};
|
|
39
37
|
|
|
38
|
+
const findIndexAfter = (options: CustomSelectOptionInterface[] = [], startIndex = -1) => {
|
|
39
|
+
if (startIndex >= options.length - 1) {
|
|
40
|
+
return -1;
|
|
41
|
+
}
|
|
42
|
+
return options.findIndex((option, i) => i > startIndex && !option.disabled);
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const findIndexBefore = (
|
|
46
|
+
options: CustomSelectOptionInterface[] = [],
|
|
47
|
+
endIndex: number = options.length,
|
|
48
|
+
) => {
|
|
49
|
+
let result = -1;
|
|
50
|
+
if (endIndex <= 0) {
|
|
51
|
+
return result;
|
|
52
|
+
}
|
|
53
|
+
for (let i = endIndex - 1; i >= 0; i--) {
|
|
54
|
+
let option = options[i];
|
|
55
|
+
|
|
56
|
+
if (!option.disabled) {
|
|
57
|
+
result = i;
|
|
58
|
+
break;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return result;
|
|
62
|
+
};
|
|
63
|
+
|
|
40
64
|
const warn = warnOnce('CustomSelect');
|
|
41
65
|
|
|
42
66
|
const checkOptionsValueType = <T extends CustomSelectOptionInterface>(options: T[]) => {
|
|
@@ -48,10 +72,33 @@ const checkOptionsValueType = <T extends CustomSelectOptionInterface>(options: T
|
|
|
48
72
|
}
|
|
49
73
|
};
|
|
50
74
|
|
|
75
|
+
function defaultRenderOptionFn<T extends CustomSelectOptionInterface>({
|
|
76
|
+
option,
|
|
77
|
+
...props
|
|
78
|
+
}: CustomSelectRenderOption<T>): React.ReactNode {
|
|
79
|
+
return <CustomSelectOption {...props} />;
|
|
80
|
+
}
|
|
81
|
+
|
|
51
82
|
const handleOptionDown: MouseEventHandler = (e: React.MouseEvent<HTMLElement>) => {
|
|
52
83
|
e.preventDefault();
|
|
53
84
|
};
|
|
54
85
|
|
|
86
|
+
function findSelectedIndex<T extends CustomSelectOptionInterface>(
|
|
87
|
+
options: T[] = [],
|
|
88
|
+
value: SelectValue,
|
|
89
|
+
withClear: boolean,
|
|
90
|
+
) {
|
|
91
|
+
if (withClear && value === '') {
|
|
92
|
+
return -1;
|
|
93
|
+
}
|
|
94
|
+
return (
|
|
95
|
+
options.findIndex((item) => {
|
|
96
|
+
value = typeof item.value === 'number' ? Number(value) : value;
|
|
97
|
+
return item.value === value;
|
|
98
|
+
}) ?? -1
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
55
102
|
const filter = <T extends CustomSelectOptionInterface>(
|
|
56
103
|
options: SelectProps<T>['options'],
|
|
57
104
|
inputValue: string,
|
|
@@ -62,7 +109,21 @@ const filter = <T extends CustomSelectOptionInterface>(
|
|
|
62
109
|
: options;
|
|
63
110
|
};
|
|
64
111
|
|
|
65
|
-
|
|
112
|
+
type SelectValue = React.SelectHTMLAttributes<HTMLSelectElement>['value'];
|
|
113
|
+
|
|
114
|
+
export interface CustomSelectOptionInterface {
|
|
115
|
+
value: SelectValue;
|
|
116
|
+
label: React.ReactElement | string;
|
|
117
|
+
disabled?: boolean;
|
|
118
|
+
[index: string]: any;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export interface CustomSelectRenderOption<T extends CustomSelectOptionInterface>
|
|
122
|
+
extends CustomSelectOptionProps {
|
|
123
|
+
option: T;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export type { CustomSelectClearButtonProps };
|
|
66
127
|
|
|
67
128
|
export interface SelectProps<
|
|
68
129
|
OptionInterfaceT extends CustomSelectOptionInterface = CustomSelectOptionInterface,
|
|
@@ -220,18 +281,14 @@ export function CustomSelect<OptionInterfaceT extends CustomSelectOptionInterfac
|
|
|
220
281
|
const scrollBoxRef = React.useRef<HTMLDivElement | null>(null);
|
|
221
282
|
const selectElRef = useExternRef(getRef);
|
|
222
283
|
const optionsWrapperRef = React.useRef<HTMLDivElement>(null);
|
|
223
|
-
const selectInputRef = useExternRef(getSelectInputRef);
|
|
224
284
|
|
|
225
285
|
const [focusedOptionIndex, setFocusedOptionIndex] = React.useState<number | undefined>(-1);
|
|
226
286
|
const [isControlledOutside, setIsControlledOutside] = React.useState(props.value !== undefined);
|
|
287
|
+
const [inputValue, setInputValue] = React.useState('');
|
|
227
288
|
const [nativeSelectValue, setNativeSelectValue] = React.useState(
|
|
228
289
|
() => props.value ?? defaultValue ?? (allowClearButton ? '' : undefined),
|
|
229
290
|
);
|
|
230
291
|
|
|
231
|
-
const [inputValue, setInputValue] = React.useState(() =>
|
|
232
|
-
calculateInputValueFromOptions(optionsProp, nativeSelectValue),
|
|
233
|
-
);
|
|
234
|
-
|
|
235
292
|
const [popperPlacement, setPopperPlacement] = React.useState<Placement>(popupDirection);
|
|
236
293
|
const [options, setOptions] = React.useState(optionsProp);
|
|
237
294
|
const [selectedOptionIndex, setSelectedOptionIndex] = React.useState<number | undefined>(
|
|
@@ -370,6 +427,7 @@ export function CustomSelect<OptionInterfaceT extends CustomSelectOptionInterfac
|
|
|
370
427
|
const close = React.useCallback(() => {
|
|
371
428
|
resetKeyboardInput();
|
|
372
429
|
|
|
430
|
+
setInputValue('');
|
|
373
431
|
setOpened(false);
|
|
374
432
|
resetFocusedOption();
|
|
375
433
|
onClose?.();
|
|
@@ -379,8 +437,8 @@ export function CustomSelect<OptionInterfaceT extends CustomSelectOptionInterfac
|
|
|
379
437
|
(index: number) => {
|
|
380
438
|
const item = options[index];
|
|
381
439
|
|
|
382
|
-
close();
|
|
383
440
|
setNativeSelectValue(item?.value);
|
|
441
|
+
close();
|
|
384
442
|
|
|
385
443
|
const shouldTriggerOnChangeWhenControlledAndInnerValueIsOutOfSync =
|
|
386
444
|
isControlledOutside &&
|
|
@@ -416,15 +474,12 @@ export function CustomSelect<OptionInterfaceT extends CustomSelectOptionInterfac
|
|
|
416
474
|
close();
|
|
417
475
|
const event = new Event('focusout', { bubbles: true });
|
|
418
476
|
selectElRef.current?.dispatchEvent(event);
|
|
419
|
-
|
|
420
|
-
setInputValue(calculateInputValueFromOptions(optionsProp, nativeSelectValue));
|
|
421
|
-
}, [close, selectElRef, optionsProp, nativeSelectValue]);
|
|
477
|
+
}, [close, selectElRef]);
|
|
422
478
|
|
|
423
479
|
const onFocus = React.useCallback(() => {
|
|
424
480
|
const event = new Event('focusin', { bubbles: true });
|
|
425
481
|
selectElRef.current?.dispatchEvent(event);
|
|
426
|
-
|
|
427
|
-
}, [selectElRef, selectInputRef]);
|
|
482
|
+
}, [selectElRef]);
|
|
428
483
|
|
|
429
484
|
const onClick = React.useCallback(() => {
|
|
430
485
|
if (opened) {
|
|
@@ -454,40 +509,27 @@ export function CustomSelect<OptionInterfaceT extends CustomSelectOptionInterfac
|
|
|
454
509
|
);
|
|
455
510
|
|
|
456
511
|
React.useEffect(
|
|
457
|
-
function
|
|
512
|
+
function updateOptionsAndSelectedOptionIndex() {
|
|
513
|
+
const value = props.value ?? nativeSelectValue ?? defaultValue;
|
|
514
|
+
|
|
458
515
|
const options =
|
|
459
516
|
searchable && inputValue !== undefined
|
|
460
517
|
? filter(optionsProp, inputValue, filterFn)
|
|
461
518
|
: optionsProp;
|
|
462
519
|
|
|
463
520
|
setOptions(options);
|
|
521
|
+
setSelectedOptionIndex(findSelectedIndex(options, value, allowClearButton));
|
|
464
522
|
},
|
|
465
|
-
[
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
const prevSelectValueRef = React.useRef(selectValue);
|
|
477
|
-
React.useEffect(
|
|
478
|
-
function updateInputValueOnSelectValueChange() {
|
|
479
|
-
if (prevSelectValueRef.current === selectValue) {
|
|
480
|
-
return;
|
|
481
|
-
}
|
|
482
|
-
setInputValue(calculateInputValueFromOptions(optionsProp, selectValue));
|
|
483
|
-
},
|
|
484
|
-
[selectValue, optionsProp],
|
|
485
|
-
);
|
|
486
|
-
React.useEffect(
|
|
487
|
-
function updatePrevSelectValue() {
|
|
488
|
-
prevSelectValueRef.current = selectValue;
|
|
489
|
-
},
|
|
490
|
-
[selectValue],
|
|
523
|
+
[
|
|
524
|
+
filterFn,
|
|
525
|
+
inputValue,
|
|
526
|
+
nativeSelectValue,
|
|
527
|
+
optionsProp,
|
|
528
|
+
defaultValue,
|
|
529
|
+
props.value,
|
|
530
|
+
searchable,
|
|
531
|
+
allowClearButton,
|
|
532
|
+
],
|
|
491
533
|
);
|
|
492
534
|
|
|
493
535
|
const onNativeSelectChange: React.ChangeEventHandler<HTMLSelectElement> = (e) => {
|
|
@@ -671,6 +713,7 @@ export function CustomSelect<OptionInterfaceT extends CustomSelectOptionInterfac
|
|
|
671
713
|
}
|
|
672
714
|
}, [emptyText, options, renderDropdown, renderOption]);
|
|
673
715
|
|
|
716
|
+
const selectInputRef = useExternRef(getSelectInputRef);
|
|
674
717
|
const focusOnInputTimerRef = React.useRef<ReturnType<typeof setTimeout>>();
|
|
675
718
|
const focusOnInput = React.useCallback(() => {
|
|
676
719
|
clearTimeout(focusOnInputTimerRef.current);
|
|
@@ -772,9 +815,7 @@ export function CustomSelect<OptionInterfaceT extends CustomSelectOptionInterfac
|
|
|
772
815
|
// но вне инпута (например по иконке дропдауна), будет убирать фокус с инпута.
|
|
773
816
|
// Чтобы в такой ситуации отключить blur инпута мы превентим mousedown событие обёртки
|
|
774
817
|
const isInputFocused = document && document.activeElement === selectInputRef.current;
|
|
775
|
-
|
|
776
|
-
const inputClicked = selectInputRef.current?.contains(clickTarget);
|
|
777
|
-
if (isInputFocused && !inputClicked) {
|
|
818
|
+
if (isInputFocused) {
|
|
778
819
|
e.preventDefault();
|
|
779
820
|
}
|
|
780
821
|
};
|
|
@@ -789,6 +830,7 @@ export function CustomSelect<OptionInterfaceT extends CustomSelectOptionInterfac
|
|
|
789
830
|
const selectInputAriaProps: React.HTMLAttributes<HTMLElement> = {
|
|
790
831
|
'role': 'combobox',
|
|
791
832
|
'aria-controls': popupAriaId,
|
|
833
|
+
'aria-owns': popupAriaId,
|
|
792
834
|
'aria-expanded': opened,
|
|
793
835
|
'aria-activedescendant':
|
|
794
836
|
ariaActiveDescendantId && opened ? `${popupAriaId}-${ariaActiveDescendantId}` : undefined,
|
|
@@ -797,6 +839,8 @@ export function CustomSelect<OptionInterfaceT extends CustomSelectOptionInterfac
|
|
|
797
839
|
'aria-autocomplete': 'none',
|
|
798
840
|
};
|
|
799
841
|
|
|
842
|
+
const focusWithin = useFocusWithin(handleRootRef);
|
|
843
|
+
|
|
800
844
|
return (
|
|
801
845
|
<div
|
|
802
846
|
className={classNames(
|
|
@@ -809,6 +853,9 @@ export function CustomSelect<OptionInterfaceT extends CustomSelectOptionInterfac
|
|
|
809
853
|
onClick={passClickAndFocusToInputOnClick}
|
|
810
854
|
onMouseDown={preventInputBlurWhenClickInsideFocusedSelectArea}
|
|
811
855
|
>
|
|
856
|
+
{focusWithin && selected && !opened && (
|
|
857
|
+
<VisuallyHidden aria-live="polite">{selected.label}</VisuallyHidden>
|
|
858
|
+
)}
|
|
812
859
|
<CustomSelectInput
|
|
813
860
|
autoComplete="off"
|
|
814
861
|
autoCapitalize="none"
|
|
@@ -820,7 +867,7 @@ export function CustomSelect<OptionInterfaceT extends CustomSelectOptionInterfac
|
|
|
820
867
|
onFocus={onFocus}
|
|
821
868
|
onBlur={onBlur}
|
|
822
869
|
className={openedClassNames}
|
|
823
|
-
|
|
870
|
+
readOnly={!searchable}
|
|
824
871
|
fetching={fetching}
|
|
825
872
|
value={inputValue}
|
|
826
873
|
onKeyUp={handleKeyUp}
|
|
@@ -830,8 +877,9 @@ export function CustomSelect<OptionInterfaceT extends CustomSelectOptionInterfac
|
|
|
830
877
|
before={before}
|
|
831
878
|
after={afterIcons}
|
|
832
879
|
selectType={selectType}
|
|
833
|
-
|
|
834
|
-
|
|
880
|
+
>
|
|
881
|
+
{selected?.label}
|
|
882
|
+
</CustomSelectInput>
|
|
835
883
|
<select
|
|
836
884
|
ref={selectElRef}
|
|
837
885
|
name={name}
|
|
@@ -2,10 +2,11 @@
|
|
|
2
2
|
position: relative;
|
|
3
3
|
}
|
|
4
4
|
|
|
5
|
-
.
|
|
5
|
+
.CustomSelectInput__el {
|
|
6
6
|
position: absolute;
|
|
7
7
|
inset-block-start: 0;
|
|
8
8
|
inset-inline-start: 0;
|
|
9
|
+
z-index: var(--vkui_internal--z_index_form_field_element);
|
|
9
10
|
inline-size: 100%;
|
|
10
11
|
block-size: var(--vkui--size_field_height--regular);
|
|
11
12
|
line-height: var(--vkui--size_field_height--regular);
|
|
@@ -15,90 +16,63 @@
|
|
|
15
16
|
box-sizing: border-box;
|
|
16
17
|
box-shadow: none;
|
|
17
18
|
appearance: none;
|
|
18
|
-
color:
|
|
19
|
+
color: var(--vkui--color_text_primary);
|
|
19
20
|
padding-block: 0;
|
|
20
21
|
padding-inline: 12px;
|
|
21
22
|
background: transparent;
|
|
22
|
-
/*
|
|
23
|
-
* По типy option.label может принимать React-компонент,
|
|
24
|
-
* но React-компонент нельзя отрендерить как value в input.
|
|
25
|
-
* Поэтому мы всегда стараемся прятать input и поверх рисовать конейнер,
|
|
26
|
-
* в который можно положить label как строку или как React-компонент.
|
|
27
|
-
* В то же время у input в value лежит текстовое представление
|
|
28
|
-
* React компонента специально для скринридера.
|
|
29
|
-
*/
|
|
30
|
-
opacity: 0;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/*
|
|
34
|
-
* Но в режиме searchable, в фокусе мы наоборот, намеренно показываем
|
|
35
|
-
* input и прячем декоративный label, так как нам важно дать пользователю
|
|
36
|
-
* возможность изменить значение input для поиска.
|
|
37
|
-
* А пользователям скринридера надо дать возможность прочитать выбранное значение.
|
|
38
|
-
*/
|
|
39
|
-
.CustomSelectInput__input:not(:read-only):focus {
|
|
40
|
-
opacity: 1;
|
|
41
23
|
}
|
|
42
24
|
|
|
43
|
-
.
|
|
25
|
+
.CustomSelectInput__el--cursor-pointer {
|
|
44
26
|
cursor: pointer;
|
|
45
27
|
}
|
|
46
28
|
|
|
47
|
-
.CustomSelectInput--sizeY-compact .
|
|
29
|
+
.CustomSelectInput--sizeY-compact .CustomSelectInput__el {
|
|
48
30
|
block-size: var(--vkui--size_field_height--compact);
|
|
49
31
|
}
|
|
50
32
|
|
|
51
33
|
@media (--sizeY-compact) {
|
|
52
|
-
.CustomSelectInput--sizeY-none .
|
|
34
|
+
.CustomSelectInput--sizeY-none .CustomSelectInput__el {
|
|
53
35
|
block-size: var(--vkui--size_field_height--compact);
|
|
54
36
|
}
|
|
55
37
|
}
|
|
56
38
|
|
|
57
|
-
.CustomSelectInput--hasBefore .
|
|
39
|
+
.CustomSelectInput--hasBefore .CustomSelectInput__el {
|
|
58
40
|
padding-inline-start: 0;
|
|
59
41
|
}
|
|
60
42
|
|
|
61
|
-
.CustomSelectInput--hasAfter .
|
|
43
|
+
.CustomSelectInput--hasAfter .CustomSelectInput__el {
|
|
62
44
|
padding-inline-end: 0;
|
|
63
45
|
}
|
|
64
46
|
|
|
65
|
-
.
|
|
66
|
-
|
|
67
|
-
/* Для Firefox: взято из Input.module.css */
|
|
68
|
-
opacity: 1;
|
|
47
|
+
.CustomSelectInput__el:disabled {
|
|
48
|
+
opacity: var(--vkui--opacity_disable_accessibility);
|
|
69
49
|
}
|
|
70
50
|
|
|
71
|
-
.
|
|
51
|
+
.CustomSelectInput__container {
|
|
52
|
+
z-index: var(--vkui_internal--z_index_form_field_element);
|
|
72
53
|
inline-size: 100%;
|
|
73
54
|
max-block-size: 100%;
|
|
74
55
|
padding-inline: 12px 0;
|
|
56
|
+
color: var(--vkui--color_text_primary);
|
|
75
57
|
box-sizing: border-box;
|
|
76
58
|
overflow: hidden;
|
|
77
59
|
pointer-events: none;
|
|
78
60
|
}
|
|
79
61
|
|
|
80
|
-
.
|
|
81
|
-
display: none;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
.CustomSelectInput__input:disabled ~ .CustomSelectInput__label-wrapper {
|
|
85
|
-
opacity: var(--vkui--opacity_disable_accessibility);
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
.CustomSelectInput--hasBefore .CustomSelectInput__label-wrapper {
|
|
62
|
+
.CustomSelectInput--hasBefore .CustomSelectInput__container {
|
|
89
63
|
padding-inline-start: 0;
|
|
90
64
|
}
|
|
91
65
|
|
|
92
|
-
.CustomSelectInput--multiline .
|
|
66
|
+
.CustomSelectInput--multiline .CustomSelectInput__container {
|
|
93
67
|
padding-block: 12px;
|
|
94
68
|
}
|
|
95
69
|
|
|
96
|
-
.CustomSelectInput--sizeY-compact.CustomSelectInput--multiline .
|
|
70
|
+
.CustomSelectInput--sizeY-compact.CustomSelectInput--multiline .CustomSelectInput__container {
|
|
97
71
|
padding-block: 8px;
|
|
98
72
|
}
|
|
99
73
|
|
|
100
74
|
@media (--sizeY-compact) {
|
|
101
|
-
.CustomSelectInput--sizeY-none.CustomSelectInput--multiline .
|
|
75
|
+
.CustomSelectInput--sizeY-none.CustomSelectInput--multiline .CustomSelectInput__container {
|
|
102
76
|
padding-block: 8px;
|
|
103
77
|
}
|
|
104
78
|
}
|
|
@@ -110,34 +84,40 @@
|
|
|
110
84
|
align-items: center;
|
|
111
85
|
flex: 1;
|
|
112
86
|
overflow: hidden;
|
|
113
|
-
color: var(--vkui--color_text_primary);
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
.CustomSelectInput--empty .CustomSelectInput__label-wrapper {
|
|
117
|
-
color: var(--vkui--color_text_secondary);
|
|
118
87
|
}
|
|
119
88
|
|
|
120
89
|
.CustomSelectInput--hasBefore .CustomSelectInput__input-group {
|
|
121
90
|
border-radius: 0;
|
|
122
91
|
}
|
|
123
92
|
|
|
124
|
-
.
|
|
93
|
+
.CustomSelectInput__title {
|
|
125
94
|
display: block;
|
|
126
95
|
}
|
|
127
96
|
|
|
128
|
-
.CustomSelectInput:not(.CustomSelectInput--multiline) .
|
|
97
|
+
.CustomSelectInput:not(.CustomSelectInput--multiline) .CustomSelectInput__title {
|
|
129
98
|
overflow: hidden;
|
|
130
99
|
white-space: nowrap;
|
|
131
100
|
text-overflow: ellipsis;
|
|
132
101
|
}
|
|
133
102
|
|
|
134
|
-
.CustomSelectInput--
|
|
135
|
-
|
|
103
|
+
.CustomSelectInput--empty .CustomSelectInput__title {
|
|
104
|
+
color: var(--vkui--color_text_secondary);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/* Для доступности placeholder в инпуте задан, но визуально не виден, потому что
|
|
108
|
+
* для комфортного управления видом плейсходера мы рендерим его отдельно, так же как и лэйбл
|
|
109
|
+
*/
|
|
110
|
+
.CustomSelectInput__el::placeholder {
|
|
111
|
+
opacity: 0;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
.CustomSelectInput--align-right .CustomSelectInput__title,
|
|
115
|
+
.CustomSelectInput--align-right .CustomSelectInput__el {
|
|
136
116
|
text-align: end;
|
|
137
117
|
}
|
|
138
118
|
|
|
139
|
-
.CustomSelectInput--align-center .
|
|
140
|
-
.CustomSelectInput--align-center .
|
|
119
|
+
.CustomSelectInput--align-center .CustomSelectInput__title,
|
|
120
|
+
.CustomSelectInput--align-center .CustomSelectInput__el {
|
|
141
121
|
text-align: center;
|
|
142
122
|
}
|
|
143
123
|
|
|
@@ -146,6 +126,6 @@
|
|
|
146
126
|
* CalendarHeader
|
|
147
127
|
*/
|
|
148
128
|
/* stylelint-disable-next-line selector-pseudo-class-disallowed-list */
|
|
149
|
-
:global(.vkuiInternalCalendarHeader__picker) .
|
|
129
|
+
:global(.vkuiInternalCalendarHeader__picker) .CustomSelectInput__container {
|
|
150
130
|
padding-inline-end: 4px;
|
|
151
131
|
}
|
|
@@ -2,12 +2,14 @@ import * as React from 'react';
|
|
|
2
2
|
import { classNames } from '@vkontakte/vkjs';
|
|
3
3
|
import { useAdaptivity } from '../../hooks/useAdaptivity';
|
|
4
4
|
import { useExternRef } from '../../hooks/useExternRef';
|
|
5
|
+
import { useFocusWithin } from '../../hooks/useFocusWithin';
|
|
5
6
|
import { usePlatform } from '../../hooks/usePlatform';
|
|
6
7
|
import { getFormFieldModeFromSelectType } from '../../lib/select';
|
|
7
8
|
import type { HasAlign, HasRef, HasRootRef } from '../../types';
|
|
8
9
|
import { FormField, type FormFieldProps } from '../FormField/FormField';
|
|
9
10
|
import type { SelectType } from '../Select/Select';
|
|
10
11
|
import { SelectTypography } from '../SelectTypography/SelectTypography';
|
|
12
|
+
import { Text } from '../Typography/Text/Text';
|
|
11
13
|
import { VisuallyHidden } from '../VisuallyHidden/VisuallyHidden';
|
|
12
14
|
import styles from './CustomSelectInput.module.css';
|
|
13
15
|
|
|
@@ -26,8 +28,6 @@ export interface CustomSelectInputProps
|
|
|
26
28
|
multiline?: boolean;
|
|
27
29
|
labelTextTestId?: string;
|
|
28
30
|
fetching?: boolean;
|
|
29
|
-
searchable?: boolean;
|
|
30
|
-
selectedOptionLabel?: React.ReactElement | string;
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
/**
|
|
@@ -43,35 +43,42 @@ export const CustomSelectInput = ({
|
|
|
43
43
|
before,
|
|
44
44
|
after,
|
|
45
45
|
status,
|
|
46
|
-
|
|
46
|
+
children,
|
|
47
|
+
placeholder,
|
|
47
48
|
selectType = 'default',
|
|
48
49
|
multiline,
|
|
49
50
|
disabled,
|
|
50
51
|
fetching,
|
|
51
52
|
labelTextTestId,
|
|
52
|
-
|
|
53
|
-
...restInputProps
|
|
53
|
+
...restProps
|
|
54
54
|
}: CustomSelectInputProps): React.ReactNode => {
|
|
55
55
|
const { sizeY = 'none' } = useAdaptivity();
|
|
56
56
|
|
|
57
|
-
const
|
|
57
|
+
const title = children || placeholder;
|
|
58
|
+
const showLabelOrPlaceholder = !Boolean(restProps.value);
|
|
58
59
|
|
|
59
|
-
const
|
|
60
|
+
const handleRootRef = useExternRef(getRootRef);
|
|
61
|
+
const focusWithin = useFocusWithin(handleRootRef);
|
|
60
62
|
|
|
61
63
|
const input = (
|
|
62
|
-
<
|
|
63
|
-
selectType={selectType}
|
|
64
|
+
<Text
|
|
64
65
|
type="text"
|
|
65
|
-
{...
|
|
66
|
+
{...restProps}
|
|
66
67
|
disabled={disabled && !fetching}
|
|
67
|
-
readOnly={
|
|
68
|
+
readOnly={restProps.readOnly || (disabled && fetching)}
|
|
68
69
|
Component="input"
|
|
69
70
|
normalize={false}
|
|
70
|
-
className={
|
|
71
|
+
className={classNames(
|
|
72
|
+
styles['CustomSelectInput__el'],
|
|
73
|
+
(restProps.readOnly || (showLabelOrPlaceholder && !focusWithin)) &&
|
|
74
|
+
styles['CustomSelectInput__el--cursor-pointer'],
|
|
75
|
+
)}
|
|
71
76
|
getRootRef={getRef}
|
|
77
|
+
placeholder={children ? '' : placeholder}
|
|
72
78
|
/>
|
|
73
79
|
);
|
|
74
80
|
|
|
81
|
+
const platform = usePlatform();
|
|
75
82
|
return (
|
|
76
83
|
<FormField
|
|
77
84
|
Component="div"
|
|
@@ -80,7 +87,7 @@ export const CustomSelectInput = ({
|
|
|
80
87
|
styles['CustomSelectInput'],
|
|
81
88
|
align === 'right' && styles['CustomSelectInput--align-right'],
|
|
82
89
|
align === 'center' && styles['CustomSelectInput--align-center'],
|
|
83
|
-
!
|
|
90
|
+
!children && styles['CustomSelectInput--empty'],
|
|
84
91
|
multiline && styles['CustomSelectInput--multiline'],
|
|
85
92
|
sizeY !== 'regular' && sizeYClassNames[sizeY],
|
|
86
93
|
before && styles['CustomSelectInput--hasBefore'],
|
|
@@ -95,6 +102,16 @@ export const CustomSelectInput = ({
|
|
|
95
102
|
status={status}
|
|
96
103
|
>
|
|
97
104
|
<div className={styles['CustomSelectInput__input-group']}>
|
|
105
|
+
<div
|
|
106
|
+
className={classNames(styles['CustomSelectInput__container'], className)}
|
|
107
|
+
tabIndex={-1}
|
|
108
|
+
aria-hidden
|
|
109
|
+
data-testid={labelTextTestId}
|
|
110
|
+
>
|
|
111
|
+
<SelectTypography selectType={selectType} className={styles['CustomSelectInput__title']}>
|
|
112
|
+
{showLabelOrPlaceholder && title}
|
|
113
|
+
</SelectTypography>
|
|
114
|
+
</div>
|
|
98
115
|
{/* Чтобы отключить autosuggestion в iOS, тултипы которого начинают всплывать даже когда input
|
|
99
116
|
* в режиме readonly, мы оборачиваем инпут в VisuallyHidden.
|
|
100
117
|
* Тултипы появляются при каждом клике на input.
|
|
@@ -104,17 +121,11 @@ export const CustomSelectInput = ({
|
|
|
104
121
|
* Делаем это только для режима read-only. Потому что проблема именно в режиме read-only.
|
|
105
122
|
* Обертка вокруг инпута обрабатывает клики и передаёт фокус, так что на взаимодействии с инпутом это никак не скажется.
|
|
106
123
|
**/}
|
|
107
|
-
{
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
data-testid={labelTextTestId}
|
|
113
|
-
>
|
|
114
|
-
<SelectTypography selectType={selectType} className={styles['CustomSelectInput__label']}>
|
|
115
|
-
{selectedOptionLabel || restInputProps.placeholder}
|
|
116
|
-
</SelectTypography>
|
|
117
|
-
</div>
|
|
124
|
+
{restProps.readOnly && platform === 'ios' ? (
|
|
125
|
+
<VisuallyHidden>{input}</VisuallyHidden>
|
|
126
|
+
) : (
|
|
127
|
+
input
|
|
128
|
+
)}
|
|
118
129
|
</div>
|
|
119
130
|
</FormField>
|
|
120
131
|
);
|
|
@@ -69,8 +69,8 @@ export const Select = <OptionT extends CustomSelectOptionInterface>({
|
|
|
69
69
|
className={classNames(className, deviceType.mobile.className)}
|
|
70
70
|
{...nativeProps}
|
|
71
71
|
>
|
|
72
|
-
{options.map(({ label, value }) => (
|
|
73
|
-
<option value={value} key={`${value}`}>
|
|
72
|
+
{options.map(({ label, value, disabled }) => (
|
|
73
|
+
<option value={value} key={`${value}`} disabled={disabled}>
|
|
74
74
|
{label}
|
|
75
75
|
</option>
|
|
76
76
|
))}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
.SimpleCell {
|
|
2
|
+
--vkui_internal--SimpleCell-before-inline-padding-end: var(--vkui--spacing_size_xl);
|
|
3
|
+
|
|
2
4
|
display: flex;
|
|
3
5
|
align-items: center;
|
|
4
6
|
min-block-size: 48px;
|
|
@@ -24,7 +26,7 @@
|
|
|
24
26
|
display: flex;
|
|
25
27
|
align-items: center;
|
|
26
28
|
padding-block: var(--vkui--spacing_size_s);
|
|
27
|
-
padding-inline-end: var(--
|
|
29
|
+
padding-inline-end: var(--vkui_internal--SimpleCell-before-inline-padding-end);
|
|
28
30
|
color: var(--vkui_internal--icon_color, var(--vkui--color_icon_accent));
|
|
29
31
|
}
|
|
30
32
|
|
|
@@ -61,8 +61,10 @@ export interface SimpleCellOwnProps extends HasComponent {
|
|
|
61
61
|
*/
|
|
62
62
|
disabled?: boolean;
|
|
63
63
|
/**
|
|
64
|
-
*
|
|
65
|
-
*
|
|
64
|
+
* Управляет видимостью иконки шеврона `›`
|
|
65
|
+
*
|
|
66
|
+
* - `auto` - добавляет шеврон справа только для платформы `ios`;
|
|
67
|
+
* - `always` - всегда показывает шеврон.
|
|
66
68
|
*/
|
|
67
69
|
expandable?: 'auto' | 'always';
|
|
68
70
|
/**
|
|
@@ -2,8 +2,7 @@
|
|
|
2
2
|
--vkui_internal--Spacing_gap: 0;
|
|
3
3
|
|
|
4
4
|
position: relative;
|
|
5
|
-
block
|
|
6
|
-
padding-block: calc(1px * var(--vkui_internal--Spacing_gap) / 2);
|
|
5
|
+
padding-block: calc(var(--vkui_internal--Spacing_gap) / 2);
|
|
7
6
|
box-sizing: border-box;
|
|
8
7
|
}
|
|
9
8
|
|
|
@@ -35,7 +35,7 @@ export const Spacing = ({ size = 'm', style, ...restProps }: SpacingProps): Reac
|
|
|
35
35
|
<RootComponent
|
|
36
36
|
{...restProps}
|
|
37
37
|
style={{
|
|
38
|
-
...(typeof size === 'number' && { [CUSTOM_CSS_TOKEN_FOR_USER_GAP]: size }),
|
|
38
|
+
...(typeof size === 'number' && { [CUSTOM_CSS_TOKEN_FOR_USER_GAP]: `${size}px` }),
|
|
39
39
|
...style,
|
|
40
40
|
}}
|
|
41
41
|
baseClassName={classNames(
|