fix(textarea): textarea with autogrow will size to its contents (#24205)
Resolves #24793, #21242
4
core/src/components.d.ts
vendored
@ -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;
|
||||||
/**
|
/**
|
||||||
|
@ -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`);
|
||||||
|
After Width: | Height: | Size: 411 KiB |
After Width: | Height: | Size: 117 KiB |
After Width: | Height: | Size: 298 KiB |
After Width: | Height: | Size: 411 KiB |
After Width: | Height: | Size: 118 KiB |
After Width: | Height: | Size: 298 KiB |
After Width: | Height: | Size: 374 KiB |
After Width: | Height: | Size: 81 KiB |
After Width: | Height: | Size: 252 KiB |
After Width: | Height: | Size: 375 KiB |
After Width: | Height: | Size: 81 KiB |
After Width: | Height: | Size: 252 KiB |
@ -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">
|
||||||
|
@ -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
|
||||||
// ----------------------------------------------------------------
|
// ----------------------------------------------------------------
|
||||||
|
@ -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
|
||||||
*/
|
*/
|
||||||
|