feat(textarea): add support for the shape property with soft, round and rectangular options (#29788)
- Adds support for `"soft"`, `"round"` and `"rectangular"` shapes in the Ionic theme, with `"round"` as the default - Adds a `shape` folder and test with examples - Adds an e2e test with screenshots for all shapes
@@ -2279,7 +2279,7 @@ ion-textarea,prop,placeholder,string | undefined,undefined,false,false
|
||||
ion-textarea,prop,readonly,boolean,false,false,false
|
||||
ion-textarea,prop,required,boolean,false,false,false
|
||||
ion-textarea,prop,rows,number | undefined,undefined,false,false
|
||||
ion-textarea,prop,shape,"round" | undefined,undefined,false,false
|
||||
ion-textarea,prop,shape,"rectangular" | "round" | "soft" | undefined,undefined,false,false
|
||||
ion-textarea,prop,size,"large" | "medium" | "small" | undefined,'medium',false,false
|
||||
ion-textarea,prop,spellcheck,boolean,false,false,false
|
||||
ion-textarea,prop,theme,"ios" | "md" | "ionic",undefined,false,false
|
||||
|
||||
8
core/src/components.d.ts
vendored
@@ -3617,9 +3617,9 @@ export namespace Components {
|
||||
*/
|
||||
"setFocus": () => Promise<void>;
|
||||
/**
|
||||
* The shape of the textarea. If "round" it will have an increased border radius.
|
||||
* Set to `"soft"` for a textarea with slightly rounded corners, `"round"` for a textarea with fully rounded corners, or `"rectangular"` for a textarea without rounded corners. Defaults to `"round"` for the `ionic` theme, undefined for all other themes.
|
||||
*/
|
||||
"shape"?: 'round';
|
||||
"shape"?: 'soft' | 'round' | 'rectangular';
|
||||
/**
|
||||
* The size of the textarea. If "large" it will increase the height of the textarea, while "small" and "medium" provide progressively smaller heights. The default size is "medium". This property only applies to the `"ionic"` theme.
|
||||
*/
|
||||
@@ -8977,9 +8977,9 @@ declare namespace LocalJSX {
|
||||
*/
|
||||
"rows"?: number;
|
||||
/**
|
||||
* The shape of the textarea. If "round" it will have an increased border radius.
|
||||
* Set to `"soft"` for a textarea with slightly rounded corners, `"round"` for a textarea with fully rounded corners, or `"rectangular"` for a textarea without rounded corners. Defaults to `"round"` for the `ionic` theme, undefined for all other themes.
|
||||
*/
|
||||
"shape"?: 'round';
|
||||
"shape"?: 'soft' | 'round' | 'rectangular';
|
||||
/**
|
||||
* The size of the textarea. If "large" it will increase the height of the textarea, while "small" and "medium" provide progressively smaller heights. The default size is "medium". This property only applies to the `"ionic"` theme.
|
||||
*/
|
||||
|
||||
97
core/src/components/textarea/test/shape/index.html
Normal file
@@ -0,0 +1,97 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" dir="ltr">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>Textarea - Shape</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>
|
||||
<style>
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(250px, 1fr));
|
||||
grid-row-gap: 20px;
|
||||
grid-column-gap: 20px;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 12px;
|
||||
font-weight: normal;
|
||||
|
||||
color: #6f7378;
|
||||
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 800px) {
|
||||
.grid {
|
||||
grid-template-columns: 1fr;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<ion-app>
|
||||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-title>Textarea - Shape</ion-title>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
||||
<ion-content id="content" class="ion-padding">
|
||||
<div class="grid">
|
||||
<div class="grid-item">
|
||||
<h2>Default</h2>
|
||||
<ion-textarea
|
||||
fill="outline"
|
||||
label="Label"
|
||||
label-placement="stacked"
|
||||
placeholder="Placeholder"
|
||||
></ion-textarea>
|
||||
</div>
|
||||
|
||||
<div class="grid-item">
|
||||
<h2>Round</h2>
|
||||
<ion-textarea
|
||||
shape="round"
|
||||
fill="outline"
|
||||
label="Label"
|
||||
label-placement="stacked"
|
||||
placeholder="Placeholder"
|
||||
></ion-textarea>
|
||||
</div>
|
||||
|
||||
<div class="grid-item">
|
||||
<h2>Soft</h2>
|
||||
<ion-textarea
|
||||
shape="soft"
|
||||
fill="outline"
|
||||
label="Label"
|
||||
label-placement="stacked"
|
||||
placeholder="Placeholder"
|
||||
></ion-textarea>
|
||||
</div>
|
||||
|
||||
<div class="grid-item">
|
||||
<h2>Rectangular</h2>
|
||||
<ion-textarea
|
||||
shape="rectangular"
|
||||
fill="outline"
|
||||
label="Label"
|
||||
label-placement="stacked"
|
||||
placeholder="Placeholder"
|
||||
></ion-textarea>
|
||||
</div>
|
||||
</div>
|
||||
</ion-content>
|
||||
</ion-app>
|
||||
</body>
|
||||
</html>
|
||||
100
core/src/components/textarea/test/shape/textarea.e2e.ts
Normal file
@@ -0,0 +1,100 @@
|
||||
import { expect } from '@playwright/test';
|
||||
import { configs, test } from '@utils/test/playwright';
|
||||
|
||||
/**
|
||||
* This behavior does not vary across directions
|
||||
* The round shape is only available in the Ionic and Material Design themes
|
||||
*/
|
||||
configs({ modes: ['md', 'ionic-md'], directions: ['ltr'] }).forEach(({ title, screenshot, config }) => {
|
||||
test.describe(title('textarea: shape'), () => {
|
||||
test.describe('default', () => {
|
||||
test('should not have visual regressions', async ({ page }) => {
|
||||
await page.setContent(
|
||||
`
|
||||
<ion-textarea
|
||||
fill="outline"
|
||||
label="Email"
|
||||
label-placement="stacked"
|
||||
value="hi@ionic.io"
|
||||
></ion-textarea>
|
||||
`,
|
||||
config
|
||||
);
|
||||
|
||||
const textarea = page.locator('ion-textarea');
|
||||
|
||||
await expect(textarea).toHaveScreenshot(screenshot(`textarea-default`));
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('round', () => {
|
||||
test('should not have visual regressions', async ({ page }) => {
|
||||
await page.setContent(
|
||||
`
|
||||
<ion-textarea
|
||||
shape="round"
|
||||
fill="outline"
|
||||
label="Email"
|
||||
label-placement="stacked"
|
||||
value="hi@ionic.io"
|
||||
></ion-textarea>
|
||||
`,
|
||||
config
|
||||
);
|
||||
|
||||
const textarea = page.locator('ion-textarea');
|
||||
|
||||
await expect(textarea).toHaveScreenshot(screenshot(`textarea-round`));
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* The soft and rectangular shapes are only available in the Ionic theme
|
||||
*/
|
||||
configs({ modes: ['ionic-md'], directions: ['ltr'] }).forEach(({ title, screenshot, config }) => {
|
||||
test.describe(title('textarea: shape'), () => {
|
||||
test.describe('soft', () => {
|
||||
test('should not have visual regressions', async ({ page }) => {
|
||||
await page.setContent(
|
||||
`
|
||||
<ion-textarea
|
||||
shape="soft"
|
||||
fill="outline"
|
||||
label="Email"
|
||||
label-placement="stacked"
|
||||
value="hi@ionic.io"
|
||||
></ion-textarea>
|
||||
`,
|
||||
config
|
||||
);
|
||||
|
||||
const textarea = page.locator('ion-textarea');
|
||||
|
||||
await expect(textarea).toHaveScreenshot(screenshot(`textarea-soft`));
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('rectangular', () => {
|
||||
test('should not have visual regressions', async ({ page }) => {
|
||||
await page.setContent(
|
||||
`
|
||||
<ion-textarea
|
||||
shape="rectangular"
|
||||
fill="outline"
|
||||
label="Email"
|
||||
label-placement="stacked"
|
||||
value="hi@ionic.io"
|
||||
></ion-textarea>
|
||||
`,
|
||||
config
|
||||
);
|
||||
|
||||
const textarea = page.locator('ion-textarea');
|
||||
|
||||
await expect(textarea).toHaveScreenshot(screenshot(`textarea-rectangular`));
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
After Width: | Height: | Size: 3.0 KiB |
|
After Width: | Height: | Size: 4.5 KiB |
|
After Width: | Height: | Size: 3.1 KiB |
|
After Width: | Height: | Size: 2.1 KiB |
|
After Width: | Height: | Size: 2.7 KiB |
|
After Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 3.6 KiB |
|
After Width: | Height: | Size: 2.2 KiB |
|
After Width: | Height: | Size: 3.0 KiB |
|
After Width: | Height: | Size: 4.5 KiB |
|
After Width: | Height: | Size: 3.1 KiB |
|
After Width: | Height: | Size: 3.1 KiB |
|
After Width: | Height: | Size: 3.5 KiB |
|
After Width: | Height: | Size: 3.0 KiB |
|
After Width: | Height: | Size: 2.6 KiB |
|
After Width: | Height: | Size: 4.1 KiB |
|
After Width: | Height: | Size: 2.6 KiB |
@@ -15,7 +15,7 @@
|
||||
<style>
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(250px, 1fr));
|
||||
grid-template-columns: repeat(3, minmax(250px, 1fr));
|
||||
grid-row-gap: 20px;
|
||||
grid-column-gap: 20px;
|
||||
}
|
||||
@@ -27,7 +27,6 @@
|
||||
color: #6f7378;
|
||||
|
||||
margin-top: 10px;
|
||||
padding-left: 16px;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 800px) {
|
||||
@@ -60,18 +59,6 @@
|
||||
></ion-textarea>
|
||||
</div>
|
||||
|
||||
<div class="grid-item">
|
||||
<h2>Outline: Small Size, Round Shape</h2>
|
||||
<ion-textarea
|
||||
size="small"
|
||||
fill="outline"
|
||||
shape="round"
|
||||
label="Label"
|
||||
label-placement="stacked"
|
||||
placeholder="Placeholder"
|
||||
></ion-textarea>
|
||||
</div>
|
||||
|
||||
<div class="grid-item">
|
||||
<h2>Outline: No Size</h2>
|
||||
<ion-textarea
|
||||
@@ -82,17 +69,6 @@
|
||||
></ion-textarea>
|
||||
</div>
|
||||
|
||||
<div class="grid-item">
|
||||
<h2>Outline: No Size, Round Shape</h2>
|
||||
<ion-textarea
|
||||
fill="outline"
|
||||
shape="round"
|
||||
label="Label"
|
||||
label-placement="stacked"
|
||||
placeholder="Placeholder"
|
||||
></ion-textarea>
|
||||
</div>
|
||||
|
||||
<div class="grid-item">
|
||||
<h2>Outline: Large Size</h2>
|
||||
<ion-textarea
|
||||
@@ -103,18 +79,6 @@
|
||||
placeholder="Placeholder"
|
||||
></ion-textarea>
|
||||
</div>
|
||||
|
||||
<div class="grid-item">
|
||||
<h2>Outline: Large Size, Round Shape</h2>
|
||||
<ion-textarea
|
||||
size="large"
|
||||
fill="outline"
|
||||
shape="round"
|
||||
label="Label"
|
||||
label-placement="stacked"
|
||||
placeholder="Placeholder"
|
||||
></ion-textarea>
|
||||
</div>
|
||||
</div>
|
||||
</ion-content>
|
||||
</ion-app>
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
// --------------------------------------------------
|
||||
|
||||
:host {
|
||||
--border-radius: #{globals.$ionic-border-radius-400};
|
||||
--color: #{globals.$ionic-color-neutral-1200};
|
||||
--highlight-color-valid: #{globals.$ionic-color-success-base};
|
||||
--highlight-color-invalid: #{globals.$ionic-color-danger-base};
|
||||
@@ -53,6 +52,21 @@
|
||||
min-height: globals.$ionic-scale-3600;
|
||||
}
|
||||
|
||||
// Ionic Textarea Shapes
|
||||
// --------------------------------------------------
|
||||
|
||||
:host(.textarea-shape-soft) {
|
||||
--border-radius: #{globals.$ionic-border-radius-200};
|
||||
}
|
||||
|
||||
:host(.textarea-shape-round) {
|
||||
--border-radius: #{globals.$ionic-border-radius-400};
|
||||
}
|
||||
|
||||
:host(.textarea-shape-rectangular) {
|
||||
--border-radius: #{globals.$ionic-border-radius-0};
|
||||
}
|
||||
|
||||
// Textarea Wrapper
|
||||
// ----------------------------------------------------------------
|
||||
|
||||
|
||||
@@ -244,9 +244,13 @@ export class Textarea implements ComponentInterface {
|
||||
@Prop() labelPlacement: 'start' | 'end' | 'floating' | 'stacked' | 'fixed' = 'start';
|
||||
|
||||
/**
|
||||
* The shape of the textarea. If "round" it will have an increased border radius.
|
||||
* Set to `"soft"` for a textarea with slightly rounded corners,
|
||||
* `"round"` for a textarea with fully rounded corners, or `"rectangular"`
|
||||
* for a textarea without rounded corners.
|
||||
*
|
||||
* Defaults to `"round"` for the `ionic` theme, undefined for all other themes.
|
||||
*/
|
||||
@Prop() shape?: 'round';
|
||||
@Prop() shape?: 'soft' | 'round' | 'rectangular';
|
||||
|
||||
/**
|
||||
* The size of the textarea. If "large" it will increase the height of the textarea, while
|
||||
@@ -541,6 +545,18 @@ export class Textarea implements ComponentInterface {
|
||||
return this.label !== undefined || this.labelSlot !== null;
|
||||
}
|
||||
|
||||
private getShape(): string | undefined {
|
||||
const theme = getIonTheme(this);
|
||||
const { shape } = this;
|
||||
|
||||
// TODO(ROU-11050): Update theme check when shapes are defined for all themes.
|
||||
if (theme === 'ionic' && shape === undefined) {
|
||||
return 'round';
|
||||
}
|
||||
|
||||
return shape;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the border container when fill="outline".
|
||||
*/
|
||||
@@ -626,8 +642,9 @@ export class Textarea implements ComponentInterface {
|
||||
}
|
||||
|
||||
render() {
|
||||
const { inputId, disabled, fill, shape, size, labelPlacement, el, hasFocus } = this;
|
||||
const { inputId, disabled, fill, size, labelPlacement, el, hasFocus } = this;
|
||||
const theme = getIonTheme(this);
|
||||
const shape = this.getShape();
|
||||
const value = this.getValue();
|
||||
const inItem = hostContext('ion-item', this.el);
|
||||
const shouldRenderHighlight = theme === 'md' && fill !== 'outline' && !inItem;
|
||||
|
||||