@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.
Files changed (70) hide show
  1. package/dist/cjs/components/CustomSelect/CustomSelect.d.ts +12 -2
  2. package/dist/cjs/components/CustomSelect/CustomSelect.d.ts.map +1 -1
  3. package/dist/cjs/components/CustomSelect/CustomSelect.js +68 -43
  4. package/dist/cjs/components/CustomSelect/CustomSelect.js.map +1 -1
  5. package/dist/cjs/components/CustomSelect/CustomSelectInput.d.ts +1 -3
  6. package/dist/cjs/components/CustomSelect/CustomSelectInput.d.ts.map +1 -1
  7. package/dist/cjs/components/CustomSelect/CustomSelectInput.js +24 -19
  8. package/dist/cjs/components/CustomSelect/CustomSelectInput.js.map +1 -1
  9. package/dist/cjs/components/ImageBase/ImageBase.js +4 -1
  10. package/dist/cjs/components/ImageBase/ImageBase.js.map +1 -1
  11. package/dist/components/CustomSelect/CustomSelect.d.ts +12 -2
  12. package/dist/components/CustomSelect/CustomSelect.d.ts.map +1 -1
  13. package/dist/components/CustomSelect/CustomSelect.js +60 -35
  14. package/dist/components/CustomSelect/CustomSelect.js.map +1 -1
  15. package/dist/components/CustomSelect/CustomSelectInput.d.ts +1 -3
  16. package/dist/components/CustomSelect/CustomSelectInput.d.ts.map +1 -1
  17. package/dist/components/CustomSelect/CustomSelectInput.js +24 -19
  18. package/dist/components/CustomSelect/CustomSelectInput.js.map +1 -1
  19. package/dist/components/ImageBase/ImageBase.js +4 -1
  20. package/dist/components/ImageBase/ImageBase.js.map +1 -1
  21. package/dist/components.css +2 -2
  22. package/dist/components.css.map +1 -1
  23. package/dist/components.js.tmp +100 -148
  24. package/dist/cssm/components/CustomSelect/CustomSelect.d.ts +12 -2
  25. package/dist/cssm/components/CustomSelect/CustomSelect.d.ts.map +1 -1
  26. package/dist/cssm/components/CustomSelect/CustomSelect.js +57 -34
  27. package/dist/cssm/components/CustomSelect/CustomSelect.js.map +1 -1
  28. package/dist/cssm/components/CustomSelect/CustomSelectInput.d.ts +1 -3
  29. package/dist/cssm/components/CustomSelect/CustomSelectInput.d.ts.map +1 -1
  30. package/dist/cssm/components/CustomSelect/CustomSelectInput.js +21 -16
  31. package/dist/cssm/components/CustomSelect/CustomSelectInput.js.map +1 -1
  32. package/dist/cssm/components/CustomSelect/CustomSelectInput.module.css +40 -74
  33. package/dist/cssm/components/ImageBase/ImageBase.js +4 -1
  34. package/dist/cssm/components/ImageBase/ImageBase.js.map +1 -1
  35. package/dist/cssm/components/ImageBase/ImageBase.module.css +13 -2
  36. package/dist/vkui.css +2 -2
  37. package/dist/vkui.css.map +1 -1
  38. package/dist/vkui.js.tmp +100 -148
  39. package/package.json +1 -1
  40. package/src/components/CustomSelect/CustomSelect.tsx +98 -47
  41. package/src/components/CustomSelect/CustomSelectInput.module.css +35 -55
  42. package/src/components/CustomSelect/CustomSelectInput.tsx +35 -24
  43. package/src/components/ImageBase/ImageBase.module.css +13 -2
  44. package/src/components/ImageBase/ImageBase.tsx +1 -1
  45. package/dist/cjs/components/CustomSelect/helpers.d.ts +0 -8
  46. package/dist/cjs/components/CustomSelect/helpers.d.ts.map +0 -1
  47. package/dist/cjs/components/CustomSelect/helpers.js +0 -76
  48. package/dist/cjs/components/CustomSelect/helpers.js.map +0 -1
  49. package/dist/cjs/components/CustomSelect/types.d.ts +0 -12
  50. package/dist/cjs/components/CustomSelect/types.d.ts.map +0 -1
  51. package/dist/cjs/components/CustomSelect/types.js +0 -6
  52. package/dist/cjs/components/CustomSelect/types.js.map +0 -1
  53. package/dist/components/CustomSelect/helpers.d.ts +0 -8
  54. package/dist/components/CustomSelect/helpers.d.ts.map +0 -1
  55. package/dist/components/CustomSelect/helpers.js +0 -48
  56. package/dist/components/CustomSelect/helpers.js.map +0 -1
  57. package/dist/components/CustomSelect/types.d.ts +0 -12
  58. package/dist/components/CustomSelect/types.d.ts.map +0 -1
  59. package/dist/components/CustomSelect/types.js +0 -3
  60. package/dist/components/CustomSelect/types.js.map +0 -1
  61. package/dist/cssm/components/CustomSelect/helpers.d.ts +0 -8
  62. package/dist/cssm/components/CustomSelect/helpers.d.ts.map +0 -1
  63. package/dist/cssm/components/CustomSelect/helpers.js +0 -44
  64. package/dist/cssm/components/CustomSelect/helpers.js.map +0 -1
  65. package/dist/cssm/components/CustomSelect/types.d.ts +0 -12
  66. package/dist/cssm/components/CustomSelect/types.d.ts.map +0 -1
  67. package/dist/cssm/components/CustomSelect/types.js +0 -3
  68. package/dist/cssm/components/CustomSelect/types.js.map +0 -1
  69. package/src/components/CustomSelect/helpers.tsx +0 -61
  70. 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
- 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,
@@ -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 filterOptions() {
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
- [filterFn, inputValue, optionsProp, searchable],
463
- );
464
-
465
- const selectValue = props.value ?? nativeSelectValue ?? defaultValue;
466
- React.useEffect(
467
- function updateSelectedOptionIndexOnValueChange() {
468
- setSelectedOptionIndex(findSelectedIndex(options, selectValue, allowClearButton));
469
- },
470
- [selectValue, allowClearButton, options],
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
- searchable={searchable}
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
- selectedOptionLabel={selected?.label}
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
- .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 { 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
- 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
  );
@@ -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"}