@true-engineering/true-react-common-ui-kit 3.14.2 → 3.15.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@true-engineering/true-react-common-ui-kit",
3
- "version": "3.14.2",
3
+ "version": "3.15.1",
4
4
  "description": "True Engineering React UI Kit with theming support",
5
5
  "author": "True Engineering (https://trueengineering.ru)",
6
6
  "keywords": [
@@ -1,7 +1,7 @@
1
1
  import { useEffect, useState, useMemo, useRef, useCallback, ReactNode } from 'react';
2
2
  import clsx from 'clsx';
3
3
  import { debounce } from 'ts-debounce';
4
- import { isNotEmpty } from '@true-engineering/true-react-platform-helpers';
4
+ import { isArrayNotEmpty, isNotEmpty } from '@true-engineering/true-react-platform-helpers';
5
5
  import { addDataAttributes } from '../../helpers';
6
6
  import { useIsMounted, useTweakStyles } from '../../hooks';
7
7
  import { ICommonProps } from '../../types';
@@ -317,20 +317,17 @@ export function MultiSelectList<Value extends IMultiSelectListValues<Option>, Op
317
317
 
318
318
  const mainOptionsList = isGroupingEnabled ? unchosenOptions : allOptions;
319
319
 
320
- const hasSelectedOptionsGroup =
321
- isGroupingEnabled && chosenValues !== undefined && chosenValues.length > 0;
320
+ const hasSelectedOptionsGroup = isGroupingEnabled && isArrayNotEmpty(chosenValues);
322
321
 
323
- const shouldShowNothingFoundMessage = !isLoading && allOptions.length === 0;
322
+ const shouldShowNothingFoundMessage = !isLoading && !isArrayNotEmpty(allOptions);
324
323
 
325
324
  const shouldShowAllOptionsLabel =
326
- isGroupingEnabled &&
327
- unchosenOptions.length > 0 &&
328
- chosenValues !== undefined &&
329
- chosenValues.length > 0;
325
+ hasSelectedOptionsGroup && (isArrayNotEmpty(unchosenOptions) || !isArrayNotEmpty(allOptions));
330
326
 
331
327
  const shouldShowPreloader = isLoading || isLoadingOptionsOnScroll;
332
328
 
333
- const shouldShowOptionsList = !isLoading && allOptions.length !== 0;
329
+ const shouldShowOptionsList =
330
+ !isLoading && (isArrayNotEmpty(allOptions) || isArrayNotEmpty(chosenValues));
334
331
 
335
332
  return (
336
333
  <div className={classes.root} {...addDataAttributes(data)}>
@@ -32,4 +32,5 @@ Default.args = {
32
32
  isActive: false,
33
33
  isDisabled: false,
34
34
  isRequired: false,
35
+ isAutoSizeable: true,
35
36
  };
@@ -1,4 +1,4 @@
1
- import { ITweakStyles, animations, createThemedStyles } from '../../theme';
1
+ import { ITweakStyles, animations, createThemedStyles, helpers } from '../../theme';
2
2
 
3
3
  const PADDING_X = 12;
4
4
 
@@ -23,10 +23,11 @@ export const useStyles = createThemedStyles('TextArea', {
23
23
  },
24
24
 
25
25
  textarea: {
26
+ ...helpers.withScrollBar,
27
+
26
28
  width: '100%',
27
29
  height: '100%',
28
30
  outline: 'none',
29
- boxSizing: 'border-box',
30
31
  outlineStyle: 'none',
31
32
  fontFamily: 'inherit',
32
33
  fontSize: 16,
@@ -35,7 +36,6 @@ export const useStyles = createThemedStyles('TextArea', {
35
36
  transitionProperty: 'background-color',
36
37
  border: 'none',
37
38
  resize: 'none',
38
- overflow: 'auto',
39
39
 
40
40
  '&:disabled': {
41
41
  extend: 'disabled',
@@ -52,13 +52,33 @@ export const useStyles = createThemedStyles('TextArea', {
52
52
  },
53
53
  },
54
54
 
55
+ autosize: {
56
+ display: 'inline-grid',
57
+ gridTemplateRows: 'minmax(0, 100%)',
58
+
59
+ '& > $textarea, &::after': {
60
+ gridArea: '1 / 1 / 2 / 2',
61
+ },
62
+
63
+ '&::after': {
64
+ extend: 'textarea',
65
+ content: 'attr(data-value) " "',
66
+ overflowWrap: 'break-word',
67
+ whiteSpace: 'pre-wrap',
68
+ visibility: 'hidden',
69
+ scrollbarGutter: 'stable',
70
+ },
71
+ },
72
+
55
73
  focused: {
56
74
  position: 'relative',
57
75
  zIndex: 1,
58
76
  },
59
77
 
60
78
  withFloatingLabel: {
61
- paddingTop: 24,
79
+ '& $textArea, &::after': {
80
+ paddingTop: 24,
81
+ },
62
82
  },
63
83
 
64
84
  floating: {},
@@ -1,21 +1,19 @@
1
- import {
2
- useRef,
3
- useState,
4
- FC,
5
- useEffect,
6
- FormEvent,
7
- FocusEvent,
8
- ChangeEvent,
9
- ClipboardEvent,
10
- } from 'react';
1
+ import { forwardRef, useState, FormEvent, FocusEvent, ChangeEvent, ReactNode } from 'react';
11
2
  import clsx from 'clsx';
3
+ import {
4
+ addDataTestId,
5
+ isNotEmpty,
6
+ isReactNodeNotEmpty,
7
+ isStringNotEmpty,
8
+ } from '@true-engineering/true-react-platform-helpers';
12
9
  import { addDataAttributes, trimStringToMaxLength } from '../../helpers';
13
10
  import { ICommonProps } from '../../types';
11
+ import { ITextAreaHTMLBaseProps } from './types';
14
12
  import { useStyles, ITextAreaStyles } from './TextArea.styles';
15
13
 
16
- export interface ITextAreaProps extends ICommonProps<ITextAreaStyles> {
14
+ export interface ITextAreaProps extends ICommonProps<ITextAreaStyles>, ITextAreaHTMLBaseProps {
17
15
  value?: string;
18
- label?: string;
16
+ label?: ReactNode;
19
17
  placeholder?: string;
20
18
  /** @default false */
21
19
  isDisabled?: boolean;
@@ -25,11 +23,15 @@ export interface ITextAreaProps extends ICommonProps<ITextAreaStyles> {
25
23
  isInvalid?: boolean;
26
24
  /** @default false */
27
25
  isActive?: boolean;
26
+ /**
27
+ * Должна ли высота и ширина textarea подстраиваться под содержимое
28
+ * @default true
29
+ */
30
+ isAutoSizeable?: boolean;
28
31
  infoMessage?: string;
29
32
  errorMessage?: string;
30
33
  /** @default false */
31
34
  isRequired?: boolean;
32
- name?: string;
33
35
  /** @default false */
34
36
  hasRequiredLabel?: boolean;
35
37
  /** @default false */
@@ -38,143 +40,140 @@ export interface ITextAreaProps extends ICommonProps<ITextAreaStyles> {
38
40
  hasCounter?: boolean;
39
41
  /** @default false */
40
42
  shouldTrimAfterMaxLength?: boolean;
41
- maxLength?: number;
42
- rows?: number;
43
43
  onChange: (value: string, event?: FormEvent<HTMLTextAreaElement>) => void;
44
- onFocus?: (event: FocusEvent<HTMLTextAreaElement>) => void;
45
- onBlur?: (event: FocusEvent<HTMLTextAreaElement>) => void;
46
- onPaste?: (event: ClipboardEvent<HTMLTextAreaElement>) => void;
47
44
  }
48
45
 
49
46
  const DEFAULT_VALUE = '';
50
47
 
51
- export const TextArea: FC<ITextAreaProps> = ({
52
- value = DEFAULT_VALUE,
53
- label,
54
- placeholder,
55
- isDisabled,
56
- hasFloatingLabel = true,
57
- isInvalid = false,
58
- isActive = false,
59
- infoMessage,
60
- errorMessage,
61
- isRequired = false,
62
- name,
63
- hasRequiredLabel = false,
64
- shouldFocusOnMount = false,
65
- hasCounter = true,
66
- shouldTrimAfterMaxLength = false,
67
- maxLength,
68
- rows,
69
- testId,
70
- data,
71
- tweakStyles,
72
- onChange,
73
- onPaste,
74
- onFocus,
75
- onBlur,
76
- }) => {
77
- const classes = useStyles({ theme: tweakStyles });
78
- const [isFocused, setFocused] = useState(false);
79
- const ref = useRef<HTMLTextAreaElement>(null);
48
+ export const TextArea = forwardRef<HTMLTextAreaElement, ITextAreaProps>(
49
+ (
50
+ {
51
+ value = DEFAULT_VALUE,
52
+ label,
53
+ placeholder,
54
+ isDisabled,
55
+ hasFloatingLabel = true,
56
+ isInvalid = false,
57
+ isActive = false,
58
+ infoMessage,
59
+ errorMessage,
60
+ isRequired = false,
61
+ name,
62
+ hasRequiredLabel = false,
63
+ shouldFocusOnMount = false,
64
+ hasCounter = true,
65
+ shouldTrimAfterMaxLength = false,
66
+ isAutoSizeable = true,
67
+ maxLength,
68
+ rows,
69
+ testId,
70
+ data,
71
+ tweakStyles,
72
+ onChange,
73
+ onPaste,
74
+ onFocus,
75
+ onBlur,
76
+ },
77
+ ref,
78
+ ) => {
79
+ const classes = useStyles({ theme: tweakStyles });
80
80
 
81
- const handleOnChange = (event: ChangeEvent<HTMLTextAreaElement>) => {
82
- const newValue = event.currentTarget.value;
83
- onChange(
84
- shouldTrimAfterMaxLength && maxLength !== undefined
85
- ? trimStringToMaxLength(newValue, maxLength)
86
- : newValue,
87
- event,
88
- );
89
- };
81
+ const [isFocused, setFocused] = useState(false);
90
82
 
91
- const handleOnFocus = (event: FocusEvent<HTMLTextAreaElement>) => {
92
- setFocused(true);
93
- if (onFocus !== undefined) {
94
- onFocus(event);
95
- }
96
- };
83
+ const handleOnChange = (event: ChangeEvent<HTMLTextAreaElement>) => {
84
+ const rawValue = event.currentTarget.value;
85
+ const newValue =
86
+ shouldTrimAfterMaxLength && maxLength !== undefined
87
+ ? trimStringToMaxLength(rawValue, maxLength)
88
+ : rawValue;
97
89
 
98
- const handleOnBlur = (event: FocusEvent<HTMLTextAreaElement>) => {
99
- setFocused(false);
100
- if (onBlur !== undefined) {
101
- onBlur(event);
102
- }
103
- };
90
+ onChange(newValue, event);
91
+ };
104
92
 
105
- const hasFocus = isFocused || isActive;
106
- const hasValue = value !== undefined && value !== '';
107
- const hasLabel = label !== undefined && label !== '';
108
- const hasPlaceholder = (!hasLabel || hasFocus) && placeholder !== undefined && placeholder !== '';
93
+ const handleOnFocus = (event: FocusEvent<HTMLTextAreaElement>) => {
94
+ setFocused(true);
95
+ if (onFocus !== undefined) {
96
+ onFocus(event);
97
+ }
98
+ };
109
99
 
110
- const props = {
111
- className: clsx(classes.textarea, hasFloatingLabel && hasLabel && classes.withFloatingLabel),
112
- onFocus: handleOnFocus,
113
- onBlur: handleOnBlur,
114
- onChange: handleOnChange,
115
- value,
116
- onPaste,
117
- disabled: isDisabled,
118
- placeholder: hasPlaceholder ? placeholder : undefined,
119
- name,
120
- autoFocus: shouldFocusOnMount,
121
- 'data-testid': testId,
122
- rows,
123
- };
100
+ const handleOnBlur = (event: FocusEvent<HTMLTextAreaElement>) => {
101
+ setFocused(false);
102
+ if (onBlur !== undefined) {
103
+ onBlur(event);
104
+ }
105
+ };
124
106
 
125
- useEffect(() => {
126
- const { current: textarea } = ref;
127
- if (textarea === null) {
128
- return;
129
- }
130
- // Нужно для того, чтобы TextArea уменьшалась при стирании значения.
131
- textarea.style.height = 'auto';
132
- textarea.style.height = `${textarea.scrollHeight}px`;
133
- }, [value]);
107
+ const hasFocus = isFocused || isActive;
108
+ // в hasValue нельзя использовать isStringNotEmpty из-за того что isStringNotEmpty делает trim
109
+ const hasValue = value !== undefined && value !== '';
110
+ const hasLabel = isReactNodeNotEmpty(label);
111
+ const hasPlaceholder = (!hasLabel || hasFocus) && isStringNotEmpty(placeholder);
134
112
 
135
- const hasInfoMessage = infoMessage !== undefined && infoMessage !== '';
136
- const hasErrorMessage = errorMessage !== undefined && errorMessage !== '';
113
+ const props = {
114
+ className: classes.textarea,
115
+ onFocus: handleOnFocus,
116
+ onBlur: handleOnBlur,
117
+ onChange: handleOnChange,
118
+ value,
119
+ onPaste,
120
+ disabled: isDisabled,
121
+ placeholder: hasPlaceholder ? placeholder : undefined,
122
+ name,
123
+ autoFocus: shouldFocusOnMount,
124
+ rows,
125
+ ...addDataTestId(testId),
126
+ };
137
127
 
138
- return (
139
- <div className={classes.root} {...addDataAttributes(data)}>
140
- <div
141
- className={clsx(classes.wrapper, {
142
- [classes.required]: isRequired && !hasRequiredLabel,
143
- [classes.invalid]: isInvalid,
144
- [classes.focused]: hasFocus,
145
- [classes.disabled]: isDisabled,
146
- })}
147
- >
148
- {label && (
149
- <span
150
- className={clsx(classes.label, {
151
- [classes.invalidLabel]: isInvalid,
152
- [classes.requiredLabel]: hasRequiredLabel && !isRequired,
153
- [classes.activeLabel]: hasFocus || hasValue,
154
- [classes.floating]: hasFloatingLabel,
155
- })}
156
- >
157
- {label}
158
- </span>
159
- )}
160
- <textarea ref={ref} {...props} />
161
- </div>
128
+ const hasInfoMessage = isStringNotEmpty(infoMessage);
129
+ const hasErrorMessage = isStringNotEmpty(errorMessage);
162
130
 
163
- <div className={classes.footer}>
164
- {hasInfoMessage && <div className={classes.info}>{infoMessage}</div>}
165
- {!hasInfoMessage && hasErrorMessage && <div className={classes.error}>{errorMessage}</div>}
166
- {hasCounter && maxLength !== undefined && (
167
- <span
168
- className={clsx(
169
- classes.symbolsCount,
170
- value.length > maxLength && classes.symbolsCountError,
171
- )}
172
- >
173
- {value.length} / {maxLength}
174
- </span>
175
- )}
131
+ return (
132
+ <div className={classes.root} {...addDataAttributes(data)}>
133
+ <div
134
+ className={clsx(classes.wrapper, {
135
+ [classes.required]: isRequired && !hasRequiredLabel,
136
+ [classes.invalid]: isInvalid,
137
+ [classes.focused]: hasFocus,
138
+ [classes.disabled]: isDisabled,
139
+ [classes.autosize]: isAutoSizeable,
140
+ [classes.withFloatingLabel]: hasFloatingLabel && hasLabel,
141
+ })}
142
+ data-value={isAutoSizeable ? value : undefined}
143
+ >
144
+ {hasLabel && (
145
+ <span
146
+ className={clsx(classes.label, {
147
+ [classes.invalidLabel]: isInvalid,
148
+ [classes.requiredLabel]: hasRequiredLabel && !isRequired,
149
+ [classes.activeLabel]: hasFocus || hasValue,
150
+ [classes.floating]: hasFloatingLabel,
151
+ })}
152
+ >
153
+ {label}
154
+ </span>
155
+ )}
156
+
157
+ <textarea ref={ref} {...props} />
158
+ </div>
159
+
160
+ <div className={classes.footer}>
161
+ {hasInfoMessage && <div className={classes.info}>{infoMessage}</div>}
162
+ {!hasInfoMessage && hasErrorMessage && (
163
+ <div className={classes.error}>{errorMessage}</div>
164
+ )}
165
+ {hasCounter && isNotEmpty(maxLength) && (
166
+ <span
167
+ className={clsx(classes.symbolsCount, {
168
+ [classes.symbolsCountError]: value.length > maxLength,
169
+ })}
170
+ >
171
+ {value.length} / {maxLength}
172
+ </span>
173
+ )}
174
+ </div>
175
+ {hasInfoMessage && hasErrorMessage && <div className={classes.error}>{errorMessage}</div>}
176
176
  </div>
177
- {hasInfoMessage && hasErrorMessage && <div className={classes.error}>{errorMessage}</div>}
178
- </div>
179
- );
180
- };
177
+ );
178
+ },
179
+ );
@@ -1,2 +1,3 @@
1
1
  export * from './TextArea';
2
+ export * from './types';
2
3
  export type { ITextAreaStyles } from './TextArea.styles';
@@ -0,0 +1,6 @@
1
+ import { TextareaHTMLAttributes } from 'react';
2
+
3
+ export type ITextAreaHTMLBaseProps = Pick<
4
+ TextareaHTMLAttributes<HTMLTextAreaElement>,
5
+ 'placeholder' | 'name' | 'maxLength' | 'rows' | 'onPaste' | 'onFocus' | 'onBlur'
6
+ >;