@vkontakte/vkui 6.5.2 → 6.5.4
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/CustomSelect/CustomSelect.d.ts +12 -2
- package/dist/cjs/components/CustomSelect/CustomSelect.d.ts.map +1 -1
- package/dist/cjs/components/CustomSelect/CustomSelect.js +68 -43
- 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/ImageBase/ImageBase.js +4 -1
- package/dist/cjs/components/ImageBase/ImageBase.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 +60 -35
- 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/ImageBase/ImageBase.js +4 -1
- package/dist/components/ImageBase/ImageBase.js.map +1 -1
- package/dist/components.css +2 -2
- package/dist/components.css.map +1 -1
- package/dist/components.js.tmp +100 -148
- 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 +57 -34
- 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/ImageBase/ImageBase.js +4 -1
- package/dist/cssm/components/ImageBase/ImageBase.js.map +1 -1
- package/dist/cssm/components/ImageBase/ImageBase.module.css +13 -2
- package/dist/vkui.css +2 -2
- package/dist/vkui.css.map +1 -1
- package/dist/vkui.js.tmp +100 -148
- package/package.json +1 -1
- package/src/components/CustomSelect/CustomSelect.tsx +98 -47
- package/src/components/CustomSelect/CustomSelectInput.module.css +35 -55
- package/src/components/CustomSelect/CustomSelectInput.tsx +35 -24
- package/src/components/ImageBase/ImageBase.module.css +13 -2
- package/src/components/ImageBase/ImageBase.tsx +1 -1
- 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
|
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 { FormFieldProps } from '../FormField/FormField';
|
|
17
22
|
import { NativeSelectProps } from '../NativeSelect/NativeSelect';
|
|
18
23
|
import { 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 } 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,
|
|
@@ -222,14 +283,11 @@ export function CustomSelect<OptionInterfaceT extends CustomSelectOptionInterfac
|
|
|
222
283
|
|
|
223
284
|
const [focusedOptionIndex, setFocusedOptionIndex] = React.useState<number | undefined>(-1);
|
|
224
285
|
const [isControlledOutside, setIsControlledOutside] = React.useState(props.value !== undefined);
|
|
286
|
+
const [inputValue, setInputValue] = React.useState('');
|
|
225
287
|
const [nativeSelectValue, setNativeSelectValue] = React.useState(
|
|
226
288
|
() => props.value ?? defaultValue ?? (allowClearButton ? '' : undefined),
|
|
227
289
|
);
|
|
228
290
|
|
|
229
|
-
const [inputValue, setInputValue] = React.useState(() =>
|
|
230
|
-
calculateInputValueFromOptions(optionsProp, nativeSelectValue),
|
|
231
|
-
);
|
|
232
|
-
|
|
233
291
|
const [popperPlacement, setPopperPlacement] = React.useState<Placement>(popupDirection);
|
|
234
292
|
const [options, setOptions] = React.useState(optionsProp);
|
|
235
293
|
const [selectedOptionIndex, setSelectedOptionIndex] = React.useState<number | undefined>(
|
|
@@ -368,6 +426,7 @@ export function CustomSelect<OptionInterfaceT extends CustomSelectOptionInterfac
|
|
|
368
426
|
const close = React.useCallback(() => {
|
|
369
427
|
resetKeyboardInput();
|
|
370
428
|
|
|
429
|
+
setInputValue('');
|
|
371
430
|
setOpened(false);
|
|
372
431
|
resetFocusedOption();
|
|
373
432
|
onClose?.();
|
|
@@ -377,8 +436,8 @@ export function CustomSelect<OptionInterfaceT extends CustomSelectOptionInterfac
|
|
|
377
436
|
(index: number) => {
|
|
378
437
|
const item = options[index];
|
|
379
438
|
|
|
380
|
-
close();
|
|
381
439
|
setNativeSelectValue(item?.value);
|
|
440
|
+
close();
|
|
382
441
|
|
|
383
442
|
const shouldTriggerOnChangeWhenControlledAndInnerValueIsOutOfSync =
|
|
384
443
|
isControlledOutside &&
|
|
@@ -414,9 +473,7 @@ export function CustomSelect<OptionInterfaceT extends CustomSelectOptionInterfac
|
|
|
414
473
|
close();
|
|
415
474
|
const event = new Event('focusout', { bubbles: true });
|
|
416
475
|
selectElRef.current?.dispatchEvent(event);
|
|
417
|
-
|
|
418
|
-
setInputValue(calculateInputValueFromOptions(optionsProp, nativeSelectValue));
|
|
419
|
-
}, [close, selectElRef, optionsProp, nativeSelectValue]);
|
|
476
|
+
}, [close, selectElRef]);
|
|
420
477
|
|
|
421
478
|
const onFocus = React.useCallback(() => {
|
|
422
479
|
const event = new Event('focusin', { bubbles: true });
|
|
@@ -451,40 +508,27 @@ export function CustomSelect<OptionInterfaceT extends CustomSelectOptionInterfac
|
|
|
451
508
|
);
|
|
452
509
|
|
|
453
510
|
React.useEffect(
|
|
454
|
-
function
|
|
511
|
+
function updateOptionsAndSelectedOptionIndex() {
|
|
512
|
+
const value = props.value ?? nativeSelectValue ?? defaultValue;
|
|
513
|
+
|
|
455
514
|
const options =
|
|
456
515
|
searchable && inputValue !== undefined
|
|
457
516
|
? filter(optionsProp, inputValue, filterFn)
|
|
458
517
|
: optionsProp;
|
|
459
518
|
|
|
460
519
|
setOptions(options);
|
|
520
|
+
setSelectedOptionIndex(findSelectedIndex(options, value, allowClearButton));
|
|
461
521
|
},
|
|
462
|
-
[
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
const prevSelectValueRef = React.useRef(selectValue);
|
|
474
|
-
React.useEffect(
|
|
475
|
-
function updateInputValueOnSelectValueChange() {
|
|
476
|
-
if (prevSelectValueRef.current === selectValue) {
|
|
477
|
-
return;
|
|
478
|
-
}
|
|
479
|
-
setInputValue(calculateInputValueFromOptions(optionsProp, selectValue));
|
|
480
|
-
},
|
|
481
|
-
[selectValue, optionsProp],
|
|
482
|
-
);
|
|
483
|
-
React.useEffect(
|
|
484
|
-
function updatePrevSelectValue() {
|
|
485
|
-
prevSelectValueRef.current = selectValue;
|
|
486
|
-
},
|
|
487
|
-
[selectValue],
|
|
522
|
+
[
|
|
523
|
+
filterFn,
|
|
524
|
+
inputValue,
|
|
525
|
+
nativeSelectValue,
|
|
526
|
+
optionsProp,
|
|
527
|
+
defaultValue,
|
|
528
|
+
props.value,
|
|
529
|
+
searchable,
|
|
530
|
+
allowClearButton,
|
|
531
|
+
],
|
|
488
532
|
);
|
|
489
533
|
|
|
490
534
|
const onNativeSelectChange: React.ChangeEventHandler<HTMLSelectElement> = (e) => {
|
|
@@ -785,6 +829,7 @@ export function CustomSelect<OptionInterfaceT extends CustomSelectOptionInterfac
|
|
|
785
829
|
const selectInputAriaProps: React.HTMLAttributes<HTMLElement> = {
|
|
786
830
|
'role': 'combobox',
|
|
787
831
|
'aria-controls': popupAriaId,
|
|
832
|
+
'aria-owns': popupAriaId,
|
|
788
833
|
'aria-expanded': opened,
|
|
789
834
|
['aria-activedescendant']:
|
|
790
835
|
ariaActiveDescendantId && opened ? `${popupAriaId}-${ariaActiveDescendantId}` : undefined,
|
|
@@ -793,6 +838,8 @@ export function CustomSelect<OptionInterfaceT extends CustomSelectOptionInterfac
|
|
|
793
838
|
'aria-autocomplete': 'none',
|
|
794
839
|
};
|
|
795
840
|
|
|
841
|
+
const focusWithin = useFocusWithin(handleRootRef);
|
|
842
|
+
|
|
796
843
|
return (
|
|
797
844
|
<div
|
|
798
845
|
className={classNames(
|
|
@@ -805,6 +852,9 @@ export function CustomSelect<OptionInterfaceT extends CustomSelectOptionInterfac
|
|
|
805
852
|
onClick={passClickAndFocusToInputOnClick}
|
|
806
853
|
onMouseDown={preventInputBlurWhenClickInsideFocusedSelectArea}
|
|
807
854
|
>
|
|
855
|
+
{focusWithin && selected && !opened && (
|
|
856
|
+
<VisuallyHidden aria-live="polite">{selected.label}</VisuallyHidden>
|
|
857
|
+
)}
|
|
808
858
|
<CustomSelectInput
|
|
809
859
|
autoComplete="off"
|
|
810
860
|
autoCapitalize="none"
|
|
@@ -816,7 +866,7 @@ export function CustomSelect<OptionInterfaceT extends CustomSelectOptionInterfac
|
|
|
816
866
|
onFocus={onFocus}
|
|
817
867
|
onBlur={onBlur}
|
|
818
868
|
className={openedClassNames}
|
|
819
|
-
|
|
869
|
+
readOnly={!searchable}
|
|
820
870
|
fetching={fetching}
|
|
821
871
|
value={inputValue}
|
|
822
872
|
onKeyUp={handleKeyUp}
|
|
@@ -826,8 +876,9 @@ export function CustomSelect<OptionInterfaceT extends CustomSelectOptionInterfac
|
|
|
826
876
|
before={before}
|
|
827
877
|
after={afterIcons}
|
|
828
878
|
selectType={selectType}
|
|
829
|
-
|
|
830
|
-
|
|
879
|
+
>
|
|
880
|
+
{selected?.label}
|
|
881
|
+
</CustomSelectInput>
|
|
831
882
|
<select
|
|
832
883
|
ref={selectElRef}
|
|
833
884
|
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 { HasAlign, HasRef, HasRootRef } from '../../types';
|
|
8
9
|
import { FormField, 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
|
);
|
|
@@ -16,14 +16,25 @@
|
|
|
16
16
|
background-color: transparent;
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
+
.ImageBase__children,
|
|
19
20
|
.ImageBase__border {
|
|
20
|
-
pointer-events: none;
|
|
21
21
|
position: absolute;
|
|
22
|
-
z-index: var(--vkui_internal--z_index_image_base_border);
|
|
23
22
|
inset-inline-start: 0;
|
|
24
23
|
inset-block-start: 0;
|
|
25
24
|
inline-size: 100%;
|
|
26
25
|
block-size: 100%;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
.ImageBase__children {
|
|
29
|
+
display: inherit;
|
|
30
|
+
align-items: inherit;
|
|
31
|
+
justify-content: inherit;
|
|
32
|
+
border-radius: inherit;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.ImageBase__border {
|
|
36
|
+
pointer-events: none;
|
|
37
|
+
z-index: var(--vkui_internal--z_index_image_base_border);
|
|
27
38
|
box-sizing: border-box;
|
|
28
39
|
transform-origin: left top;
|
|
29
40
|
border: var(--vkui--size_border--regular) solid var(--vkui--color_image_border_alpha);
|
|
@@ -248,7 +248,7 @@ export const ImageBase: React.FC<ImageBaseProps> & {
|
|
|
248
248
|
/>
|
|
249
249
|
)}
|
|
250
250
|
{fallbackIcon && <div className={styles['ImageBase__fallback']}>{fallbackIcon}</div>}
|
|
251
|
-
{children}
|
|
251
|
+
{children && <div className={styles['ImageBase__children']}>{children}</div>}
|
|
252
252
|
{!noBorder && <div aria-hidden className={styles['ImageBase__border']} />}
|
|
253
253
|
</Clickable>
|
|
254
254
|
</ImageBaseContext.Provider>
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
import * as React from 'react';
|
|
2
|
-
import type { CustomSelectOptionInterface, CustomSelectRenderOption, SelectValue } from './types';
|
|
3
|
-
export declare const findIndexAfter: (options?: CustomSelectOptionInterface[], startIndex?: number) => number;
|
|
4
|
-
export declare const findIndexBefore: (options?: CustomSelectOptionInterface[], endIndex?: number) => number;
|
|
5
|
-
export declare function findSelectedIndex<T extends CustomSelectOptionInterface>(options: T[] | undefined, value: SelectValue, withClear: boolean): number;
|
|
6
|
-
export declare function calculateInputValueFromOptions<T extends CustomSelectOptionInterface>(options: T[] | undefined, selectValue: SelectValue): string;
|
|
7
|
-
export declare function defaultRenderOptionFn<T extends CustomSelectOptionInterface>({ option, ...props }: CustomSelectRenderOption<T>): React.ReactNode;
|
|
8
|
-
//# sourceMappingURL=helpers.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"helpers.d.ts","sourceRoot":"","sources":["../../../../src/components/CustomSelect/helpers.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAG/B,OAAO,KAAK,EAAE,2BAA2B,EAAE,wBAAwB,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAElG,eAAO,MAAM,cAAc,aAAa,2BAA2B,EAAE,gCAKpE,CAAC;AAEF,eAAO,MAAM,eAAe,aACjB,2BAA2B,EAAE,aAC5B,MAAM,WAejB,CAAC;AAEF,wBAAgB,iBAAiB,CAAC,CAAC,SAAS,2BAA2B,EACrE,OAAO,EAAE,CAAC,EAAE,YAAK,EACjB,KAAK,EAAE,WAAW,EAClB,SAAS,EAAE,OAAO,UAWnB;AAED,wBAAgB,8BAA8B,CAAC,CAAC,SAAS,2BAA2B,EAClF,OAAO,EAAE,CAAC,EAAE,YAAK,EACjB,WAAW,EAAE,WAAW,UAIzB;AAED,wBAAgB,qBAAqB,CAAC,CAAC,SAAS,2BAA2B,EAAE,EAC3E,MAAM,EACN,GAAG,KAAK,EACT,EAAE,wBAAwB,CAAC,CAAC,CAAC,GAAG,KAAK,CAAC,SAAS,CAE/C"}
|