@true-engineering/true-react-common-ui-kit 3.14.2 → 3.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.
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.0",
4
4
  "description": "True Engineering React UI Kit with theming support",
5
5
  "author": "True Engineering (https://trueengineering.ru)",
6
6
  "keywords": [
@@ -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
+ >;