Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions core/api.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2423,7 +2423,7 @@ ion-text,prop,color,"danger" | "dark" | "light" | "medium" | "primary" | "second
ion-text,prop,mode,"ios" | "md",undefined,false,false
ion-text,prop,theme,"ios" | "md" | "ionic",undefined,false,false

ion-textarea,scoped
ion-textarea,shadow
ion-textarea,prop,autoGrow,boolean,false,false,true
ion-textarea,prop,autocapitalize,string,'none',false,false
ion-textarea,prop,autofocus,boolean,false,false,false
Expand All @@ -2447,7 +2447,7 @@ ion-textarea,prop,mode,"ios" | "md",undefined,false,false
ion-textarea,prop,name,string,this.inputId,false,false
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,required,boolean,false,false,true
ion-textarea,prop,rows,number | 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
Expand Down Expand Up @@ -2515,6 +2515,14 @@ ion-textarea,css-prop,--placeholder-font-weight,md
ion-textarea,css-prop,--placeholder-opacity,ionic
ion-textarea,css-prop,--placeholder-opacity,ios
ion-textarea,css-prop,--placeholder-opacity,md
ion-textarea,part,bottom
ion-textarea,part,container
ion-textarea,part,counter
ion-textarea,part,error-text
ion-textarea,part,helper-text
ion-textarea,part,label
ion-textarea,part,native
ion-textarea,part,supporting-text

ion-thumbnail,shadow
ion-thumbnail,prop,mode,"ios" | "md",undefined,false,false
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -157,12 +157,12 @@ configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, screenshot, co
await page.setContent(
`
<style>
ion-textarea.custom-textarea.md .textarea-bottom .helper-text {
ion-textarea::part(helper-text) {
font-size: 20px;
color: green;
}
</style>
<ion-textarea class="custom-textarea" label="Label" helper-text="Helper text"></ion-textarea>
<ion-textarea label="Label" helper-text="Helper text"></ion-textarea>
`,
config
);
Expand All @@ -174,12 +174,12 @@ configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, screenshot, co
await page.setContent(
`
<style>
ion-textarea.custom-textarea.md .textarea-bottom .error-text {
ion-textarea::part(error-text) {
font-size: 20px;
color: purple;
}
</style>
<ion-textarea class="ion-invalid ion-touched custom-textarea" label="Label" error-text="Error text"></ion-textarea>
<ion-textarea class="ion-invalid ion-touched" label="Label" error-text="Error text"></ion-textarea>
`,
config
);
Expand All @@ -193,11 +193,11 @@ configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, screenshot, co
await page.setContent(
`
<style>
ion-textarea.custom-textarea {
ion-textarea {
--highlight-color-invalid: purple;
}
</style>
<ion-textarea class="ion-invalid ion-touched custom-textarea" label="Label" error-text="Error text"></ion-textarea>
<ion-textarea class="ion-invalid ion-touched" label="Label" error-text="Error text"></ion-textarea>
`,
config
);
Expand Down
222 changes: 222 additions & 0 deletions core/src/components/textarea/test/custom/textarea.e2e.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
import { expect } from '@playwright/test';
import { configs, test } from '@utils/test/playwright';

