diff --git a/core/src/components.d.ts b/core/src/components.d.ts index a85b7cfcfb..45d1be2995 100644 --- a/core/src/components.d.ts +++ b/core/src/components.d.ts @@ -2992,7 +2992,7 @@ export namespace Components { */ "inputmode"?: 'none' | 'text' | 'tel' | 'url' | 'email' | 'numeric' | 'decimal' | 'search'; /** - * The visible label associated with the textarea. + * 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. */ "label"?: string; /** @@ -7090,7 +7090,7 @@ declare namespace LocalJSX { */ "inputmode"?: 'none' | 'text' | 'tel' | 'url' | 'email' | 'numeric' | 'decimal' | 'search'; /** - * The visible label associated with the textarea. + * 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. */ "label"?: string; /** diff --git a/core/src/components/textarea/test/a11y/index.html b/core/src/components/textarea/test/a11y/index.html index 08c3feebf2..f210a9e08c 100644 --- a/core/src/components/textarea/test/a11y/index.html +++ b/core/src/components/textarea/test/a11y/index.html @@ -15,6 +15,7 @@

Textarea - a11y

+
Slotted Label



diff --git a/core/src/components/textarea/test/autogrow/textarea.e2e.ts-snapshots/textarea-autogrow-word-break-ios-ltr-Mobile-Chrome-linux.png b/core/src/components/textarea/test/autogrow/textarea.e2e.ts-snapshots/textarea-autogrow-word-break-ios-ltr-Mobile-Chrome-linux.png index 0792e3e01f..35787b7817 100644 Binary files a/core/src/components/textarea/test/autogrow/textarea.e2e.ts-snapshots/textarea-autogrow-word-break-ios-ltr-Mobile-Chrome-linux.png and b/core/src/components/textarea/test/autogrow/textarea.e2e.ts-snapshots/textarea-autogrow-word-break-ios-ltr-Mobile-Chrome-linux.png differ diff --git a/core/src/components/textarea/test/autogrow/textarea.e2e.ts-snapshots/textarea-autogrow-word-break-ios-ltr-Mobile-Firefox-linux.png b/core/src/components/textarea/test/autogrow/textarea.e2e.ts-snapshots/textarea-autogrow-word-break-ios-ltr-Mobile-Firefox-linux.png index 3743984b7d..81eb3ac5cd 100644 Binary files a/core/src/components/textarea/test/autogrow/textarea.e2e.ts-snapshots/textarea-autogrow-word-break-ios-ltr-Mobile-Firefox-linux.png and b/core/src/components/textarea/test/autogrow/textarea.e2e.ts-snapshots/textarea-autogrow-word-break-ios-ltr-Mobile-Firefox-linux.png differ diff --git a/core/src/components/textarea/test/autogrow/textarea.e2e.ts-snapshots/textarea-autogrow-word-break-ios-ltr-Mobile-Safari-linux.png b/core/src/components/textarea/test/autogrow/textarea.e2e.ts-snapshots/textarea-autogrow-word-break-ios-ltr-Mobile-Safari-linux.png index 4a1e2cad31..8fed0f36bb 100644 Binary files a/core/src/components/textarea/test/autogrow/textarea.e2e.ts-snapshots/textarea-autogrow-word-break-ios-ltr-Mobile-Safari-linux.png and b/core/src/components/textarea/test/autogrow/textarea.e2e.ts-snapshots/textarea-autogrow-word-break-ios-ltr-Mobile-Safari-linux.png differ diff --git a/core/src/components/textarea/test/autogrow/textarea.e2e.ts-snapshots/textarea-autogrow-word-break-md-ltr-Mobile-Chrome-linux.png b/core/src/components/textarea/test/autogrow/textarea.e2e.ts-snapshots/textarea-autogrow-word-break-md-ltr-Mobile-Chrome-linux.png index e805bd9d7c..ceb201c302 100644 Binary files a/core/src/components/textarea/test/autogrow/textarea.e2e.ts-snapshots/textarea-autogrow-word-break-md-ltr-Mobile-Chrome-linux.png and b/core/src/components/textarea/test/autogrow/textarea.e2e.ts-snapshots/textarea-autogrow-word-break-md-ltr-Mobile-Chrome-linux.png differ diff --git a/core/src/components/textarea/test/autogrow/textarea.e2e.ts-snapshots/textarea-autogrow-word-break-md-ltr-Mobile-Firefox-linux.png b/core/src/components/textarea/test/autogrow/textarea.e2e.ts-snapshots/textarea-autogrow-word-break-md-ltr-Mobile-Firefox-linux.png index aaedb9db7d..3b1c5f54ea 100644 Binary files a/core/src/components/textarea/test/autogrow/textarea.e2e.ts-snapshots/textarea-autogrow-word-break-md-ltr-Mobile-Firefox-linux.png and b/core/src/components/textarea/test/autogrow/textarea.e2e.ts-snapshots/textarea-autogrow-word-break-md-ltr-Mobile-Firefox-linux.png differ diff --git a/core/src/components/textarea/test/autogrow/textarea.e2e.ts-snapshots/textarea-autogrow-word-break-md-ltr-Mobile-Safari-linux.png b/core/src/components/textarea/test/autogrow/textarea.e2e.ts-snapshots/textarea-autogrow-word-break-md-ltr-Mobile-Safari-linux.png index 84e3c0abeb..f4eae27310 100644 Binary files a/core/src/components/textarea/test/autogrow/textarea.e2e.ts-snapshots/textarea-autogrow-word-break-md-ltr-Mobile-Safari-linux.png and b/core/src/components/textarea/test/autogrow/textarea.e2e.ts-snapshots/textarea-autogrow-word-break-md-ltr-Mobile-Safari-linux.png differ diff --git a/core/src/components/textarea/test/fill/textarea.e2e.ts b/core/src/components/textarea/test/fill/textarea.e2e.ts index d517fd4130..708b257d76 100644 --- a/core/src/components/textarea/test/fill/textarea.e2e.ts +++ b/core/src/components/textarea/test/fill/textarea.e2e.ts @@ -17,7 +17,7 @@ configs({ modes: ['md'] }).forEach(({ title, screenshot, config }) => { helper-text="Enter your email" maxlength="20" counter="true" - > + > `, config ); @@ -180,3 +180,74 @@ configs({ modes: ['md'] }).forEach(({ title, screenshot, config }) => { }); }); }); + +configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, config }) => { + test.describe(title('textarea: notch cutout'), () => { + test('notch cutout should be hidden when no label is passed', async ({ page }) => { + await page.setContent( + ` + + `, + config + ); + + const notchCutout = page.locator('ion-textarea .textarea-outline-notch'); + await expect(notchCutout).toBeHidden(); + }); + }); +}); + +configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, screenshot, config }) => { + test.describe(title('textarea: label slot'), () => { + test('should render the notch correctly with a slotted label', async ({ page }) => { + await page.setContent( + ` + + +
My Label Content
+
+ `, + config + ); + + const textarea = page.locator('ion-textarea'); + expect(await textarea.screenshot()).toMatchSnapshot(screenshot(`textarea-fill-outline-slotted-label`)); + }); + test('should render the notch correctly with a slotted label after the textarea was originally hidden', async ({ + page, + }) => { + await page.setContent( + ` + + +
My Label Content
+
+ `, + config + ); + + const textarea = page.locator('ion-textarea'); + + await textarea.evaluate((el: HTMLIonSelectElement) => el.style.removeProperty('display')); + + expect(await textarea.screenshot()).toMatchSnapshot(screenshot(`textarea-fill-outline-hidden-slotted-label`)); + }); + }); +}); diff --git a/core/src/components/textarea/test/fill/textarea.e2e.ts-snapshots/textarea-fill-outline-hidden-slotted-label-md-ltr-Mobile-Chrome-linux.png b/core/src/components/textarea/test/fill/textarea.e2e.ts-snapshots/textarea-fill-outline-hidden-slotted-label-md-ltr-Mobile-Chrome-linux.png new file mode 100644 index 0000000000..a2f6303f5c Binary files /dev/null and b/core/src/components/textarea/test/fill/textarea.e2e.ts-snapshots/textarea-fill-outline-hidden-slotted-label-md-ltr-Mobile-Chrome-linux.png differ diff --git a/core/src/components/textarea/test/fill/textarea.e2e.ts-snapshots/textarea-fill-outline-hidden-slotted-label-md-ltr-Mobile-Firefox-linux.png b/core/src/components/textarea/test/fill/textarea.e2e.ts-snapshots/textarea-fill-outline-hidden-slotted-label-md-ltr-Mobile-Firefox-linux.png new file mode 100644 index 0000000000..0a625b6019 Binary files /dev/null and b/core/src/components/textarea/test/fill/textarea.e2e.ts-snapshots/textarea-fill-outline-hidden-slotted-label-md-ltr-Mobile-Firefox-linux.png differ diff --git a/core/src/components/textarea/test/fill/textarea.e2e.ts-snapshots/textarea-fill-outline-hidden-slotted-label-md-ltr-Mobile-Safari-linux.png b/core/src/components/textarea/test/fill/textarea.e2e.ts-snapshots/textarea-fill-outline-hidden-slotted-label-md-ltr-Mobile-Safari-linux.png new file mode 100644 index 0000000000..92dfc995c1 Binary files /dev/null and b/core/src/components/textarea/test/fill/textarea.e2e.ts-snapshots/textarea-fill-outline-hidden-slotted-label-md-ltr-Mobile-Safari-linux.png differ diff --git a/core/src/components/textarea/test/fill/textarea.e2e.ts-snapshots/textarea-fill-outline-slotted-label-md-ltr-Mobile-Chrome-linux.png b/core/src/components/textarea/test/fill/textarea.e2e.ts-snapshots/textarea-fill-outline-slotted-label-md-ltr-Mobile-Chrome-linux.png new file mode 100644 index 0000000000..a2f6303f5c Binary files /dev/null and b/core/src/components/textarea/test/fill/textarea.e2e.ts-snapshots/textarea-fill-outline-slotted-label-md-ltr-Mobile-Chrome-linux.png differ diff --git a/core/src/components/textarea/test/fill/textarea.e2e.ts-snapshots/textarea-fill-outline-slotted-label-md-ltr-Mobile-Firefox-linux.png b/core/src/components/textarea/test/fill/textarea.e2e.ts-snapshots/textarea-fill-outline-slotted-label-md-ltr-Mobile-Firefox-linux.png new file mode 100644 index 0000000000..0a625b6019 Binary files /dev/null and b/core/src/components/textarea/test/fill/textarea.e2e.ts-snapshots/textarea-fill-outline-slotted-label-md-ltr-Mobile-Firefox-linux.png differ diff --git a/core/src/components/textarea/test/fill/textarea.e2e.ts-snapshots/textarea-fill-outline-slotted-label-md-ltr-Mobile-Safari-linux.png b/core/src/components/textarea/test/fill/textarea.e2e.ts-snapshots/textarea-fill-outline-slotted-label-md-ltr-Mobile-Safari-linux.png new file mode 100644 index 0000000000..92dfc995c1 Binary files /dev/null and b/core/src/components/textarea/test/fill/textarea.e2e.ts-snapshots/textarea-fill-outline-slotted-label-md-ltr-Mobile-Safari-linux.png differ diff --git a/core/src/components/textarea/test/fill/textarea.e2e.ts-snapshots/textarea-fill-solid-md-ltr-Mobile-Chrome-linux.png b/core/src/components/textarea/test/fill/textarea.e2e.ts-snapshots/textarea-fill-solid-md-ltr-Mobile-Chrome-linux.png index 04256c0c46..72b85b2f66 100644 Binary files a/core/src/components/textarea/test/fill/textarea.e2e.ts-snapshots/textarea-fill-solid-md-ltr-Mobile-Chrome-linux.png and b/core/src/components/textarea/test/fill/textarea.e2e.ts-snapshots/textarea-fill-solid-md-ltr-Mobile-Chrome-linux.png differ diff --git a/core/src/components/textarea/test/fill/textarea.e2e.ts-snapshots/textarea-fill-solid-md-ltr-Mobile-Firefox-linux.png b/core/src/components/textarea/test/fill/textarea.e2e.ts-snapshots/textarea-fill-solid-md-ltr-Mobile-Firefox-linux.png index 30edbc417f..41fe31ef23 100644 Binary files a/core/src/components/textarea/test/fill/textarea.e2e.ts-snapshots/textarea-fill-solid-md-ltr-Mobile-Firefox-linux.png and b/core/src/components/textarea/test/fill/textarea.e2e.ts-snapshots/textarea-fill-solid-md-ltr-Mobile-Firefox-linux.png differ diff --git a/core/src/components/textarea/test/fill/textarea.e2e.ts-snapshots/textarea-fill-solid-md-ltr-Mobile-Safari-linux.png b/core/src/components/textarea/test/fill/textarea.e2e.ts-snapshots/textarea-fill-solid-md-ltr-Mobile-Safari-linux.png index 5481392476..b93dda953e 100644 Binary files a/core/src/components/textarea/test/fill/textarea.e2e.ts-snapshots/textarea-fill-solid-md-ltr-Mobile-Safari-linux.png and b/core/src/components/textarea/test/fill/textarea.e2e.ts-snapshots/textarea-fill-solid-md-ltr-Mobile-Safari-linux.png differ diff --git a/core/src/components/textarea/test/fill/textarea.e2e.ts-snapshots/textarea-fill-solid-md-rtl-Mobile-Chrome-linux.png b/core/src/components/textarea/test/fill/textarea.e2e.ts-snapshots/textarea-fill-solid-md-rtl-Mobile-Chrome-linux.png index 4840ab7475..dde66277fc 100644 Binary files a/core/src/components/textarea/test/fill/textarea.e2e.ts-snapshots/textarea-fill-solid-md-rtl-Mobile-Chrome-linux.png and b/core/src/components/textarea/test/fill/textarea.e2e.ts-snapshots/textarea-fill-solid-md-rtl-Mobile-Chrome-linux.png differ diff --git a/core/src/components/textarea/test/fill/textarea.e2e.ts-snapshots/textarea-fill-solid-md-rtl-Mobile-Firefox-linux.png b/core/src/components/textarea/test/fill/textarea.e2e.ts-snapshots/textarea-fill-solid-md-rtl-Mobile-Firefox-linux.png index 8e0e574d13..ee850d070e 100644 Binary files a/core/src/components/textarea/test/fill/textarea.e2e.ts-snapshots/textarea-fill-solid-md-rtl-Mobile-Firefox-linux.png and b/core/src/components/textarea/test/fill/textarea.e2e.ts-snapshots/textarea-fill-solid-md-rtl-Mobile-Firefox-linux.png differ diff --git a/core/src/components/textarea/test/fill/textarea.e2e.ts-snapshots/textarea-fill-solid-md-rtl-Mobile-Safari-linux.png b/core/src/components/textarea/test/fill/textarea.e2e.ts-snapshots/textarea-fill-solid-md-rtl-Mobile-Safari-linux.png index 669f898b1f..e75266ed78 100644 Binary files a/core/src/components/textarea/test/fill/textarea.e2e.ts-snapshots/textarea-fill-solid-md-rtl-Mobile-Safari-linux.png and b/core/src/components/textarea/test/fill/textarea.e2e.ts-snapshots/textarea-fill-solid-md-rtl-Mobile-Safari-linux.png differ diff --git a/core/src/components/textarea/test/label-placement/textarea.e2e.ts b/core/src/components/textarea/test/label-placement/textarea.e2e.ts index affa8f1489..7aff19c45c 100644 --- a/core/src/components/textarea/test/label-placement/textarea.e2e.ts +++ b/core/src/components/textarea/test/label-placement/textarea.e2e.ts @@ -25,18 +25,6 @@ configs().forEach(({ title, screenshot, config }) => { const textarea = page.locator('ion-textarea'); expect(await textarea.screenshot()).toMatchSnapshot(screenshot(`textarea-placement-start-multi-line-value`)); }); - - test('label should be truncated', async ({ page }) => { - await page.setContent( - ` - - `, - config - ); - - const textarea = page.locator('ion-textarea'); - expect(await textarea.screenshot()).toMatchSnapshot(screenshot(`textarea-placement-start-label-truncated`)); - }); }); test.describe(title('textarea: label placement end'), () => { test('label should appear on the ending side of the textarea', async ({ page }) => { @@ -61,17 +49,6 @@ configs().forEach(({ title, screenshot, config }) => { const textarea = page.locator('ion-textarea'); expect(await textarea.screenshot()).toMatchSnapshot(screenshot(`textarea-placement-end-multi-line-value`)); }); - test('label should be truncated', async ({ page }) => { - await page.setContent( - ` - - `, - config - ); - - const textarea = page.locator('ion-textarea'); - expect(await textarea.screenshot()).toMatchSnapshot(screenshot(`textarea-placement-end-label-truncated`)); - }); }); test.describe(title('textarea: label placement fixed'), () => { test('label should appear on the starting side of the textarea and have a fixed width', async ({ page }) => { @@ -234,3 +211,59 @@ configs().forEach(({ title, screenshot, config }) => { }); }); }); + +configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, screenshot, config }) => { + test.describe(title('textarea: label overflow'), () => { + test('label property should be truncated with an ellipsis', async ({ page }) => { + await page.setContent( + ` + + `, + config + ); + + const textarea = page.locator('ion-textarea'); + expect(await textarea.screenshot()).toMatchSnapshot(screenshot(`textarea-label-truncate`)); + }); + test('label slot should be truncated with an ellipsis', async ({ page }) => { + await page.setContent( + ` + +
Label Label Label Label Label
+
+ `, + config + ); + + const textarea = page.locator('ion-textarea'); + expect(await textarea.screenshot()).toMatchSnapshot(screenshot(`textarea-label-slot-truncate`)); + }); + }); +}); + +configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, screenshot, config }) => { + test.describe(title('textarea: async label'), () => { + test('textarea should re-render when label slot is added async', async ({ page }) => { + await page.setContent( + ` + + `, + config + ); + + const textarea = page.locator('ion-textarea'); + + await textarea.evaluate((el: HTMLIonInputElement) => { + const labelEl = document.createElement('div'); + labelEl.slot = 'label'; + labelEl.innerHTML = 'Comments * + + + + Textarea - Slot + + + + + + + + + + + + + + Textarea - Slot + + + + +
+
+

No Fill / Start

+ +
Email *
+
+
+ +
+

Solid / Start

+ +
Email *
+
+
+ +
+

Outline / Start

+ +
Email *
+
+
+ +
+

No Fill / Floating

+ +
Email *
+
+
+ +
+

Solid / Floating

+ +
Email *
+
+
+ +
+

Outline / Floating

+ +
Email *
+
+
+ +
+

Outline / Floating / Async

+ +
+
+ + Add Slotted Content + Update Slotted Content + Remove Slotted Content +
+
+ + + + diff --git a/core/src/components/textarea/test/textarea.spec.ts b/core/src/components/textarea/test/textarea.spec.ts index a3e8a7ec13..890d53e544 100644 --- a/core/src/components/textarea/test/textarea.spec.ts +++ b/core/src/components/textarea/test/textarea.spec.ts @@ -12,3 +12,57 @@ it('should inherit attributes', async () => { expect(nativeEl.getAttribute('tabindex')).toBe('-1'); expect(nativeEl.getAttribute('data-form-type')).toBe('password'); }); + +/** + * Textarea uses emulated slots, so the internal + * behavior will not exactly match IonSelect's slots. + * For example, Textarea does not render an actual `` element + * internally, so we do not check for that here. Instead, + * we check to see which label text is being used. + * If Textarea is updated to use Shadow DOM (and therefore native slots), + * then we can update these tests to more closely match the Select tests. + **/ +describe('textarea: label rendering', () => { + it('should render label prop if only prop provided', async () => { + const page = await newSpecPage({ + components: [Textarea], + html: ` + + `, + }); + + const textarea = page.body.querySelector('ion-textarea'); + + const labelText = textarea.querySelector('.label-text-wrapper'); + + expect(labelText.textContent).toBe('Label Prop Text'); + }); + it('should render label slot if only slot provided', async () => { + const page = await newSpecPage({ + components: [Textarea], + html: ` +
Label Prop Slot
+ `, + }); + + const textarea = page.body.querySelector('ion-textarea'); + + const labelText = textarea.querySelector('.label-text-wrapper'); + + expect(labelText.textContent).toBe('Label Prop Slot'); + }); + it('should render label prop if both prop and slot provided', async () => { + const page = await newSpecPage({ + components: [Textarea], + html: ` +
Label Prop Slot
+ `, + }); + + const textarea = page.body.querySelector('ion-textarea'); + + const labelText = textarea.querySelector('.label-text-wrapper'); + + expect(labelText.textContent).toBe('Label Prop Text'); + }); +}); diff --git a/core/src/components/textarea/textarea.md.outline.scss b/core/src/components/textarea/textarea.md.outline.scss index 0493cbc5bf..76f20279ae 100644 --- a/core/src/components/textarea/textarea.md.outline.scss +++ b/core/src/components/textarea/textarea.md.outline.scss @@ -176,6 +176,16 @@ opacity: 0; pointer-events: none; + + /** + * The spacer currently inherits + * border-box sizing from the Ionic reset styles. + * However, we do not want to include padding in + * the calculation of the element dimensions. + * This code can be removed if textarea is updated + * to use the Shadow DOM. + */ + box-sizing: content-box; } :host(.textarea-fill-outline) .textarea-outline-start { diff --git a/core/src/components/textarea/textarea.scss b/core/src/components/textarea/textarea.scss index df0cf470b3..e9878dffa4 100644 --- a/core/src/components/textarea/textarea.scss +++ b/core/src/components/textarea/textarea.scss @@ -62,8 +62,6 @@ font-family: $font-family-base; - white-space: pre-wrap; - z-index: $z-index-item-input; box-sizing: border-box; @@ -74,6 +72,8 @@ flex: 1; background: var(--background); + + white-space: pre-wrap; } // TODO: FW-2876 - Remove this selector @@ -131,9 +131,8 @@ outline: none; background: transparent; - box-sizing: border-box; - resize: none; - appearance: none; + + white-space: pre-wrap; /** * This ensures the textarea @@ -145,6 +144,9 @@ * contrast of the textarea. */ z-index: 1; + box-sizing: border-box; + resize: none; + appearance: none; &::placeholder { @include padding(0); @@ -159,6 +161,11 @@ } } +// TODO: FW-2876 - Remove this selector +:host(.legacy-textarea) .native-textarea { + white-space: inherit; +} + // TODO: FW-2876 - Remove this selector :host(.legacy-textarea) .native-textarea, :host(.legacy-textarea) .textarea-legacy-wrapper::after { @@ -455,7 +462,8 @@ * works on block-level elements. A flex item is * considered blockified (https://www.w3.org/TR/css-display-3/#blockify). */ -.label-text { +.label-text, +::slotted([slot="label"]) { text-overflow: ellipsis; white-space: nowrap; @@ -463,6 +471,16 @@ overflow: hidden; } +/** + * If no label text is placed into the slot + * then the element should be hidden otherwise + * there will be additional margins added. + */ +.label-text-wrapper-hidden, +.textarea-outline-notch-hidden { + display: none; +} + .textarea-wrapper textarea { /** * When the floating label appears on top of the diff --git a/core/src/components/textarea/textarea.tsx b/core/src/components/textarea/textarea.tsx index 0087813d54..71973ac929 100644 --- a/core/src/components/textarea/textarea.tsx +++ b/core/src/components/textarea/textarea.tsx @@ -1,10 +1,25 @@ import type { ComponentInterface, EventEmitter } from '@stencil/core'; -import { Build, Component, Element, Event, Host, Method, Prop, State, Watch, h, writeTask } from '@stencil/core'; -import type { LegacyFormController } from '@utils/forms'; -import { createLegacyFormController } from '@utils/forms'; +import { + Build, + Component, + Element, + Event, + Host, + Method, + Prop, + State, + Watch, + forceUpdate, + h, + writeTask, +} from '@stencil/core'; +import type { LegacyFormController, NotchController } from '@utils/forms'; +import { createLegacyFormController, createNotchController } from '@utils/forms'; import type { Attributes } from '@utils/helpers'; import { inheritAriaAttributes, debounceEvent, findItemLabel, inheritAttributes } from '@utils/helpers'; import { printIonWarning } from '@utils/logging'; +import { createSlotMutationController } from '@utils/slot-mutation-controller'; +import type { SlotMutationController } from '@utils/slot-mutation-controller'; import { createColorClasses, hostContext } from '@utils/theme'; import { getIonMode } from '../../global/ionic-global'; @@ -15,6 +30,8 @@ import type { TextareaChangeEventDetail, TextareaInputEventDetail } from './text /** * @virtualProp {"ios" | "md"} mode - The mode determines which platform styles to use. + * + * @slot label - The label text to associate with the textarea. Use the `labelPlacement` property to control where the label is placed relative to the textarea. Use this if you need to render a label with custom HTML. (EXPERIMENTAL) */ @Component({ tag: 'ion-textarea', @@ -38,6 +55,11 @@ export class Textarea implements ComponentInterface { private inheritedAttributes: Attributes = {}; private originalIonInput?: EventEmitter; private legacyFormController!: LegacyFormController; + private notchSpacerEl: HTMLElement | undefined; + + private slotMutationController?: SlotMutationController; + + private notchController?: NotchController; // This flag ensures we log the deprecation warning at most once. private hasLoggedDeprecationWarning = false; @@ -205,6 +227,10 @@ export class Textarea implements ComponentInterface { /** * 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. */ @Prop() label?: string; @@ -286,6 +312,12 @@ export class Textarea implements ComponentInterface { connectedCallback() { const { el } = this; this.legacyFormController = createLegacyFormController(el); + this.slotMutationController = createSlotMutationController(el, 'label', () => forceUpdate(this)); + this.notchController = createNotchController( + el, + () => this.notchSpacerEl, + () => this.labelSlot + ); this.emitStyle(); this.debounceChanged(); if (Build.isBrowser) { @@ -305,6 +337,16 @@ export class Textarea implements ComponentInterface { }) ); } + + if (this.slotMutationController) { + this.slotMutationController.destroy(); + this.slotMutationController = undefined; + } + + if (this.notchController) { + this.notchController.destroy(); + this.notchController = undefined; + } } componentWillLoad() { @@ -319,6 +361,10 @@ export class Textarea implements ComponentInterface { this.runAutoGrow(); } + componentDidRender() { + this.notchController?.calculateNotchWidth(); + } + /** * Sets focus on the native `textarea` in `ion-textarea`. Use this method instead of the global * `textarea.focus()`. @@ -530,17 +576,37 @@ Developers can use the "legacy" property to continue using the legacy form marku private renderLabel() { const { label } = this; - if (label === undefined) { - return; - } return ( -
-
{this.label}
+
+ {label === undefined ? :
{label}
}
); } + /** + * Gets any content passed into the `label` slot, + * not the definition. + */ + private get labelSlot() { + return this.el.querySelector('[slot="label"]'); + } + + /** + * Returns `true` if label content is provided + * either by a prop or a content. If you want + * to get the plaintext value of the label use + * the `labelText` getter instead. + */ + private get hasLabel() { + return this.label !== undefined || this.labelSlot !== null; + } + /** * Renders the border container when fill="outline". */ @@ -559,8 +625,13 @@ Developers can use the "legacy" property to continue using the legacy form marku return [
-
-