fix(textarea): textarea with autogrow will size to its contents (#24205)

Resolves #24793, #21242
This commit is contained in:
Sean Perkins
2022-07-27 11:36:48 -04:00
committed by GitHub
parent 0390509919
commit a9cf2ab870
17 changed files with 56 additions and 34 deletions

View File

@ -2803,7 +2803,7 @@ export namespace Components {
} }
interface IonTextarea { interface IonTextarea {
/** /**
* If `true`, the element height will increase based on the value. * If `true`, the textarea container will grow and shrink based on the contents of the textarea.
*/ */
"autoGrow": boolean; "autoGrow": boolean;
/** /**
@ -6786,7 +6786,7 @@ declare namespace LocalJSX {
} }
interface IonTextarea { interface IonTextarea {
/** /**
* If `true`, the element height will increase based on the value. * If `true`, the textarea container will grow and shrink based on the contents of the textarea.
*/ */
"autoGrow"?: boolean; "autoGrow"?: boolean;
/** /**

View File

@ -2,11 +2,9 @@ import { expect } from '@playwright/test';
import { test } from '@utils/test/playwright'; import { test } from '@utils/test/playwright';
test.describe('textarea: autogrow', () => { test.describe('textarea: autogrow', () => {
test.skip('should not have visual regressions', async ({ page }) => { test('should not have visual regressions', async ({ page }) => {
await page.goto(`/src/components/textarea/test/autogrow`); await page.goto(`/src/components/textarea/test/autogrow`);
await page.waitForChanges();
await page.setIonViewport(); await page.setIonViewport();
expect(await page.screenshot()).toMatchSnapshot(`textarea-autogrow-diff-${page.getSnapshotSettings()}.png`); expect(await page.screenshot()).toMatchSnapshot(`textarea-autogrow-diff-${page.getSnapshotSettings()}.png`);

View File

@ -67,12 +67,6 @@
<ion-label color="primary">Clear on Edit</ion-label> <ion-label color="primary">Clear on Edit</ion-label>
<ion-textarea clear-on-edit="true"></ion-textarea> <ion-textarea clear-on-edit="true"></ion-textarea>
</ion-item> </ion-item>
<!-- TODO: Re-add auto grow with PR#24205 -->
<!-- <ion-item>
<ion-label color="primary">Autogrow</ion-label>
<ion-textarea auto-grow="true"></ion-textarea>
</ion-item> -->
</ion-list> </ion-list>
<div class="ion-text-center"> <div class="ion-text-center">

View File

@ -26,7 +26,7 @@
--placeholder-color: initial; --placeholder-color: initial;
--placeholder-font-style: initial; --placeholder-font-style: initial;
--placeholder-font-weight: initial; --placeholder-font-weight: initial;
--placeholder-opacity: .5; --placeholder-opacity: 0.5;
--padding-top: 0; --padding-top: 0;
--padding-end: 0; --padding-end: 0;
--padding-bottom: 0; --padding-bottom: 0;
@ -71,22 +71,41 @@
--padding-start: 0; --padding-start: 0;
} }
// Native Textarea // Native Textarea
// -------------------------------------------------- // --------------------------------------------------
.textarea-wrapper { .textarea-wrapper {
display: grid;
min-width: inherit; min-width: inherit;
max-width: inherit; max-width: inherit;
min-height: inherit; min-height: inherit;
max-height: inherit; max-height: inherit;
&::after {
// This technique is used for an auto-resizing textarea.
// The text contents are reflected as a pseudo-element that is visually hidden.
// This causes the textarea container to grow as needed to fit the contents.
white-space: pre-wrap;
content: attr(data-replicated-value) " ";
visibility: hidden;
}
}
.native-textarea,
.textarea-wrapper::after {
@include padding(var(--padding-top), var(--padding-end), var(--padding-bottom), var(--padding-start));
@include text-inherit();
grid-area: 1 / 1 / 2 / 2;
} }
.native-textarea { .native-textarea {
@include border-radius(var(--border-radius)); @include border-radius(var(--border-radius));
@include margin(0); @include margin(0);
@include padding(var(--padding-top), var(--padding-end), var(--padding-bottom), var(--padding-start));
@include text-inherit();
display: block; display: block;
@ -103,6 +122,8 @@
resize: none; resize: none;
appearance: none; appearance: none;
overflow: hidden;
&::placeholder { &::placeholder {
@include padding(0); @include padding(0);
@ -117,7 +138,7 @@
} }
.native-textarea[disabled] { .native-textarea[disabled] {
opacity: .4; opacity: 0.4;
} }
// Input Cover: Unfocused // Input Cover: Unfocused
@ -136,6 +157,15 @@
pointer-events: none; pointer-events: none;
} }
:host([auto-grow]) .cloned-input {
// Workaround for webkit rendering issue with scroll assist.
// When cloning the textarea and scrolling into view,
// a white box is rendered from the difference in height
// from the auto grow container.
// This change forces the cloned input to match the true
// height of the textarea container.
height: 100%;
}
// Item Floating: Placeholder // Item Floating: Placeholder
// ---------------------------------------------------------------- // ----------------------------------------------------------------

View File

@ -1,10 +1,10 @@
import type { ComponentInterface, EventEmitter } from '@stencil/core'; import type { ComponentInterface, EventEmitter } from '@stencil/core';
import { Build, Component, Element, Event, Host, Method, Prop, State, Watch, h, readTask } from '@stencil/core'; import { Build, Component, Element, Event, Host, Method, Prop, State, Watch, h, writeTask } from '@stencil/core';
import { getIonMode } from '../../global/ionic-global'; import { getIonMode } from '../../global/ionic-global';
import type { Color, StyleEventDetail, TextareaChangeEventDetail } from '../../interface'; import type { Color, StyleEventDetail, TextareaChangeEventDetail } from '../../interface';
import type { Attributes } from '../../utils/helpers'; import type { Attributes } from '../../utils/helpers';
import { inheritAriaAttributes, debounceEvent, findItemLabel, inheritAttributes, raf } from '../../utils/helpers'; import { inheritAriaAttributes, debounceEvent, findItemLabel, inheritAttributes } from '../../utils/helpers';
import { createColorClasses } from '../../utils/theme'; import { createColorClasses } from '../../utils/theme';
/** /**
@ -147,9 +147,10 @@ export class Textarea implements ComponentInterface {
@Prop() wrap?: 'hard' | 'soft' | 'off'; @Prop() wrap?: 'hard' | 'soft' | 'off';
/** /**
* If `true`, the element height will increase based on the value. * If `true`, the textarea container will grow and shrink based
* on the contents of the textarea.
*/ */
@Prop() autoGrow = false; @Prop({ reflect: true }) autoGrow = false;
/** /**
* The value of the textarea. * The value of the textarea.
@ -227,20 +228,7 @@ export class Textarea implements ComponentInterface {
} }
componentDidLoad() { componentDidLoad() {
raf(() => this.runAutoGrow()); this.runAutoGrow();
}
private runAutoGrow() {
const nativeInput = this.nativeInput;
if (nativeInput && this.autoGrow) {
readTask(() => {
nativeInput.style.height = 'auto';
nativeInput.style.height = nativeInput.scrollHeight + 'px';
if (this.textareaWrapper) {
this.textareaWrapper.style.height = nativeInput.scrollHeight + 'px';
}
});
}
} }
/** /**
@ -286,6 +274,18 @@ export class Textarea implements ComponentInterface {
}); });
} }
private runAutoGrow() {
if (this.nativeInput && this.autoGrow) {
writeTask(() => {
if (this.textareaWrapper) {
// Replicated value is an attribute to be used in the stylesheet
// to set the inner contents of a pseudo element.
this.textareaWrapper.dataset.replicatedValue = this.value ?? '';
}
});
}
}
/** /**
* Check if we need to clear the text input if clearOnEdit is enabled * Check if we need to clear the text input if clearOnEdit is enabled
*/ */