-
Notifications
You must be signed in to change notification settings - Fork 7
Expand file tree
/
Copy pathTextArea.tsx
More file actions
166 lines (138 loc) · 4.7 KB
/
TextArea.tsx
File metadata and controls
166 lines (138 loc) · 4.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
import {
ForwardedRef,
forwardRef,
useEffect,
useLayoutEffect,
useRef,
} from 'react';
import { useTextField } from 'react-aria';
import { useEvent } from '../../../_internal/index';
import { useProviderProps } from '../../../provider';
import { chain, mergeProps } from '../../../utils/react';
import {
castNullableStringValue,
WithNullableValue,
} from '../../../utils/react/nullableValue';
import { useFieldProps } from '../../form';
import { CubeTextInputBaseProps, TextInputBase } from '../TextInput';
export interface CubeTextAreaProps extends CubeTextInputBaseProps {
/** Whether the textarea should change its size depends on the content */
autoSize?: boolean;
/** Max number of visible rows when autoSize is `true`. Defaults to 10 */
maxRows?: number;
/** The `rows` attribute in HTML is used to specify the number of visible text lines for the
* control i.e. the number of rows to display. Defaults to 3 */
rows?: number;
}
function TextArea(
props: WithNullableValue<CubeTextAreaProps>,
ref: ForwardedRef<HTMLElement>,
) {
props = castNullableStringValue(props);
props = useProviderProps(props);
props = useFieldProps(props, {
defaultValidationTrigger: 'onBlur',
valuePropsMapper: ({ value, onChange }) => ({
onChange,
value: value?.toString() ?? '',
}),
});
let {
autoSize = false,
isDisabled = false,
isReadOnly = false,
isRequired = false,
onChange,
maxRows = 10,
rows = 3,
labelProps: userLabelProps,
inputRef: propsInputRef,
value,
...otherProps
} = props;
rows = Math.max(rows, 1);
maxRows = Math.max(maxRows, rows);
let localInputRef = useRef<HTMLTextAreaElement>(null);
let inputRef = propsInputRef ?? localInputRef;
const adjustHeight = useEvent(() => {
const textarea = inputRef.current;
if (!textarea || !autoSize) return;
// Reset height to get the correct scrollHeight
textarea.style.height = 'auto';
// Get computed styles to account for padding
const computedStyle = getComputedStyle(textarea);
const paddingTop = parseFloat(computedStyle.paddingTop) || 0;
const paddingBottom = parseFloat(computedStyle.paddingBottom) || 0;
const borderTop = parseFloat(computedStyle.borderTopWidth) || 0;
const borderBottom = parseFloat(computedStyle.borderBottomWidth) || 0;
// Calculate line height (approximately)
const lineHeight = parseInt(computedStyle.lineHeight) || 20;
// Calculate content height (excluding padding and border)
const contentHeight = textarea.scrollHeight - paddingTop - paddingBottom;
// Calculate rows based on content height
const computedRows = Math.ceil(contentHeight / lineHeight);
// Apply min/max constraints
const targetRows = Math.max(Math.min(computedRows, maxRows), rows);
// Set the height including padding and border
const totalHeight =
targetRows * lineHeight +
paddingTop +
paddingBottom +
borderTop +
borderBottom;
textarea.style.height = `${totalHeight}px`;
});
let { labelProps, inputProps } = useTextField(
{
...otherProps,
value,
isDisabled,
isReadOnly,
isRequired,
onChange: chain(onChange, adjustHeight),
inputElementType: 'textarea',
},
inputRef,
);
// Merge user-provided labelProps with aria labelProps
const mergedLabelProps = mergeProps(labelProps, userLabelProps);
const useEnvironmentalEffect =
typeof window !== 'undefined' ? useLayoutEffect : useEffect;
// Also call adjustHeight on element resize as that can affect wrapping
useEnvironmentalEffect(() => {
if (!autoSize || !inputRef.current) return;
adjustHeight();
const resizeObserver = new ResizeObserver(adjustHeight);
resizeObserver.observe(inputRef?.current);
return () => resizeObserver.disconnect();
}, [autoSize, inputRef?.current]);
// Adjust height when value changes programmatically (controlled mode with autoSize)
useEnvironmentalEffect(() => {
if (autoSize && inputRef.current) {
adjustHeight();
}
}, [value]);
return (
<TextInputBase
ref={ref}
{...otherProps}
multiLine
inputRef={inputRef}
labelProps={mergedLabelProps}
inputProps={{ ...inputProps, 'data-input-type': 'textarea' }}
isDisabled={isDisabled}
isReadOnly={isReadOnly}
isRequired={isRequired}
rows={rows}
/>
);
}
/**
* TextInputs are text inputs that allow users to input custom text entries
* with a keyboard. Various decorations can be displayed around the field to
* communicate the entry requirements.
*/
const _TextArea = forwardRef(TextArea);
(_TextArea as any).cubeInputType = 'Text';
_TextArea.displayName = 'TextArea';
export { _TextArea as TextArea };