feat(input, textarea, select): add start and end slots (#28583)
Issue number: Resolves #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-input`, `ion-textarea`, or `ion-select`, 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 to each component. This PR is a combination of several others that were already approved. If needed, it might be easiest to review the PRs individually by looking at the commit history here. ## 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. --> Docs PR: https://github.com/ionic-team/ionic-docs/pull/3271 Dev build: `7.5.4-dev.11701112913.1ea61220` --------- Co-authored-by: ionitron <hi@ionicframework.com> Co-authored-by: Maria Hutt <thetaPC@users.noreply.github.com> Co-authored-by: Liam DeBeasi <liamdebeasi@users.noreply.github.com>
@ -21,7 +21,13 @@
|
||||
<ion-textarea label="Email" label-placement="stacked" value="hi@ionic.io"></ion-textarea>
|
||||
<ion-textarea label="Email" label-placement="floating"></ion-textarea>
|
||||
<ion-textarea label="Email" label-placement="floating" fill="outline" value="hi@ionic.io"></ion-textarea> <br />
|
||||
<ion-textarea label="Email" label-placement="floating" fill="solid" value="hi@ionic.io"></ion-textarea>
|
||||
<ion-textarea label="Email" label-placement="floating" fill="solid" value="hi@ionic.io"></ion-textarea><br />
|
||||
<ion-textarea label="Email" label-placement="floating" fill="solid" value="hi@ionic.io">
|
||||
<ion-icon slot="start" name="lock-closed" aria-hidden="true"></ion-icon>
|
||||
<ion-button slot="end" aria-label="button">
|
||||
<ion-icon slot="icon-only" name="lock-closed" aria-hidden="true"></ion-icon>
|
||||
</ion-button>
|
||||
</ion-textarea>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@ -49,53 +49,135 @@
|
||||
</ion-header>
|
||||
|
||||
<ion-content id="content" class="ion-padding">
|
||||
<h1>Label Slot</h1>
|
||||
<div class="grid">
|
||||
<div class="grid-item">
|
||||
<h2>No Fill / Start</h2>
|
||||
<h2>No Fill / Start Label</h2>
|
||||
<ion-textarea label-placement="start" value="hi@ionic.io">
|
||||
<div slot="label">Email <span class="required">*</span></div>
|
||||
</ion-textarea>
|
||||
</div>
|
||||
|
||||
<div class="grid-item">
|
||||
<h2>Solid / Start</h2>
|
||||
<h2>Solid / Start Label</h2>
|
||||
<ion-textarea label-placement="start" fill="solid" value="hi@ionic.io">
|
||||
<div slot="label">Email <span class="required">*</span></div>
|
||||
</ion-textarea>
|
||||
</div>
|
||||
|
||||
<div class="grid-item">
|
||||
<h2>Outline / Start</h2>
|
||||
<h2>Outline / Start Label</h2>
|
||||
<ion-textarea label-placement="start" fill="outline" value="hi@ionic.io">
|
||||
<div slot="label">Email <span class="required">*</span></div>
|
||||
</ion-textarea>
|
||||
</div>
|
||||
|
||||
<div class="grid-item">
|
||||
<h2>No Fill / Floating</h2>
|
||||
<h2>No Fill / Floating Label</h2>
|
||||
<ion-textarea label-placement="floating" value="hi@ionic.io">
|
||||
<div slot="label">Email <span class="required">*</span></div>
|
||||
</ion-textarea>
|
||||
</div>
|
||||
|
||||
<div class="grid-item">
|
||||
<h2>Solid / Floating</h2>
|
||||
<h2>Solid / Floating Label</h2>
|
||||
<ion-textarea label-placement="floating" fill="solid" value="hi@ionic.io">
|
||||
<div slot="label">Email <span class="required">*</span></div>
|
||||
</ion-textarea>
|
||||
</div>
|
||||
|
||||
<div class="grid-item">
|
||||
<h2>Outline / Floating</h2>
|
||||
<h2>Outline / Floating Label</h2>
|
||||
<ion-textarea label-placement="floating" fill="outline" value="hi@ionic.io">
|
||||
<div slot="label">Email <span class="required">*</span></div>
|
||||
</ion-textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h1>Start/End Slots</h1>
|
||||
<div class="grid">
|
||||
<div class="grid-item">
|
||||
<h2>No Fill / Start Label</h2>
|
||||
<ion-textarea label-placement="start" value="hi@ionic.io" label="Email">
|
||||
<ion-icon slot="start" name="lock-closed" aria-hidden="true"></ion-icon>
|
||||
<ion-button fill="clear" slot="end" aria-label="Show/hide password">
|
||||
<ion-icon slot="icon-only" name="eye" aria-hidden="true"></ion-icon>
|
||||
</ion-button>
|
||||
</ion-textarea>
|
||||
</div>
|
||||
|
||||
<div class="grid-item">
|
||||
<h2>Outline / Floating / Async</h2>
|
||||
<h2>Solid / Start Label</h2>
|
||||
<ion-textarea label-placement="start" fill="solid" value="hi@ionic.io" label="Email">
|
||||
<ion-icon slot="start" name="lock-closed" aria-hidden="true"></ion-icon>
|
||||
<ion-button fill="clear" slot="end" aria-label="Show/hide password">
|
||||
<ion-icon slot="icon-only" name="eye" aria-hidden="true"></ion-icon>
|
||||
</ion-button>
|
||||
</ion-textarea>
|
||||
</div>
|
||||
|
||||
<div class="grid-item">
|
||||
<h2>Outline / Start Label</h2>
|
||||
<ion-textarea label-placement="start" fill="outline" value="hi@ionic.io" label="Email">
|
||||
<ion-icon slot="start" name="lock-closed" aria-hidden="true"></ion-icon>
|
||||
<ion-button fill="clear" slot="end" aria-label="Show/hide password">
|
||||
<ion-icon slot="icon-only" name="eye" aria-hidden="true"></ion-icon>
|
||||
</ion-button>
|
||||
</ion-textarea>
|
||||
</div>
|
||||
|
||||
<div class="grid-item">
|
||||
<h2>No Fill / Floating Label</h2>
|
||||
<ion-textarea label-placement="floating" value="hi@ionic.io" label="Email">
|
||||
<ion-icon slot="start" name="lock-closed" aria-hidden="true"></ion-icon>
|
||||
<ion-button fill="clear" slot="end" aria-label="Show/hide password">
|
||||
<ion-icon slot="icon-only" name="eye" aria-hidden="true"></ion-icon>
|
||||
</ion-button>
|
||||
</ion-textarea>
|
||||
</div>
|
||||
|
||||
<div class="grid-item">
|
||||
<h2>Solid / Floating Label</h2>
|
||||
<ion-textarea label-placement="floating" fill="solid" value="hi@ionic.io" label="Email">
|
||||
<ion-icon slot="start" name="lock-closed" aria-hidden="true"></ion-icon>
|
||||
<ion-button fill="clear" slot="end" aria-label="Show/hide password">
|
||||
<ion-icon slot="icon-only" name="eye" aria-hidden="true"></ion-icon>
|
||||
</ion-button>
|
||||
</ion-textarea>
|
||||
</div>
|
||||
|
||||
<div class="grid-item">
|
||||
<h2>Outline / Floating Label</h2>
|
||||
<ion-textarea label-placement="floating" fill="outline" value="hi@ionic.io" label="Email">
|
||||
<ion-icon slot="start" name="lock-closed" aria-hidden="true"></ion-icon>
|
||||
<ion-button fill="clear" slot="end" aria-label="Show/hide password">
|
||||
<ion-icon slot="icon-only" name="eye" aria-hidden="true"></ion-icon>
|
||||
</ion-button>
|
||||
</ion-textarea>
|
||||
</div>
|
||||
|
||||
<div class="grid-item">
|
||||
<h2>Outline / Autogrow</h2>
|
||||
<ion-textarea label-placement="start" fill="outline" label="Email" auto-grow="true" value="hi@ionic.io">
|
||||
<ion-icon slot="start" name="lock-closed" aria-hidden="true"></ion-icon>
|
||||
<ion-button fill="clear" slot="end" aria-label="Show/hide password">
|
||||
<ion-icon slot="icon-only" name="eye" aria-hidden="true"></ion-icon>
|
||||
</ion-button>
|
||||
</ion-textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h1>Async Slot Content</h1>
|
||||
<div class="grid">
|
||||
<div class="grid-item">
|
||||
<h2>Outline / Async Label</h2>
|
||||
<ion-textarea id="solid-async" label-placement="floating" fill="outline" value="hi@ionic.io"></ion-textarea>
|
||||
</div>
|
||||
|
||||
<div class="grid-item">
|
||||
<h2>Outline / Async Decorations</h2>
|
||||
<ion-textarea id="async-decorations" label-placement="floating" fill="outline" label="Email"></ion-textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ion-button onclick="addSlot()">Add Slotted Content</ion-button>
|
||||
@ -106,29 +188,65 @@
|
||||
|
||||
<script>
|
||||
const solidAsync = document.querySelector('#solid-async');
|
||||
const asyncDecos = document.querySelector('#async-decorations');
|
||||
|
||||
const getSlottedContent = () => {
|
||||
const getSlottedLabel = () => {
|
||||
return solidAsync.querySelector('[slot="label"]');
|
||||
};
|
||||
|
||||
const getSlottedStartDeco = () => {
|
||||
return asyncDecos.querySelector('[slot="start"]');
|
||||
};
|
||||
|
||||
const getSlottedEndDeco = () => {
|
||||
return asyncDecos.querySelector('[slot="end"]');
|
||||
};
|
||||
|
||||
const addSlot = () => {
|
||||
if (getSlottedContent() === null) {
|
||||
if (getSlottedLabel() === null) {
|
||||
const labelEl = document.createElement('div');
|
||||
labelEl.slot = 'label';
|
||||
labelEl.innerHTML = 'Comments <span class="required">*</span>';
|
||||
|
||||
solidAsync.appendChild(labelEl);
|
||||
}
|
||||
|
||||
if (getSlottedStartDeco() === null) {
|
||||
const startEl = document.createElement('div');
|
||||
startEl.slot = 'start';
|
||||
startEl.innerHTML = 'Start';
|
||||
|
||||
asyncDecos.insertAdjacentElement('afterbegin', startEl);
|
||||
}
|
||||
|
||||
if (getSlottedEndDeco() === null) {
|
||||
const endEl = document.createElement('div');
|
||||
endEl.slot = 'end';
|
||||
endEl.innerHTML = 'End';
|
||||
|
||||
asyncDecos.insertAdjacentElement('beforeend', endEl);
|
||||
}
|
||||
};
|
||||
|
||||
const removeSlot = () => {
|
||||
if (getSlottedContent() !== null) {
|
||||
solidAsync.querySelector('[slot="label"]').remove();
|
||||
const slottedLabel = getSlottedLabel();
|
||||
if (slottedLabel !== null) {
|
||||
slottedLabel.remove();
|
||||
}
|
||||
|
||||
const slottedStartDeco = getSlottedStartDeco();
|
||||
if (slottedStartDeco !== null) {
|
||||
slottedStartDeco.remove();
|
||||
}
|
||||
|
||||
const slottedEndDeco = getSlottedEndDeco();
|
||||
if (slottedEndDeco !== null) {
|
||||
slottedEndDeco.remove();
|
||||
}
|
||||
};
|
||||
|
||||
const updateSlot = () => {
|
||||
const slottedContent = getSlottedContent();
|
||||
const slottedContent = getSlottedLabel();
|
||||
|
||||
if (slottedContent !== null) {
|
||||
slottedContent.textContent = 'This is my really really really long text';
|
||||
|
||||
68
core/src/components/textarea/test/slot/textarea.e2e.ts
Normal file
@ -0,0 +1,68 @@
|
||||
import { expect } from '@playwright/test';
|
||||
import { configs, test } from '@utils/test/playwright';
|
||||
|
||||
configs().forEach(({ title, screenshot, config }) => {
|
||||
test.describe(title('textarea: start and end slots (visual checks)'), () => {
|
||||
test('should not have visual regressions with a start-positioned label', async ({ page }) => {
|
||||
await page.setContent(
|
||||
`
|
||||
<ion-textarea label-placement="start" fill="solid" value="100" label="Weight">
|
||||
<ion-icon slot="start" name="barbell" aria-hidden="true"></ion-icon>
|
||||
<ion-label slot="end">lbs</ion-label>
|
||||
</ion-textarea>
|
||||
`,
|
||||
config
|
||||
);
|
||||
|
||||
const textarea = page.locator('ion-textarea');
|
||||
await expect(textarea).toHaveScreenshot(screenshot(`textarea-slots-label-start`));
|
||||
});
|
||||
|
||||
test('should not have visual regressions with a floating label', async ({ page }) => {
|
||||
await page.setContent(
|
||||
`
|
||||
<ion-textarea label-placement="floating" fill="solid" value="100" label="Weight">
|
||||
<ion-icon slot="start" name="barbell" aria-hidden="true"></ion-icon>
|
||||
<ion-label slot="end">lbs</ion-label>
|
||||
</ion-textarea>
|
||||
`,
|
||||
config
|
||||
);
|
||||
|
||||
const textarea = page.locator('ion-textarea');
|
||||
await expect(textarea).toHaveScreenshot(screenshot(`textarea-slots-label-floating`));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, config }) => {
|
||||
test.describe(title('textarea: start and end slots (functionality checks)'), () => {
|
||||
test('should raise floating label when there is content in the start slot', async ({ page }) => {
|
||||
await page.setContent(
|
||||
`
|
||||
<ion-textarea label-placement="floating" fill="solid" label="Weight">
|
||||
<ion-icon slot="start" name="barbell" aria-hidden="true"></ion-icon>
|
||||
</ion-textarea>
|
||||
`,
|
||||
config
|
||||
);
|
||||
|
||||
const textarea = page.locator('ion-textarea');
|
||||
await expect(textarea).toHaveClass(/label-floating/);
|
||||
});
|
||||
|
||||
test('should raise floating label when there is content in the end slot', async ({ page }) => {
|
||||
await page.setContent(
|
||||
`
|
||||
<ion-textarea label-placement="floating" fill="solid" label="Weight">
|
||||
<ion-icon slot="end" name="barbell" aria-hidden="true"></ion-icon>
|
||||
</ion-textarea>
|
||||
`,
|
||||
config
|
||||
);
|
||||
|
||||
const textarea = page.locator('ion-textarea');
|
||||
await expect(textarea).toHaveClass(/label-floating/);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
After Width: | Height: | Size: 2.5 KiB |
|
After Width: | Height: | Size: 3.0 KiB |
|
After Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 2.5 KiB |
|
After Width: | Height: | Size: 3.2 KiB |
|
After Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 2.5 KiB |
|
After Width: | Height: | Size: 2.9 KiB |
|
After Width: | Height: | Size: 2.2 KiB |
|
After Width: | Height: | Size: 2.5 KiB |
|
After Width: | Height: | Size: 2.9 KiB |
|
After Width: | Height: | Size: 2.2 KiB |
|
After Width: | Height: | Size: 2.8 KiB |
|
After Width: | Height: | Size: 3.2 KiB |
|
After Width: | Height: | Size: 2.5 KiB |
|
After Width: | Height: | Size: 2.8 KiB |
|
After Width: | Height: | Size: 3.0 KiB |
|
After Width: | Height: | Size: 2.5 KiB |
|
After Width: | Height: | Size: 2.8 KiB |
|
After Width: | Height: | Size: 3.0 KiB |
|
After Width: | Height: | Size: 2.4 KiB |
|
After Width: | Height: | Size: 2.8 KiB |
|
After Width: | Height: | Size: 3.0 KiB |
|
After Width: | Height: | Size: 2.4 KiB |
@ -89,9 +89,7 @@
|
||||
/**
|
||||
* This makes the label sit above the textarea.
|
||||
*/
|
||||
:host(.has-focus.textarea-fill-outline.textarea-label-placement-floating) .label-text-wrapper,
|
||||
:host(.has-value.textarea-fill-outline.textarea-label-placement-floating) .label-text-wrapper,
|
||||
:host(.textarea-fill-outline.textarea-label-placement-stacked) .label-text-wrapper {
|
||||
:host(.label-floating.textarea-fill-outline) .label-text-wrapper {
|
||||
@include transform(translateY(-32%), scale(#{$form-control-label-stacked-scale}));
|
||||
@include margin(0);
|
||||
|
||||
@ -116,6 +114,13 @@
|
||||
@include margin(12px, 0px, 0px, 0px);
|
||||
}
|
||||
|
||||
:host(.textarea-fill-outline.textarea-label-placement-stacked) ::slotted([slot="start"]),
|
||||
:host(.textarea-fill-outline.textarea-label-placement-stacked) ::slotted([slot="end"]),
|
||||
:host(.textarea-fill-outline.textarea-label-placement-floating) ::slotted([slot="start"]),
|
||||
:host(.textarea-fill-outline.textarea-label-placement-floating) ::slotted([slot="end"]) {
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
// Textarea Fill: Outline Outline Container
|
||||
// ----------------------------------------------------------------
|
||||
|
||||
@ -220,8 +225,6 @@
|
||||
* the floating/stacked label. We simulate this "cut out"
|
||||
* by removing the top border from the notch fragment.
|
||||
*/
|
||||
:host(.has-focus.textarea-fill-outline.textarea-label-placement-floating) .textarea-outline-notch,
|
||||
:host(.has-value.textarea-fill-outline.textarea-label-placement-floating) .textarea-outline-notch,
|
||||
:host(.textarea-fill-outline.textarea-label-placement-stacked) .textarea-outline-notch {
|
||||
:host(.label-floating.textarea-fill-outline) .textarea-outline-notch {
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
@ -67,9 +67,7 @@
|
||||
// Textarea Label
|
||||
// ----------------------------------------------------------------
|
||||
|
||||
:host(.textarea-fill-solid.textarea-label-placement-stacked) .label-text-wrapper,
|
||||
:host(.has-focus.textarea-fill-solid.textarea-label-placement-floating) .label-text-wrapper,
|
||||
:host(.has-value.textarea-fill-solid.textarea-label-placement-floating) .label-text-wrapper {
|
||||
:host(.label-floating.textarea-fill-solid) .label-text-wrapper {
|
||||
/**
|
||||
* Label text should not extend
|
||||
* beyond the bounds of the textarea.
|
||||
|
||||
@ -330,12 +330,8 @@
|
||||
// ----------------------------------------------------------------
|
||||
|
||||
.native-wrapper {
|
||||
display: flex;
|
||||
|
||||
position: relative;
|
||||
|
||||
flex-grow: 1;
|
||||
|
||||
width: 100%;
|
||||
|
||||
height: 100%;
|
||||
@ -414,6 +410,13 @@
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.textarea-wrapper-inner {
|
||||
display: flex;
|
||||
|
||||
width: 100%;
|
||||
min-height: inherit;
|
||||
}
|
||||
|
||||
// Textarea Highlight
|
||||
// ----------------------------------------------------------------
|
||||
|
||||
@ -688,6 +691,13 @@
|
||||
@include margin(8px, 0px, 0px, 0px);
|
||||
}
|
||||
|
||||
:host(.textarea-label-placement-stacked) ::slotted([slot="start"]),
|
||||
:host(.textarea-label-placement-stacked) ::slotted([slot="end"]),
|
||||
:host(.textarea-label-placement-floating) ::slotted([slot="start"]),
|
||||
:host(.textarea-label-placement-floating) ::slotted([slot="end"]) {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
/**
|
||||
* This makes the label sit over the textarea
|
||||
* when the textarea is blurred and has no value.
|
||||
@ -713,9 +723,7 @@
|
||||
/**
|
||||
* This makes the label sit above the textarea.
|
||||
*/
|
||||
:host(.textarea-label-placement-stacked) .label-text-wrapper,
|
||||
:host(.has-focus.textarea-label-placement-floating) .label-text-wrapper,
|
||||
:host(.has-value.textarea-label-placement-floating) .label-text-wrapper {
|
||||
:host(.label-floating) .label-text-wrapper {
|
||||
@include transform(translateY(50%), scale(#{$form-control-label-stacked-scale}));
|
||||
|
||||
/**
|
||||
@ -724,3 +732,29 @@
|
||||
*/
|
||||
max-width: calc(100% / #{$form-control-label-stacked-scale});
|
||||
}
|
||||
|
||||
// Start/End Slots
|
||||
// ----------------------------------------------------------------
|
||||
|
||||
.start-slot-wrapper, .end-slot-wrapper {
|
||||
@include padding(var(--padding-top), 0, var(--padding-bottom), 0);
|
||||
|
||||
display: flex;
|
||||
|
||||
flex-shrink: 0;
|
||||
|
||||
align-self: start;
|
||||
}
|
||||
|
||||
::slotted([slot="start"]),
|
||||
::slotted([slot="end"]) {
|
||||
margin-top: 0; // ensure slot content is vertically aligned with label
|
||||
}
|
||||
|
||||
::slotted([slot="start"]) {
|
||||
margin-inline-end: $form-control-label-margin;
|
||||
}
|
||||
|
||||
::slotted([slot="end"]) {
|
||||
margin-inline-start: $form-control-label-margin;
|
||||
}
|
||||
|
||||
@ -38,6 +38,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)
|
||||
* @slot start - Content to display at the leading edge of the textarea. (EXPERIMENTAL)
|
||||
* @slot end - Content to display at the trailing edge of the textarea. (EXPERIMENTAL)
|
||||
*/
|
||||
@Component({
|
||||
tag: 'ion-textarea',
|
||||
@ -316,7 +318,7 @@ export class Textarea implements ComponentInterface {
|
||||
connectedCallback() {
|
||||
const { el } = this;
|
||||
this.legacyFormController = createLegacyFormController(el);
|
||||
this.slotMutationController = createSlotMutationController(el, 'label', () => forceUpdate(this));
|
||||
this.slotMutationController = createSlotMutationController(el, ['label', 'start', 'end'], () => forceUpdate(this));
|
||||
this.notchController = createNotchController(
|
||||
el,
|
||||
() => this.notchSpacerEl,
|
||||
@ -705,55 +707,100 @@ Developers can use the "legacy" property to continue using the legacy form marku
|
||||
}
|
||||
|
||||
private renderTextarea() {
|
||||
const { inputId, disabled, fill, shape, labelPlacement } = this;
|
||||
const { inputId, disabled, fill, shape, labelPlacement, el, hasFocus } = this;
|
||||
const mode = getIonMode(this);
|
||||
const value = this.getValue();
|
||||
const inItem = hostContext('ion-item', this.el);
|
||||
const shouldRenderHighlight = mode === 'md' && fill !== 'outline' && !inItem;
|
||||
|
||||
const hasValue = this.hasValue();
|
||||
const hasStartEndSlots = el.querySelector('[slot="start"], [slot="end"]') !== null;
|
||||
|
||||
/**
|
||||
* If the label is stacked, it should always sit above the textarea.
|
||||
* For floating labels, the label should move above the textarea if
|
||||
* the textarea has a value, is focused, or has anything in either
|
||||
* the start or end slot.
|
||||
*
|
||||
* If there is content in the start slot, the label would overlap
|
||||
* it if not forced to float. This is also applied to the end slot
|
||||
* because with the default or solid fills, the textarea is not
|
||||
* vertically centered in the container, but the label is. This
|
||||
* causes the slots and label to appear vertically offset from each
|
||||
* other when the label isn't floating above the input. This doesn't
|
||||
* apply to the outline fill, but this was not accounted for to keep
|
||||
* things consistent.
|
||||
*
|
||||
* TODO(FW-5592): Remove hasStartEndSlots condition
|
||||
*/
|
||||
const labelShouldFloat =
|
||||
labelPlacement === 'stacked' || (labelPlacement === 'floating' && (hasValue || hasFocus || hasStartEndSlots));
|
||||
|
||||
return (
|
||||
<Host
|
||||
class={createColorClasses(this.color, {
|
||||
[mode]: true,
|
||||
'has-value': this.hasValue(),
|
||||
'has-focus': this.hasFocus,
|
||||
'has-value': hasValue,
|
||||
'has-focus': hasFocus,
|
||||
'label-floating': labelShouldFloat,
|
||||
[`textarea-fill-${fill}`]: fill !== undefined,
|
||||
[`textarea-shape-${shape}`]: shape !== undefined,
|
||||
[`textarea-label-placement-${labelPlacement}`]: true,
|
||||
'textarea-disabled': disabled,
|
||||
})}
|
||||
>
|
||||
<label class="textarea-wrapper">
|
||||
{/**
|
||||
* htmlFor is needed so that clicking the label always focuses
|
||||
* the textarea. Otherwise, if the start slot has something
|
||||
* interactable, clicking the label would focus that instead
|
||||
* since it comes before the textarea in the DOM.
|
||||
*/}
|
||||
<label class="textarea-wrapper" htmlFor={inputId}>
|
||||
{this.renderLabelContainer()}
|
||||
<div class="native-wrapper" ref={(el) => (this.textareaWrapper = el)}>
|
||||
<textarea
|
||||
class="native-textarea"
|
||||
ref={(el) => (this.nativeInput = el)}
|
||||
id={inputId}
|
||||
disabled={disabled}
|
||||
autoCapitalize={this.autocapitalize}
|
||||
autoFocus={this.autofocus}
|
||||
enterKeyHint={this.enterkeyhint}
|
||||
inputMode={this.inputmode}
|
||||
minLength={this.minlength}
|
||||
maxLength={this.maxlength}
|
||||
name={this.name}
|
||||
placeholder={this.placeholder || ''}
|
||||
readOnly={this.readonly}
|
||||
required={this.required}
|
||||
spellcheck={this.spellcheck}
|
||||
cols={this.cols}
|
||||
rows={this.rows}
|
||||
wrap={this.wrap}
|
||||
onInput={this.onInput}
|
||||
onChange={this.onChange}
|
||||
onBlur={this.onBlur}
|
||||
onFocus={this.onFocus}
|
||||
onKeyDown={this.onKeyDown}
|
||||
{...this.inheritedAttributes}
|
||||
>
|
||||
{value}
|
||||
</textarea>
|
||||
<div class="textarea-wrapper-inner">
|
||||
{/**
|
||||
* Some elements have their own padding styles which may
|
||||
* interfere with slot content alignment (such as icon-
|
||||
* only buttons setting --padding-top=0). To avoid this,
|
||||
* we wrap both the start and end slots in separate
|
||||
* elements and apply our padding styles to that instead.
|
||||
*/}
|
||||
<div class="start-slot-wrapper">
|
||||
<slot name="start"></slot>
|
||||
</div>
|
||||
<div class="native-wrapper" ref={(el) => (this.textareaWrapper = el)}>
|
||||
<textarea
|
||||
class="native-textarea"
|
||||
ref={(el) => (this.nativeInput = el)}
|
||||
id={inputId}
|
||||
disabled={disabled}
|
||||
autoCapitalize={this.autocapitalize}
|
||||
autoFocus={this.autofocus}
|
||||
enterKeyHint={this.enterkeyhint}
|
||||
inputMode={this.inputmode}
|
||||
minLength={this.minlength}
|
||||
maxLength={this.maxlength}
|
||||
name={this.name}
|
||||
placeholder={this.placeholder || ''}
|
||||
readOnly={this.readonly}
|
||||
required={this.required}
|
||||
spellcheck={this.spellcheck}
|
||||
cols={this.cols}
|
||||
rows={this.rows}
|
||||
wrap={this.wrap}
|
||||
onInput={this.onInput}
|
||||
onChange={this.onChange}
|
||||
onBlur={this.onBlur}
|
||||
onFocus={this.onFocus}
|
||||
onKeyDown={this.onKeyDown}
|
||||
{...this.inheritedAttributes}
|
||||
>
|
||||
{value}
|
||||
</textarea>
|
||||
</div>
|
||||
<div class="end-slot-wrapper">
|
||||
<slot name="end"></slot>
|
||||
</div>
|
||||
</div>
|
||||
{shouldRenderHighlight && <div class="textarea-highlight"></div>}
|
||||
</label>
|
||||
|
||||