/**
* This behavior does not vary across modes/directions
*/
configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, config }) => {
test.describe(title('textarea: custom'), () => {
test('should allow styling the container part', async ({ page }) => {
await page.setContent(
`
<style>
ion-textarea::part(container) {
background-color: blue;
}
</style>

<ion-textarea label="textarea"></ion-textarea>
`,
config
);

const textarea = await page.locator('ion-textarea');
const container = await textarea.evaluate((el: HTMLIonTextareaElement) => {
const containerEl = el.shadowRoot?.querySelector('[part="container"]') as HTMLElement | null;
if (!containerEl) {
return '';
}
return getComputedStyle(containerEl).backgroundColor;
});

expect(container).toBe('rgb(0, 0, 255)');
});

test('should allow styling the label part', async ({ page }) => {
await page.setContent(
`
<style>
ion-textarea::part(label) {
color: green;
}
</style>

<ion-textarea label="Test Label"></ion-textarea>
`,
config
);

const textarea = await page.locator('ion-textarea');
const labelColor = await textarea.evaluate((el: HTMLIonTextareaElement) => {
const labelEl = el.shadowRoot?.querySelector('[part="label"]') as HTMLElement | null;
if (!labelEl) {
return '';
}
return getComputedStyle(labelEl).color;
});

expect(labelColor).toBe('rgb(0, 128, 0)');
});

test('should allow styling the native textarea', async ({ page }) => {
await page.setContent(
`
<style>
ion-textarea::part(native) {
color: red;
}
</style>

<ion-textarea label="textarea"></ion-textarea>
`,
config
);

const textarea = await page.locator('ion-textarea');
const color = await textarea.evaluate(
(el: HTMLIonTextareaElement) =>
getComputedStyle(el.shadowRoot?.querySelector('textarea') as HTMLTextAreaElement).color
);

expect(color).toBe('rgb(255, 0, 0)');
});

test('should allow styling the supporting-text part', async ({ page }) => {
await page.setContent(
`
<style>
ion-textarea::part(supporting-text) {
color: blue;
}
</style>

<ion-textarea label="textarea" helper-text="Helper text"></ion-textarea>
`,
config
);

const textarea = await page.locator('ion-textarea');
await textarea.waitFor();

const supportingTextColor = await textarea.evaluate((el: HTMLIonTextareaElement) => {
// Query for the visible helper-text element which has the supporting-text part
// Use attribute selector that matches space-separated part values
const helperTextEl = el.shadowRoot?.querySelector('[part~="helper-text"]') as HTMLElement | null;
if (!helperTextEl) {
return '';
}
return getComputedStyle(helperTextEl).color;
});

expect(supportingTextColor).toBe('rgb(0, 0, 255)');
});

test('should allow styling the helper-text part', async ({ page }) => {
await page.setContent(
`
<style>
ion-textarea::part(helper-text) {
color: red;
}
</style>

<ion-textarea label="textarea" helper-text="Helper text"></ion-textarea>
`,
config
);

const textarea = await page.locator('ion-textarea');
await textarea.waitFor();

const helperTextColor = await textarea.evaluate((el: HTMLIonTextareaElement) => {
const helperTextEl = el.shadowRoot?.querySelector('[part~="helper-text"]') as HTMLElement | null;
if (!helperTextEl) {
return '';
}
return getComputedStyle(helperTextEl).color;
});

expect(helperTextColor).toBe('rgb(255, 0, 0)');
});

test('should allow styling the error-text part', async ({ page }) => {
await page.setContent(
`
<style>
ion-textarea::part(error-text) {
color: red;
}
</style>

<ion-textarea label="textarea" class="ion-invalid ion-touched" error-text="Error text"></ion-textarea>
`,
config
);

const textarea = await page.locator('ion-textarea');
await textarea.waitFor();

const errorTextColor = await textarea.evaluate((el: HTMLIonTextareaElement) => {
const errorTextEl = el.shadowRoot?.querySelector('[part~="error-text"]') as HTMLElement | null;
if (!errorTextEl) {
return '';
}
return getComputedStyle(errorTextEl).color;
});

expect(errorTextColor).toBe('rgb(255, 0, 0)');
});

test('should allow styling the counter part', async ({ page }) => {
await page.setContent(
`
<style>
ion-textarea::part(counter) {
color: green;
}
</style>

<ion-textarea label="textarea" counter="true" maxlength="100"></ion-textarea>
`,
config
);

const textarea = await page.locator('ion-textarea');
const counterColor = await textarea.evaluate((el: HTMLIonTextareaElement) => {
const counterEl = el.shadowRoot?.querySelector('[part="counter"]') as HTMLElement | null;
if (!counterEl) {
return '';
}
return getComputedStyle(counterEl).color;
});

expect(counterColor).toBe('rgb(0, 128, 0)');
});

test('should allow styling the bottom part', async ({ page }) => {
await page.setContent(
`
<style>
ion-textarea::part(bottom) {
background-color: blue;
}
</style>

<ion-textarea label="textarea" helper-text="Helper text"></ion-textarea>
`,
config
);

const textarea = await page.locator('ion-textarea');
const bottomBgColor = await textarea.evaluate((el: HTMLIonTextareaElement) => {
const bottomEl = el.shadowRoot?.querySelector('[part="bottom"]') as HTMLElement | null;
if (!bottomEl) {
return '';
}
return getComputedStyle(bottomEl).backgroundColor;
});

expect(bottomBgColor).toBe('rgb(0, 0, 255)');
});
});
});
77 changes: 77 additions & 0 deletions core/src/components/textarea/test/form/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
<meta charset="UTF-8" />
<title>Textarea - Form</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>
.required-label {
display: none;
}

.textarea-required .required-label {
display: inline;
color: red;
}
</style>
</head>

<body>
<ion-app>
<ion-header>
<ion-toolbar>
<ion-title>Textarea - Form</ion-title>
</ion-toolbar>
</ion-header>

<ion-content id="content" class="ion-padding">
<form onsubmit="return onSubmit(event)">
<ion-textarea
fill="outline"
label-placement="stacked"
placeholder="Placeholder"
helper-text="Helper message"
counter="true"
maxlength="999"
>
<div slot="label">Label <span class="required-label">(Required) *</span></div>
<ion-icon slot="end" name="square-outline"></ion-icon>
</ion-textarea>

<p>
Click the first button below to toggle the required prop and then click the submit button to attempt to
submit the form. It should show a popup warning to fill out the field when textarea is required and empty.
When the textarea is not required, or the field is filled, the form should submit by sending a console log.
</p>
<button type="button" onClick="toggleRequired()">Toggle Required</button>
<button type="submit">Submit</button>
</form>
</ion-content>
</ion-app>

<script>
function onSubmit(event) {
event.preventDefault();
const textarea = document.querySelector('ion-textarea');
console.log('Form submitted with value:', textarea.value);
return true;
}

function toggleRequired() {
const textarea = document.querySelector('ion-textarea');
textarea.required = textarea.required ? false : true;
textarea.classList.toggle('textarea-required', textarea.required);
console.log('Textarea required set to:', textarea.required);
}
</script>
</body>
</html>
Loading
Loading