@ultraviolet/ui 1.32.2 → 1.33.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.
package/dist/index.d.ts CHANGED
@@ -1343,9 +1343,9 @@ type BadgeProps = {
1343
1343
  */
1344
1344
  declare const Badge: ({ sentiment, size, prominence, icon, disabled, className, children, "data-testid": dataTestId, }: BadgeProps) => _emotion_react_jsx_runtime.JSX.Element;
1345
1345
 
1346
- type Variant$1 = 'intro' | 'promotional';
1346
+ type Variant = 'intro' | 'promotional';
1347
1347
  type BannerProps = {
1348
- variant?: Variant$1;
1348
+ variant?: Variant;
1349
1349
  size?: 'small' | 'medium';
1350
1350
  title: string;
1351
1351
  children: ReactNode;
@@ -2455,7 +2455,7 @@ type SeparatorProps = {
2455
2455
  */
2456
2456
  declare const Separator: ({ direction, thickness, color, icon, className, "data-testid": dataTestId, }: SeparatorProps) => _emotion_react_jsx_runtime.JSX.Element;
2457
2457
 
2458
- declare const variants$1: {
2458
+ declare const variants: {
2459
2459
  readonly block: ({ length }: {
2460
2460
  length?: number | undefined;
2461
2461
  }) => _emotion_react_jsx_runtime.JSX.Element;
@@ -2484,7 +2484,7 @@ declare const variants$1: {
2484
2484
  as?: react.ElementType<any, keyof react.JSX.IntrinsicElements> | undefined;
2485
2485
  }, react.DetailedHTMLProps<react.HTMLAttributes<HTMLDivElement>, HTMLDivElement>, {}>;
2486
2486
  };
2487
- type SkeletonVariant = keyof typeof variants$1;
2487
+ type SkeletonVariant = keyof typeof variants;
2488
2488
  type SkeletonProps = {
2489
2489
  variant?: SkeletonVariant;
2490
2490
  length?: number;
@@ -2791,18 +2791,12 @@ type TagProps = {
2791
2791
  */
2792
2792
  declare const Tag: ({ children, isLoading, onClose, icon, copiable, copyText, copiedText, disabled, sentiment, className, "data-testid": dataTestId, }: TagProps) => _emotion_react_jsx_runtime.JSX.Element;
2793
2793
 
2794
- declare const variants: {
2795
- readonly base: ({ theme: { colors, shadows, radii } }: {
2796
- theme: Theme;
2797
- }) => string;
2798
- readonly bordered: ({ theme: { shadows } }: {
2799
- theme: Theme;
2800
- }) => string;
2801
- readonly 'no-border': ({ theme: { shadows } }: {
2802
- theme: Theme;
2803
- }) => string;
2794
+ declare const TAGINPUT_SIZE_PADDING: {
2795
+ readonly large: "1.5";
2796
+ readonly medium: "1";
2797
+ readonly small: "0.5";
2804
2798
  };
2805
- type Variant = keyof typeof variants;
2799
+ type TagInputSize = keyof typeof TAGINPUT_SIZE_PADDING;
2806
2800
  type TagInputProp = (string | {
2807
2801
  label: string;
2808
2802
  index: string;
@@ -2810,21 +2804,46 @@ type TagInputProp = (string | {
2810
2804
  type TagInputProps = {
2811
2805
  disabled?: boolean;
2812
2806
  id?: string;
2807
+ /**
2808
+ * @deprecated this prop has no more effect
2809
+ */
2813
2810
  manualInput?: boolean;
2814
2811
  name?: string;
2815
2812
  onChange?: (tags: string[]) => void;
2813
+ /**
2814
+ * @deprecated this prop has no more effect
2815
+ */
2816
2816
  onChangeError?: (error: Error | string) => void;
2817
2817
  placeholder?: string;
2818
+ /**
2819
+ * @deprecated use `value` property instead, both properties work the same way
2820
+ */
2818
2821
  tags?: TagInputProp;
2819
- variant?: Variant;
2822
+ value?: TagInputProp;
2823
+ /**
2824
+ * @deprecated there is only one variant now, this prop has no more effect
2825
+ */
2826
+ variant?: string;
2820
2827
  className?: string;
2821
2828
  'data-testid'?: string;
2829
+ label?: string;
2830
+ /**
2831
+ * Label description displayed right next to the label. It allows you to customize the label content.
2832
+ */
2833
+ labelDescription?: ReactNode;
2834
+ required?: boolean;
2835
+ size?: TagInputSize;
2836
+ error?: string;
2837
+ success?: string | boolean;
2838
+ helper?: ReactNode;
2839
+ readOnly?: boolean;
2840
+ tooltip?: string;
2841
+ clearable?: boolean;
2822
2842
  };
2823
2843
  /**
2824
2844
  * TagInput is a component that allows users to input tags.
2825
- * @experimental This component is experimental and may be subject to breaking changes in the future.
2826
2845
  */
2827
- declare const TagInput: ({ disabled, id, manualInput, name, onChange, onChangeError, placeholder, tags, variant, className, "data-testid": dataTestId, }: TagInputProps) => _emotion_react_jsx_runtime.JSX.Element;
2846
+ declare const TagInput: ({ disabled, id, name, onChange, placeholder, tags, value, className, "data-testid": dataTestId, label, labelDescription, required, size, error, success, helper, readOnly, tooltip, clearable, }: TagInputProps) => _emotion_react_jsx_runtime.JSX.Element;
2828
2847
 
2829
2848
  type TagListProps = {
2830
2849
  /**
@@ -1,104 +1,83 @@
1
1
  import _styled from '@emotion/styled/base';
2
- import { useState, useEffect, useRef } from 'react';
2
+ import { Icon } from '@ultraviolet/icons';
3
+ import { useState, useId, useEffect, useRef, useMemo } from 'react';
4
+ import { Button } from '../Button/index.js';
5
+ import { Stack } from '../Stack/index.js';
3
6
  import { Tag } from '../Tag/index.js';
7
+ import { Text } from '../Text/index.js';
8
+ import { Tooltip } from '../Tooltip/index.js';
4
9
  import { jsxs, jsx } from '@emotion/react/jsx-runtime';
5
10
  import { getUUID } from '../../utils/ids.js';
6
11
 
12
+ const TAGINPUT_SIZE_PADDING = {
13
+ large: '1.5',
14
+ medium: '1',
15
+ small: '0.5'
16
+ };
7
17
  const STATUS = {
8
18
  IDLE: 'idle',
9
19
  LOADING: 'loading'
10
20
  };
11
- const variants = {
12
- base: ({
13
- theme: {
14
- colors,
15
- shadows,
16
- radii
17
- }
18
- }) => `
19
- padding: 8px;
20
- cursor: text;
21
- border-radius: ${radii.default};
22
- border: 1px solid ${colors.neutral.border};
23
- &:focus-within {
24
- border: 1px solid ${colors.primary.border};
25
- box-shadow: ${shadows.focusPrimary};
26
- }
27
-
28
- &:hover {
29
- border: 1px solid ${colors.primary.border};
30
- }
31
-
32
- & > * {
33
- margin: 6px;
34
- }
35
- `,
36
- bordered: ({
37
- theme: {
38
- shadows
39
- }
40
- }) => `
41
- margin-top: 0;
42
- padding: 8px 0;
43
-
44
- > input:focus {
45
- box-shadow: ${shadows.focusPrimary};
46
- }
47
-
48
- > * {
49
- margin-bottom: 6px;
50
- &:not(:last-child) {
51
- margin-right: 6px;
52
- }
53
- }
54
- `,
55
- 'no-border': ({
56
- theme: {
57
- shadows
58
- }
59
- }) => `
60
- &:focus-within {
61
- box-shadow: ${shadows.focusPrimary};
62
- }
63
-
64
- > * {
65
- margin-right: 6px;
66
- margin-bottom: 6px;
67
- }
68
- `
69
- };
70
21
  const TagInputContainer = /*#__PURE__*/_styled('div', {
71
- shouldForwardProp: prop => !['variant'].includes(prop),
72
- target: "ea7vc6o1"
73
- })("display:flex;flex-wrap:wrap;background-color:", ({
22
+ shouldForwardProp: prop => !['size'].includes(prop),
23
+ target: "ea7vc6o3"
24
+ })("display:flex;gap:", ({
25
+ theme
26
+ }) => theme.space['1'], ";background-color:", ({
74
27
  theme: {
75
28
  colors
76
29
  }
77
- }) => colors.neutral.background, ";", ({
78
- variant,
30
+ }) => colors.neutral.background, ";padding:", ({
31
+ theme,
32
+ size
33
+ }) => `calc(${theme.space[TAGINPUT_SIZE_PADDING[size]]} - 1px) ${theme.space['2']}`, ";cursor:text;background:", ({
79
34
  theme
80
- }) => variants[variant] ? variants[variant]({
35
+ }) => theme.colors.neutral.background, ";border:1px solid ", ({
81
36
  theme
82
- }) : variants.base({
37
+ }) => theme.colors.neutral.border, ";border-radius:", ({
83
38
  theme
84
- }), ";");
39
+ }) => theme.radii.default, ";&:focus-within{border-color:", ({
40
+ theme
41
+ }) => theme.colors.primary.borderHover, ";box-shadow:", ({
42
+ theme
43
+ }) => theme.shadows.focusPrimary, ";}&[data-success='true']{border-color:", ({
44
+ theme
45
+ }) => theme.colors.success.border, ";}&[data-error='true']{border-color:", ({
46
+ theme
47
+ }) => theme.colors.danger.border, ";}&:hover{border-color:", ({
48
+ theme
49
+ }) => theme.colors.primary.borderHover, ";}&[data-readonly='true']{border-color:", ({
50
+ theme
51
+ }) => theme.colors.neutral.border, ";background:", ({
52
+ theme
53
+ }) => theme.colors.neutral.backgroundWeak, ";}&[data-disabled='true']{border-color:", ({
54
+ theme
55
+ }) => theme.colors.neutral.borderDisabled, ";background:", ({
56
+ theme
57
+ }) => theme.colors.neutral.backgroundDisabled, ";cursor:not-allowed;}");
58
+ const DataContainer = /*#__PURE__*/_styled('div', {
59
+ target: "ea7vc6o2"
60
+ })("height:100%;display:flex;flex-wrap:wrap;align-items:center;gap:", ({
61
+ theme
62
+ }) => theme.space['1'], ";flex:1;");
63
+ const StateContainer = /*#__PURE__*/_styled('div', {
64
+ target: "ea7vc6o1"
65
+ })("display:flex;align-items:center;gap:", ({
66
+ theme
67
+ }) => theme.space['1'], ";");
85
68
  const StyledInput = /*#__PURE__*/_styled("input", {
86
69
  target: "ea7vc6o0"
87
70
  })("display:flex;flex:1;font-size:", ({
88
71
  theme
89
- }) => theme.typography.body.fontSize, ";color:", ({
90
- theme: {
91
- colors
92
- }
93
- }) => colors.neutral.text, ";border:none;outline:none;background-color:", ({
72
+ }) => theme.typography.body.fontSize, ";background:inherit;color:", ({
94
73
  theme: {
95
74
  colors
96
75
  }
97
- }) => colors.neutral.background, ";&::placeholder{color:", ({
76
+ }) => colors.neutral.text, ";border:none;outline:none;&::placeholder{color:", ({
98
77
  theme: {
99
78
  colors
100
79
  }
101
- }) => colors.neutral.textWeak, ";}");
80
+ }) => colors.neutral.textWeak, ";}height:100%;");
102
81
  const convertTagArrayToTagStateArray = tags => (tags || [])?.map((tag, index) => typeof tag === 'object' ? {
103
82
  ...tag,
104
83
  index: getUUID(`tag-${index}`)
@@ -108,27 +87,37 @@ const convertTagArrayToTagStateArray = tags => (tags || [])?.map((tag, index) =>
108
87
  });
109
88
  /**
110
89
  * TagInput is a component that allows users to input tags.
111
- * @experimental This component is experimental and may be subject to breaking changes in the future.
112
90
  */
113
91
  const TagInput = ({
114
92
  disabled = false,
115
93
  id,
116
- manualInput = true,
117
94
  name,
118
95
  onChange,
119
- onChangeError,
120
96
  placeholder,
121
97
  tags,
122
- variant = 'base',
98
+ value,
123
99
  className,
124
- 'data-testid': dataTestId
100
+ 'data-testid': dataTestId,
101
+ label,
102
+ labelDescription,
103
+ required = false,
104
+ size = 'medium',
105
+ error,
106
+ success,
107
+ helper,
108
+ readOnly = false,
109
+ tooltip,
110
+ clearable = false
125
111
  }) => {
126
- const [tagInputState, setTagInput] = useState(convertTagArrayToTagStateArray(tags ?? []));
112
+ const tagsProp = value ?? tags;
113
+ const [tagInputState, setTagInput] = useState(convertTagArrayToTagStateArray(tagsProp ?? []));
127
114
  const [input, setInput] = useState('');
128
115
  const [status, setStatus] = useState({});
116
+ const uniqueId = useId();
117
+ const localId = id ?? uniqueId;
129
118
  useEffect(() => {
130
- setTagInput(convertTagArrayToTagStateArray(tags));
131
- }, [tags, setTagInput]);
119
+ setTagInput(convertTagArrayToTagStateArray(tagsProp));
120
+ }, [tagsProp, setTagInput]);
132
121
  const inputRef = useRef(null);
133
122
  const dispatchOnChange = newState => {
134
123
  const changes = newState.map(tag => typeof tag === 'object' ? tag?.label : tag);
@@ -157,8 +146,7 @@ const TagInput = ({
157
146
  setStatus({
158
147
  [newTagInput[newTagInput.length - 1].index]: STATUS.IDLE
159
148
  });
160
- } catch (error) {
161
- onChangeError?.(error);
149
+ } catch (e) {
162
150
  setTagInput(tagInputState);
163
151
  }
164
152
  };
@@ -177,8 +165,7 @@ const TagInput = ({
177
165
  setStatus({
178
166
  [tagIndex]: STATUS.IDLE
179
167
  });
180
- } catch (error) {
181
- onChangeError?.(error);
168
+ } catch (e) {
182
169
  setTagInput(tagInputState);
183
170
  }
184
171
  };
@@ -207,39 +194,116 @@ const TagInput = ({
207
194
  setStatus({
208
195
  [newTagInput.length - 1]: STATUS.IDLE
209
196
  });
210
- } catch (error) {
211
- onChangeError?.(error);
197
+ } catch (err) {
212
198
  setTagInput(tagInputState);
213
199
  }
214
200
  };
215
- return jsxs(TagInputContainer, {
216
- onClick: handleContainerClick,
217
- variant: variant,
201
+ const clearAll = () => {
202
+ setInput('');
203
+ setTagInput([]);
204
+ dispatchOnChange([]);
205
+ };
206
+ const helperSentiment = useMemo(() => {
207
+ if (error) {
208
+ return 'danger';
209
+ }
210
+ if (success) {
211
+ return 'success';
212
+ }
213
+ return 'neutral';
214
+ }, [error, success]);
215
+ const computedClearable = clearable && !!tagInputState.length;
216
+ return jsxs(Stack, {
217
+ gap: "0.5",
218
218
  className: className,
219
- "data-testid": dataTestId,
220
- children: [tagInputState.map(tag => jsx(Tag, {
221
- sentiment: "neutral",
222
- disabled: disabled,
223
- isLoading: status[tag.index] === STATUS.LOADING,
224
- onClose: e => {
225
- e.stopPropagation();
226
- deleteTag(tag.index);
227
- },
228
- children: tag.label
229
- }, tag.index)), !disabled && manualInput ? jsx(StyledInput, {
230
- id: id,
231
- name: name,
232
- "aria-label": name,
233
- type: "text",
234
- placeholder: !tagInputState.length ? placeholder : '',
235
- value: input,
236
- onBlur: addTag,
237
- onChange: onInputChange,
238
- onKeyDown: handleInputKeydown,
239
- onPaste: handlePaste,
240
- ref: inputRef
219
+ children: [jsxs(Stack, {
220
+ direction: "row",
221
+ gap: "1",
222
+ alignItems: "center",
223
+ children: [jsxs(Stack, {
224
+ direction: "row",
225
+ gap: "0.5",
226
+ alignItems: "start",
227
+ children: [jsx(Text, {
228
+ as: "label",
229
+ variant: "bodyStrong",
230
+ sentiment: "neutral",
231
+ htmlFor: localId,
232
+ children: label
233
+ }), required ? jsx(Icon, {
234
+ name: "asterisk",
235
+ color: "danger",
236
+ size: 8
237
+ }) : null]
238
+ }), labelDescription ?? null]
239
+ }), jsx("div", {
240
+ children: jsx(Tooltip, {
241
+ text: tooltip,
242
+ children: jsxs(TagInputContainer, {
243
+ onClick: handleContainerClick,
244
+ className: className,
245
+ "data-testid": dataTestId,
246
+ size: size,
247
+ "data-disabled": disabled,
248
+ "data-readonly": readOnly,
249
+ "data-error": !!error,
250
+ "data-success": !!success,
251
+ children: [jsxs(DataContainer, {
252
+ children: [tagInputState.map(tag => jsx(Tag, {
253
+ sentiment: "neutral",
254
+ disabled: disabled,
255
+ isLoading: status[tag.index] === STATUS.LOADING,
256
+ onClose: !readOnly ? e => {
257
+ e.stopPropagation();
258
+ deleteTag(tag.index);
259
+ } : undefined,
260
+ children: tag.label
261
+ }, tag.index)), !disabled ? jsx(StyledInput, {
262
+ id: localId,
263
+ name: name,
264
+ "aria-label": name,
265
+ type: "text",
266
+ placeholder: !tagInputState.length ? placeholder : '',
267
+ value: input,
268
+ onBlur: addTag,
269
+ onChange: onInputChange,
270
+ onKeyDown: handleInputKeydown,
271
+ onPaste: handlePaste,
272
+ ref: inputRef,
273
+ readOnly: readOnly
274
+ }) : null]
275
+ }), computedClearable || success || error ? jsxs(StateContainer, {
276
+ children: [computedClearable ? jsx(Button, {
277
+ "aria-label": "clear value",
278
+ disabled: disabled,
279
+ variant: "ghost",
280
+ size: "xsmall",
281
+ icon: "close",
282
+ onClick: clearAll,
283
+ sentiment: "neutral"
284
+ }) : null, success ? jsx(Icon, {
285
+ name: "checkbox-circle-outline",
286
+ color: "success",
287
+ size: 16,
288
+ disabled: disabled
289
+ }) : null, error ? jsx(Icon, {
290
+ name: "alert",
291
+ color: "danger",
292
+ size: 16,
293
+ disabled: disabled
294
+ }) : null]
295
+ }) : null]
296
+ })
297
+ })
298
+ }), error || typeof success === 'string' || helper ? jsx(Text, {
299
+ variant: "caption",
300
+ as: "span",
301
+ prominence: !error && !success ? 'weak' : undefined,
302
+ sentiment: helperSentiment,
303
+ disabled: disabled || readOnly,
304
+ children: error || success || helper
241
305
  }) : null]
242
306
  });
243
307
  };
244
308
 
245
- export { TagInput };
309
+ export { TAGINPUT_SIZE_PADDING, TagInput };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ultraviolet/ui",
3
- "version": "1.32.2",
3
+ "version": "1.33.0",
4
4
  "description": "Ultraviolet UI",
5
5
  "homepage": "https://github.com/scaleway/ultraviolet#readme",
6
6
  "repository": {