Skip to content

Commit 07f99e3

Browse files
authored
Make default textarea grow based on text content unless there is an explicit height (#3379)
1 parent 2411012 commit 07f99e3

File tree

8 files changed

+101
-37
lines changed

8 files changed

+101
-37
lines changed

packages/@adobe/spectrum-css-temp/components/fieldlabel/index.css

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -74,10 +74,6 @@ governing permissions and limitations under the License.
7474
/* If the user overrides the width of the field, propagate to the inner component */
7575
width: 100%;
7676
}
77-
78-
.spectrum-Field-field--multiline {
79-
flex: 1 1 auto;
80-
}
8177
}
8278

8379
/* The side label variant of Field is inline, and fills as much space as needed
@@ -97,6 +93,9 @@ governing permissions and limitations under the License.
9793
* Should default to form field's default width and and allow users to override with custom width. */
9894
width: var(--spectrum-field-default-width);
9995

96+
/* If the user overrides the height of the field, propagate to the inner wrapper element */
97+
height: 100%;
98+
10099
.spectrum-Field-field {
101100
/* If the user overrides the width of the field, propagate to the inner component */
102101
width: 100%;
@@ -107,10 +106,6 @@ governing permissions and limitations under the License.
107106
flex: 1;
108107
min-width: 0;
109108
}
110-
111-
.spectrum-Field-field--multiline {
112-
height: 100%;
113-
}
114109
}
115110
}
116111

packages/@adobe/spectrum-css-temp/components/textfield/index.css

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -35,10 +35,6 @@ governing permissions and limitations under the License.
3535
min-width: var(--spectrum-textfield-min-width);
3636
width: var(--spectrum-component-single-line-width);
3737

38-
&:not(.spectrum-Textfield--quiet).spectrum-Textfield--multiline .spectrum-Textfield-input:not(:disabled) {
39-
resize: vertical;
40-
}
41-
4238
&.spectrum-Textfield--quiet.spectrum-Textfield--multiline .spectrum-Textfield-input {
4339
height: var(--spectrum-textfield-height);
4440
min-height: var(--spectrum-textfield-height);
@@ -164,6 +160,7 @@ governing permissions and limitations under the License.
164160
height: var(--spectrum-textfield-multiline-height);
165161
min-height: var(--spectrum-textfield-multiline-min-height);
166162
padding: var(--spectrum-textfield-multiline-padding-top) var(--spectrum-textfield-multiline-padding-x) var(--spectrum-textfield-multiline-padding-bottom) calc(var(--spectrum-textfield-multiline-padding-x) - 1px);
163+
resize: none;
167164

168165
/* Remove the default vertical scrollbar for textarea in IE. */
169166
overflow: auto;
@@ -176,10 +173,6 @@ governing permissions and limitations under the License.
176173
/* removes the side padding to align the text properly */
177174
padding-inline-start: var(--spectrum-textfield-quiet-padding-x);
178175
padding-inline-end: var(--spectrum-textfield-quiet-padding-x);
179-
180-
/* Treat all quiet inputs and textareas the same */
181-
resize: none;
182-
overflow-y: hidden;
183176
}
184177

185178
.spectrum-Textfield--valid & {
@@ -195,6 +188,24 @@ governing permissions and limitations under the License.
195188
}
196189
}
197190

