@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.
Files changed (133) hide show
  1. package/dist/cjs/components/AppRoot/AppRoot.d.ts.map +1 -1
  2. package/dist/cjs/components/AppRoot/AppRoot.js +9 -3
  3. package/dist/cjs/components/AppRoot/AppRoot.js.map +1 -1
  4. package/dist/cjs/components/BaseGallery/CarouselBase/CarouselBase.d.ts.map +1 -1
  5. package/dist/cjs/components/BaseGallery/CarouselBase/CarouselBase.js +9 -0
  6. package/dist/cjs/components/BaseGallery/CarouselBase/CarouselBase.js.map +1 -1
  7. package/dist/cjs/components/CustomSelect/CustomSelect.d.ts +12 -2
  8. package/dist/cjs/components/CustomSelect/CustomSelect.d.ts.map +1 -1
  9. package/dist/cjs/components/CustomSelect/CustomSelect.js +72 -52
  10. package/dist/cjs/components/CustomSelect/CustomSelect.js.map +1 -1
  11. package/dist/cjs/components/CustomSelect/CustomSelectInput.d.ts +1 -3
  12. package/dist/cjs/components/CustomSelect/CustomSelectInput.d.ts.map +1 -1
  13. package/dist/cjs/components/CustomSelect/CustomSelectInput.js +24 -19
  14. package/dist/cjs/components/CustomSelect/CustomSelectInput.js.map +1 -1
  15. package/dist/cjs/components/HorizontalScroll/HorizontalScroll.d.ts +0 -2
  16. package/dist/cjs/components/HorizontalScroll/HorizontalScroll.d.ts.map +1 -1
  17. package/dist/cjs/components/HorizontalScroll/HorizontalScroll.js.map +1 -1
  18. package/dist/cjs/components/Select/Select.js +2 -1
  19. package/dist/cjs/components/Select/Select.js.map +1 -1
  20. package/dist/cjs/components/SimpleCell/SimpleCell.d.ts +4 -2
  21. package/dist/cjs/components/SimpleCell/SimpleCell.d.ts.map +1 -1
  22. package/dist/cjs/components/SimpleCell/SimpleCell.js.map +1 -1
  23. package/dist/cjs/components/Spacing/Spacing.js +1 -1
  24. package/dist/cjs/components/Spacing/Spacing.js.map +1 -1
  25. package/dist/cjs/lib/floating/useFloatingWithInteractions/useFloatingWithInteractions.d.ts.map +1 -1
  26. package/dist/cjs/lib/floating/useFloatingWithInteractions/useFloatingWithInteractions.js +3 -0
  27. package/dist/cjs/lib/floating/useFloatingWithInteractions/useFloatingWithInteractions.js.map +1 -1
  28. package/dist/components/AppRoot/AppRoot.d.ts.map +1 -1
  29. package/dist/components/AppRoot/AppRoot.js +9 -3
  30. package/dist/components/AppRoot/AppRoot.js.map +1 -1
  31. package/dist/components/BaseGallery/CarouselBase/CarouselBase.d.ts.map +1 -1
  32. package/dist/components/BaseGallery/CarouselBase/CarouselBase.js +10 -1
  33. package/dist/components/BaseGallery/CarouselBase/CarouselBase.js.map +1 -1
  34. package/dist/components/CustomSelect/CustomSelect.d.ts +12 -2
  35. package/dist/components/CustomSelect/CustomSelect.d.ts.map +1 -1
  36. package/dist/components/CustomSelect/CustomSelect.js +64 -44
  37. package/dist/components/CustomSelect/CustomSelect.js.map +1 -1
  38. package/dist/components/CustomSelect/CustomSelectInput.d.ts +1 -3
  39. package/dist/components/CustomSelect/CustomSelectInput.d.ts.map +1 -1
  40. package/dist/components/CustomSelect/CustomSelectInput.js +24 -19
  41. package/dist/components/CustomSelect/CustomSelectInput.js.map +1 -1
  42. package/dist/components/HorizontalScroll/HorizontalScroll.d.ts +0 -2
  43. package/dist/components/HorizontalScroll/HorizontalScroll.d.ts.map +1 -1
  44. package/dist/components/HorizontalScroll/HorizontalScroll.js.map +1 -1
  45. package/dist/components/Select/Select.js +2 -1
  46. package/dist/components/Select/Select.js.map +1 -1
  47. package/dist/components/SimpleCell/SimpleCell.d.ts +4 -2
  48. package/dist/components/SimpleCell/SimpleCell.d.ts.map +1 -1
  49. package/dist/components/SimpleCell/SimpleCell.js.map +1 -1
  50. package/dist/components/Spacing/Spacing.js +1 -1
  51. package/dist/components/Spacing/Spacing.js.map +1 -1
  52. package/dist/components.css +3 -3
  53. package/dist/components.css.map +1 -1
  54. package/dist/components.js.tmp +128 -162
  55. package/dist/cssm/components/AppRoot/AppRoot.d.ts.map +1 -1
  56. package/dist/cssm/components/AppRoot/AppRoot.js +9 -3
  57. package/dist/cssm/components/AppRoot/AppRoot.js.map +1 -1
  58. package/dist/cssm/components/BaseGallery/CarouselBase/CarouselBase.d.ts.map +1 -1
  59. package/dist/cssm/components/BaseGallery/CarouselBase/CarouselBase.js +10 -1
  60. package/dist/cssm/components/BaseGallery/CarouselBase/CarouselBase.js.map +1 -1
  61. package/dist/cssm/components/CellButton/CellButton.module.css +9 -2
  62. package/dist/cssm/components/CustomSelect/CustomSelect.d.ts +12 -2
  63. package/dist/cssm/components/CustomSelect/CustomSelect.d.ts.map +1 -1
  64. package/dist/cssm/components/CustomSelect/CustomSelect.js +60 -41
  65. package/dist/cssm/components/CustomSelect/CustomSelect.js.map +1 -1
  66. package/dist/cssm/components/CustomSelect/CustomSelectInput.d.ts +1 -3
  67. package/dist/cssm/components/CustomSelect/CustomSelectInput.d.ts.map +1 -1
  68. package/dist/cssm/components/CustomSelect/CustomSelectInput.js +21 -16
  69. package/dist/cssm/components/CustomSelect/CustomSelectInput.js.map +1 -1
  70. package/dist/cssm/components/CustomSelect/CustomSelectInput.module.css +40 -74
  71. package/dist/cssm/components/HorizontalScroll/HorizontalScroll.d.ts +0 -2
  72. package/dist/cssm/components/HorizontalScroll/HorizontalScroll.d.ts.map +1 -1
  73. package/dist/cssm/components/HorizontalScroll/HorizontalScroll.js.map +1 -1
  74. package/dist/cssm/components/Select/Select.js +2 -1
  75. package/dist/cssm/components/Select/Select.js.map +1 -1
  76. package/dist/cssm/components/SimpleCell/SimpleCell.d.ts +4 -2
  77. package/dist/cssm/components/SimpleCell/SimpleCell.d.ts.map +1 -1
  78. package/dist/cssm/components/SimpleCell/SimpleCell.js.map +1 -1
  79. package/dist/cssm/components/SimpleCell/SimpleCell.module.css +4 -2
  80. package/dist/cssm/components/Spacing/Spacing.js +1 -1
  81. package/dist/cssm/components/Spacing/Spacing.js.map +1 -1
  82. package/dist/cssm/components/Spacing/Spacing.module.css +1 -2
  83. package/dist/cssm/components/TabsItem/TabsItem.module.css +1 -1
  84. package/dist/cssm/lib/floating/useFloatingWithInteractions/useFloatingWithInteractions.d.ts.map +1 -1
  85. package/dist/cssm/lib/floating/useFloatingWithInteractions/useFloatingWithInteractions.js +3 -0
  86. package/dist/cssm/lib/floating/useFloatingWithInteractions/useFloatingWithInteractions.js.map +1 -1
  87. package/dist/lib/floating/useFloatingWithInteractions/useFloatingWithInteractions.d.ts.map +1 -1
  88. package/dist/lib/floating/useFloatingWithInteractions/useFloatingWithInteractions.js +3 -0
  89. package/dist/lib/floating/useFloatingWithInteractions/useFloatingWithInteractions.js.map +1 -1
  90. package/dist/vkui.css +3 -3
  91. package/dist/vkui.css.map +1 -1
  92. package/dist/vkui.js.tmp +128 -162
  93. package/package.json +1 -1
  94. package/src/components/AppRoot/AppRoot.tsx +16 -13
  95. package/src/components/BaseGallery/CarouselBase/CarouselBase.tsx +16 -1
  96. package/src/components/CellButton/CellButton.module.css +5 -1
  97. package/src/components/CustomSelect/CustomSelect.tsx +101 -53
  98. package/src/components/CustomSelect/CustomSelectInput.module.css +35 -55
  99. package/src/components/CustomSelect/CustomSelectInput.tsx +35 -24
  100. package/src/components/HorizontalScroll/HorizontalScroll.tsx +0 -2
  101. package/src/components/Select/Select.tsx +2 -2
  102. package/src/components/SimpleCell/SimpleCell.module.css +3 -1
  103. package/src/components/SimpleCell/SimpleCell.tsx +4 -2
  104. package/src/components/Spacing/Spacing.module.css +1 -2
  105. package/src/components/Spacing/Spacing.tsx +1 -1
  106. package/src/components/TabsItem/TabsItem.module.css +1 -1
  107. package/src/lib/floating/useFloatingWithInteractions/useFloatingWithInteractions.ts +3 -0
  108. package/dist/cjs/components/CustomSelect/helpers.d.ts +0 -8
  109. package/dist/cjs/components/CustomSelect/helpers.d.ts.map +0 -1
  110. package/dist/cjs/components/CustomSelect/helpers.js +0 -76
  111. package/dist/cjs/components/CustomSelect/helpers.js.map +0 -1
  112. package/dist/cjs/components/CustomSelect/types.d.ts +0 -12
  113. package/dist/cjs/components/CustomSelect/types.d.ts.map +0 -1
  114. package/dist/cjs/components/CustomSelect/types.js +0 -6
  115. package/dist/cjs/components/CustomSelect/types.js.map +0 -1
  116. package/dist/components/CustomSelect/helpers.d.ts +0 -8
  117. package/dist/components/CustomSelect/helpers.d.ts.map +0 -1
  118. package/dist/components/CustomSelect/helpers.js +0 -48
  119. package/dist/components/CustomSelect/helpers.js.map +0 -1
  120. package/dist/components/CustomSelect/types.d.ts +0 -12
  121. package/dist/components/CustomSelect/types.d.ts.map +0 -1
  122. package/dist/components/CustomSelect/types.js +0 -3
  123. package/dist/components/CustomSelect/types.js.map +0 -1
  124. package/dist/cssm/components/CustomSelect/helpers.d.ts +0 -8
  125. package/dist/cssm/components/CustomSelect/helpers.d.ts.map +0 -1
  126. package/dist/cssm/components/CustomSelect/helpers.js +0 -44
  127. package/dist/cssm/components/CustomSelect/helpers.js.map +0 -1
  128. package/dist/cssm/components/CustomSelect/types.d.ts +0 -12
  129. package/dist/cssm/components/CustomSelect/types.d.ts.map +0 -1
  130. package/dist/cssm/components/CustomSelect/types.js +0 -3
  131. package/dist/cssm/components/CustomSelect/types.js.map +0 -1
  132. package/src/components/CustomSelect/helpers.tsx +0 -61
  133. 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
