@snack-uikit/fields 0.14.2 → 0.15.0

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 (62) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/README.md +15 -14
  3. package/dist/components/FieldDecorator/Header.js +1 -1
  4. package/dist/components/FieldDecorator/styles.module.css +7 -1
  5. package/dist/components/FieldSelect/FieldSelect.d.ts +1 -7
  6. package/dist/components/FieldSelect/FieldSelect.js +9 -21
  7. package/dist/components/FieldSelect/FieldSelectMultiple.d.ts +17 -0
  8. package/dist/components/FieldSelect/FieldSelectMultiple.js +118 -0
  9. package/dist/components/FieldSelect/FieldSelectSingle.d.ts +9 -28
  10. package/dist/components/FieldSelect/FieldSelectSingle.js +69 -55
  11. package/dist/components/FieldSelect/hooks.d.ts +30 -0
  12. package/dist/components/FieldSelect/hooks.js +72 -0
  13. package/dist/components/FieldSelect/index.d.ts +2 -1
  14. package/dist/components/FieldSelect/index.js +1 -1
  15. package/dist/components/FieldSelect/styles.module.css +129 -27
  16. package/dist/components/FieldSelect/types.d.ts +42 -37
  17. package/dist/components/FieldSelect/utils.d.ts +19 -0
  18. package/dist/components/FieldSelect/utils.js +112 -0
  19. package/dist/helperComponents/FieldContainerPrivate/styles.module.css +30 -6
  20. package/package.json +5 -3
  21. package/src/components/FieldDecorator/Header.tsx +6 -1
  22. package/src/components/FieldDecorator/styles.module.scss +38 -30
  23. package/src/components/FieldSelect/FieldSelect.tsx +13 -21
  24. package/src/components/FieldSelect/FieldSelectMultiple.tsx +255 -0
  25. package/src/components/FieldSelect/FieldSelectSingle.tsx +159 -99
  26. package/src/components/FieldSelect/hooks.ts +125 -0
  27. package/src/components/FieldSelect/index.ts +12 -1
  28. package/src/components/FieldSelect/styles.module.scss +71 -31
  29. package/src/components/FieldSelect/types.ts +55 -40
  30. package/src/components/FieldSelect/utils.ts +163 -0
  31. package/src/helperComponents/FieldContainerPrivate/FieldContainerPrivate.tsx +2 -0
  32. package/src/helperComponents/FieldContainerPrivate/styles.module.scss +32 -11
  33. package/dist/components/FieldSelect/FieldSelectBase.d.ts +0 -31
  34. package/dist/components/FieldSelect/FieldSelectBase.js +0 -51
  35. package/dist/components/FieldSelect/FieldSelectMulti.d.ts +0 -37
  36. package/dist/components/FieldSelect/FieldSelectMulti.js +0 -89
  37. package/dist/components/FieldSelect/constants.d.ts +0 -7
  38. package/dist/components/FieldSelect/constants.js +0 -6
  39. package/dist/components/FieldSelect/helpers/getArrowIcon.d.ts +0 -8
  40. package/dist/components/FieldSelect/helpers/getArrowIcon.js +0 -8
  41. package/dist/components/FieldSelect/helpers/getDisplayedValue.d.ts +0 -10
  42. package/dist/components/FieldSelect/helpers/getDisplayedValue.js +0 -12
  43. package/dist/components/FieldSelect/helpers/index.d.ts +0 -2
  44. package/dist/components/FieldSelect/helpers/index.js +0 -2
  45. package/dist/components/FieldSelect/hooks/index.d.ts +0 -3
  46. package/dist/components/FieldSelect/hooks/index.js +0 -3
  47. package/dist/components/FieldSelect/hooks/useFilteredOptions.d.ts +0 -7
  48. package/dist/components/FieldSelect/hooks/useFilteredOptions.js +0 -6
  49. package/dist/components/FieldSelect/hooks/useList.d.ts +0 -37
  50. package/dist/components/FieldSelect/hooks/useList.js +0 -52
  51. package/dist/components/FieldSelect/hooks/useListNavigation.d.ts +0 -26
  52. package/dist/components/FieldSelect/hooks/useListNavigation.js +0 -48
  53. package/src/components/FieldSelect/FieldSelectBase.tsx +0 -222
  54. package/src/components/FieldSelect/FieldSelectMulti.tsx +0 -163
  55. package/src/components/FieldSelect/constants.ts +0 -9
  56. package/src/components/FieldSelect/helpers/getArrowIcon.ts +0 -9
  57. package/src/components/FieldSelect/helpers/getDisplayedValue.ts +0 -25
  58. package/src/components/FieldSelect/helpers/index.ts +0 -2
  59. package/src/components/FieldSelect/hooks/index.ts +0 -3
  60. package/src/components/FieldSelect/hooks/useFilteredOptions.ts +0 -23
  61. package/src/components/FieldSelect/hooks/useList.ts +0 -87
  62. package/src/components/FieldSelect/hooks/useListNavigation.ts +0 -81