191+
.spectrum-Textfield-wrapper .spectrum-Textfield--multiline {
192+
/* when textarea is inside a label wrapper, make it flex to take up remaining height. */
193+
flex: 1 1 auto;
194+
min-height: 0;
195+
196+
&.spectrum-Textfield--quiet {
197+
/* quiet textfields grow based on text content, up to the height of the container. */
198+
/* but we need to keep help text next to input */
199+
flex: 0 1 auto;
200+
max-height: 100%;
201+
202+
.spectrum-Textfield-input {
203+
/* make textarea scroll when reaching height of container */
204+
max-height: 100%;
205+
}
206+
}
207+
}
208+
198209
.spectrum-Textfield-validationIcon {
199210
/* TODO: Confirm if this is ok for the validation icon sizing
200211
Note that the sizes are a bit different when compared with old background icons(more noticable for checkmark)

packages/@react-spectrum/label/src/Field.tsx

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -79,12 +79,23 @@ function Field(props: SpectrumFieldProps, ref: RefObject<HTMLElement>) {
7979
showErrorIcon={showErrorIcon} />
8080
);
8181

82-
let renderChildren = () => (
83-
<Flex direction="column" UNSAFE_className={classNames(labelStyles, 'spectrum-Field-wrapper')}>
84-
{children}
85-
{hasHelpText && renderHelpText()}
86-
</Flex>
87-
);
82+
let renderChildren = () => {
83+
if (labelPosition === 'side') {
84+
return (
85+
<Flex direction="column" UNSAFE_className={classNames(labelStyles, 'spectrum-Field-wrapper')}>
86+
{children}
87+
{hasHelpText && renderHelpText()}
88+
</Flex>
89+
);
90+
}
91+
92+
return (
93+
<>
94+
{children}
95+
{hasHelpText && renderHelpText()}
96+
</>
97+
);
98+
};
8899

89100
return (
90101
<div

packages/@react-spectrum/label/test/Field.test.js

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,16 @@ describe('Field', function () {
5252
let {getByRole} = renderField({ref});
5353
let field = getByRole('textbox').closest('div');
5454

55-
expect(ref.current).toBe(field.parentNode);
55+
expect(ref.current).toBe(field);
56+
});
57+
describe('labelPosition: side', function () {
58+
it('supports a ref', function () {
59+
let ref = React.createRef();
60+
let {getByRole} = renderField({ref, labelPosition: 'side'});
61+
let field = getByRole('textbox').closest('div');
62+
63+
expect(ref.current).toBe(field.parentNode);
64+
});
5665
});
5766
describe('help text', function () {
5867
describe('description', function () {

packages/@react-spectrum/textfield/src/TextArea.tsx

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,12 +35,18 @@ function TextArea(props: SpectrumTextFieldProps, ref: RefObject<TextFieldRef>) {
3535
let inputRef = useRef<HTMLTextAreaElement>();
3636

3737
let onHeightChange = useCallback(() => {
38-
if (isQuiet) {
38+
// Quiet textareas always grow based on their text content.
39+
// Standard textareas also grow by default, unless an explicit height is set.
40+
if (isQuiet || !props.height) {
3941
let input = inputRef.current;
4042
let prevAlignment = input.style.alignSelf;
43+
let prevOverflow = input.style.overflow;
4144
input.style.alignSelf = 'start';
45+
input.style.overflow = 'hidden';
4246
input.style.height = 'auto';
43-
input.style.height = `${input.scrollHeight}px`;
47+
// offsetHeight - clientHeight accounts for the border/padding.
48+
input.style.height = `${input.scrollHeight + (input.offsetHeight - input.clientHeight)}px`;
49+
input.style.overflow = prevOverflow;
4450
input.style.alignSelf = prevAlignment;
4551
}
4652
}, [isQuiet, inputRef]);

packages/@react-spectrum/textfield/src/TextFieldBase.tsx

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,6 @@ interface TextFieldBaseProps extends Omit<SpectrumTextFieldProps, 'onChange'>, P
3838

3939
function TextFieldBase(props: TextFieldBaseProps, ref: Ref<TextFieldRef>) {
4040
let {
41-
label,
4241
validationState,
4342
icon,
4443
isQuiet = false,
@@ -138,18 +137,13 @@ function TextFieldBase(props: TextFieldBaseProps, ref: Ref<TextFieldRef>) {
138137
</div>
139138
);
140139

141-
if (label) {
142-
textField = React.cloneElement(textField, mergeProps(textField.props, {
143-
className: multiLine ? 'spectrum-Field-field--multiline' : ''
144-
}));
145-
}
146-
147140
return (
148141
<Field
149142
{...props}
150143
labelProps={labelProps}
151144
descriptionProps={descriptionProps}
152145
errorMessageProps={errorMessageProps}
146+
wrapperClassName={classNames(styles, 'spectrum-Textfield-wrapper')}
153147
showErrorIcon={false}
154148
ref={domRef}>
155149
{textField}

packages/@react-spectrum/textfield/stories/TextArea.stories.js

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -136,13 +136,23 @@ storiesOf('TextArea', module)
136136
'custom height with label',
137137
() => (
138138
<Form>
139-
<TextArea label="Height size-2000" height="size-2000" />
140-
<TextArea label="Height size-2000" height="size-2000" isQuiet />
141-
<TextArea labelPosition="side" label="Height size-2000" height="size-2000" />
142-
<TextArea labelPosition="side" label="Height size-2000" height="size-2000" isQuiet />
139+
<TextArea label="Custom height" description="height: size-2000" height="size-2000" />
140+
<TextArea label="Custom height" description="height: size-2000" height="size-2000" isQuiet />
141+
<TextArea labelPosition="side" label="Custom height" description="height: size-2000" height="size-2000" />
142+
<TextArea labelPosition="side" label="Custom height" description="height: size-2000" height="size-2000" isQuiet />
143143
</Form>
144144
)
145145
)
146+
.add(
147+
'changeable helptext',
148+
() => <ValidationExample />,
149+
{description: {data: 'Verify that the changing size of the error message does not interfere with the height. To test, delete the input, then type the character "a". Height should update to match.'}}
150+
)
151+
.add(
152+
'changeable helptext custom height',
153+
() => <ValidationExample height="175px" minHeight="100px" maxHeight="50vh" />,
154+
{description: {data: 'Verify that the changing size of the error message does not interfere with the height. To test, delete the input, then type the character "a". Height should update to match.'}}
155+
)
146156
.add('controlled interactive',
147157
() => <ControlledTextArea />
148158
)
@@ -219,3 +229,20 @@ function renderInFlexRowAndBlock(props = {}) {
219229
</Flex>
220230
);
221231
}
232+
233+
function ValidationExample(props) {
234+
let [value, setValue] = React.useState('0');
235+
let isValid = React.useMemo(() => /^\d$/.test(value), [value]);
236+
237+
return (
238+
<TextArea
239+
{...props}
240+
validationState={isValid ? 'valid' : 'invalid'}
241+
value={value}
242+
onChange={setValue}
243+
label="Favorite number"
244+
maxLength={1}
245+
description="Enter a single digit number."
246+
errorMessage={value === '' ? 'Empty input not allowed.' : 'Single digit numbers are 0-9. Lorem ipsum dolor.'} />
247+
);
248+
}

packages/@react-spectrum/textfield/test/TextArea.test.js

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,9 +65,20 @@ describe('TextArea', () => {
6565
expect(input.style.height).toBe(`${newScrollHeight}px`);
6666
});
6767

68-
it('default does not change height', () => {
68+
it('default can adjust after text "grows"', () => {
6969
let tree = renderComponent(TextArea, {});
7070
let input = tree.getByTestId(testId);
71+
let newScrollHeight = 1000;
72+
expect(input.style.height).toBe(`${mockScrollHeight}px`);
73+
// this will be cleaned up in the afterEach
74+
Object.defineProperty(HTMLElement.prototype, 'scrollHeight', {configurable: true, value: newScrollHeight});
75+
typeText(input, '15');
76+
expect(input.style.height).toBe(`${newScrollHeight}px`);
77+
});
78+
79+
it('default does not change height when a height prop is set', () => {
80+
let tree = renderComponent(TextArea, {height: 'size-2000'});
81+
let input = tree.getByTestId(testId);
7182
expect(input.style.height).toBe('');
7283
typeText(input, '15');
7384
expect(input.style.height).toBe('');

0 commit comments

Comments
 (0)