Skip to content

Commit a9cf2ab

Browse files
authored
fix(textarea): textarea with autogrow will size to its contents (#24205)
Resolves #24793, #21242
1 parent 0390509 commit a9cf2ab

17 files changed

+56
-34
lines changed

core/src/components.d.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2803,7 +2803,7 @@ export namespace Components {
28032803
}
28042804
interface IonTextarea {
28052805
/**
2806-
* If `true`, the element height will increase based on the value.
2806+
* If `true`, the textarea container will grow and shrink based on the contents of the textarea.
28072807
*/
28082808
"autoGrow": boolean;
28092809
/**
@@ -6786,7 +6786,7 @@ declare namespace LocalJSX {
67866786
}
67876787
interface IonTextarea {
67886788
/**
6789-
* If `true`, the element height will increase based on the value.
6789+
* If `true`, the textarea container will grow and shrink based on the contents of the textarea.
67906790
*/
67916791
"autoGrow"?: boolean;
67926792
/**

core/src/components/textarea/test/autogrow/textarea.e2e.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,9 @@ import { expect } from '@playwright/test';
22
import { test } from '@utils/test/playwright';
33

44
test.describe('textarea: autogrow', () => {
5-
test.skip('should not have visual regressions', async ({ page }) => {
5+
test('should not have visual regressions', async ({ page }) => {
66
await page.goto(`/src/components/textarea/test/autogrow`);
77

8-
await page.waitForChanges();
9-
108
await page.setIonViewport();
119

1210
expect(await page.screenshot()).toMatchSnapshot(`textarea-autogrow-diff-${page.getSnapshotSettings()}.png`);

core/src/components/textarea/test/basic/index.html

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -67,12 +67,6 @@
6767
<ion-label color="primary">Clear on Edit</ion-label>
6868
<ion-textarea clear-on-edit="true"></ion-textarea>
6969
</ion-item>
70-
71-
<!-- TODO: Re-add auto grow with PR#24205 -->
72-
<!-- <ion-item>
73-
<ion-label color="primary">Autogrow</ion-label>
74-
<ion-textarea auto-grow="true"></ion-textarea>
75-
</ion-item> -->
7670
</ion-list>
7771

7872
<div class="ion-text-center">

core/src/components/textarea/textarea.scss

Lines changed: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
--placeholder-color: initial;
2727
--placeholder-font-style: initial;
2828
--placeholder-font-weight: initial;
29-
--placeholder-opacity: .5;
29+
--placeholder-opacity: 0.5;
3030
--padding-top: 0;
3131
--padding-end: 0;
3232
--padding-bottom: 0;
@@ -71,22 +71,41 @@
7171
--padding-start: 0;
7272
}
7373

74-
7574
// Native Textarea
7675
// --------------------------------------------------
7776

7877
.textarea-wrapper {
78+
display: grid;
79+
7980
min-width: inherit;
8081
max-width: inherit;
8182
min-height: inherit;
8283
max-height: inherit;
84+
85+
&::after {
86+
// This technique is used for an auto-resizing textarea.
87+
// The text contents are reflected as a pseudo-element that is visually hidden.
88+
// This causes the textarea container to grow as needed to fit the contents.
89+
90+
white-space: pre-wrap;
91+
92+
content: attr(data-replicated-value) " ";
93+
94+
visibility: hidden;
95+
}
96+
}
97+
98+
.native-textarea,
99+
.textarea-wrapper::after {
100+
@include padding(var(--padding-top), var(--padding-end), var(--padding-bottom), var(--padding-start));
101+
@include text-inherit();
102+
103+
grid-area: 1 / 1 / 2 / 2;
83104
}
84105

85106
.native-textarea {
86107
@include border-radius(var(--border-radius));
87108
@include margin(0);
88-
@include padding(var(--padding-top), var(--padding-end), var(--padding-bottom), var(--padding-start));
89-
@include text-inherit();
90109

91110
display: block;
92111

@@ -103,6 +122,8 @@
103122
resize: none;
104123
appearance: none;
105124

125+
overflow: hidden;
126+
106127
&::placeholder {
107128
@include padding(0);
108129

@@ -117,7 +138,7 @@
117138
}
118139

119140
.native-textarea[disabled] {
120-
opacity: .4;
141+
opacity: 0.4;
121142
}
122143

123144
// Input Cover: Unfocused
@@ -136,6 +157,15 @@
136157
pointer-events: none;
137158
}
138159

160+
:host([auto-grow]) .cloned-input {
161+
// Workaround for webkit rendering issue with scroll assist.
162+
// When cloning the textarea and scrolling into view,
163+
// a white box is rendered from the difference in height
164+
// from the auto grow container.
165+
// This change forces the cloned input to match the true
166+
// height of the textarea container.
167+
height: 100%;
168+
}
139169

140170
// Item Floating: Placeholder
141171
// ----------------------------------------------------------------

core/src/components/textarea/textarea.tsx

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import type { ComponentInterface, EventEmitter } from '@stencil/core';
2-
import { Build, Component, Element, Event, Host, Method, Prop, State, Watch, h, readTask } from '@stencil/core';
2+
import { Build, Component, Element, Event, Host, Method, Prop, State, Watch, h, writeTask } from '@stencil/core';
33

44
import { getIonMode } from '../../global/ionic-global';
55
import type { Color, StyleEventDetail, TextareaChangeEventDetail } from '../../interface';
66
import type { Attributes } from '../../utils/helpers';
7-
import { inheritAriaAttributes, debounceEvent, findItemLabel, inheritAttributes, raf } from '../../utils/helpers';
7+
import { inheritAriaAttributes, debounceEvent, findItemLabel, inheritAttributes } from '../../utils/helpers';
88
import { createColorClasses } from '../../utils/theme';
99

1010
/**
@@ -147,9 +147,10 @@ export class Textarea implements ComponentInterface {
147147
@Prop() wrap?: 'hard' | 'soft' | 'off';
148148

149149
/**
150-
* If `true`, the element height will increase based on the value.
150+
* If `true`, the textarea container will grow and shrink based
151+
* on the contents of the textarea.
151152
*/
152-
@Prop() autoGrow = false;
153+
@Prop({ reflect: true }) autoGrow = false;
153154

154155
/**
155156
* The value of the textarea.
@@ -227,20 +228,7 @@ export class Textarea implements ComponentInterface {
227228
}
228229

229230
componentDidLoad() {
230-
raf(() => this.runAutoGrow());
231-
}
232-
233-
private runAutoGrow() {
234-
const nativeInput = this.nativeInput;
235-
if (nativeInput && this.autoGrow) {
236-
readTask(() => {
237-
nativeInput.style.height = 'auto';
238-
nativeInput.style.height = nativeInput.scrollHeight + 'px';
239-
if (this.textareaWrapper) {
240-
this.textareaWrapper.style.height = nativeInput.scrollHeight + 'px';
241-
}
242-
});
243-
}
231+
this.runAutoGrow();
244232
}
245233

246234
/**
@@ -286,6 +274,18 @@ export class Textarea implements ComponentInterface {
286274
});
287275
}
288276

277+
private runAutoGrow() {
278+
if (this.nativeInput && this.autoGrow) {
279+
writeTask(() => {
280+
if (this.textareaWrapper) {
281+
// Replicated value is an attribute to be used in the stylesheet
282+
// to set the inner contents of a pseudo element.
283+
this.textareaWrapper.dataset.replicatedValue = this.value ?? '';
284+
}
285+
});
286+
}
287+
}
288+
289289
/**
290290
* Check if we need to clear the text input if clearOnEdit is enabled
291291
*/

0 commit comments

Comments
 (0)