diff --git a/core/api.txt b/core/api.txt index 26be0122bd..3eadfcd632 100644 --- a/core/api.txt +++ b/core/api.txt @@ -610,7 +610,7 @@ ion-input,prop,fill,"outline" | "solid" | undefined,undefined,false,false ion-input,prop,helperText,string | undefined,undefined,false,false ion-input,prop,inputmode,"decimal" | "email" | "none" | "numeric" | "search" | "tel" | "text" | "url" | undefined,undefined,false,false ion-input,prop,label,string | undefined,undefined,false,false -ion-input,prop,labelPlacement,"end" | "fixed" | "floating" | "stacked" | "start",'start',false,false +ion-input,prop,labelPlacement,"end" | "fixed" | "floating" | "stacked" | "start" | undefined,undefined,false,false ion-input,prop,max,number | string | undefined,undefined,false,false ion-input,prop,maxlength,number | undefined,undefined,false,false ion-input,prop,min,number | string | undefined,undefined,false,false diff --git a/core/src/components.d.ts b/core/src/components.d.ts index 9cd8c797de..6bcbbc6e03 100644 --- a/core/src/components.d.ts +++ b/core/src/components.d.ts @@ -1395,9 +1395,9 @@ export namespace Components { */ "label"?: string; /** - * Where to place the label relative to the input. `"start"`: The label will appear to the left of the input in LTR and to the right in RTL. `"end"`: The label will appear to the right of the input in LTR and to the left in RTL. `"floating"`: The label will appear smaller and above the input when the input is focused or it has a value. Otherwise it will appear on top of the input. `"stacked"`: The label will appear smaller and above the input regardless even when the input is blurred or has no value. `"fixed"`: The label has the same behavior as `"start"` except it also has a fixed width. Long text will be truncated with ellipses ("..."). + * Where to place the label relative to the input. `"start"`: The label will appear to the left of the input in LTR and to the right in RTL. `"end"`: The label will appear to the right of the input in LTR and to the left in RTL. `"floating"`: The label will appear smaller and above the input when the input is focused or it has a value. Otherwise it will appear on top of the input. `"stacked"`: The label will appear smaller and above the input regardless even when the input is blurred or has no value. `"fixed"`: The label has the same behavior as `"start"` except it also has a fixed width. Long text will be truncated with ellipses ("..."). Defaults to "stacked" for the ionic theme, or "start" for all other themes. In the ionic theme, only the values "stacked" and "floating" are supported. */ - "labelPlacement": 'start' | 'end' | 'floating' | 'stacked' | 'fixed'; + "labelPlacement"?: 'start' | 'end' | 'floating' | 'stacked' | 'fixed'; /** * The maximum value, which must not be less than its minimum (min attribute) value. */ @@ -6631,7 +6631,7 @@ declare namespace LocalJSX { */ "label"?: string; /** - * Where to place the label relative to the input. `"start"`: The label will appear to the left of the input in LTR and to the right in RTL. `"end"`: The label will appear to the right of the input in LTR and to the left in RTL. `"floating"`: The label will appear smaller and above the input when the input is focused or it has a value. Otherwise it will appear on top of the input. `"stacked"`: The label will appear smaller and above the input regardless even when the input is blurred or has no value. `"fixed"`: The label has the same behavior as `"start"` except it also has a fixed width. Long text will be truncated with ellipses ("..."). + * Where to place the label relative to the input. `"start"`: The label will appear to the left of the input in LTR and to the right in RTL. `"end"`: The label will appear to the right of the input in LTR and to the left in RTL. `"floating"`: The label will appear smaller and above the input when the input is focused or it has a value. Otherwise it will appear on top of the input. `"stacked"`: The label will appear smaller and above the input regardless even when the input is blurred or has no value. `"fixed"`: The label has the same behavior as `"start"` except it also has a fixed width. Long text will be truncated with ellipses ("..."). Defaults to "stacked" for the ionic theme, or "start" for all other themes. In the ionic theme, only the values "stacked" and "floating" are supported. */ "labelPlacement"?: 'start' | 'end' | 'floating' | 'stacked' | 'fixed'; /** diff --git a/core/src/components/input/input.ionic.outline.scss b/core/src/components/input/input.ionic.outline.scss new file mode 100644 index 0000000000..d749926339 --- /dev/null +++ b/core/src/components/input/input.ionic.outline.scss @@ -0,0 +1,111 @@ +@import "./input.vars"; +@import "../../foundations/ionic.vars"; + +// Input Fill: Outline (Ionic Theme) +// ---------------------------------------------------------------- + +:host(.input-fill-outline) { + --border-radius: #{$ionic-border-radius-rounded-small}; + --padding-start: 12px; + --padding-end: 12px; + --placeholder-color: #{$ionic-color-neutral-600}; + --placeholder-opacity: 1; +} + +/** + * The bottom content should never have + * a border with the outline style. + */ +:host(.input-fill-outline) .input-bottom { + border-top: none; +} + +:host(.input-fill-outline) .input-wrapper { + /** + * For the ionic theme, the padding needs to sit on the + * native wrapper instead, so that it sits within the + * outline container but does not affect the label text. + + * For the ionic theme, the horizontal padding needs to + * sit on the native wrapper instead, so that + */ + @include padding(0); + + /** + * Outline inputs do not have a bottom border. + * Instead, they have a border that wraps the + * input + label. + */ + border-bottom: none; +} + +:host(.input-fill-outline.input-label-placement-stacked) .label-text-wrapper { + @include transform-origin(start, top); + + /** + * Label text should not extend + * beyond the bounds of the input. + */ + max-width: calc(100% - var(--padding-start) - var(--padding-end)); +} + +:host(.input-fill-outline) .label-text-wrapper { + /** + * The label should appear on top of an outline + * container that overlaps it so it is always clickable. + */ + position: relative; + + color: #{$ionic-color-neutral-700}; +} + +:host(.input-fill-outline) .native-wrapper { + @include padding(var(--padding-top), var(--padding-end), var(--padding-bottom), var(--padding-start)); + + min-height: 40px; +} + + +// Input Fill: Outline, Outline Container +// ---------------------------------------------------------------- + +:host(.input-fill-outline) .input-outline { + @include position(0, 0, 0, 0); + @include border-radius(var(--border-radius)); + + position: absolute; + + width: 100%; + height: 100%; + + pointer-events: none; + + border: var(--border-width) var(--border-style) var(--border-color); +} + + +// Input Fill: Outline, Label Placement: Stacked +// ---------------------------------------------------------------- + +// This makes the label sit above the input. +:host(.label-floating.input-fill-outline.input-label-placement-stacked:not(.input-shape-round)) .label-text-wrapper { + @include transform(translateY(0), scale(#{$form-control-label-stacked-scale})); + @include margin(0); + + /** + * Label text should not extend + * beyond the bounds of the input. + */ + max-width: calc((100% - var(--padding-start) - var(--padding-end)) / #{$form-control-label-stacked-scale}); +} + +// Start/End Slots +// ---------------------------------------------------------------- + +:host(.input-fill-outline) ::slotted([slot="start"]) { + margin-inline-end: 8px; +} + +:host(.input-fill-outline) ::slotted([slot="end"]) { + margin-inline-start: 8px; +} diff --git a/core/src/components/input/input.ionic.scss b/core/src/components/input/input.ionic.scss index 6dd8c61da0..0f9a59f8b1 100644 --- a/core/src/components/input/input.ionic.scss +++ b/core/src/components/input/input.ionic.scss @@ -1,9 +1,14 @@ @import "./input"; @import "./input.ionic.vars"; +@import "./input.ionic.outline.scss"; // Ionic Input // -------------------------------------------------- + :host { + --border-width: #{$ionic-border-size-small}; + --border-color: #{$ionic-color-neutral-300}; + // TODO(FW-6113): Verify the ionic design token is correct once it's available and remove the hardcoded value. --highlight-color-invalid: var(--ionic-color-error-600, #970606); } @@ -11,7 +16,7 @@ // Ionic Input Sizes // -------------------------------------------------- -:host(.input-size-large) { +:host(.input-size-large) .native-wrapper { min-height: 48px; } diff --git a/core/src/components/input/input.ionic.vars.scss b/core/src/components/input/input.ionic.vars.scss index 7b3c3c6c97..5b8e433a8f 100644 --- a/core/src/components/input/input.ionic.vars.scss +++ b/core/src/components/input/input.ionic.vars.scss @@ -1,4 +1,3 @@ // Ionic Input // -------------------------------------------------- - diff --git a/core/src/components/input/input.tsx b/core/src/components/input/input.tsx index 9208f170f0..9ef48244ea 100644 --- a/core/src/components/input/input.tsx +++ b/core/src/components/input/input.tsx @@ -181,8 +181,12 @@ export class Input implements ComponentInterface { * `"floating"`: The label will appear smaller and above the input when the input is focused or it has a value. Otherwise it will appear on top of the input. * `"stacked"`: The label will appear smaller and above the input regardless even when the input is blurred or has no value. * `"fixed"`: The label has the same behavior as `"start"` except it also has a fixed width. Long text will be truncated with ellipses ("..."). + * + * Defaults to "stacked" for the ionic theme, or "start" for all other themes. + * + * In the ionic theme, only the values "stacked" and "floating" are supported. */ - @Prop() labelPlacement: 'start' | 'end' | 'floating' | 'stacked' | 'fixed' = 'start'; + @Prop({ mutable: true }) labelPlacement?: 'start' | 'end' | 'floating' | 'stacked' | 'fixed'; /** * The maximum value, which must not be less than its minimum (min attribute) value. @@ -343,6 +347,10 @@ export class Input implements ComponentInterface { ...inheritAriaAttributes(this.el), ...inheritAttributes(this.el, ['tabindex', 'title', 'data-form-type']), }; + + if (this.labelPlacement === undefined) { + this.labelPlacement = getIonTheme(this) === 'ionic' ? ionicThemeDefaultLabelPlacement : 'start'; + } } connectedCallback() { @@ -471,6 +479,21 @@ export class Input implements ComponentInterface { return typeof this.value === 'number' ? this.value.toString() : (this.value || '').toString(); } + private getLabelPlacement() { + const theme = getIonTheme(this); + const { el, labelPlacement } = this; + + if (theme === 'ionic' && labelPlacement !== 'stacked' && labelPlacement !== 'floating') { + printIonWarning( + `The "${labelPlacement}" label placement is not supported in the ${theme} theme. The default value of "${ionicThemeDefaultLabelPlacement}" will be used instead.`, + el + ); + return ionicThemeDefaultLabelPlacement; + } + + return labelPlacement; + } + private getSize() { const theme = getIonTheme(this); const { size } = this; @@ -663,9 +686,9 @@ export class Input implements ComponentInterface { */ private renderLabelContainer() { const theme = getIonTheme(this); - const hasOutlineFill = theme === 'md' && this.fill === 'outline'; + const hasOutlineFill = this.fill === 'outline'; - if (hasOutlineFill) { + if (hasOutlineFill && theme === 'md') { /** * The outline fill has a special outline * that appears around the input and the label. @@ -693,19 +716,21 @@ export class Input implements ComponentInterface { } /** - * If not using the outline style, - * we can render just the label. + * If not using the outline style, OR if using the + * ionic theme, just render the label. For the ionic + * theme, the outline will be rendered elsewhere. */ return this.renderLabel(); } render() { - const { disabled, fill, readonly, shape, inputId, labelPlacement, el, hasFocus } = this; + const { disabled, fill, readonly, shape, inputId, el, hasFocus } = this; const theme = getIonTheme(this); const value = this.getValue(); const size = this.getSize(); const inItem = hostContext('ion-item', this.el); const shouldRenderHighlight = theme === 'md' && fill !== 'outline' && !inItem; + const labelPlacement = this.getLabelPlacement(); const hasValue = this.hasValue(); const hasStartEndSlots = el.querySelector('[slot="start"], [slot="end"]') !== null; @@ -755,6 +780,18 @@ export class Input implements ComponentInterface {