refactor(textarea): remove legacy property and support for legacy syntax (#28993)
Issue number: internal --------- ## What is the current behavior? In Ionic Framework v7, we [simplified the textarea syntax](https://ionic.io/blog/ionic-7-is-here#simplified-form-control-syntax) so that it was no longer required to be placed inside of an `ion-item`. We maintained backwards compatibility by adding a `legacy` property which allowed it to continue to be styled properly when written in the following way: ```html <ion-item> <ion-label>Label</ion-label> <ion-textarea></ion-textarea> </ion-item> ``` While this was supported in v7, console warnings were logged to notify developers that they needed to update this syntax for the best accessibility experience. ## What is the new behavior? - Removes the `legacy` property and support for the legacy syntax. Developers should follow the [migration guide](https://ionicframework.com/docs/api/textarea#migrating-from-legacy-textarea-syntax) in the textarea documentation to update their apps. The new syntax requires a `label` or `aria-label` on `ion-textarea`: ```html <ion-item> <ion-textarea label="Label"></ion-textarea> </ion-item> ``` - Removes the legacy tests under `textarea/test/legacy/` and all related screenshots - Removes the textarea usage in `input/test/legacy/spec`, `item/test/disabled`, `item/test/legacy/disabled` and `item/test/legacy/fill` ## Does this introduce a breaking change? - [x] Yes - [ ] No 1. Developers have had console warnings when using the legacy syntax since the v7 release. The migration guide for the new textarea syntax is outlined in the [Textarea documentation](https://ionicframework.com/docs/api/textarea#migrating-from-legacy-textarea-syntax). 2. This change has been documented in the Breaking Changes document with a link to the migration guide. BREAKING CHANGE: The `legacy` property and support for the legacy syntax, which involved placing an `ion-textarea` inside of an `ion-item` with an `ion-label`, have been removed from textarea. For more information on migrating from the legacy textarea syntax, refer to the [Textarea documentation](https://ionicframework.com/docs/api/textarea#migrating-from-legacy-textarea-syntax). --------- Co-authored-by: ionitron <hi@ionicframework.com>
@ -1,38 +0,0 @@
|
||||
import { expect } from '@playwright/test';
|
||||
import { configs, test } from '@utils/test/playwright';
|
||||
|
||||
/**
|
||||
* This behavior does not vary across modes/directions.
|
||||
*/
|
||||
configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) => {
|
||||
test.describe(title('textarea: a11y'), () => {
|
||||
test('does not set a default aria-labelledby when there is not a neighboring ion-label', async ({ page }) => {
|
||||
await page.setContent(`<ion-textarea></ion-textarea>`, config);
|
||||
|
||||
await page.setIonViewport();
|
||||
|
||||
const textarea = page.locator('ion-textarea textarea');
|
||||
const ariaLabelledBy = await textarea.getAttribute('aria-labelledby');
|
||||
|
||||
expect(ariaLabelledBy).toBe(null);
|
||||
});
|
||||
|
||||
test('set a default aria-labelledby when a neighboring ion-label exist', async ({ page }) => {
|
||||
await page.setContent(
|
||||
`
|
||||
<ion-item>
|
||||
<ion-label>A11y Test</ion-label>
|
||||
<ion-textarea></ion-textarea>
|
||||
</ion-item>
|
||||
`,
|
||||
config
|
||||
);
|
||||
|
||||
const label = page.locator('ion-label');
|
||||
const textarea = page.locator('ion-textarea textarea');
|
||||
const ariaLabelledBy = await textarea.getAttribute('aria-labelledby');
|
||||
|
||||
expect(ariaLabelledBy).toBe(await label.getAttribute('id'));
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -1,52 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" dir="ltr">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>Textarea - Autogrow</title>
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no"
|
||||
/>
|
||||
<link href="../../../../../../css/ionic.bundle.css" rel="stylesheet" />
|
||||
<link href="../../../../../../scripts/testing/styles.css" rel="stylesheet" />
|
||||
<script src="../../../../../../scripts/testing/scripts.js"></script>
|
||||
<script nomodule src="../../../../../../dist/ionic/ionic.js"></script>
|
||||
<script type="module" src="../../../../../../dist/ionic/ionic.esm.js"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<ion-app>
|
||||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-title>Textarea - Autogrow</ion-title>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
||||
<ion-content id="content">
|
||||
<ion-list>
|
||||
<ion-item>
|
||||
<ion-label color="primary">Autogrow</ion-label>
|
||||
<ion-textarea legacy="true" auto-grow="true"></ion-textarea>
|
||||
</ion-item>
|
||||
|
||||
<ion-item fill="outline">
|
||||
<ion-label color="primary" position="stacked">Autogrow w/ stacked label</ion-label>
|
||||
<ion-textarea legacy="true" auto-grow="true" value=""></ion-textarea>
|
||||
</ion-item>
|
||||
|
||||
<ion-item fill="outline">
|
||||
<ion-label color="primary" position="floating">Autogrow w/ floating label</ion-label>
|
||||
<ion-textarea legacy="true" auto-grow="true" value=""></ion-textarea>
|
||||
</ion-item>
|
||||
</ion-list>
|
||||
</ion-content>
|
||||
</ion-app>
|
||||
|
||||
<script>
|
||||
const textareas = document.querySelectorAll('ion-textarea');
|
||||
textareas.forEach((textarea) => {
|
||||
textarea.value = `Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean at rhoncus diam. Suspendisse lobortis dolor sit amet euismod ultrices. Nam accumsan fringilla quam. Aliquam erat volutpat. Suspendisse vel nisl orci. Nunc placerat metus id elit viverra, vel ultricies felis accumsan. Nullam aliquet vel turpis et iaculis. Interdum et malesuada fames ac ante ipsum primis in faucibus.`;
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@ -1,79 +0,0 @@
|
||||
import { expect } from '@playwright/test';
|
||||
import { configs, test } from '@utils/test/playwright';
|
||||
|
||||
/**
|
||||
* This behavior does not vary across directions
|
||||
*/
|
||||
configs({ directions: ['ltr'] }).forEach(({ title, screenshot, config }) => {
|
||||
test.describe(title('textarea: autogrow'), () => {
|
||||
test('should not have visual regressions', async ({ page }) => {
|
||||
await page.goto(`/src/components/textarea/test/legacy/autogrow`, config);
|
||||
|
||||
await page.setIonViewport();
|
||||
|
||||
await expect(page).toHaveScreenshot(screenshot(`textarea-autogrow-diff`));
|
||||
});
|
||||
|
||||
test('should grow when typing', async ({ page }) => {
|
||||
await page.setContent(
|
||||
`
|
||||
<ion-app>
|
||||
<ion-content>
|
||||
<ion-list>
|
||||
<ion-item>
|
||||
<ion-textarea auto-grow="true" legacy="true"></ion-textarea>
|
||||
</ion-item>
|
||||
</ion-list>
|
||||
</ion-content>
|
||||
</ion-app>`,
|
||||
config
|
||||
);
|
||||
|
||||
const textarea = page.locator('ion-textarea');
|
||||
|
||||
await textarea.click();
|
||||
|
||||
await page.waitForChanges();
|
||||
|
||||
await textarea.type('Now, this is a story all about how');
|
||||
|
||||
await page.setIonViewport();
|
||||
|
||||
await expect(textarea).toHaveScreenshot(screenshot(`textarea-autogrow-initial`));
|
||||
|
||||
await textarea.type(
|
||||
[
|
||||
`\nMy life got flipped-turned upside down`,
|
||||
`And I'd like to take a minute`,
|
||||
`Just sit right there`,
|
||||
`I'll tell you how I became the prince of a town called Bel-Air`,
|
||||
].join('\n')
|
||||
);
|
||||
|
||||
await expect(textarea).toHaveScreenshot(screenshot(`textarea-autogrow-after`));
|
||||
});
|
||||
|
||||
test('should break long lines without white space', async ({ page }) => {
|
||||
test.info().annotations.push({
|
||||
type: 'issue',
|
||||
description: 'https://github.com/ionic-team/ionic-framework/issues/25893',
|
||||
});
|
||||
|
||||
await page.setContent(
|
||||
`<ion-app>
|
||||
<ion-content>
|
||||
<ion-textarea
|
||||
legacy="true"
|
||||
auto-grow="true"
|
||||
value="abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz"></ion-textarea>
|
||||
</ion-content>
|
||||
</ion-app>`,
|
||||
config
|
||||
);
|
||||
|
||||
const textarea = page.locator('ion-textarea');
|
||||
|
||||
await expect(textarea).toHaveScreenshot(screenshot(`textarea-autogrow-word-break`));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 9.6 KiB |
|
Before Width: | Height: | Size: 62 KiB |
|
Before Width: | Height: | Size: 90 KiB |
|
Before Width: | Height: | Size: 57 KiB |
|
Before Width: | Height: | Size: 59 KiB |
|
Before Width: | Height: | Size: 82 KiB |
|
Before Width: | Height: | Size: 49 KiB |
|
Before Width: | Height: | Size: 3.0 KiB |
|
Before Width: | Height: | Size: 4.0 KiB |
|
Before Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 39 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 8.0 KiB |
@ -1,107 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" dir="ltr">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>Textarea - Basic</title>
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no"
|
||||
/>
|
||||
<link href="../../../../../../css/ionic.bundle.css" rel="stylesheet" />
|
||||
<link href="../../../../../../scripts/testing/styles.css" rel="stylesheet" />
|
||||
<script src="../../../../../../scripts/testing/scripts.js"></script>
|
||||
<script nomodule src="../../../../../../dist/ionic/ionic.js"></script>
|
||||
<script type="module" src="../../../../../../dist/ionic/ionic.esm.js"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<ion-app>
|
||||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-title>Textarea - Basic</ion-title>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
||||
<ion-content id="content">
|
||||
<ion-list>
|
||||
<ion-item>
|
||||
<ion-label color="primary">Inline Label</ion-label>
|
||||
<ion-textarea legacy="true" placeholder="Textarea"></ion-textarea>
|
||||
</ion-item>
|
||||
|
||||
<ion-item>
|
||||
<ion-label color="primary" position="fixed">Fixed Label</ion-label>
|
||||
<ion-textarea legacy="true" placeholder="Textarea"></ion-textarea>
|
||||
</ion-item>
|
||||
|
||||
<ion-item>
|
||||
<ion-textarea legacy="true" placeholder="Textarea with no label" legacy="true"></ion-textarea>
|
||||
</ion-item>
|
||||
|
||||
<ion-item>
|
||||
<ion-label color="primary" position="stacked">Stacked Label</ion-label>
|
||||
<ion-textarea legacy="true" placeholder="Textarea"></ion-textarea>
|
||||
</ion-item>
|
||||
|
||||
<ion-item>
|
||||
<ion-label color="primary" position="floating">Floating Label</ion-label>
|
||||
<ion-textarea legacy="true"></ion-textarea>
|
||||
</ion-item>
|
||||
|
||||
<ion-item>
|
||||
<ion-label>Disabled</ion-label>
|
||||
<ion-textarea legacy="true" id="dynamicDisabled" value="Disabled" disabled></ion-textarea>
|
||||
</ion-item>
|
||||
|
||||
<ion-item>
|
||||
<ion-label>Readonly</ion-label>
|
||||
<ion-textarea legacy="true" id="dynamicReadonly" value="Readonly" readonly></ion-textarea>
|
||||
</ion-item>
|
||||
|
||||
<ion-item>
|
||||
<ion-label position="floating">Dynamic Value</ion-label>
|
||||
<ion-textarea legacy="true" id="dynamicValue" value="dynamic"></ion-textarea>
|
||||
</ion-item>
|
||||
|
||||
<ion-item>
|
||||
<ion-label color="primary">Clear on Edit</ion-label>
|
||||
<ion-textarea legacy="true" clear-on-edit="true"></ion-textarea>
|
||||
</ion-item>
|
||||
</ion-list>
|
||||
|
||||
<div class="ion-text-center">
|
||||
<ion-button onclick="toggleBoolean('dynamicDisabled', 'disabled')"> Toggle Disabled </ion-button>
|
||||
|
||||
<ion-button color="secondary" onclick="toggleBoolean('dynamicReadonly', 'readonly')">
|
||||
Toggle Readonly
|
||||
</ion-button>
|
||||
|
||||
<ion-button color="light" onclick="toggleString('dynamicValue', 'value');"> Toggle Value </ion-button>
|
||||
|
||||
<ion-button color="danger" onclick="clearString('dynamicValue', 'value');"> Clear Value </ion-button>
|
||||
</div>
|
||||
</ion-content>
|
||||
|
||||
<script>
|
||||
function toggleBoolean(id, prop) {
|
||||
var el = document.getElementById(id);
|
||||
|
||||
var isTrue = el[prop] ? false : true;
|
||||
el[prop] = isTrue;
|
||||
}
|
||||
|
||||
function toggleString(id, prop) {
|
||||
var el = document.getElementById(id);
|
||||
var newString = el[prop] === 'dynamic' ? 'changed' : 'dynamic';
|
||||
el[prop] = newString;
|
||||
}
|
||||
|
||||
function clearString(id, prop) {
|
||||
var el = document.getElementById(id);
|
||||
|
||||
el[prop] = '';
|
||||
}
|
||||
</script>
|
||||
</ion-app>
|
||||
</body>
|
||||
</html>
|
||||
@ -1,54 +0,0 @@
|
||||
import { expect } from '@playwright/test';
|
||||
import { configs, test } from '@utils/test/playwright';
|
||||
|
||||
configs().forEach(({ title, screenshot, config }) => {
|
||||
test.describe(title('textarea: basic'), () => {
|
||||
test('should not have visual regressions', async ({ page }) => {
|
||||
await page.goto(`/src/components/textarea/test/legacy/basic`, config);
|
||||
|
||||
/**
|
||||
* The auto grow implementation uses a requestAnimationFrame to append styles to the textarea
|
||||
* on load. We need to wait for changes otherwise the screenshot can be taken before the
|
||||
* styles are applied.
|
||||
*/
|
||||
await page.waitForChanges();
|
||||
|
||||
await page.setIonViewport();
|
||||
|
||||
await expect(page).toHaveScreenshot(screenshot(`textarea-diff`));
|
||||
});
|
||||
|
||||
test.describe(title('with floating labels'), () => {
|
||||
/**
|
||||
* Verifies the display of a floating label above an `ion-textarea`.
|
||||
* Captures a screenshot of the initial state (without a value) and verifies
|
||||
* that the label translates correctly after the value is set.
|
||||
*/
|
||||
test('should not have visual regressions', async ({ page }) => {
|
||||
await page.setContent(
|
||||
`
|
||||
<ion-item>
|
||||
<ion-label position="floating">Floating Label</ion-label>
|
||||
<ion-textarea></ion-textarea>
|
||||
</ion-item>`,
|
||||
config
|
||||
);
|
||||
|
||||
const item = page.locator('ion-item');
|
||||
const textarea = page.locator('ion-textarea');
|
||||
|
||||
await expect(item).toHaveScreenshot(screenshot(`textarea-floating-label-initial`));
|
||||
|
||||
await textarea.evaluate((el: HTMLIonTextareaElement) => {
|
||||
el.value = 'Updated value';
|
||||
});
|
||||
|
||||
await page.waitForChanges();
|
||||
|
||||
await page.setIonViewport();
|
||||
|
||||
await expect(item).toHaveScreenshot(screenshot(`textarea-floating-label-diff`));
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
Before Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 46 KiB |
|
Before Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 46 KiB |
|
Before Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 44 KiB |
|
Before Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 44 KiB |
|
Before Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 3.6 KiB |
|
Before Width: | Height: | Size: 4.3 KiB |
|
Before Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 3.6 KiB |
|
Before Width: | Height: | Size: 4.5 KiB |
|
Before Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 3.9 KiB |
|
Before Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 1.8 KiB |
@ -1,32 +0,0 @@
|
||||
import { expect } from '@playwright/test';
|
||||
import { configs, test } from '@utils/test/playwright';
|
||||
|
||||
configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) => {
|
||||
test.describe(title('textarea: clearOnEdit'), () => {
|
||||
test('should clear the textarea on first keystroke of textarea being focused', async ({ page }) => {
|
||||
await page.setContent(`<ion-textarea value="some value" clear-on-edit="true"></ion-textarea>`, config);
|
||||
|
||||
const textarea = page.locator('ion-textarea');
|
||||
|
||||
await textarea.click();
|
||||
await textarea.type('h');
|
||||
|
||||
expect(await textarea.evaluate((el: HTMLIonTextareaElement) => el.value)).toBe('h');
|
||||
|
||||
await textarea.type('ello world');
|
||||
|
||||
expect(await textarea.evaluate((el: HTMLIonTextareaElement) => el.value)).toBe('hello world');
|
||||
});
|
||||
|
||||
test('should not clear the textarea if it does not have an initial value when typing', async ({ page }) => {
|
||||
await page.setContent(`<ion-textarea value="" clear-on-edit="true"></ion-textarea>`, config);
|
||||
|
||||
const textarea = page.locator('ion-textarea');
|
||||
|
||||
await textarea.click();
|
||||
await textarea.type('hello world');
|
||||
|
||||
expect(await textarea.evaluate((el: HTMLIonTextareaElement) => el.value)).toBe('hello world');
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -1,248 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" dir="ltr">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>Textarea - Spec</title>
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no"
|
||||
/>
|
||||
<link href="../../../../../../css/ionic.bundle.css" rel="stylesheet" />
|
||||
<link href="../../../../../../scripts/testing/styles.css" rel="stylesheet" />
|
||||
<script src="../../../../../../scripts/testing/scripts.js"></script>
|
||||
<script nomodule src="../../../../../../dist/ionic/ionic.js"></script>
|
||||
<script type="module" src="../../../../../../dist/ionic/ionic.esm.js"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<ion-app>
|
||||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-title>Textarea - Spec</ion-title>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
<ion-content>
|
||||
<h1>Floating Textareas</h1>
|
||||
|
||||
<div class="grid">
|
||||
<div class="column">
|
||||
<h2>Inactive</h2>
|
||||
<ion-item>
|
||||
<ion-label position="floating">Label</ion-label>
|
||||
<ion-textarea placeholder="Placeholder Text"></ion-textarea>
|
||||
</ion-item>
|
||||
</div>
|
||||
|
||||
<div class="column">
|
||||
<h2>Focused</h2>
|
||||
<ion-item class="item-has-focus">
|
||||
<ion-label position="floating">Label</ion-label>
|
||||
<ion-textarea placeholder="Placeholder Text"></ion-textarea>
|
||||
</ion-item>
|
||||
</div>
|
||||
|
||||
<div class="column">
|
||||
<h2>Activated</h2>
|
||||
<ion-item>
|
||||
<ion-label position="floating">Label</ion-label>
|
||||
<ion-textarea placeholder="Placeholder Text" value="Textarea Text"></ion-textarea>
|
||||
</ion-item>
|
||||
</div>
|
||||
|
||||
<div class="column">
|
||||
<h2>Hover</h2>
|
||||
<ion-item class="item-hovered">
|
||||
<ion-label position="floating">Label</ion-label>
|
||||
<ion-textarea></ion-textarea>
|
||||
</ion-item>
|
||||
</div>
|
||||
|
||||
<div class="column">
|
||||
<h2>Disabled</h2>
|
||||
<ion-item>
|
||||
<ion-label position="floating">Label</ion-label>
|
||||
<ion-textarea disabled></ion-textarea>
|
||||
</ion-item>
|
||||
</div>
|
||||
|
||||
<div class="column">
|
||||
<h2>Toggle Placeholder</h2>
|
||||
<ion-item>
|
||||
<ion-label position="floating">Label</ion-label>
|
||||
<ion-textarea id="floatingToggle" type="password"></ion-textarea>
|
||||
<ion-button
|
||||
fill="clear"
|
||||
slot="end"
|
||||
onClick="togglePlaceholder('#floatingToggle')"
|
||||
class="ion-align-self-center"
|
||||
>
|
||||
Toggle
|
||||
</ion-button>
|
||||
</ion-item>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h1>Stacked textareas</h1>
|
||||
|
||||
<div class="grid">
|
||||
<div class="column">
|
||||
<h2>Inactive</h2>
|
||||
<ion-item>
|
||||
<ion-label position="stacked">Label</ion-label>
|
||||
<ion-textarea></ion-textarea>
|
||||
</ion-item>
|
||||
</div>
|
||||
|
||||
<div class="column">
|
||||
<h2>Focused</h2>
|
||||
<ion-item class="item-has-focus">
|
||||
<ion-label position="stacked">Label</ion-label>
|
||||
<ion-textarea placeholder="Placeholder Text"></ion-textarea>
|
||||
</ion-item>
|
||||
</div>
|
||||
|
||||
<div class="column">
|
||||
<h2>Activated</h2>
|
||||
<ion-item>
|
||||
<ion-label position="stacked">Label</ion-label>
|
||||
<ion-textarea placeholder="Placeholder Text" value="textarea Text"></ion-textarea>
|
||||
</ion-item>
|
||||
</div>
|
||||
|
||||
<div class="column">
|
||||
<h2>Hover</h2>
|
||||
<ion-item class="item-hovered">
|
||||
<ion-label position="stacked">Label</ion-label>
|
||||
<ion-textarea></ion-textarea>
|
||||
</ion-item>
|
||||
</div>
|
||||
|
||||
<div class="column">
|
||||
<h2>Disabled</h2>
|
||||
<ion-item>
|
||||
<ion-label position="stacked">Label</ion-label>
|
||||
<ion-textarea disabled></ion-textarea>
|
||||
</ion-item>
|
||||
</div>
|
||||
|
||||
<div class="column">
|
||||
<h2>Toggle Placeholder</h2>
|
||||
<ion-item>
|
||||
<ion-label position="stacked">Label</ion-label>
|
||||
<ion-textarea id="stackedToggle" type="password"></ion-textarea>
|
||||
<ion-button
|
||||
fill="clear"
|
||||
slot="end"
|
||||
onClick="togglePlaceholder('#stackedToggle')"
|
||||
class="ion-align-self-center"
|
||||
>
|
||||
Toggle
|
||||
</ion-button>
|
||||
</ion-item>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<h2>Stacked Div</h2>
|
||||
<ion-item>
|
||||
<ion-label position="stacked">Label</ion-label>
|
||||
<div>A div</div>
|
||||
</ion-item>
|
||||
|
||||
<ion-item class="ion-align-items-center">
|
||||
<ion-icon slot="start" name="planet"></ion-icon>
|
||||
<ion-label position="stacked">Align items: center</ion-label>
|
||||
<div>A div</div>
|
||||
</ion-item>
|
||||
<ion-item class="ion-align-items-center">
|
||||
<ion-icon slot="start" name="planet"></ion-icon>
|
||||
<ion-label position="stacked">Align items: center</ion-label>
|
||||
<div class="ion-align-self-end">A div: align self right</div>
|
||||
</ion-item>
|
||||
<ion-item>
|
||||
<ion-label position="floating">Floating: textarea</ion-label>
|
||||
<ion-textarea></ion-textarea>
|
||||
</ion-item>
|
||||
<ion-item>
|
||||
<ion-label position="floating">Floating: textarea value</ion-label>
|
||||
<ion-textarea value="value"></ion-textarea>
|
||||
</ion-item>
|
||||
<ion-item>
|
||||
<ion-label position="stacked">Stacked: textarea</ion-label>
|
||||
<ion-textarea></ion-textarea>
|
||||
</ion-item>
|
||||
<ion-item class="item-has-focus">
|
||||
<ion-label position="stacked">Stacked: textarea focused value</ion-label>
|
||||
<ion-textarea value="value"></ion-textarea>
|
||||
</ion-item>
|
||||
<ion-item class="custom item-has-focus">
|
||||
<ion-label position="stacked">Stacked: textarea focus</ion-label>
|
||||
<ion-textarea></ion-textarea>
|
||||
</ion-item>
|
||||
</ion-content>
|
||||
|
||||
<style>
|
||||
h1 {
|
||||
font-size: 14px;
|
||||
color: #54575e;
|
||||
|
||||
margin: 25px 0 5px 25px;
|
||||
}
|
||||
|
||||
h2 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
font-size: 12px;
|
||||
font-weight: normal;
|
||||
|
||||
color: #a1a7b0;
|
||||
|
||||
margin-top: 10px;
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
hr {
|
||||
background: #eff1f3;
|
||||
|
||||
margin-top: 18px;
|
||||
margin-bottom: 25px;
|
||||
}
|
||||
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
row-gap: 20px;
|
||||
column-gap: 20px;
|
||||
padding: 0 20px 20px;
|
||||
}
|
||||
|
||||
ion-item {
|
||||
--background: #e0e0e0;
|
||||
--background-hover: #d3d3d3;
|
||||
}
|
||||
|
||||
.custom {
|
||||
--background: lightblue;
|
||||
}
|
||||
|
||||
.custom ion-label {
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
.custom.item-has-focus ion-label {
|
||||
color: purple !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
function togglePlaceholder(id) {
|
||||
const textarea = document.querySelector(id);
|
||||
textarea.placeholder = textarea.placeholder ? undefined : 'Placeholder Text';
|
||||
}
|
||||
</script>
|
||||
</ion-app>
|
||||
</body>
|
||||
</html>
|
||||
@ -16,14 +16,6 @@
|
||||
font-size: $textarea-ios-font-size;
|
||||
}
|
||||
|
||||
// TODO: FW-2876 - Remove this selector
|
||||
:host(.legacy-textarea) {
|
||||
--padding-top: #{$textarea-ios-padding-top};
|
||||
--padding-end: #{$textarea-ios-padding-end};
|
||||
--padding-bottom: #{$textarea-ios-padding-bottom};
|
||||
--padding-start: #{$textarea-ios-padding-start};
|
||||
}
|
||||
|
||||
:host-context(.item-label-stacked),
|
||||
:host-context(.item-label-floating) {
|
||||
--padding-top: 8px;
|
||||
|
||||
@ -18,24 +18,6 @@
|
||||
font-size: $textarea-md-font-size;
|
||||
}
|
||||
|
||||
// TODO: FW-2876 - Remove this selector
|
||||
:host(.legacy-textarea) {
|
||||
--padding-top: #{$textarea-md-padding-top};
|
||||
--padding-end: #{$textarea-md-padding-end};
|
||||
--padding-bottom: #{$textarea-md-padding-bottom};
|
||||
--padding-start: #{$textarea-md-padding-start};
|
||||
|
||||
@include margin(8px, 0, 0, 0);
|
||||
}
|
||||
|
||||
// TODO: FW-2876 - Re-evaluate this selector, it may not be needed
|
||||
:host-context(.item-label-stacked),
|
||||
:host-context(.item-label-floating) {
|
||||
--padding-top: 8px;
|
||||
--padding-bottom: 8px;
|
||||
--padding-start: 0;
|
||||
}
|
||||
|
||||
// Textarea Max Length Counter
|
||||
// ----------------------------------------------------------------
|
||||
.textarea-bottom .counter {
|
||||
|
||||
@ -37,7 +37,7 @@
|
||||
--placeholder-opacity: #{$placeholder-opacity};
|
||||
--padding-top: 0;
|
||||
--padding-end: 0;
|
||||
--padding-bottom: 0;
|
||||
--padding-bottom: #{$textarea-padding-bottom};
|
||||
--padding-start: 0;
|
||||
--border-radius: 0;
|
||||
--border-style: solid;
|
||||
@ -58,6 +58,8 @@
|
||||
|
||||
width: 100%;
|
||||
|
||||
min-height: 44px;
|
||||
|
||||
color: var(--color);
|
||||
|
||||
font-family: $font-family-base;
|
||||
@ -70,11 +72,6 @@
|
||||
// Textarea Wrapper
|
||||
// ----------------------------------------------------------------
|
||||
|
||||
// TODO: FW-2876 - Make this style a :host style, remove :not selector
|
||||
:host(:not(.legacy-textarea)) {
|
||||
min-height: 44px;
|
||||
}
|
||||
|
||||
/**
|
||||
* Since the label sits on top of the element,
|
||||
* the component needs to be taller otherwise the
|
||||
@ -106,25 +103,6 @@
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
// TODO: FW-2876 - Remove this selector
|
||||
:host(.legacy-textarea) {
|
||||
flex: 1;
|
||||
|
||||
background: var(--background);
|
||||
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
// TODO: FW-2876 - Remove this selector
|
||||
:host(.legacy-textarea.ion-color) {
|
||||
color: current-color(base);
|
||||
}
|
||||
|
||||
// TODO: FW-2876 - Remove this selector, move styles to :host
|
||||
:host(:not(.legacy-textarea)) {
|
||||
--padding-bottom: #{$textarea-padding-bottom};
|
||||
}
|
||||
|
||||
:host(.ion-color) {
|
||||
--highlight-color-focused: #{current-color(base)};
|
||||
|
||||
@ -200,18 +178,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: FW-2876 - Remove this selector
|
||||
:host(.legacy-textarea) .native-textarea {
|
||||
white-space: inherit;
|
||||
}
|
||||
|
||||
// TODO: FW-2876 - Remove this selector
|
||||
:host(.legacy-textarea) .native-textarea,
|
||||
:host(.legacy-textarea) .textarea-legacy-wrapper::after {
|
||||
@include padding(var(--padding-top), var(--padding-end), var(--padding-bottom), var(--padding-start));
|
||||
@include border-radius(var(--border-radius));
|
||||
}
|
||||
|
||||
.native-textarea {
|
||||
color: inherit;
|
||||
|
||||
@ -233,15 +199,6 @@
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
// TODO: FW-2876 - Remove this selector
|
||||
:host(.legacy-textarea) .textarea-legacy-wrapper::after {
|
||||
@include text-inherit();
|
||||
|
||||
grid-area: 1 / 1 / 2 / 2;
|
||||
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
// Input Cover: Unfocused
|
||||
// --------------------------------------------------
|
||||
// The input cover is the div that actually receives the
|
||||
@ -267,11 +224,6 @@
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
// TODO: FW-2876 - Remove this selector
|
||||
:host(.legacy-textarea[auto-grow]) .cloned-input {
|
||||
@include margin(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
:host([auto-grow]) .cloned-input {
|
||||
// Workaround for webkit rendering issue with scroll assist.
|
||||
// When cloning the textarea and scrolling into view,
|
||||
@ -348,9 +300,7 @@
|
||||
@include padding(var(--padding-top), 0px, var(--padding-bottom), 0px);
|
||||
}
|
||||
|
||||
.native-wrapper,
|
||||
// TODO: FW-2876 - Remove this selector, keep .native-wrapper
|
||||
.textarea-legacy-wrapper {
|
||||
.native-wrapper {
|
||||
display: grid;
|
||||
|
||||
min-width: inherit;
|
||||
|
||||
@ -13,17 +13,10 @@ import {
|
||||
h,
|
||||
writeTask,
|
||||
} from '@stencil/core';
|
||||
import type { LegacyFormController, NotchController } from '@utils/forms';
|
||||
import { createLegacyFormController, createNotchController } from '@utils/forms';
|
||||
import type { NotchController } from '@utils/forms';
|
||||
import { createNotchController } from '@utils/forms';
|
||||
import type { Attributes } from '@utils/helpers';
|
||||
import {
|
||||
inheritAriaAttributes,
|
||||
debounceEvent,
|
||||
findItemLabel,
|
||||
inheritAttributes,
|
||||
componentOnReady,
|
||||
} from '@utils/helpers';
|
||||
import { printIonWarning } from '@utils/logging';
|
||||
import { inheritAriaAttributes, debounceEvent, inheritAttributes, componentOnReady } from '@utils/helpers';
|
||||
import { createSlotMutationController } from '@utils/slot-mutation-controller';
|
||||
import type { SlotMutationController } from '@utils/slot-mutation-controller';
|
||||
import { createColorClasses, hostContext } from '@utils/theme';
|
||||
@ -62,16 +55,12 @@ export class Textarea implements ComponentInterface {
|
||||
private textareaWrapper?: HTMLElement;
|
||||
private inheritedAttributes: Attributes = {};
|
||||
private originalIonInput?: EventEmitter<TextareaInputEventDetail>;
|
||||
private legacyFormController!: LegacyFormController;
|
||||
private notchSpacerEl: HTMLElement | undefined;
|
||||
|
||||
private slotMutationController?: SlotMutationController;
|
||||
|
||||
private notchController?: NotchController;
|
||||
|
||||
// This flag ensures we log the deprecation warning at most once.
|
||||
private hasLoggedDeprecationWarning = false;
|
||||
|
||||
/**
|
||||
* The value of the textarea when the textarea is focused.
|
||||
*/
|
||||
@ -127,11 +116,6 @@ export class Textarea implements ComponentInterface {
|
||||
*/
|
||||
@Prop() disabled = false;
|
||||
|
||||
@Watch('disabled')
|
||||
protected disabledChanged() {
|
||||
this.emitStyle();
|
||||
}
|
||||
|
||||
/**
|
||||
* The fill for the item. If `"solid"` the item will have a background. If
|
||||
* `"outline"` the item will be transparent with a border. Only available in `md` mode.
|
||||
@ -257,17 +241,6 @@ export class Textarea implements ComponentInterface {
|
||||
*/
|
||||
@Prop() labelPlacement: 'start' | 'end' | 'floating' | 'stacked' | 'fixed' = 'start';
|
||||
|
||||
/**
|
||||
* Set the `legacy` property to `true` to forcibly use the legacy form control markup.
|
||||
* Ionic will only opt components in to the modern form markup when they are
|
||||
* using either the `aria-label` attribute or the default slot that contains
|
||||
* the label text. As a result, the `legacy` property should only be used as
|
||||
* an escape hatch when you want to avoid this automatic opt-in behavior.
|
||||
* Note that this property will be removed in an upcoming major release
|
||||
* of Ionic, and all form components will be opted-in to using the modern form markup.
|
||||
*/
|
||||
@Prop() legacy?: boolean;
|
||||
|
||||
/**
|
||||
* The shape of the textarea. If "round" it will have an increased border radius.
|
||||
*/
|
||||
@ -284,7 +257,6 @@ export class Textarea implements ComponentInterface {
|
||||
nativeInput.value = value;
|
||||
}
|
||||
this.runAutoGrow();
|
||||
this.emitStyle();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -322,14 +294,12 @@ export class Textarea implements ComponentInterface {
|
||||
|
||||
connectedCallback() {
|
||||
const { el } = this;
|
||||
this.legacyFormController = createLegacyFormController(el);
|
||||
this.slotMutationController = createSlotMutationController(el, ['label', 'start', 'end'], () => forceUpdate(this));
|
||||
this.notchController = createNotchController(
|
||||
el,
|
||||
() => this.notchSpacerEl,
|
||||
() => this.labelSlot
|
||||
);
|
||||
this.emitStyle();
|
||||
this.debounceChanged();
|
||||
if (Build.isBrowser) {
|
||||
document.dispatchEvent(
|
||||
@ -404,22 +374,6 @@ export class Textarea implements ComponentInterface {
|
||||
return Promise.resolve(this.nativeInput!);
|
||||
}
|
||||
|
||||
private emitStyle() {
|
||||
if (this.legacyFormController.hasLegacyControl()) {
|
||||
this.ionStyle.emit({
|
||||
interactive: true,
|
||||
textarea: true,
|
||||
input: true,
|
||||
'interactive-disabled': this.disabled,
|
||||
'has-placeholder': this.placeholder !== undefined,
|
||||
'has-value': this.hasValue(),
|
||||
'has-focus': this.hasFocus,
|
||||
// TODO(FW-2876): remove this
|
||||
legacy: !!this.legacy,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Emits an `ionChange` event.
|
||||
*
|
||||
@ -499,10 +453,6 @@ export class Textarea implements ComponentInterface {
|
||||
}
|
||||
}
|
||||
|
||||
private focusChange() {
|
||||
this.emitStyle();
|
||||
}
|
||||
|
||||
private hasValue(): boolean {
|
||||
return this.getValue() !== '';
|
||||
}
|
||||
@ -531,14 +481,12 @@ export class Textarea implements ComponentInterface {
|
||||
private onFocus = (ev: FocusEvent) => {
|
||||
this.hasFocus = true;
|
||||
this.focusedValue = this.value;
|
||||
this.focusChange();
|
||||
|
||||
this.ionFocus.emit(ev);
|
||||
};
|
||||
|
||||
private onBlur = (ev: FocusEvent) => {
|
||||
this.hasFocus = false;
|
||||
this.focusChange();
|
||||
|
||||
if (this.focusedValue !== this.value) {
|
||||
/**
|
||||
@ -555,73 +503,6 @@ export class Textarea implements ComponentInterface {
|
||||
this.checkClearOnEdit(ev);
|
||||
};
|
||||
|
||||
// TODO: FW-2876 - Remove this render function
|
||||
private renderLegacyTextarea() {
|
||||
if (!this.hasLoggedDeprecationWarning) {
|
||||
printIonWarning(
|
||||
`ion-textarea now requires providing a label with either the "label" property or the "aria-label" attribute. To migrate, remove any usage of "ion-label" and pass the label text to either the "label" property or the "aria-label" attribute.
|
||||
|
||||
Example: <ion-textarea label="Comments"></ion-textarea>
|
||||
Example with aria-label: <ion-textarea aria-label="Comments"></ion-textarea>
|
||||
|
||||
For textareas that do not render the label immediately next to the input, developers may continue to use "ion-label" but must manually associate the label with the textarea by using "aria-labelledby".
|
||||
|
||||
Developers can use the "legacy" property to continue using the legacy form markup. This property will be removed in an upcoming major release of Ionic where this form control will use the modern form markup.`,
|
||||
this.el
|
||||
);
|
||||
this.hasLoggedDeprecationWarning = true;
|
||||
}
|
||||
|
||||
const mode = getIonMode(this);
|
||||
const value = this.getValue();
|
||||
const labelId = this.inputId + '-lbl';
|
||||
const label = findItemLabel(this.el);
|
||||
if (label) {
|
||||
label.id = labelId;
|
||||
}
|
||||
|
||||
return (
|
||||
<Host
|
||||
aria-disabled={this.disabled ? 'true' : null}
|
||||
class={createColorClasses(this.color, {
|
||||
[mode]: true,
|
||||
'legacy-textarea': true,
|
||||
})}
|
||||
>
|
||||
<div class="textarea-legacy-wrapper" ref={(el) => (this.textareaWrapper = el)}>
|
||||
<textarea
|
||||
class="native-textarea"
|
||||
aria-labelledby={label ? label.id : null}
|
||||
ref={(el) => (this.nativeInput = el)}
|
||||
autoCapitalize={this.autocapitalize}
|
||||
autoFocus={this.autofocus}
|
||||
enterKeyHint={this.enterkeyhint}
|
||||
inputMode={this.inputmode}
|
||||
disabled={this.disabled}
|
||||
maxLength={this.maxlength}
|
||||
minLength={this.minlength}
|
||||
name={this.name}
|
||||
placeholder={this.placeholder || ''}
|
||||
readOnly={this.readonly}
|
||||
required={this.required}
|
||||
spellcheck={this.spellcheck}
|
||||
cols={this.cols}
|
||||
rows={this.rows}
|
||||
wrap={this.wrap}
|
||||
onInput={this.onInput}
|
||||
onChange={this.onChange}
|
||||
onBlur={this.onBlur}
|
||||
onFocus={this.onFocus}
|
||||
onKeyDown={this.onKeyDown}
|
||||
{...this.inheritedAttributes}
|
||||
>
|
||||
{value}
|
||||
</textarea>
|
||||
</div>
|
||||
</Host>
|
||||
);
|
||||
}
|
||||
|
||||
private renderLabel() {
|
||||
const { label } = this;
|
||||
|
||||
@ -739,7 +620,7 @@ Developers can use the "legacy" property to continue using the legacy form marku
|
||||
);
|
||||
}
|
||||
|
||||
private renderTextarea() {
|
||||
render() {
|
||||
const { inputId, disabled, fill, shape, labelPlacement, el, hasFocus } = this;
|
||||
const mode = getIonMode(this);
|
||||
const value = this.getValue();
|
||||
@ -841,12 +722,6 @@ Developers can use the "legacy" property to continue using the legacy form marku
|
||||
</Host>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { legacyFormController } = this;
|
||||
|
||||
return legacyFormController.hasLegacyControl() ? this.renderLegacyTextarea() : this.renderTextarea();
|
||||
}
|
||||
}
|
||||
|
||||
let textareaIds = 0;
|
||||
|
||||