Skip to content

Commit d3cf4a1

Browse files
vasilev-alextenphi
andauthored
feat: max visible rows option to autosizing textarea (#645)
Co-authored-by: Andrey Yamanov <[email protected]>
1 parent 2dbc001 commit d3cf4a1

File tree

4 files changed

+91
-33
lines changed

4 files changed

+91
-33
lines changed

.changeset/brave-chefs-dance.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@cube-dev/ui-kit': patch
3+
---
4+
5+
Add maxRows prop to TextArea in control maximum visible rows in auto-size mode.

.changeset/silver-pans-shake.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@cube-dev/ui-kit': patch
3+
---
4+
5+
Improved calculation of height in auto-sized TextArea.

src/components/fields/TextArea/TextArea.stories.tsx

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,4 +40,12 @@ export const Password = Template.bind({});
4040
Password.args = { icon: true, type: 'password', defaultValue: 'hidden value' };
4141

4242
export const AutoSize = Template.bind({});
43-
AutoSize.args = { autoSize: true, defaultValue: '1\n2\n3\n4', rows: 1 };
43+
AutoSize.args = { autoSize: true, defaultValue: '1\n2\n3\n4', rows: 2 };
44+
45+
export const AutoSizeMaxRows = Template.bind({});
46+
AutoSizeMaxRows.args = {
47+
autoSize: true,
48+
maxRows: 3,
49+
defaultValue: '1\n2\n3\n4',
50+
rows: 2,
51+
};

src/components/fields/TextArea/TextArea.tsx

Lines changed: 72 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,24 @@
1-
import { forwardRef, useRef } from 'react';
1+
import { forwardRef, useEffect, useLayoutEffect, useRef } from 'react';
22
import { useControlledState } from '@react-stately/utils';
33
import { useTextField } from 'react-aria';
44

5+
import { useEvent } from '../../../_internal/index';
56
import { CubeTextInputBaseProps, TextInputBase } from '../TextInput';
67
import { useProviderProps } from '../../../provider';
78
import {
89
castNullableStringValue,
910
WithNullableValue,
1011
} from '../../../utils/react/nullableValue';
11-
import { chain, useLayoutEffect } from '../../../utils/react';
12+
import { chain } from '../../../utils/react';
1213
import { useFieldProps } from '../../form';
13-
import { useEvent } from '../../../_internal';
1414

1515
export interface CubeTextAreaProps extends CubeTextInputBaseProps {
16-
/** Whether the textarea should change its size depends on content */
16+
/** Whether the textarea should change its size depends on the content */
1717
autoSize?: boolean;
18-
/** The rows attribute in HTML is used to specify the number of visible text lines for the
19-
* control i.e. the number of rows to display. */
18+
/** Max number of visible rows when autoSize is `true`. Defaults to 10 */
19+
maxRows?: number;
20+
/** The `rows` attribute in HTML is used to specify the number of visible text lines for the
21+
* control i.e. the number of rows to display. Defaults to 3 */
2022
rows?: number;
2123
}
2224

@@ -36,42 +38,21 @@ function TextArea(props: WithNullableValue<CubeTextAreaProps>, ref) {
3638
isReadOnly = false,
3739
isRequired = false,
3840
onChange,
41+
maxRows = 10,
3942
rows = 3,
4043
...otherProps
4144
} = props;
4245

43-
let [inputValue, setInputValue] = useControlledState(
46+
rows = Math.max(rows, 1);
47+
maxRows = Math.max(maxRows, rows);
48+
49+
let [inputValue, setInputValue] = useControlledState<string>(
4450
props.value,
4551
props.defaultValue,
4652
() => {},
4753
);
4854
let inputRef = useRef<HTMLTextAreaElement>(null);
4955

50-
let onHeightChange = useEvent(() => {
51-
if (autoSize && inputRef.current) {
52-
let input = inputRef.current;
53-
let prevAlignment = input.style.alignSelf;
54-
let computedStyle = getComputedStyle(input);
55-
input.style.alignSelf = 'start';
56-
input.style.height = 'auto';
57-
input.style.height = input.scrollHeight
58-
? `calc(${input.scrollHeight}px + (2 * var(--border-width)))`
59-
: `${
60-
parseFloat(computedStyle.paddingTop) +
61-
parseFloat(computedStyle.paddingBottom) +
62-
parseFloat(computedStyle.lineHeight) * (rows || 3) +
63-
2
64-
}px`;
65-
input.style.alignSelf = prevAlignment;
66-
}
67-
});
68-
69-
useLayoutEffect(() => {
70-
if (inputRef.current) {
71-
onHeightChange();
72-
}
73-
}, [inputValue, inputRef.current]);
74-
7556
let { labelProps, inputProps } = useTextField(
7657
{
7758
...props,
@@ -81,6 +62,65 @@ function TextArea(props: WithNullableValue<CubeTextAreaProps>, ref) {
8162
inputRef,
8263
);
8364

65+
const adjustHeight = useEvent(() => {
66+
const textarea = inputRef.current;
67+
68+
if (!textarea || !autoSize) return;
69+
70+
// Reset height to get the correct scrollHeight
71+
textarea.style.height = 'auto';
72+
73+
// Get computed styles to account for padding
74+
const computedStyle = getComputedStyle(textarea);
75+
const paddingTop = parseFloat(computedStyle.paddingTop) || 0;
76+
const paddingBottom = parseFloat(computedStyle.paddingBottom) || 0;
77+
const borderTop = parseFloat(computedStyle.borderTopWidth) || 0;
78+
const borderBottom = parseFloat(computedStyle.borderBottomWidth) || 0;
79+
80+
// Calculate line height (approximately)
81+
const lineHeight = parseInt(computedStyle.lineHeight) || 20;
82+
83+
// Calculate content height (excluding padding and border)
84+
const contentHeight = textarea.scrollHeight - paddingTop - paddingBottom;
85+
86+
// Calculate rows based on content height
87+
const computedRows = Math.ceil(contentHeight / lineHeight);
88+
89+
// Apply min/max constraints
90+
const targetRows = Math.max(Math.min(computedRows, maxRows), rows);
91+
92+
// Set the height including padding and border
93+
const totalHeight =
94+
targetRows * lineHeight +
95+
paddingTop +
96+
paddingBottom +
97+
borderTop +
98+
borderBottom;
99+
100+
textarea.style.height = `${totalHeight}px`;
101+
});
102+
103+
const useEnvironmentalEffect =
104+
typeof window !== 'undefined' ? useLayoutEffect : useEffect;
105+
106+
// Call adjustHeight on content change
107+
useEnvironmentalEffect(() => {
108+
adjustHeight();
109+
}, [inputValue]);
110+
111+
// Also call it on element resize as that can affect wrapping
112+
useEnvironmentalEffect(() => {
113+
if (!autoSize || !inputRef.current) return;
114+
115+
adjustHeight();
116+
117+
const resizeObserver = new ResizeObserver(adjustHeight);
118+
119+
resizeObserver.observe(inputRef?.current);
120+
121+
return () => resizeObserver.disconnect();
122+
}, [autoSize, inputRef?.current]);
123+
84124
return (
85125
<TextInputBase
86126
ref={ref}

0 commit comments

Comments
 (0)