Skip to content

Commit 20ada84

Browse files
committed
feat(TextArea): add maxRows prop
1 parent 15806e0 commit 20ada84

File tree

4 files changed

+86
-45
lines changed

4 files changed

+86
-45
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: 67 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,24 @@
1-
import { forwardRef, useRef } from 'react';
1+
import { forwardRef, 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-
/** Max number of visible rows when autoSize is `true` */
19-
maxVisibleRows?: 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. */
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 */
2222
rows?: number;
2323
}
2424

@@ -38,62 +38,85 @@ function TextArea(props: WithNullableValue<CubeTextAreaProps>, ref) {
3838
isReadOnly = false,
3939
isRequired = false,
4040
onChange,
41-
maxVisibleRows,
41+
maxRows = 10,
4242
rows = 3,
4343
...otherProps
4444
} = props;
4545

46-
let [inputValue, setInputValue] = useControlledState(
46+
rows = Math.max(rows, 1);
47+
maxRows = Math.max(maxRows, rows);
48+
49+
let [inputValue, setInputValue] = useControlledState<string>(
4750
props.value,
4851
props.defaultValue,
4952
() => {},
5053
);
5154
let inputRef = useRef<HTMLTextAreaElement>(null);
5255

53-
let onHeightChange = useEvent(() => {
54-
if (autoSize && inputRef.current) {
55-
let input = inputRef.current;
56-
const prevAlignment = input.style.alignSelf;
57-
const computedStyle = getComputedStyle(input);
56+
let { labelProps, inputProps } = useTextField(
57+
{
58+
...props,
59+
onChange: chain(onChange, setInputValue),
60+
inputElementType: 'textarea',
61+
},
62+
inputRef,
63+
);
64+
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;
5879

59-
const lineHeight = parseFloat(computedStyle.lineHeight);
60-
const paddingTop = parseFloat(computedStyle.paddingTop);
61-
const paddingBottom = parseFloat(computedStyle.paddingBottom);
62-
const borderTop = parseFloat(computedStyle.borderTopWidth);
63-
const borderBottom = parseFloat(computedStyle.borderBottomWidth);
80+
// Calculate line height (approximately)
81+
const lineHeight = parseInt(computedStyle.lineHeight) || 20;
6482

65-
input.style.alignSelf = 'start';
66-
input.style.height = 'auto';
83+
// Calculate content height (excluding padding and border)
84+
const contentHeight = textarea.scrollHeight - paddingTop - paddingBottom;
6785

68-
const scrollHeight = input.scrollHeight;
69-
input.style.height = `calc(${scrollHeight}px + (2 * var(--border-width)))`;
86+
// Calculate rows based on content height
87+
const computedRows = Math.ceil(contentHeight / lineHeight);
7088

71-
input.style.alignSelf = prevAlignment;
89+
// Apply min/max constraints
90+
const targetRows = Math.max(Math.min(computedRows, maxRows), rows);
7291

73-
const contentHeight =
74-
scrollHeight - paddingTop - paddingBottom - borderTop - borderBottom;
75-
const rowCount = Math.round(contentHeight / lineHeight);
92+
// Set the height including padding and border
93+
const totalHeight =
94+
targetRows * lineHeight +
95+
paddingTop +
96+
paddingBottom +
97+
borderTop +
98+
borderBottom;
7699

77-
if (autoSize && maxVisibleRows != null && rowCount > maxVisibleRows) {
78-
input.style.height = `${maxVisibleRows * lineHeight + paddingTop + paddingBottom}px`;
79-
}
80-
}
100+
textarea.style.height = `${totalHeight}px`;
81101
});
82102

103+
// Call adjustHeight on content change
83104
useLayoutEffect(() => {
84-
if (inputRef.current) {
85-
onHeightChange();
86-
}
87-
}, [inputValue, inputRef.current]);
105+
adjustHeight();
106+
}, [inputValue]);
88107

89-
let { labelProps, inputProps } = useTextField(
90-
{
91-
...props,
92-
onChange: chain(onChange, setInputValue),
93-
inputElementType: 'textarea',
94-
},
95-
inputRef,
96-
);
108+
// Also call it on window resize as that can affect wrapping
109+
useLayoutEffect(() => {
110+
if (!autoSize) return;
111+
112+
const handleResize = () => {
113+
adjustHeight();
114+
};
115+
116+
window.addEventListener('resize', handleResize);
117+
118+
return () => window.removeEventListener('resize', handleResize);
119+
}, [adjustHeight, autoSize]);
97120

98121
return (
99122
<TextInputBase

0 commit comments

Comments
 (0)