feat(input): add experimental label slot (#27650)

Issue number: resolves #27061

---------

<!-- 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. -->

Input does not accept custom HTML labels

## What is the new behavior?
<!-- Please describe the behavior or changes that are being added by
this PR. -->

- Input accepts custom HTML labels as an experimental feature. We marked
this as experimental because it makes use of "scoped slots" which is an
emulated version of Web Component slots. As a result, there may be
instances where the slot behavior does not exactly match the native slot
behavior.

Note to reviewers: This is a combination of previously reviewed PRs. The
implementation is complete, so feel free to bikeshed.

## 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/2997

---------

Co-authored-by: Brandy Carney <brandyscarney@users.noreply.github.com>
This commit is contained in:
Liam DeBeasi
2023-06-15 15:23:41 -04:00
committed by GitHub
parent 606a892e40
commit a45395cc02
52 changed files with 721 additions and 165 deletions

View File

@ -1,10 +1,12 @@
import type { ComponentInterface, EventEmitter } from '@stencil/core';
import { Build, Component, Element, Event, Host, Method, Prop, State, Watch, h } 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 } 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 { closeCircle, closeSharp } from 'ionicons/icons';
@ -16,6 +18,8 @@ import { getCounterText } from './input.utils';
/**
* @virtualProp {"ios" | "md"} mode - The mode determines which platform styles to use.
*
* @slot label - The label text to associate with the input. Use the `labelPlacement` property to control where the label is placed relative to the input. Use this if you need to render a label with custom HTML. (EXPERIMENTAL)
*/
@Component({
tag: 'ion-input',
@ -31,6 +35,9 @@ export class Input implements ComponentInterface {
private inheritedAttributes: Attributes = {};
private isComposing = false;
private legacyFormController!: LegacyFormController;
private slotMutationController?: SlotMutationController;
private notchController?: NotchController;
private notchSpacerEl: HTMLElement | undefined;
// This flag ensures we log the deprecation warning at most once.
private hasLoggedDeprecationWarning = false;
@ -165,6 +172,10 @@ export class Input implements ComponentInterface {
/**
* The visible label associated with the input.
*
* 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;
@ -353,6 +364,12 @@ export class Input implements ComponentInterface {
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();
@ -369,6 +386,10 @@ export class Input implements ComponentInterface {
this.originalIonInput = this.ionInput;
}
componentDidRender() {
this.notchController?.calculateNotchWidth();
}
disconnectedCallback() {
if (Build.isBrowser) {
document.dispatchEvent(
@ -377,6 +398,16 @@ export class Input implements ComponentInterface {
})
);
}
if (this.slotMutationController) {
this.slotMutationController.destroy();
this.slotMutationController = undefined;
}
if (this.notchController) {
this.notchController.destroy();
this.notchController = undefined;
}
}
/**
@ -578,17 +609,37 @@ export class Input implements ComponentInterface {
private renderLabel() {
const { label } = this;
if (label === undefined) {
return;
}
return (
<div class="label-text-wrapper">
<div class="label-text">{this.label}</div>
<div
class={{
'label-text-wrapper': true,
'label-text-wrapper-hidden': !this.hasLabel,
}}
>
{label === undefined ? <slot name="label"></slot> : <div class="label-text">{label}</div>}
</div>
);
}
/**
* Gets any content passed into the `label` slot,
* not the <slot> 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".
@ -608,8 +659,13 @@ export class Input implements ComponentInterface {
return [
<div class="input-outline-container">
<div class="input-outline-start"></div>
<div class="input-outline-notch">
<div class="notch-spacer" aria-hidden="true">
<div
class={{
'input-outline-notch': true,
'input-outline-notch-hidden': !this.hasLabel,
}}
>
<div class="notch-spacer" aria-hidden="true" ref={(el) => (this.notchSpacerEl = el)}>
{this.label}
</div>
</div>