- export type { CustomSelectClearButtonProps, CustomSelectOptionInterface, CustomSelectRenderOption };
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
- selectInputRef.current?.select();
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 filterOptions() {
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
- [filterFn, inputValue, optionsProp, searchable],
466
- );
467
-
468
- const selectValue = props.value ?? nativeSelectValue ?? defaultValue;
469
- React.useEffect(
470
- function updateSelectedOptionIndexOnValueChange() {
471
- setSelectedOptionIndex(findSelectedIndex(options, selectValue, allowClearButton));
472
- },
473
- [selectValue, allowClearButton, options],
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
- const clickTarget = e.target as HTMLElement;
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
- searchable={searchable}
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
- selectedOptionLabel={selected?.label}
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
- .CustomSelectInput__input {
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: inherit;
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
- .CustomSelectInput__input:read-only:not(:disabled) {
25
+ .CustomSelectInput__el--cursor-pointer {
44
26
  cursor: pointer;
45
27
  }
46
28
 
47
- .CustomSelectInput--sizeY-compact .CustomSelectInput__input {
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 .CustomSelectInput__input {
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 .CustomSelectInput__input {
39
+ .CustomSelectInput--hasBefore .CustomSelectInput__el {
58
40
  padding-inline-start: 0;
59
41
  }
60
42
 
61
- .CustomSelectInput--hasAfter .CustomSelectInput__input {
43
+ .CustomSelectInput--hasAfter .CustomSelectInput__el {
62
44
  padding-inline-end: 0;
63
45
  }
64
46
 
65
- .CustomSelectInput__input::placeholder {
66
- color: var(--vkui--color_text_secondary);
67
- /* Для Firefox: взято из Input.module.css */
68
- opacity: 1;
47
+ .CustomSelectInput__el:disabled {
48
+ opacity: var(--vkui--opacity_disable_accessibility);
69
49
  }
70
50
 
71
- .CustomSelectInput__label-wrapper {
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
- .CustomSelectInput__input:focus:not(:read-only) ~ .CustomSelectInput__label-wrapper {
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 .CustomSelectInput__label-wrapper {
66
+ .CustomSelectInput--multiline .CustomSelectInput__container {
93
67
  padding-block: 12px;
94
68
  }
95
69
 
96
- .CustomSelectInput--sizeY-compact.CustomSelectInput--multiline .CustomSelectInput__label-wrapper {
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 .CustomSelectInput__label-wrapper {
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
- .CustomSelectInput__label {
93
+ .CustomSelectInput__title {
125
94
  display: block;
126
95
  }
127
96
 
128
- .CustomSelectInput:not(.CustomSelectInput--multiline) .CustomSelectInput__label {
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--align-right .CustomSelectInput__label,
135
- .CustomSelectInput--align-right .CustomSelectInput__input {
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 .CustomSelectInput__label,
140
- .CustomSelectInput--align-center .CustomSelectInput__input {
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) .CustomSelectInput__label-wrapper {
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
- selectedOptionLabel,
46
+ children,
47
+ placeholder,
47
48
  selectType = 'default',
48
49
  multiline,
49
50
  disabled,
50
51
  fetching,
51
52
  labelTextTestId,
52
- searchable,
53
- ...restInputProps
53
+ ...restProps
54
54
  }: CustomSelectInputProps): React.ReactNode => {
55
55
  const { sizeY = 'none' } = useAdaptivity();
56
56
 
57
- const handleRootRef = useExternRef(getRootRef);
57
+ const title = children || placeholder;
58
+ const showLabelOrPlaceholder = !Boolean(restProps.value);
58
59
 
59
- const platform = usePlatform();
60
+ const handleRootRef = useExternRef(getRootRef);
61
+ const focusWithin = useFocusWithin(handleRootRef);
60
62
 
61
63
  const input = (
62
- <SelectTypography
63
- selectType={selectType}
64
+ <Text
64
65
  type="text"
65
- {...restInputProps}
66
+ {...restProps}
66
67
  disabled={disabled && !fetching}
67
- readOnly={restInputProps.readOnly || !searchable || (disabled && fetching)}
68
+ readOnly={restProps.readOnly || (disabled && fetching)}
68
69
  Component="input"
69
70
  normalize={false}
70
- className={styles['CustomSelectInput__input']}
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
- !selectedOptionLabel && styles['CustomSelectInput--empty'],
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
- {!searchable && platform === 'ios' ? <VisuallyHidden>{input}</VisuallyHidden> : input}
108
- <div
109
- className={classNames(styles['CustomSelectInput__label-wrapper'], className)}
110
- tabIndex={-1}
111
- aria-hidden
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
  );
@@ -53,8 +53,6 @@ export interface HorizontalScrollProps
53
53
  scrollOnAnyWheel?: boolean;
54
54
  /**
55
55
  * Задает потомкам инлайновое положение (горизонально)
56
- *
57
- * TODO [>=7]: Сделать по умолчанию `true` (или удалить, применяя стили всегда)
58
56
  */
59
57
  inline?: boolean;
60
58
  }
@@ -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(--vkui--spacing_size_xl);
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
- * В режиме `auto` в iOS добавляет chevron справа.
65
- * Передавать `always`, если предполагается переход при клике по ячейке.
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-size: var(--vkui_internal--Spacing_gap);
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(