Skip to content

Commit 5a35918

Browse files
authored
feat(textarea): add label slot (#27647)
1 parent cea8a22 commit 5a35918

File tree

49 files changed

+261
-37
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

49 files changed

+261
-37
lines changed

core/src/components.d.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2987,7 +2987,7 @@ export namespace Components {
29872987
*/
29882988
"inputmode"?: 'none' | 'text' | 'tel' | 'url' | 'email' | 'numeric' | 'decimal' | 'search';
29892989
/**
2990-
* The visible label associated with the textarea.
2990+
* The visible label associated with the textarea. Use this if you need to render a plaintext label. The `label` property will take priority over the `label` slot if both are used.
29912991
*/
29922992
"label"?: string;
29932993
/**
@@ -7083,7 +7083,7 @@ declare namespace LocalJSX {
70837083
*/
70847084
"inputmode"?: 'none' | 'text' | 'tel' | 'url' | 'email' | 'numeric' | 'decimal' | 'search';
70857085
/**
7086-
* The visible label associated with the textarea.
7086+
* The visible label associated with the textarea. Use this if you need to render a plaintext label. The `label` property will take priority over the `label` slot if both are used.
70877087
*/
70887088
"label"?: string;
70897089
/**

core/src/components/textarea/test/a11y/index.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
<main>
1616
<h1>Textarea - a11y</h1>
1717

18+
<ion-textarea><div slot="label">Slotted Label</div></ion-textarea><br />
1819
<ion-textarea label="my label"></ion-textarea><br />
1920
<ion-textarea aria-label="my aria label"></ion-textarea><br />
2021
<ion-textarea label="Email" label-placement="stacked" value="[email protected]"></ion-textarea>

core/src/components/textarea/test/fill/textarea.e2e.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,3 +180,19 @@ configs({ modes: ['md'] }).forEach(({ title, screenshot, config }) => {
180180
});
181181
});
182182
});
183+
184+
configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, config }) => {
185+
test.describe(title('textarea: notch cutout'), () => {
186+
test('notch cutout should be hidden when no label is passed', async ({ page }) => {
187+
await page.setContent(
188+
`
189+
<ion-textarea fill="outline" label-placement="stacked" aria-label="my textarea"></ion-textarea>
190+
`,
191+
config
192+
);
193+
194+
const notchCutout = page.locator('ion-textarea .textarea-outline-notch');
195+
await expect(notchCutout).toBeHidden();
196+
});
197+
});
198+
});

core/src/components/textarea/test/label-placement/textarea.e2e.ts

Lines changed: 29 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -25,18 +25,6 @@ configs().forEach(({ title, screenshot, config }) => {
2525
const textarea = page.locator('ion-textarea');
2626
expect(await textarea.screenshot()).toMatchSnapshot(screenshot(`textarea-placement-start-multi-line-value`));
2727
});
28-
29-
test('label should be truncated', async ({ page }) => {
30-
await page.setContent(
31-
`
32-
<ion-textarea label="Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur." label-placement="start"></ion-textarea>
33-
`,
34-
config
35-
);
36-
37-
const textarea = page.locator('ion-textarea');
38-
expect(await textarea.screenshot()).toMatchSnapshot(screenshot(`textarea-placement-start-label-truncated`));
39-
});
4028
});
4129
test.describe(title('textarea: label placement end'), () => {
4230
test('label should appear on the ending side of the textarea', async ({ page }) => {
@@ -61,17 +49,6 @@ configs().forEach(({ title, screenshot, config }) => {
6149
const textarea = page.locator('ion-textarea');
6250
expect(await textarea.screenshot()).toMatchSnapshot(screenshot(`textarea-placement-end-multi-line-value`));
6351
});
64-
test('label should be truncated', async ({ page }) => {
65-
await page.setContent(
66-
`
67-
<ion-textarea label="Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur." label-placement="end"></ion-textarea>
68-
`,
69-
config
70-
);
71-
72-
const textarea = page.locator('ion-textarea');
73-
expect(await textarea.screenshot()).toMatchSnapshot(screenshot(`textarea-placement-end-label-truncated`));
74-
});
7552
});
7653
test.describe(title('textarea: label placement fixed'), () => {
7754
test('label should appear on the starting side of the textarea and have a fixed width', async ({ page }) => {
@@ -234,3 +211,32 @@ configs().forEach(({ title, screenshot, config }) => {
234211
});
235212
});
236213
});
214+
215+
configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, screenshot, config }) => {
216+
test.describe(title('textarea: label overflow'), () => {
217+
test('label property should be truncated with an ellipsis', async ({ page }) => {
218+
await page.setContent(
219+
`
220+
<ion-textarea label="Label Label Label Label Label" placeholder="Text Input"></ion-textarea>
221+
`,
222+
config
223+
);
224+
225+
const textarea = page.locator('ion-textarea');
226+
expect(await textarea.screenshot()).toMatchSnapshot(screenshot(`textarea-label-truncate`));
227+
});
228+
test('label slot should be truncated with an ellipsis', async ({ page }) => {
229+
await page.setContent(
230+
`
231+
<ion-textarea placeholder="Text Input">
232+
<div slot="label">Label Label Label Label Label</div>
233+
</ion-textarea>
234+
`,
235+
config
236+
);
237+
238+
const textarea = page.locator('ion-textarea');
239+
expect(await textarea.screenshot()).toMatchSnapshot(screenshot(`textarea-label-slot-truncate`));
240+
});
241+
});
242+
});
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
<!DOCTYPE html>
2+
<html lang="en" dir="ltr">
3+
<head>
4+
<meta charset="UTF-8" />
5+
<title>Textarea - Slot</title>
6+
<meta
7+
name="viewport"
8+
content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no"
9+
/>
10+
<link href="../../../../../css/ionic.bundle.css" rel="stylesheet" />
11+
<link href="../../../../../scripts/testing/styles.css" rel="stylesheet" />
12+
<script src="../../../../../scripts/testing/scripts.js"></script>
13+
<script nomodule src="../../../../../dist/ionic/ionic.js"></script>
14+
<script type="module" src="../../../../../dist/ionic/ionic.esm.js"></script>
15+
<style>
16+
.grid {
17+
display: grid;
18+
grid-template-columns: repeat(3, minmax(250px, 1fr));
19+
grid-row-gap: 20px;
20+
grid-column-gap: 20px;
21+
}
22+
h2 {
23+
font-size: 12px;
24+
font-weight: normal;
25+
26+
color: #6f7378;
27+
28+
margin-top: 10px;
29+
}
30+
@media screen and (max-width: 800px) {
31+
.grid {
32+
grid-template-columns: 1fr;
33+
padding: 0;
34+
}
35+
}
36+
37+
.required {
38+
color: red;
39+
}
40+
</style>
41+
</head>
42+
43+
<body>
44+
<ion-app>
45+
<ion-header>
46+
<ion-toolbar>
47+
<ion-title>Textarea - Slot</ion-title>
48+
</ion-toolbar>
49+
</ion-header>
50+
51+
<ion-content id="content" class="ion-padding">
52+
<div class="grid">
53+
<div class="grid-item">
54+
<h2>No Fill / Start</h2>
55+
<ion-textarea label-placement="start" value="[email protected]">
56+
<div slot="label">Email <span class="required">*</span></div>
57+
</ion-textarea>
58+
</div>
59+
60+
<div class="grid-item">
61+
<h2>Solid / Start</h2>
62+
<ion-textarea label-placement="start" fill="solid" value="[email protected]">
63+
<div slot="label">Email <span class="required">*</span></div>
64+
</ion-textarea>
65+
</div>
66+
67+
<div class="grid-item">
68+
<h2>Outline / Start</h2>
69+
<ion-textarea label-placement="start" fill="outline" value="[email protected]">
70+
<div slot="label">Email <span class="required">*</span></div>
71+
</ion-textarea>
72+
</div>
73+
74+
<div class="grid-item">
75+
<h2>No Fill / Floating</h2>
76+
<ion-textarea label-placement="floating" value="[email protected]">
77+
<div slot="label">Email <span class="required">*</span></div>
78+
</ion-textarea>
79+
</div>
80+
81+
<div class="grid-item">
82+
<h2>Solid / Floating</h2>
83+
<ion-textarea label-placement="floating" fill="solid" value="[email protected]">
84+
<div slot="label">Email <span class="required">*</span></div>
85+
</ion-textarea>
86+
</div>
87+
88+
<div class="grid-item">
89+
<h2>Outline / Floating</h2>
90+
<ion-textarea label-placement="floating" fill="outline" value="[email protected]">
91+
<div slot="label">Email <span class="required">*</span></div>
92+
</ion-textarea>
93+
</div>
94+
</div>
95+
</ion-content>
96+
</ion-app>
97+
</body>
98+
</html>

core/src/components/textarea/test/textarea.spec.ts

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,57 @@ it('should inherit attributes', async () => {
1212
expect(nativeEl.getAttribute('tabindex')).toBe('-1');
1313
expect(nativeEl.getAttribute('data-form-type')).toBe('password');
1414
});
15+
16+
/**
17+
* Textarea uses emulated slots, so the internal
18+
* behavior will not exactly match IonSelect's slots.
19+
* For example, Textarea does not render an actual `<slot>` element
20+
* internally, so we do not check for that here. Instead,
21+
* we check to see which label text is being used.
22+
* If Textarea is updated to use Shadow DOM (and therefore native slots),
23+
* then we can update these tests to more closely match the Select tests.
24+
**/
25+
describe('textarea: label rendering', () => {
26+
it('should render label prop if only prop provided', async () => {
27+
const page = await newSpecPage({
28+
components: [Textarea],
29+
html: `
30+
<ion-textarea label="Label Prop Text"></ion-textarea>
31+
`,
32+
});
33+
34+
const textarea = page.body.querySelector('ion-textarea');
35+
36+
const labelText = textarea.querySelector('.label-text-wrapper');
37+
38+
expect(labelText.textContent).toBe('Label Prop Text');
39+
});
40+
it('should render label slot if only slot provided', async () => {
41+
const page = await newSpecPage({
42+
components: [Textarea],
43+
html: `
44+
<ion-textarea><div slot="label">Label Prop Slot</div></ion-textarea>
45+
`,
46+
});
47+
48+
const textarea = page.body.querySelector('ion-textarea');
49+
50+
const labelText = textarea.querySelector('.label-text-wrapper');
51+
52+
expect(labelText.textContent).toBe('Label Prop Slot');
53+
});
54+
it('should render label prop if both prop and slot provided', async () => {
55+
const page = await newSpecPage({
56+
components: [Textarea],
57+
html: `
58+
<ion-textarea label="Label Prop Text"><div slot="label">Label Prop Slot</div></ion-textarea>
59+
`,
60+
});
61+
62+
const textarea = page.body.querySelector('ion-textarea');
63+
64+
const labelText = textarea.querySelector('.label-text-wrapper');
65+
66+
expect(labelText.textContent).toBe('Label Prop Text');
67+
});
68+
});

core/src/components/textarea/textarea.scss

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -62,8 +62,6 @@
6262

6363
font-family: $font-family-base;
6464

65-
white-space: pre-wrap;
66-
6765
z-index: $z-index-item-input;
6866

6967
box-sizing: border-box;
@@ -74,6 +72,8 @@
7472
flex: 1;
7573

7674
background: var(--background);
75+
76+
white-space: pre-wrap;
7777
}
7878

7979
// TODO: FW-2876 - Remove this selector
@@ -131,9 +131,8 @@
131131
outline: none;
132132

133133
background: transparent;
134-
box-sizing: border-box;
135-
resize: none;
136-
appearance: none;
134+
135+
white-space: pre-wrap;
137136

138137
/**
139138
* This ensures the textarea
@@ -145,6 +144,9 @@
145144
* contrast of the textarea.
146145
*/
147146
z-index: 1;
147+
box-sizing: border-box;
148+
resize: none;
149+
appearance: none;
148150

149151
&::placeholder {
150152
@include padding(0);
@@ -159,6 +161,11 @@
159161
}
160162
}
161163

164+
// TODO: FW-2876 - Remove this selector
165+
:host(.legacy-textarea) .native-textarea {
166+
white-space: inherit;
167+
}
168+
162169
// TODO: FW-2876 - Remove this selector
163170
:host(.legacy-textarea) .native-textarea,
164171
:host(.legacy-textarea) .textarea-legacy-wrapper::after {
@@ -455,14 +462,25 @@
455462
* works on block-level elements. A flex item is
456463
* considered blockified (https://www.w3.org/TR/css-display-3/#blockify).
457464
*/
458-
.label-text {
465+
.label-text,
466+
::slotted([slot="label"]) {
459467
text-overflow: ellipsis;
460468

461469
white-space: nowrap;
462470

463471
overflow: hidden;
464472
}
465473

474+
/**
475+
* If no label text is placed into the slot
476+
* then the element should be hidden otherwise
477+
* there will be additional margins added.
478+
*/
479+
.label-text-wrapper-hidden,
480+
.textarea-outline-notch-hidden {
481+
display: none;
482+
}
483+
466484
.textarea-wrapper textarea {
467485
/**
468486
* When the floating label appears on top of the

0 commit comments

Comments
 (0)