@@ -5,17 +5,17 @@ $sizes: 's', 'm', 'l';
5
5
  $header-typography: (
6
6
  's': $sans-label-m,
7
7
  'm': $sans-label-l,
8
- 'l': $sans-label-l
8
+ 'l': $sans-label-l,
9
9
  );
10
10
  $footer-typography: (
11
11
  's': $sans-body-s,
12
12
  'm': $sans-body-m,
13
- 'l': $sans-body-m
13
+ 'l': $sans-body-m,
14
14
  );
15
15
  $hint-icon-container: (
16
16
  's': $fields-hint-icon-container-s,
17
17
  'm': $fields-hint-icon-container-m,
18
- 'l': $fields-hint-icon-container-m
18
+ 'l': $fields-hint-icon-container-m,
19
19
  );
20
20
 
21
21
  .decorator {
@@ -57,8 +57,9 @@ $hint-icon-container: (
57
57
  }
58
58
 
59
59
  .footer {
60
+ @include composite-var($fields-hint-container);
61
+
60
62
  display: flex;
61
- gap: $dimension-050m;
62
63
  justify-content: space-between;
63
64
  box-sizing: border-box;
64
65
 
@@ -97,19 +98,19 @@ $hint-icon-container: (
97
98
  display: grid;
98
99
  flex-grow: 1;
99
100
 
100
- &[data-validation="default"] {
101
+ &[data-validation='default'] {
101
102
  color: simple-var($sys-neutral-text-light);
102
103
  }
103
104
 
104
- &[data-validation="error"] {
105
+ &[data-validation='error'] {
105
106
  color: simple-var($sys-red-text-main);
106
107
  }
107
108
 
108
- &[data-validation="warning"] {
109
+ &[data-validation='warning'] {
109
110
  color: simple-var($sys-yellow-text-main);
110
111
  }
111
112
 
112
- &[data-validation="success"] {
113
+ &[data-validation='success'] {
113
114
  color: simple-var($sys-green-text-main);
114
115
  }
115
116
  }
@@ -124,58 +125,65 @@ $hint-icon-container: (
124
125
  flex-shrink: 0;
125
126
  box-sizing: content-box;
126
127
 
127
- &[data-validation="default"] {
128
+ &[data-validation='default'] {
128
129
  color: simple-var($sys-neutral-text-light);
129
130
  }
130
131
 
131
- &[data-validation="error"] {
132
+ &[data-validation='error'] {
132
133
  color: simple-var($sys-red-accent-default);
133
134
  }
134
135
 
135
- &[data-validation="warning"] {
136
+ &[data-validation='warning'] {
136
137
  color: simple-var($sys-yellow-accent-default);
137
138
  }
138
139
 
139
- &[data-validation="success"] {
140
+ &[data-validation='success'] {
140
141
  color: simple-var($sys-green-accent-default);
141
142
  }
142
143
  }
143
144
 
144
145
  .counterLimit {
145
146
  > span {
146
- &[data-validation="default"] {
147
- color: simple-var($sys-neutral-text-light);
148
- }
147
+ &[data-validation='default'] {
148
+ color: simple-var($sys-neutral-text-light);
149
+ }
149
150
 
150
- &[data-limit-exceeded],
151
- &[data-validation="error"] {
152
- color: simple-var($sys-red-text-light);
153
- }
151
+ &[data-limit-exceeded],
152
+ &[data-validation='error'] {
153
+ color: simple-var($sys-red-text-light);
154
+ }
154
155
 
155
- &[data-validation="warning"] {
156
- color: simple-var($sys-yellow-text-light);
157
- }
156
+ &[data-validation='warning'] {
157
+ color: simple-var($sys-yellow-text-light);
158
+ }
158
159
 
159
- &[data-validation="success"] {
160
- color: simple-var($sys-green-text-light);
161
- }}
160
+ &[data-validation='success'] {
161
+ color: simple-var($sys-green-text-light);
162
+ }
163
+ }
162
164
  }
163
165
 
164
166
  .counterCurrentValue {
165
167
  &[data-limit-exceeded] {
166
- &[data-validation="default"] {
168
+ &[data-validation='default'] {
167
169
  color: simple-var($sys-neutral-text-main);
168
170
  }
169
- &[data-validation="error"] {
171
+ &[data-validation='error'] {
170
172
  color: simple-var($sys-red-text-main);
171
173
  }
172
174
 
173
- &[data-validation="warning"] {
175
+ &[data-validation='warning'] {
174
176
  color: simple-var($sys-yellow-text-main);
175
177
  }
176
178
 
177
- &[data-validation="success"] {
179
+ &[data-validation='success'] {
178
180
  color: simple-var($sys-green-text-light);
179
181
  }
180
182
  }
181
- }
183
+ }
184
+
185
+ .labelTooltipTrigger {
186
+ display: flex;
187
+ align-items: center;
188
+ height: 100%;
189
+ }
@@ -1,26 +1,18 @@
1
1
  import { forwardRef } from 'react';
2
2
 
3
- import { SELECTION_MODE } from './constants';
4
- import { FieldSelectMulti } from './FieldSelectMulti';
3
+ import { FieldSelectMultiple } from './FieldSelectMultiple';
5
4
  import { FieldSelectSingle } from './FieldSelectSingle';
6
- import { FieldSelectMultiProps, FieldSelectSingleProps } from './types';
5
+ import { FieldSelectProps } from './types';
6
+ import { isFieldSelectMultipleProps, isFieldSelectSingleProps } from './utils';
7
7
 
8
- export type FieldSelectProps =
9
- | ({
10
- selectionMode?: typeof SELECTION_MODE.Single;
11
- } & FieldSelectSingleProps)
12
- | ({
13
- selectionMode: typeof SELECTION_MODE.Multi;
14
- } & FieldSelectMultiProps);
8
+ export const FieldSelect = forwardRef<HTMLInputElement, FieldSelectProps>((props, ref) => {
9
+ if (isFieldSelectMultipleProps(props)) {
10
+ return <FieldSelectMultiple {...props} ref={ref} />;
11
+ }
15
12
 
16
- export const FieldSelect = forwardRef<HTMLInputElement, FieldSelectProps>(
17
- ({ selectionMode = SELECTION_MODE.Single, ...props }, ref) => {
18
- switch (selectionMode) {
19
- case SELECTION_MODE.Multi:
20
- return <FieldSelectMulti {...(props as FieldSelectMultiProps)} ref={ref} />;
21
- case SELECTION_MODE.Single:
22
- default:
23
- return <FieldSelectSingle {...(props as FieldSelectSingleProps)} ref={ref} />;
24
- }
25
- },
26
- );
13
+ if (isFieldSelectSingleProps(props)) {
14
+ return <FieldSelectSingle {...props} ref={ref} />;
15
+ }
16
+
17
+ return null;
18
+ });
@@ -0,0 +1,255 @@
1
+ import cn from 'classnames';
2
+ import mergeRefs from 'merge-refs';
3
+ import { FocusEvent, forwardRef, KeyboardEvent, KeyboardEventHandler, useMemo, useRef, useState } from 'react';
4
+ import { useUncontrolledProp } from 'uncontrollable';
5
+
6
+ import { InputPrivate } from '@snack-uikit/input-private';
7
+ import { Droplist, SelectionSingleValueType, useFuzzySearch } from '@snack-uikit/list';
8
+ import { Tag } from '@snack-uikit/tag';
9
+ import { extractSupportProps } from '@snack-uikit/utils';
10
+
11
+ import { FieldContainerPrivate } from '../../helperComponents';
12
+ import { FieldDecorator } from '../FieldDecorator';
13
+ import { useButtons, useHandleDeleteItem, useHandleOnKeyDown, useSearchInput } from './hooks';
14
+ import styles from './styles.module.scss';
15
+ import { FieldSelectMultipleProps } from './types';
16
+ import { extractSelectedMultipleOptions, getArrowIcon, transformOptionsToItems } from './utils';
17
+
18
+ const BASE_MIN_WIDTH = 4;
19
+
20
+ export const FieldSelectMultiple = forwardRef<HTMLInputElement, FieldSelectMultipleProps>(
21
+ (
22
+ {
23
+ id,
24
+ name,
25
+ placeholder,
26
+ size = 's',
27
+ options,
28
+ value: valueProp,
29
+ defaultValue,
30
+ onChange: onChangeProp,
31
+ loading,
32
+ disabled = false,
33
+ readonly = false,
34
+ searchable = true,
35
+ showCopyButton = true,
36
+ showClearButton = true,
37
+ onKeyDown: onInputKeyDownProp,
38
+ label,
39
+ labelTooltip,
40
+ labelTooltipPlacement,
41
+ required = false,
42
+ hint,
43
+ showHintIcon,
44
+ validationState = 'default',
45
+ footer,
46
+ search,
47
+ autocomplete = false,
48
+ prefixIcon,
49
+ ...rest
50
+ },
51
+ ref,
52
+ ) => {
53
+ const localRef = useRef<HTMLInputElement>(null);
54
+ const inputPlugRef = useRef<HTMLSpanElement>(null);
55
+ const contentRef = useRef<HTMLDivElement>(null);
56
+
57
+ const [open, setOpen] = useState<boolean>(false);
58
+ const items = useMemo(() => transformOptionsToItems(options), [options]);
59
+ const [value, setValue] = useUncontrolledProp<SelectionSingleValueType[] | undefined>(
60
+ valueProp,
61
+ defaultValue,
62
+ onChangeProp,
63
+ );
64
+
65
+ const selectedOption = useMemo(() => {
66
+ const notSortSelectedOption = extractSelectedMultipleOptions(options, value);
67
+
68
+ if (notSortSelectedOption) {
69
+ return notSortSelectedOption.sort((a, b) => {
70
+ if (b.disabled && !a.disabled) {
71
+ return 1;
72
+ }
73
+
74
+ if (a.disabled && !b.disabled) {
75
+ return -1;
76
+ }
77
+
78
+ return 0;
79
+ });
80
+ }
81
+ }, [options, value]);
82
+
83
+ const { inputValue, onInputValueChange, prevInputValue } = useSearchInput({
84
+ ...search,
85
+ defaultValue: String(selectedOption ?? ''),
86
+ });
87
+
88
+ const onClear = () => {
89
+ setValue(undefined);
90
+ onInputValueChange('');
91
+
92
+ localRef.current?.focus();
93
+ setOpen(true);
94
+ };
95
+
96
+ const { ArrowIcon, arrowIconSize } = getArrowIcon({ size, open });
97
+
98
+ const { buttons, inputKeyDownNavigationHandler, buttonsRefs } = useButtons({
99
+ readonly,
100
+ size,
101
+ showClearButton: showClearButton && Boolean(value),
102
+ showCopyButton,
103
+ inputRef: localRef,
104
+ onClear,
105
+ valueToCopy: String(selectedOption?.map(option => option.option).join(', ') ?? ''),
106
+ });
107
+
108
+ const commonHandleOnKeyDown = useHandleOnKeyDown({
109
+ inputKeyDownNavigationHandler,
110
+ onInputKeyDownProp,
111
+ setOpen,
112
+ });
113
+
114
+ const handleItemDelete = useHandleDeleteItem(setValue);
115
+ const handleOnKeyDown = (onKeyDown?: KeyboardEventHandler<HTMLElement>) => (e: KeyboardEvent<HTMLInputElement>) => {
116
+ if (e.code === 'Backspace' && inputValue === '') {
117
+ if (selectedOption?.length && !selectedOption.slice(-1)[0].disabled) {
118
+ handleItemDelete(selectedOption.pop())();
119
+ }
120
+ }
121
+
122
+ if (!open && prevInputValue.current !== inputValue) {
123
+ setOpen(true);
124
+ }
125
+
126
+ commonHandleOnKeyDown(onKeyDown)(e);
127
+ };
128
+
129
+ const handleOpenChange = (open: boolean) => {
130
+ if (!readonly && !disabled && !buttonsRefs.includes(document.activeElement)) {
131
+ setOpen(open);
132
+ if (!open) {
133
+ prevInputValue.current = inputValue;
134
+ }
135
+ if (open) {
136
+ prevInputValue.current = '';
137
+ }
138
+ }
139
+ };
140
+
141
+ const handleBlur = (e: FocusEvent<HTMLInputElement>) => {
142
+ if (!open) {
143
+ onInputValueChange('');
144
+ }
145
+
146
+ rest?.onBlur?.(e);
147
+ };
148
+
149
+ const fuzzySearch = useFuzzySearch(items);
150
+ const result = autocomplete ? items : fuzzySearch(prevInputValue.current !== inputValue ? inputValue : '');
151
+
152
+ return (
153
+ <FieldDecorator
154
+ {...extractSupportProps(rest)}
155
+ required={required}
156
+ readonly={readonly}
157
+ label={label}
158
+ labelTooltip={labelTooltip}
159
+ labelTooltipPlacement={labelTooltipPlacement}
160
+ labelFor={id}
161
+ hint={hint}
162
+ disabled={disabled}
163
+ showHintIcon={showHintIcon}
164
+ size={size}
165
+ validationState={validationState}
166
+ >
167
+ <Droplist
168
+ trigger='clickAndFocusVisible'
169
+ placement='bottom'
170
+ data-test-id='field-select__list'
171
+ items={result}
172
+ triggerElemRef={localRef}
173
+ scroll
174
+ marker
175
+ footer={footer}
176
+ selection={{
177
+ mode: 'multiple',
178
+ value: value,
179
+ onChange: setValue,
180
+ }}
181
+ size={size}
182
+ open={!disabled && !readonly && open}
183
+ onOpenChange={handleOpenChange}
184
+ loading={loading}
185
+ >
186
+ {({ onKeyDown }) => (
187
+ <FieldContainerPrivate
188
+ className={cn(styles.container, styles.tagContainer)}
189
+ validationState={validationState}
190
+ disabled={disabled}
191
+ readonly={readonly}
192
+ focused={open}
193
+ variant='single-line-container'
194
+ inputRef={localRef}
195
+ size={size}
196
+ prefix={prefixIcon}
197
+ >
198
+ <>
199
+ <div className={styles.contentWrapper} ref={contentRef}>
200
+ {selectedOption &&
201
+ selectedOption.map(option => (
202
+ <Tag
203
+ size={size === 'l' ? 's' : 'xs'}
204
+ tabIndex={-1}
205
+ label={String(option.option)}
206
+ key={option.value}
207
+ onDelete={!option.disabled ? handleItemDelete(option) : undefined}
208
+ />
209
+ ))}
210
+
211
+ <div
212
+ className={styles.inputWrapper}
213
+ style={{
214
+ minWidth: value
215
+ ? Math.min(
216
+ contentRef.current?.clientWidth ?? BASE_MIN_WIDTH,
217
+ inputPlugRef.current?.clientWidth ?? BASE_MIN_WIDTH,
218
+ )
219
+ : '100%',
220
+ }}
221
+ >
222
+ <InputPrivate
223
+ id={id}
224
+ name={name}
225
+ type='text'
226
+ disabled={disabled}
227
+ placeholder={!selectedOption ? placeholder : undefined}
228
+ ref={mergeRefs(ref, localRef)}
229
+ onChange={searchable ? onInputValueChange : undefined}
230
+ value={searchable ? inputValue : ''}
231
+ readonly={!searchable || readonly}
232
+ data-test-id='field-select__input'
233
+ onKeyDown={handleOnKeyDown(onKeyDown)}
234
+ onBlur={handleBlur}
235
+ className={styles.input}
236
+ />
237
+ </div>
238
+ </div>
239
+
240
+ <div className={styles.postfix}>
241
+ {buttons}
242
+ <ArrowIcon size={arrowIconSize} className={styles.arrowIcon} />
243
+ </div>
244
+
245
+ <span ref={inputPlugRef} className={styles.inputPlug}>
246
+ {inputValue}
247
+ </span>
248
+ </>
249
+ </FieldContainerPrivate>
250
+ )}
251
+ </Droplist>
252
+ </FieldDecorator>
253
+ );
254
+ },
255
+ );