Skip to content

Commit de36cc8

Browse files
averyjohnstonIonitronthetaPC
authored
feat(textarea): add start and end slots (#28441)
Issue number: Part of #26297 --------- <!-- Please do not submit updates to dependencies unless it fixes an issue. --> <!-- Please try to limit your pull request to one type (bugfix, feature, etc). Submit multiple pull requests if needed. --> ## What is the current behavior? <!-- Please describe the current behavior that you are modifying. --> With the modern form control syntax, it is not possible to add icon buttons or other decorators to the sides of `ion-textarea`, as you can with `ion-item`. ## What is the new behavior? <!-- Please describe the behavior or changes that are being added by this PR. --> `start` and `end` slots added. While making this change, I also tweaked the CSS selectors responsible for translating the label above the input with `"stacked"` and `"floating"` placements, merging this logic into a single class managed by the TSX. I needed to add a new class for whether slot content is present, so the selectors were getting unwieldy otherwise. Docs PR TBA; I plan on knocking out all three components at once when the features are all complete, to make dev builds easier to manage. ## Does this introduce a breaking change? - [ ] Yes - [x] No <!-- If this introduces a breaking change, please describe the impact and migration path for existing applications below. --> ## Other information <!-- Any other information that is important to this PR such as screenshots of how the component looks before and after the change. --> --------- Co-authored-by: ionitron <[email protected]> Co-authored-by: Maria Hutt <[email protected]>
1 parent 3648520 commit de36cc8

File tree

31 files changed

+390
-63
lines changed

31 files changed

+390
-63
lines changed

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

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,15 @@ <h1>Textarea - a11y</h1>
2121
<ion-textarea label="Email" label-placement="stacked" value="[email protected]"></ion-textarea>
2222
<ion-textarea label="Email" label-placement="floating"></ion-textarea>
2323
<ion-textarea label="Email" label-placement="floating" fill="outline" value="[email protected]"></ion-textarea> <br />
24-
<ion-textarea label="Email" label-placement="floating" fill="solid" value="[email protected]"></ion-textarea>
24+
<ion-textarea label="Email" label-placement="floating" fill="solid" value="[email protected]"></ion-textarea><br />
25+
<ion-textarea label="Email" label-placement="floating" fill="solid" value="[email protected]">
26+
<ion-button slot="start" aria-label="button">
27+
<ion-icon slot="icon-only" name="lock-closed" aria-hidden="true"></ion-icon>
28+
</ion-button>
29+
<ion-button slot="end" aria-label="button">
30+
<ion-icon slot="icon-only" name="lock-closed" aria-hidden="true"></ion-icon>
31+
</ion-button>
32+
</ion-textarea>
2533
</main>
2634
</body>
2735
</html>

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

Lines changed: 181 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -51,51 +51,184 @@
5151
<ion-content id="content" class="ion-padding">
5252
<div class="grid">
5353
<div class="grid-item">
54-
<h2>No Fill / Start</h2>
54+
<h2>No Fill / Start Label</h2>
5555
<ion-textarea label-placement="start" value="[email protected]">
5656
<div slot="label">Email <span class="required">*</span></div>
5757
</ion-textarea>
5858
</div>
5959

6060
<div class="grid-item">
61-
<h2>Solid / Start</h2>
61+
<h2>Solid / Start Label</h2>
6262
<ion-textarea label-placement="start" fill="solid" value="[email protected]">
6363
<div slot="label">Email <span class="required">*</span></div>
6464
</ion-textarea>
6565
</div>
6666

6767
<div class="grid-item">
68-
<h2>Outline / Start</h2>
68+
<h2>Outline / Start Label</h2>
6969
<ion-textarea label-placement="start" fill="outline" value="[email protected]">
7070
<div slot="label">Email <span class="required">*</span></div>
7171
</ion-textarea>
7272
</div>
7373

7474
<div class="grid-item">
75-
<h2>No Fill / Floating</h2>
75+
<h2>No Fill / Floating Label</h2>
7676
<ion-textarea label-placement="floating" value="[email protected]">
7777
<div slot="label">Email <span class="required">*</span></div>
7878
</ion-textarea>
7979
</div>
8080

8181
<div class="grid-item">
82-
<h2>Solid / Floating</h2>
82+
<h2>Solid / Floating Label</h2>
8383
<ion-textarea label-placement="floating" fill="solid" value="[email protected]">
8484
<div slot="label">Email <span class="required">*</span></div>
8585
</ion-textarea>
8686
</div>
8787

8888
<div class="grid-item">
89-
<h2>Outline / Floating</h2>
89+
<h2>Outline / Floating Label</h2>
9090
<ion-textarea label-placement="floating" fill="outline" value="[email protected]">
9191
<div slot="label">Email <span class="required">*</span></div>
9292
</ion-textarea>
9393
</div>
9494

9595
<div class="grid-item">
96-
<h2>Outline / Floating / Async</h2>
96+
<h2>No Fill / Start Label / Buttons</h2>
97+
<ion-textarea label-placement="start" value="[email protected]" label="Email">
98+
<ion-button fill="clear" slot="start" aria-label="Lock">
99+
<ion-icon slot="icon-only" name="lock-closed" aria-hidden="true"></ion-icon>
100+
</ion-button>
101+
<ion-button fill="clear" slot="end" aria-label="Show/hide password">
102+
<ion-icon slot="icon-only" name="eye" aria-hidden="true"></ion-icon>
103+
</ion-button>
104+
</ion-textarea>
105+
</div>
106+
107+
<div class="grid-item">
108+
<h2>Solid / Start Label / Buttons</h2>
109+
<ion-textarea label-placement="start" fill="solid" value="[email protected]" label="Email">
110+
<ion-button fill="clear" slot="start" aria-label="Lock">
111+
<ion-icon slot="icon-only" name="lock-closed" aria-hidden="true"></ion-icon>
112+
</ion-button>
113+
<ion-button fill="clear" slot="end" aria-label="Show/hide password">
114+
<ion-icon slot="icon-only" name="eye" aria-hidden="true"></ion-icon>
115+
</ion-button>
116+
</ion-textarea>
117+
</div>
118+
119+
<div class="grid-item">
120+
<h2>Outline / Start Label / Buttons</h2>
121+
<ion-textarea label-placement="start" fill="outline" value="[email protected]" label="Email">
122+
<ion-button fill="clear" slot="start" aria-label="Lock">
123+
<ion-icon slot="icon-only" name="lock-closed" aria-hidden="true"></ion-icon>
124+
</ion-button>
125+
<ion-button fill="clear" slot="end" aria-label="Show/hide password">
126+
<ion-icon slot="icon-only" name="eye" aria-hidden="true"></ion-icon>
127+
</ion-button>
128+
</ion-textarea>
129+
</div>
130+
131+
<div class="grid-item">
132+
<h2>No Fill / Floating Label / Buttons</h2>
133+
<ion-textarea label-placement="floating" value="[email protected]" label="Email">
134+
<ion-button fill="clear" slot="start" aria-label="Lock">
135+
<ion-icon slot="icon-only" name="lock-closed" aria-hidden="true"></ion-icon>
136+
</ion-button>
137+
<ion-button fill="clear" slot="end" aria-label="Show/hide password">
138+
<ion-icon slot="icon-only" name="eye" aria-hidden="true"></ion-icon>
139+
</ion-button>
140+
</ion-textarea>
141+
</div>
142+
143+
<div class="grid-item">
144+
<h2>Solid / Floating Label / Buttons</h2>
145+
<ion-textarea label-placement="floating" fill="solid" value="[email protected]" label="Email">
146+
<ion-button fill="clear" slot="start" aria-label="Lock">
147+
<ion-icon slot="icon-only" name="lock-closed" aria-hidden="true"></ion-icon>
148+
</ion-button>
149+
<ion-button fill="clear" slot="end" aria-label="Show/hide password">
150+
<ion-icon slot="icon-only" name="eye" aria-hidden="true"></ion-icon>
151+
</ion-button>
152+
</ion-textarea>
153+
</div>
154+
155+
<div class="grid-item">
156+
<h2>Outline / Floating Label / Buttons</h2>
157+
<ion-textarea label-placement="floating" fill="outline" value="[email protected]" label="Email">
158+
<ion-button fill="clear" slot="start" aria-label="Lock">
159+
<ion-icon slot="icon-only" name="lock-closed" aria-hidden="true"></ion-icon>
160+
</ion-button>
161+
<ion-button fill="clear" slot="end" aria-label="Show/hide password">
162+
<ion-icon slot="icon-only" name="eye" aria-hidden="true"></ion-icon>
163+
</ion-button>
164+
</ion-textarea>
165+
</div>
166+
167+
<div class="grid-item">
168+
<h2>No Fill / Start Label / Decorations</h2>
169+
<ion-textarea label-placement="start" value="100" label="Weight">
170+
<ion-icon slot="start" name="barbell" aria-hidden="true"></ion-icon>
171+
<ion-label slot="end">lbs</ion-label>
172+
</ion-textarea>
173+
</div>
174+
175+
<div class="grid-item">
176+
<h2>Solid / Start Label / Decorations</h2>
177+
<ion-textarea label-placement="start" fill="solid" value="100" label="Weight">
178+
<ion-icon slot="start" name="barbell" aria-hidden="true"></ion-icon>
179+
<ion-label slot="end">lbs</ion-label>
180+
</ion-textarea>
181+
</div>
182+
183+
<div class="grid-item">
184+
<h2>Outline / Start Label / Decorations</h2>
185+
<ion-textarea label-placement="start" fill="outline" value="100" label="Weight">
186+
<ion-icon slot="start" name="barbell" aria-hidden="true"></ion-icon>
187+
<ion-label slot="end">lbs</ion-label>
188+
</ion-textarea>
189+
</div>
190+
191+
<div class="grid-item">
192+
<h2>No Fill / Floating Label / Decorations</h2>
193+
<ion-textarea label-placement="floating" value="100" label="Weight">
194+
<ion-icon slot="start" name="barbell" aria-hidden="true"></ion-icon>
195+
<ion-label slot="end">lbs</ion-label>
196+
</ion-textarea>
197+
</div>
198+
199+
<div class="grid-item">
200+
<h2>Solid / Floating Label / Decorations</h2>
201+
<ion-textarea label-placement="floating" fill="solid" value="100" label="Weight">
202+
<ion-icon slot="start" name="barbell" aria-hidden="true"></ion-icon>
203+
<ion-label slot="end">lbs</ion-label>
204+
</ion-textarea>
205+
</div>
206+
207+
<div class="grid-item">
208+
<h2>Outline / Floating Label / Decorations</h2>
209+
<ion-textarea label-placement="floating" fill="outline" value="100" label="Weight">
210+
<ion-icon slot="start" name="barbell" aria-hidden="true"></ion-icon>
211+
<ion-label slot="end">lbs</ion-label>
212+
</ion-textarea>
213+
</div>
214+
215+
<div class="grid-item">
216+
<h2>Outline / Async Label</h2>
97217
<ion-textarea id="solid-async" label-placement="floating" fill="outline" value="[email protected]"></ion-textarea>
98218
</div>
219+
220+
<div class="grid-item">
221+
<h2>Outline / Async Decorations</h2>
222+
<ion-textarea id="async-decorations" label-placement="floating" fill="outline" label="Email"></ion-textarea>
223+
</div>
224+
225+
<div class="grid-item">
226+
<h2>Outline / Autogrow / Decorations</h2>
227+
<ion-textarea label-placement="start" fill="outline" label="Email" auto-grow="true" value="[email protected]">
228+
<ion-icon slot="start" name="barbell" aria-hidden="true"></ion-icon>
229+
<ion-label slot="end">lbs</ion-label>
230+
</ion-textarea>
231+
</div>
99232
</div>
100233

101234
<ion-button onclick="addSlot()">Add Slotted Content</ion-button>
@@ -106,29 +239,65 @@ <h2>Outline / Floating / Async</h2>
106239

107240
<script>
108241
const solidAsync = document.querySelector('#solid-async');
242+
const asyncDecos = document.querySelector('#async-decorations');
109243

110-
const getSlottedContent = () => {
244+
const getSlottedLabel = () => {
111245
return solidAsync.querySelector('[slot="label"]');
112246
};
113247

248+
const getSlottedStartDeco = () => {
249+
return asyncDecos.querySelector('[slot="start"]');
250+
};
251+
252+
const getSlottedEndDeco = () => {
253+
return asyncDecos.querySelector('[slot="end"]');
254+
};
255+
114256
const addSlot = () => {
115-
if (getSlottedContent() === null) {
257+
if (getSlottedLabel() === null) {
116258
const labelEl = document.createElement('div');
117259
labelEl.slot = 'label';
118260
labelEl.innerHTML = 'Comments <span class="required">*</span>';
119261

120262
solidAsync.appendChild(labelEl);
121263
}
264+
265+
if (getSlottedStartDeco() === null) {
266+
const startEl = document.createElement('div');
267+
startEl.slot = 'start';
268+
startEl.innerHTML = 'Start';
269+
270+
asyncDecos.insertAdjacentElement('afterbegin', startEl);
271+
}
272+
273+
if (getSlottedEndDeco() === null) {
274+
const endEl = document.createElement('div');
275+
endEl.slot = 'end';
276+
endEl.innerHTML = 'End';
277+
278+
asyncDecos.insertAdjacentElement('beforeend', endEl);
279+
}
122280
};
123281

124282
const removeSlot = () => {
125-
if (getSlottedContent() !== null) {
126-
solidAsync.querySelector('[slot="label"]').remove();
283+
const slottedLabel = getSlottedLabel();
284+
if (slottedLabel !== null) {
285+
slottedLabel.remove();
286+
}
287+
288+
const slottedStartDeco = getSlottedStartDeco();
289+
if (slottedStartDeco !== null) {
290+
slottedStartDeco.remove();
291+
}
292+
293+
const slottedEndDeco = getSlottedEndDeco();
294+
if (slottedEndDeco !== null) {
295+
slottedEndDeco.remove();
127296
}
128297
};
129298

130299
const updateSlot = () => {
131-
const slottedContent = getSlottedContent();
300+
const slottedContent = getSlottedLabel();
132301

133302
if (slottedContent !== null) {
134303
slottedContent.textContent = 'This is my really really really long text';
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import { expect } from '@playwright/test';
2+
import { configs, test } from '@utils/test/playwright';
3+
4+
configs().forEach(({ title, screenshot, config }) => {
5+
test.describe(title('textarea: start and end slots (visual checks)'), () => {
6+
test('should not have visual regressions with a start-positioned label', async ({ page }) => {
7+
await page.setContent(
8+
`
9+
<ion-textarea label-placement="start" fill="solid" value="100" label="Weight">
10+
<ion-icon slot="start" name="barbell" aria-hidden="true"></ion-icon>
11+
<ion-label slot="end">lbs</ion-label>
12+
</ion-textarea>
13+
`,
14+
config
15+
);
16+
17+
const textarea = page.locator('ion-textarea');
18+
await expect(textarea).toHaveScreenshot(screenshot(`textarea-slots-label-start`));
19+
});
20+
21+
test('should not have visual regressions with a floating label', async ({ page }) => {
22+
await page.setContent(
23+
`
24+
<ion-textarea label-placement="floating" fill="solid" value="100" label="Weight">
25+
<ion-icon slot="start" name="barbell" aria-hidden="true"></ion-icon>
26+
<ion-label slot="end">lbs</ion-label>
27+
</ion-textarea>
28+
`,
29+
config
30+
);
31+
32+
const textarea = page.locator('ion-textarea');
33+
await expect(textarea).toHaveScreenshot(screenshot(`textarea-slots-label-floating`));
34+
});
35+
});
36+
});
37+
38+
configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, config }) => {
39+
test.describe(title('textarea: start and end slots (functionality checks)'), () => {
40+
test('should raise floating label when there is content in the start slot', async ({ page }) => {
41+
await page.setContent(
42+
`
43+
<ion-textarea label-placement="floating" fill="solid" label="Weight">
44+
<ion-icon slot="start" name="barbell" aria-hidden="true"></ion-icon>
45+
</ion-textarea>
46+
`,
47+
config
48+
);
49+
50+
const textarea = page.locator('ion-textarea');
51+
await expect(textarea).toHaveClass(/label-floating/);
52+
});
53+
54+
test('should raise floating label when there is content in the end slot', async ({ page }) => {
55+
await page.setContent(
56+
`
57+
<ion-textarea label-placement="floating" fill="solid" label="Weight">
58+
<ion-icon slot="end" name="barbell" aria-hidden="true"></ion-icon>
59+
</ion-textarea>
60+
`,
61+
config
62+
);
63+
64+
const textarea = page.locator('ion-textarea');
65+
await expect(textarea).toHaveClass(/label-floating/);
66+
});
67+
});
68+
});

core/src/components/textarea/textarea.md.outline.scss

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -89,9 +89,7 @@
8989
/**
9090
* This makes the label sit above the textarea.
9191
*/
92-
:host(.has-focus.textarea-fill-outline.textarea-label-placement-floating) .label-text-wrapper,
93-
:host(.has-value.textarea-fill-outline.textarea-label-placement-floating) .label-text-wrapper,
94-
:host(.textarea-fill-outline.textarea-label-placement-stacked) .label-text-wrapper {
92+
:host(.label-floating.textarea-fill-outline) .label-text-wrapper {
9593
@include transform(translateY(-32%), scale(#{$form-control-label-stacked-scale}));
9694
@include margin(0);
9795

@@ -116,6 +114,13 @@
116114
@include margin(12px, 0px, 0px, 0px);
117115
}
118116

117+
:host(.textarea-fill-outline.textarea-label-placement-stacked) ::slotted([slot="start"]),
118+
:host(.textarea-fill-outline.textarea-label-placement-stacked) ::slotted([slot="end"]),
119+
:host(.textarea-fill-outline.textarea-label-placement-floating) ::slotted([slot="start"]),
120+
:host(.textarea-fill-outline.textarea-label-placement-floating) ::slotted([slot="end"]) {
121+
margin-top: 12px;
122+
}
123+
119124
// Textarea Fill: Outline Outline Container
120125
// ----------------------------------------------------------------
121126

@@ -220,8 +225,6 @@
220225
* the floating/stacked label. We simulate this "cut out"
221226
* by removing the top border from the notch fragment.
222227
*/
223-
:host(.has-focus.textarea-fill-outline.textarea-label-placement-floating) .textarea-outline-notch,
224-
:host(.has-value.textarea-fill-outline.textarea-label-placement-floating) .textarea-outline-notch,
225-
:host(.textarea-fill-outline.textarea-label-placement-stacked) .textarea-outline-notch {
228+
:host(.label-floating.textarea-fill-outline) .textarea-outline-notch {
226229
border-top: none;
227230
}

core/src/components/textarea/textarea.md.solid.scss

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -67,9 +67,7 @@
6767
// Textarea Label
6868
// ----------------------------------------------------------------
6969

70-
:host(.textarea-fill-solid.textarea-label-placement-stacked) .label-text-wrapper,
71-
:host(.has-focus.textarea-fill-solid.textarea-label-placement-floating) .label-text-wrapper,
72-
:host(.has-value.textarea-fill-solid.textarea-label-placement-floating) .label-text-wrapper {
70+
:host(.label-floating.textarea-fill-solid) .label-text-wrapper {
7371
/**
7472
* Label text should not extend
7573
* beyond the bounds of the textarea.

0 commit comments

Comments
 (0)