Skip to content

Commit cb31b86

Browse files
authored
feat(textarea): add workaround for dynamic slot content (#27661)
1 parent 884fecd commit cb31b86

File tree

6 files changed

+92
-1
lines changed

6 files changed

+92
-1
lines changed

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

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,3 +240,30 @@ configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, screenshot, co
240240
});
241241
});
242242
});
243+
244+
configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, screenshot, config }) => {
245+
test.describe(title('textarea: async label'), () => {
246+
test('textarea should re-render when label slot is added async', async ({ page }) => {
247+
await page.setContent(
248+
`
249+
<ion-textarea fill="solid" label-placement="stacked" placeholder="Text Input"></ion-textarea>
250+
`,
251+
config
252+
);
253+
254+
const textarea = page.locator('ion-textarea');
255+
256+
await textarea.evaluate((el: HTMLIonInputElement) => {
257+
const labelEl = document.createElement('div');
258+
labelEl.slot = 'label';
259+
labelEl.innerHTML = 'Comments <span class="required" style="color: red">*</span';
260+
261+
el.appendChild(labelEl);
262+
});
263+
264+
await page.waitForChanges();
265+
266+
expect(await textarea.screenshot()).toMatchSnapshot(screenshot(`textarea-async-label`));
267+
});
268+
});
269+
});

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

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,8 +91,49 @@ <h2>Outline / Floating</h2>
9191
<div slot="label">Email <span class="required">*</span></div>
9292
</ion-textarea>
9393
</div>
94+
95+
<div class="grid-item">
96+
<h2>Outline / Floating / Async</h2>
97+
<ion-textarea id="solid-async" label-placement="floating" fill="outline" value="[email protected]"></ion-textarea>
98+
</div>
9499
</div>
100+
101+
<ion-button onclick="addSlot()">Add Slotted Content</ion-button>
102+
<ion-button onclick="updateSlot()">Update Slotted Content</ion-button>
103+
<ion-button onclick="removeSlot()">Remove Slotted Content</ion-button>
95104
</ion-content>
96105
</ion-app>
106+
107+
<script>
108+
const solidAsync = document.querySelector('#solid-async');
109+
110+
const getSlottedContent = () => {
111+
return solidAsync.querySelector('[slot="label"]');
112+
};
113+
114+
const addSlot = () => {
115+
if (getSlottedContent() === null) {
116+
const labelEl = document.createElement('div');
117+
labelEl.slot = 'label';
118+
labelEl.innerHTML = 'Comments <span class="required">*</span>';
119+
120+
solidAsync.appendChild(labelEl);
121+
}
122+
};
123+
124+
const removeSlot = () => {
125+
if (getSlottedContent() !== null) {
126+
solidAsync.querySelector('[slot="label"]').remove();
127+
}
128+
};
129+
130+
const updateSlot = () => {
131+
const slottedContent = getSlottedContent();
132+
133+
if (slottedContent !== null) {
134+
slottedContent.textContent = 'This is my really really really long text';
135+
}
136+
};
137+
</script>
97138
</body>
98139
</html>

core/src/components/textarea/textarea.tsx

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,25 @@
11
import type { ComponentInterface, EventEmitter } from '@stencil/core';
2-
import { Build, Component, Element, Event, Host, Method, Prop, State, Watch, h, writeTask } from '@stencil/core';
2+
import {
3+
Build,
4+
Component,
5+
Element,
6+
Event,
7+
Host,
8+
Method,
9+
Prop,
10+
State,
11+
Watch,
12+
forceUpdate,
13+
h,
14+
writeTask,
15+
} from '@stencil/core';
316
import type { LegacyFormController, NotchController } from '@utils/forms';
417
import { createLegacyFormController, createNotchController } from '@utils/forms';
518
import type { Attributes } from '@utils/helpers';
619
import { inheritAriaAttributes, debounceEvent, findItemLabel, inheritAttributes } from '@utils/helpers';
720
import { printIonWarning } from '@utils/logging';
21+
import { createSlotMutationController } from '@utils/slot-mutation-controller';
22+
import type { SlotMutationController } from '@utils/slot-mutation-controller';
823
import { createColorClasses, hostContext } from '@utils/theme';
924

1025
import { getIonMode } from '../../global/ionic-global';
@@ -42,6 +57,8 @@ export class Textarea implements ComponentInterface {
4257
private legacyFormController!: LegacyFormController;
4358
private notchSpacerEl: HTMLElement | undefined;
4459

60+
private slotMutationController?: SlotMutationController;
61+
4562
private notchController?: NotchController;
4663

4764
// This flag ensures we log the deprecation warning at most once.
@@ -295,6 +312,7 @@ export class Textarea implements ComponentInterface {
295312
connectedCallback() {
296313
const { el } = this;
297314
this.legacyFormController = createLegacyFormController(el);
315+
this.slotMutationController = createSlotMutationController(el, 'label', () => forceUpdate(this));
298316
this.notchController = createNotchController(
299317
el,
300318
() => this.notchSpacerEl,
@@ -320,6 +338,11 @@ export class Textarea implements ComponentInterface {
320338
);
321339
}
322340

341+
if (this.slotMutationController) {
342+
this.slotMutationController.destroy();
343+
this.slotMutationController = undefined;
344+
}
345+
323346
if (this.notchController) {
324347
this.notchController.destroy();
325348
this.notchController = undefined;

0 commit comments

Comments
 (0)