From faefe97da6a9d5beff1183d10efd0df9c4e3ebd7 Mon Sep 17 00:00:00 2001 From: William Martin Date: Thu, 17 Jun 2021 17:21:03 -0400 Subject: [PATCH] feat(item): add helper text, error text, counter, shape, and fill mode (#23354) resolves #19619 --- angular/src/directives/proxies.ts | 4 +- core/api.txt | 3 + core/src/components.d.ts | 24 + core/src/components/datetime/readme.md | 1 + core/src/components/item/item.ios.scss | 22 +- core/src/components/item/item.md.scss | 131 +++++- core/src/components/item/item.md.vars.scss | 27 ++ core/src/components/item/item.scss | 100 +++- core/src/components/item/item.tsx | 98 +++- core/src/components/item/readme.md | 94 +++- core/src/components/item/test/fill/index.html | 427 ++++++++++++++++++ core/src/components/item/usage/angular.md | 17 + core/src/components/item/usage/javascript.md | 17 + core/src/components/item/usage/react.md | 19 +- core/src/components/item/usage/stencil.md | 17 + core/src/components/item/usage/vue.md | 19 + core/src/components/label/label.md.scss | 94 +++- core/src/components/label/label.tsx | 3 +- core/src/components/note/readme.md | 13 + core/src/themes/test/css-variables/index.html | 18 + packages/vue/src/proxies.ts | 3 + 21 files changed, 1099 insertions(+), 52 deletions(-) create mode 100644 core/src/components/item/test/fill/index.html diff --git a/angular/src/directives/proxies.ts b/angular/src/directives/proxies.ts index 5131507a1d..c1e4265728 100644 --- a/angular/src/directives/proxies.ts +++ b/angular/src/directives/proxies.ts @@ -368,8 +368,8 @@ export class IonInput { } export declare interface IonItem extends Components.IonItem { } -@ProxyCmp({ inputs: ["button", "color", "detail", "detailIcon", "disabled", "download", "href", "lines", "mode", "rel", "routerAnimation", "routerDirection", "target", "type"] }) -@Component({ selector: "ion-item", changeDetection: ChangeDetectionStrategy.OnPush, template: "", inputs: ["button", "color", "detail", "detailIcon", "disabled", "download", "href", "lines", "mode", "rel", "routerAnimation", "routerDirection", "target", "type"] }) +@ProxyCmp({ inputs: ["button", "color", "counter", "detail", "detailIcon", "disabled", "download", "fill", "href", "lines", "mode", "rel", "routerAnimation", "routerDirection", "shape", "target", "type"] }) +@Component({ selector: "ion-item", changeDetection: ChangeDetectionStrategy.OnPush, template: "", inputs: ["button", "color", "counter", "detail", "detailIcon", "disabled", "download", "fill", "href", "lines", "mode", "rel", "routerAnimation", "routerDirection", "shape", "target", "type"] }) export class IonItem { protected el: HTMLElement; constructor(c: ChangeDetectorRef, r: ElementRef, protected z: NgZone) { diff --git a/core/api.txt b/core/api.txt index c2c5f53f3e..defe234470 100644 --- a/core/api.txt +++ b/core/api.txt @@ -512,16 +512,19 @@ ion-input,css-prop,--placeholder-opacity ion-item,shadow ion-item,prop,button,boolean,false,false,false ion-item,prop,color,string | undefined,undefined,false,true +ion-item,prop,counter,boolean,true,false,false ion-item,prop,detail,boolean | undefined,undefined,false,false ion-item,prop,detailIcon,string,'chevron-forward',false,false ion-item,prop,disabled,boolean,false,false,false ion-item,prop,download,string | undefined,undefined,false,false +ion-item,prop,fill,"outline" | "solid" | undefined,undefined,false,false ion-item,prop,href,string | undefined,undefined,false,false ion-item,prop,lines,"full" | "inset" | "none" | undefined,undefined,false,false ion-item,prop,mode,"ios" | "md",undefined,false,false ion-item,prop,rel,string | undefined,undefined,false,false ion-item,prop,routerAnimation,((baseEl: any, opts?: any) => Animation) | undefined,undefined,false,false ion-item,prop,routerDirection,"back" | "forward" | "root",'forward',false,false +ion-item,prop,shape,"round" | undefined,undefined,false,false ion-item,prop,target,string | undefined,undefined,false,false ion-item,prop,type,"button" | "reset" | "submit",'button',false,false ion-item,css-prop,--background diff --git a/core/src/components.d.ts b/core/src/components.d.ts index 8d52ae80af..98d4891e0d 100644 --- a/core/src/components.d.ts +++ b/core/src/components.d.ts @@ -1013,6 +1013,10 @@ export namespace Components { * The color to use from your application's color palette. Default options are: `"primary"`, `"secondary"`, `"tertiary"`, `"success"`, `"warning"`, `"danger"`, `"light"`, `"medium"`, and `"dark"`. For more information on colors, see [theming](/docs/theming/basics). */ "color"?: Color; + /** + * If `true`, a character counter will display the ratio of characters used and the total character limit. Only applies when the `maxlength` property is set on the inner `ion-input` or `ion-textarea`. + */ + "counter": boolean; /** * If `true`, a detail arrow will appear on the item. Defaults to `false` unless the `mode` is `ios` and an `href` or `button` property is present. */ @@ -1029,6 +1033,10 @@ export namespace Components { * This attribute instructs browsers to download a URL instead of navigating to it, so the user will be prompted to save it as a local file. If the attribute has a value, it is used as the pre-filled file name in the Save prompt (the user can still change the file name if they want). */ "download": string | undefined; + /** + * The fill for the item. If `'solid'` the item will have a background. If `'outline'` the item will be transparent with a border. Only available in `md` mode. + */ + "fill"?: 'outline' | 'solid'; /** * Contains a URL or a URL fragment that the hyperlink points to. If this property is set, an anchor tag will be rendered. */ @@ -1053,6 +1061,10 @@ export namespace Components { * When using a router, it specifies the transition direction when navigating to another page using `href`. */ "routerDirection": RouterDirection; + /** + * The shape of the item. If "round" it will have increased border radius. + */ + "shape"?: 'round'; /** * Specifies where to display the linked URL. Only applies when an `href` is provided. Special keywords: `"_blank"`, `"_self"`, `"_parent"`, `"_top"`. */ @@ -4476,6 +4488,10 @@ declare namespace LocalJSX { * The color to use from your application's color palette. Default options are: `"primary"`, `"secondary"`, `"tertiary"`, `"success"`, `"warning"`, `"danger"`, `"light"`, `"medium"`, and `"dark"`. For more information on colors, see [theming](/docs/theming/basics). */ "color"?: Color; + /** + * If `true`, a character counter will display the ratio of characters used and the total character limit. Only applies when the `maxlength` property is set on the inner `ion-input` or `ion-textarea`. + */ + "counter"?: boolean; /** * If `true`, a detail arrow will appear on the item. Defaults to `false` unless the `mode` is `ios` and an `href` or `button` property is present. */ @@ -4492,6 +4508,10 @@ declare namespace LocalJSX { * This attribute instructs browsers to download a URL instead of navigating to it, so the user will be prompted to save it as a local file. If the attribute has a value, it is used as the pre-filled file name in the Save prompt (the user can still change the file name if they want). */ "download"?: string | undefined; + /** + * The fill for the item. If `'solid'` the item will have a background. If `'outline'` the item will be transparent with a border. Only available in `md` mode. + */ + "fill"?: 'outline' | 'solid'; /** * Contains a URL or a URL fragment that the hyperlink points to. If this property is set, an anchor tag will be rendered. */ @@ -4516,6 +4536,10 @@ declare namespace LocalJSX { * When using a router, it specifies the transition direction when navigating to another page using `href`. */ "routerDirection"?: RouterDirection; + /** + * The shape of the item. If "round" it will have increased border radius. + */ + "shape"?: 'round'; /** * Specifies where to display the linked URL. Only applies when an `href` is provided. Special keywords: `"_blank"`, `"_self"`, `"_parent"`, `"_top"`. */ diff --git a/core/src/components/datetime/readme.md b/core/src/components/datetime/readme.md index fa99ef2ff3..962028ddc8 100644 --- a/core/src/components/datetime/readme.md +++ b/core/src/components/datetime/readme.md @@ -623,6 +623,7 @@ graph TD; ion-button --> ion-ripple-effect ion-item --> ion-icon ion-item --> ion-ripple-effect + ion-item --> ion-note ion-segment-button --> ion-ripple-effect style ion-datetime fill:#f9f,stroke:#333,stroke-width:4px ``` diff --git a/core/src/components/item/item.ios.scss b/core/src/components/item/item.ios.scss index 30f2c00062..8923902364 100644 --- a/core/src/components/item/item.ios.scss +++ b/core/src/components/item/item.ios.scss @@ -19,11 +19,11 @@ --background-hover-opacity: .04; --border-color: #{$item-ios-border-bottom-color}; --color: #{$item-ios-color}; - --highlight-height: 0; + --highlight-height: 0px; --highlight-color-focused: #{$item-ios-input-highlight-color}; --highlight-color-valid: #{$item-ios-input-highlight-color-valid}; --highlight-color-invalid: #{$item-ios-input-highlight-color-invalid}; - + --bottom-padding-start: 0px; font-size: $item-ios-font-size; } @@ -88,6 +88,18 @@ --show-inset-highlight: 0; } +.item-highlight, +.item-inner-highlight { + transition: none; +} + +:host(.item-has-focus) .item-inner-highlight, +:host(.item-has-focus) .item-highlight { + border-top: none; + border-right: none; + border-left: none; +} + // iOS Item Slots // -------------------------------------------------- @@ -212,6 +224,12 @@ --padding-start: 0; } +// Item Bottom +// -------------------------------------------------- + +:host(.item-has-start-slot) .item-bottom { + --bottom-padding-start: 48px; +} // FROM TEXTAREA // iOS Stacked & Floating Textarea diff --git a/core/src/components/item/item.md.scss b/core/src/components/item/item.md.scss index cee199c05e..900d263bb1 100644 --- a/core/src/components/item/item.md.scss +++ b/core/src/components/item/item.md.scss @@ -18,7 +18,6 @@ --color: #{$item-md-color}; --transition: opacity 15ms linear, background-color 15ms linear; --padding-start: #{$item-md-padding-start}; - --border-color: #{$item-md-border-bottom-color}; --inner-padding-end: #{$item-md-padding-end}; --inner-border-width: #{0 0 $item-md-border-bottom-width 0}; --highlight-height: 2px; @@ -42,6 +41,9 @@ } } +:host(.item-has-focus) .item-native { + caret-color: var(--highlight-color-focused); +} // Material Design Item Lines // -------------------------------------------------- @@ -83,6 +85,23 @@ --show-inset-highlight: 0; } +/** + * When `fill="outline"`, reposition the highlight element to cover everything but the `.item-bottom` + */ +:host(.item-fill-outline) .item-highlight { + --position-offset: calc(-1 * var(--border-width)); + + @include position(var(--position-offset), null, null, var(--position-offset)); + + width: calc(100% + 2 * var(--border-width)); + height: calc(100% + 2 * var(--border-width)); + + transition: none; +} + +:host(.item-fill-outline.item-has-focus) .item-native { + border-color: transparent; +} // Material Design Multi-line Item // -------------------------------------------------- @@ -107,6 +126,13 @@ @include margin-horizontal($item-md-end-slot-margin-start, $item-md-end-slot-margin-end); } +:host(.item-fill-solid) ::slotted([slot="start"]), +:host(.item-fill-solid) ::slotted([slot="end"]), +:host(.item-fill-outline) ::slotted([slot="start"]), +:host(.item-fill-outline) ::slotted([slot="end"]) { + align-self: center; +} + // Material Design Slotted Icon // -------------------------------------------------- @@ -117,7 +143,7 @@ font-size: $item-md-icon-slot-font-size; } -:host(.ion-color) ::slotted(ion-icon) { +:host(.ion-color:not(.item-fill-solid):not(.item-fill-outline)) ::slotted(ion-icon) { color: current-color(contrast); } @@ -133,6 +159,11 @@ @include margin-horizontal($item-md-icon-end-slot-margin-start, $item-md-icon-end-slot-margin-end); } +:host(.item-fill-solid) ::slotted(ion-icon[slot="start"]), +:host(.item-fill-outline) ::slotted(ion-icon[slot="start"]) { + @include margin-horizontal($item-md-icon-start-slot-margin-start, $item-md-input-icon-start-slot-margin-end); +} + // Material Design Slotted Toggle // -------------------------------------------------- @@ -154,7 +185,7 @@ font-size: $item-md-note-slot-font-size; } -::slotted(ion-note[slot]) { +::slotted(ion-note[slot]:not([slot="helper"]):not([slot="error"])) { @include padding($item-md-note-slot-padding-top, $item-md-note-slot-padding-end, $item-md-note-slot-padding-bottom, $item-md-note-slot-padding-start); } @@ -292,3 +323,97 @@ :host(.item-label-color) { --highlight-color-focused: #{current-color(base)}; } + +:host(.item-fill-solid.ion-color), +:host(.item-fill-outline.ion-color) { + --highlight-background: #{current-color(base)}; + --highlight-color-focused: #{current-color(base)}; +} + +// Material Design Item: Fill Solid +// -------------------------------------------------- + +:host(.item-fill-solid) { + --background: #{$item-md-input-fill-solid-background-color}; + --background-hover: #{$item-md-input-fill-solid-background-color-hover}; + --background-focused: #{$item-md-input-fill-solid-background-color-focus}; + --border-width: 0 0 #{$item-md-border-bottom-width} 0; + --inner-border-width: 0; + + @include border-radius(4px, 4px, 0, 0); +} + +:host(.item-fill-solid) .item-native { + --border-color: #{$item-md-input-fill-border-color}; +} + +:host(.item-fill-solid) .item-native:hover { + --background: var(--background-hover); + --border-color: #{$item-md-input-fill-border-color-hover}; +} + +:host(.item-fill-solid.item-has-focus) .item-native { + --background: var(--background-focused); + border-bottom-color: var(--highlight-color-focused); +} + +:host(.item-fill-solid.item-shape-round) { + @include border-radius(16px, 16px, 0, 0); +} + +// Material Design Item: Fill Outline +// -------------------------------------------------- + +:host(.item-fill-outline) { + --border-color: #{$item-md-input-fill-border-color}; + --border-width: #{$item-md-border-bottom-width}; + + border: none; + + overflow: visible; +} + +:host(.item-fill-outline) .item-native { + --native-padding-left: 16px; + + @include border-radius(4px); +} + +:host(.item-fill-outline) .item-native:hover { + --border-color: #{$item-md-input-fill-border-color-hover}; +} + +:host(.item-fill-outline.item-shape-round) .item-native { + --inner-padding-start: 16px; + + @include border-radius(28px); +} + +:host(.item-fill-outline.item-shape-round) .item-bottom { + @include padding-horizontal(32px, null); +} + + +:host(.item-fill-outline.item-label-floating.item-has-focus) .item-native ::slotted(ion-input:not(:first-child)), +:host(.item-fill-outline.item-label-floating.item-has-value) .item-native ::slotted(ion-input:not(:first-child)), +:host(.item-fill-outline.item-label-floating.item-has-focus) .item-native ::slotted(ion-textarea:not(:first-child)), +:host(.item-fill-outline.item-label-floating.item-has-value) .item-native ::slotted(ion-textarea:not(:first-child)) { + transform: translateY(-25%); +} + +// Material Design Item: Invalid +// -------------------------------------------------- + +:host(.item-fill-outline.ion-invalid:not(.ion-color)) .item-native, +:host(.item-fill-solid.ion-invalid:not(.ion-color)) .item-native { + caret-color: var(--highlight-color-invalid); +} + +:host(.item-fill-outline.ion-invalid), +:host(.item-fill-outline.ion-invalid) .item-native, +:host(.item-fill-outline.ion-invalid:not(.ion-color)) .item-highlight, +:host(.item-fill-solid.ion-invalid:not(.ion-color)), +:host(.item-fill-solid.ion-invalid:not(.ion-color)) .item-native, +:host(.item-fill-solid.ion-invalid:not(.ion-color)) .item-highlight { + border-color: var(--highlight-color-invalid); +} \ No newline at end of file diff --git a/core/src/components/item/item.md.vars.scss b/core/src/components/item/item.md.vars.scss index 1c2a532c46..f6f3c7696d 100644 --- a/core/src/components/item/item.md.vars.scss +++ b/core/src/components/item/item.md.vars.scss @@ -51,6 +51,18 @@ $item-md-border-bottom-color: $item-md-border-color !default; /// @prop - Border bottom for the item when lines are displayed $item-md-border-bottom: $item-md-border-bottom-width $item-md-border-bottom-style $item-md-border-color !default; +// Item Input +// -------------------------------------------------- + +/// @prop - Color of the item input background +$item-md-input-fill-solid-background-color: $background-color-step-50 !default; + +/// @prop - Color of the item input background when hovered +$item-md-input-fill-solid-background-color-hover: $background-color-step-100 !default; + +/// @prop - Color of the item input background when focused +$item-md-input-fill-solid-background-color-focus: $background-color-step-150 !default; + /// @prop - Color of the item input highlight $item-md-input-highlight-color: ion-color(primary, base) !default; @@ -60,6 +72,12 @@ $item-md-input-highlight-color-valid: ion-color(success, base) !default; /// @prop - Color of the item input highlight when invalid $item-md-input-highlight-color-invalid: ion-color(danger, base) !default; +/// @prop - Color of the item border when `fill` is set +$item-md-input-fill-border-color: $background-color-step-500 !default; + +/// @prop - Color of the item border when `fill` is set and hovered +$item-md-input-fill-border-color-hover: $background-color-step-750 !default; + // Item Label // -------------------------------------------------- @@ -76,6 +94,12 @@ $item-md-label-margin-bottom: 10px !default; /// @prop - Margin start of the label $item-md-label-margin-start: 0 !default; +/// @prop - X translation for floating labels +$item-md-fill-outline-label-translate-x: -32px !default; + +/// @prop - Padding for floating labels +$item-md-fill-outline-label-padding: 4px !default; + // Item Slots // -------------------------------------------------- @@ -126,6 +150,9 @@ $item-md-icon-start-slot-margin-start: null !default; /// @prop - Margin end for an icon in the start slot $item-md-icon-start-slot-margin-end: 32px !default; +/// @prop - Margin end for an icon in the start slot +$item-md-input-icon-start-slot-margin-end: 8px !default; + /// @prop - Margin start for an icon in the end slot $item-md-icon-end-slot-margin-start: 16px !default; diff --git a/core/src/components/item/item.scss b/core/src/components/item/item.scss index 882cce3023..a0c7fb3eb7 100644 --- a/core/src/components/item/item.scss +++ b/core/src/components/item/item.scss @@ -99,13 +99,13 @@ // Item: Color // -------------------------------------------------- -:host(.ion-color) .item-native { +:host(.ion-color:not(.item-fill-solid):not(.item-fill-outline)) .item-native { background: current-color(base); color: current-color(contrast); } -:host(.ion-color) .item-native, -:host(.ion-color) .item-inner { +:host(.ion-color:not(.item-fill-solid):not(.item-fill-outline)) .item-native, +:host(.ion-color:not(.item-fill-solid):not(.item-fill-outline)) .item-inner { border-color: current-color(shade); } @@ -352,21 +352,53 @@ button, a { .item-highlight, .item-inner-highlight { - @include position(null, 0, 0, 0); - + @include position(0, 0, 0, 0); + @include border-radius(inherit); position: absolute; - background: var(--highlight-background); + width: 100%; - z-index: 1; + height: 100%; + + transform: scaleX(0); + + transition: transform 200ms, border-bottom-width 200ms; + + z-index: 2; + + box-sizing: border-box; + pointer-events: none; } -.item-highlight { - height: var(--full-highlight-height); +:host(.item-has-focus) .item-highlight, +:host(.item-has-focus) .item-inner-highlight { + transform: scaleX(1); + + border-style: var(--border-style); + border-color: var(--highlight-background); } -.item-inner-highlight { - height: var(--inset-highlight-height); +:host(.item-has-focus) .item-highlight { + border-width: var(--full-highlight-height); + + opacity: var(--show-full-highlight); +} + +:host(.item-has-focus) .item-inner-highlight { + border-bottom-width: var(--inset-highlight-height); + + opacity: var(--show-inset-highlight); +} + +:host(.item-has-focus.item-fill-solid) .item-highlight { + border-width: calc(var(--full-highlight-height) - 1px); +} + +:host(.item-has-focus) .item-inner-highlight, +:host(.item-has-focus:not(.item-fill-outline)) .item-highlight { + border-top: none; + border-right: none; + border-left: none; } @@ -403,6 +435,13 @@ button, a { --highlight-background: var(--highlight-color-invalid); } +:host(.item-interactive.ion-invalid) ::slotted([slot="helper"]) { + display: none; +} + +:host(.item-interactive.ion-invalid) ::slotted([slot="error"]) { + display: block; +} // Item Select // -------------------------------------------------- @@ -476,3 +515,42 @@ button, a { ion-ripple-effect { color: var(--ripple-color); } + +// Item Button Ripple effect +// -------------------------------------------------- + +.item-bottom { + @include margin(0); + @include padding( + var(--padding-top), + var(--inner-padding-end), + var(--padding-bottom), + calc(var(--padding-start) + var(--ion-safe-area-left, 0px) + var(--bottom-padding-start, 0px)) + ); + display: flex; + + justify-content: space-between; +} + +:host(.item-fill-solid) ::slotted([slot="start"]), +:host(.item-fill-solid) ::slotted([slot="end"]), +:host(.item-fill-outline) ::slotted([slot="start"]), +:host(.item-fill-outline) ::slotted([slot="end"]) { + align-self: center; +} + +::slotted([slot="helper"]), +::slotted([slot="error"]), +.item-counter { + padding-top: 5px; + + font-size: 12px; + + z-index: 1; +} + +::slotted([slot="error"]) { + display: none; + + color: var(--highlight-color-invalid); +} \ No newline at end of file diff --git a/core/src/components/item/item.tsx b/core/src/components/item/item.tsx index f173b3f984..608abd0830 100644 --- a/core/src/components/item/item.tsx +++ b/core/src/components/item/item.tsx @@ -5,6 +5,7 @@ import { AnimationBuilder, Color, CssClassMap, RouterDirection, StyleEventDetail import { AnchorInterface, ButtonInterface } from '../../utils/element-interface'; import { raf } from '../../utils/helpers'; import { createColorClasses, hostContext, openURL } from '../../utils/theme'; +import { InputChangeEventDetail } from '../input/input-interface'; /** * @virtualProp {"ios" | "md"} mode - The mode determines which platform styles to use. @@ -72,6 +73,17 @@ export class Item implements ComponentInterface, AnchorInterface, ButtonInterfac */ @Prop() download: string | undefined; + /** + * The fill for the item. If `'solid'` the item will have a background. If + * `'outline'` the item will be transparent with a border. Only available in `md` mode. + */ + @Prop() fill?: 'outline' | 'solid'; + + /** + * The shape of the item. If "round" it will have increased + * border radius. + */ + @Prop() shape?: 'round'; /** * Contains a URL or a URL fragment that the hyperlink points to. * If this property is set, an anchor tag will be rendered. @@ -89,6 +101,11 @@ export class Item implements ComponentInterface, AnchorInterface, ButtonInterfac */ @Prop() lines?: 'full' | 'inset' | 'none'; + /** + * If `true`, a character counter will display the ratio of characters used and the total character limit. Only applies when the `maxlength` property is set on the inner `ion-input` or `ion-textarea`. + */ + @Prop() counter = true; + /** * When using a router, it specifies the transition animation when navigating to * another page using `href`. @@ -113,6 +130,15 @@ export class Item implements ComponentInterface, AnchorInterface, ButtonInterfac */ @Prop() type: 'submit' | 'reset' | 'button' = 'button'; + @State() counterString: string | null | undefined; + + @Listen('ionChange') + handleIonChange(ev: CustomEvent) { + if (this.counter && ev.target === this.getFirstInput()) { + this.updateCounterOutput(ev.target as HTMLIonInputElement | HTMLIonTextareaElement); + } + } + @Listen('ionColor') labelColorChanged(ev: CustomEvent) { const { color } = this; @@ -153,6 +179,14 @@ export class Item implements ComponentInterface, AnchorInterface, ButtonInterfac } } + connectedCallback() { + if (this.counter) { + this.updateCounterOutput(this.getFirstInput()); + } + + this.hasStartEl(); + } + componentDidUpdate() { // Do not use @Listen here to avoid making all items // appear as clickable to screen readers @@ -239,7 +273,7 @@ export class Item implements ComponentInterface, AnchorInterface, ButtonInterfac // Only focus the first input if we clicked on an ion-item // and the first input exists - if (clickedItem && firstActive) { + if (clickedItem && (firstActive || !this.multipleInputs)) { input.fireFocusEvents = false; input.setBlur(); input.setFocus(); @@ -249,12 +283,27 @@ export class Item implements ComponentInterface, AnchorInterface, ButtonInterfac } } + private updateCounterOutput(inputEl: HTMLIonInputElement | HTMLIonTextareaElement) { + if (this.counter && !this.multipleInputs && inputEl?.maxlength !== undefined) { + const length = inputEl?.value?.toString().length ?? '0'; + this.counterString = `${length}/${inputEl.maxlength}`; + } + } + + private hasStartEl() { + const startEl = this.el.querySelector('[slot="start"]'); + if (startEl !== null) { + this.el.classList.add('item-has-start-slot'); + } + } + render() { - const { detail, detailIcon, download, labelColorStyles, lines, disabled, href, rel, target, routerAnimation, routerDirection } = this; + const { counterString, detail, detailIcon, download, fill, labelColorStyles, lines, disabled, href, rel, shape, target, routerAnimation, routerDirection } = this; const childStyles = {}; const mode = getIonMode(this); const clickable = this.isClickable(); const canActivate = this.canActivate(); + const hasFill = fill === 'outline' || fill === 'solid'; const TagType = clickable ? (href === undefined ? 'button' : 'a') : 'div' as any; const attrs = (TagType === 'button') ? { type: this.type } @@ -284,33 +333,42 @@ export class Item implements ComponentInterface, AnchorInterface, ButtonInterfac 'item': true, [mode]: true, [`item-lines-${lines}`]: lines !== undefined, + [`item-fill-${fill}`]: fill !== undefined, + [`item-shape-${shape}`]: shape !== undefined, 'item-disabled': disabled, 'in-list': hostContext('ion-list', this.el), 'item-multiple-inputs': this.multipleInputs, 'ion-activatable': canActivate, 'ion-focusable': true, + 'item-rtl': document.dir === 'rtl' }) }} > - - -
-
- -
- - {showDetail && } -
+ + +
+
+
- {canActivate && mode === 'md' && } - -
+ + {showDetail && } +
+
+ {canActivate && mode === 'md' && } + {hasFill &&
} +
+ {!hasFill &&
} +
+ + + {counterString && {counterString}} +
); } diff --git a/core/src/components/item/readme.md b/core/src/components/item/readme.md index f1326058a2..ca65c5db08 100644 --- a/core/src/components/item/readme.md +++ b/core/src/components/item/readme.md @@ -369,6 +369,23 @@ The highlight color changes based on the item state, but all of the states use I + + Input (Fill: Solid) + + + + + Input (Fill: Outline) + + + + + Helper and Error Text + + Helper Text + Error Text + + Checkbox @@ -691,6 +708,23 @@ The highlight color changes based on the item state, but all of the states use I + + Input (Fill: Solid) + + + + + Input (Fill: Outline) + + + + + Helper and Error Text + + Helper Text + Error Text + + Checkbox @@ -707,7 +741,7 @@ The highlight color changes based on the item state, but all of the states use I ```tsx import React from 'react'; -import { IonContent, IonHeader, IonPage, IonTitle, IonToolbar, IonItem, IonLabel, IonList, IonText, IonAvatar, IonThumbnail, IonButton, IonIcon, IonDatetime, IonSelect, IonSelectOption, IonToggle, IonInput, IonCheckbox, IonRange } from '@ionic/react'; +import { IonContent, IonHeader, IonPage, IonTitle, IonToolbar, IonItem, IonLabel, IonList, IonText, IonAvatar, IonThumbnail, IonButton, IonIcon, IonDatetime, IonSelect, IonSelectOption, IonToggle, IonInput, IonCheckbox, IonRange, IonNote } from '@ionic/react'; import { closeCircle, home, star, navigate, informationCircle, checkmarkCircle, shuffle } from 'ionicons/icons'; export const ItemExamples: React.FC = () => { @@ -998,6 +1032,23 @@ export const ItemExamples: React.FC = () => { + + Input (Fill: Solid) + + + + + Input (Fill: Outline) + + + + + Helper and Error Text + + Helper Text + Error Text + + Checkbox @@ -1427,6 +1478,23 @@ export class ItemExample { , + + Input (Fill: Solid) + + , + + + Input (Fill: Outline) + + , + + + Helper and Error Text + + Helper Text + Error Text + , + Checkbox @@ -1767,6 +1835,23 @@ export class ItemExample { + + Input (Fill: Solid) + + + + + Input (Fill: Outline) + + + + + Helper and Error Text + + Helper Text + Error Text + + Checkbox @@ -1784,6 +1869,7 @@ import { IonButton, IonCheckbox, IonDatetime, + IonNote, IonIcon, IonInput, IonItem, @@ -1812,6 +1898,7 @@ export default defineComponent({ IonButton, IonCheckbox, IonDatetime, + IonNote, IonIcon, IonInput, IonItem, @@ -1846,16 +1933,19 @@ export default defineComponent({ | ----------------- | ------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------- | ------------------- | | `button` | `button` | If `true`, a button tag will be rendered and the item will be tappable. | `boolean` | `false` | | `color` | `color` | The color to use from your application's color palette. Default options are: `"primary"`, `"secondary"`, `"tertiary"`, `"success"`, `"warning"`, `"danger"`, `"light"`, `"medium"`, and `"dark"`. For more information on colors, see [theming](/docs/theming/basics). | `string \| undefined` | `undefined` | +| `counter` | `counter` | If `true`, a character counter will display the ratio of characters used and the total character limit. Only applies when the `maxlength` property is set on the inner `ion-input` or `ion-textarea`. | `boolean` | `true` | | `detail` | `detail` | If `true`, a detail arrow will appear on the item. Defaults to `false` unless the `mode` is `ios` and an `href` or `button` property is present. | `boolean \| undefined` | `undefined` | | `detailIcon` | `detail-icon` | The icon to use when `detail` is set to `true`. | `string` | `'chevron-forward'` | | `disabled` | `disabled` | If `true`, the user cannot interact with the item. | `boolean` | `false` | | `download` | `download` | This attribute instructs browsers to download a URL instead of navigating to it, so the user will be prompted to save it as a local file. If the attribute has a value, it is used as the pre-filled file name in the Save prompt (the user can still change the file name if they want). | `string \| undefined` | `undefined` | +| `fill` | `fill` | The fill for the item. If `'solid'` the item will have a background. If `'outline'` the item will be transparent with a border. Only available in `md` mode. | `"outline" \| "solid" \| undefined` | `undefined` | | `href` | `href` | Contains a URL or a URL fragment that the hyperlink points to. If this property is set, an anchor tag will be rendered. | `string \| undefined` | `undefined` | | `lines` | `lines` | How the bottom border should be displayed on the item. | `"full" \| "inset" \| "none" \| undefined` | `undefined` | | `mode` | `mode` | The mode determines which platform styles to use. | `"ios" \| "md"` | `undefined` | | `rel` | `rel` | Specifies the relationship of the target object to the link object. The value is a space-separated list of [link types](https://developer.mozilla.org/en-US/docs/Web/HTML/Link_types). | `string \| undefined` | `undefined` | | `routerAnimation` | -- | When using a router, it specifies the transition animation when navigating to another page using `href`. | `((baseEl: any, opts?: any) => Animation) \| undefined` | `undefined` | | `routerDirection` | `router-direction` | When using a router, it specifies the transition direction when navigating to another page using `href`. | `"back" \| "forward" \| "root"` | `'forward'` | +| `shape` | `shape` | The shape of the item. If "round" it will have increased border radius. | `"round" \| undefined` | `undefined` | | `target` | `target` | Specifies where to display the linked URL. Only applies when an `href` is provided. Special keywords: `"_blank"`, `"_self"`, `"_parent"`, `"_top"`. | `string \| undefined` | `undefined` | | `type` | `type` | The type of the button. Only used when an `onclick` or `button` property is present. | `"button" \| "reset" \| "submit"` | `'button'` | @@ -1929,12 +2019,14 @@ export default defineComponent({ - ion-icon - [ion-ripple-effect](../ripple-effect) +- [ion-note](../note) ### Graph ```mermaid graph TD; ion-item --> ion-icon ion-item --> ion-ripple-effect + ion-item --> ion-note ion-datetime --> ion-item ion-select-popover --> ion-item style ion-item fill:#f9f,stroke:#333,stroke-width:4px diff --git a/core/src/components/item/test/fill/index.html b/core/src/components/item/test/fill/index.html new file mode 100644 index 0000000000..d50b46ed70 --- /dev/null +++ b/core/src/components/item/test/fill/index.html @@ -0,0 +1,427 @@ + + + + + + Item - Fill + + + + + + + + + + + + + Item - Fill + + + + +

Filled

+ + + + + Standard + + Helper Text + Error Text + + + + + + Standard + + Helper Text + Error Text + + + + + Standard + + + Helper Text + + + + + + + Standard + + Helper Text + Error Text + + + + + + Standard + + Helper Text + Error Text + + + + + Standard + + + Helper Text + + + + + +

Shaped Filled

+ + + + + Standard + + Helper Text + + + + + + Standard + + Helper Text + + + + + Standard + + + Helper Text + + + + + + + Standard + + Helper Text + Error Text + + + + + + Standard + + Helper Text + Error Text + + + + + Standard + + + Helper Text + + + + + +

Outlined

+ + + + + Standards + + Helper Text + + + + + + Standard + + Helper Text + + + + + Standard + + + Helper Text + + + + + + + Standards + + Helper Text + Error Text + + + + + + Standard + + Helper Text + Error Text + + + + + Standard + + + Helper Text + + + + + +

Shaped Outlined

+ + + + + Standard + + Helper Text + + + + + + Standard + + Helper Text + + + + + Standard + + + Helper Text + + + + + + + Standard + + Helper Text + Error Text + + + + + + Standard + + Helper Text + Error Text + + + + + Standard + + + Helper Text + + + + + +

Input Without Label

+ + + + + + Helper Text + + + + + + Helper Text + + + + + + Helper Text + + + + + +

Input With Character Counter

+ + + + + + Helper Text + + + + + + Helper Text + + + + + + Helper Text + + + + + +

Disable

+ + + + + Standard + + Helper Text + + + + + Standard + + Helper Text + + + + + Standard + + Helper Text + + + + + + + Standard + + Helper Text + + + + + Standard + + Helper Text + + + + + Standard + + Helper Text + + + + + +

Textarea

+ + + + + + Helper Text + + + + + + Helper Text + + + + + + Helper Text + + + + + + + Standard + + Helper Text + Error Text + + + + + Standard + + Helper Text + Error Text + + + + + Standard + + Helper Text + Error Text + + + + + + + Standard + + Helper Text + Error Text + + + + + Standard + + Helper Text + Error Text + + + + + Standard + + Helper Text + + + + +
+
+ + + diff --git a/core/src/components/item/usage/angular.md b/core/src/components/item/usage/angular.md index 6ea952267a..18851d0dfb 100644 --- a/core/src/components/item/usage/angular.md +++ b/core/src/components/item/usage/angular.md @@ -306,6 +306,23 @@
+ + Input (Fill: Solid) + + + + + Input (Fill: Outline) + + + + + Helper and Error Text + + Helper Text + Error Text + + Checkbox diff --git a/core/src/components/item/usage/javascript.md b/core/src/components/item/usage/javascript.md index 01f23b6aaf..a23543adda 100644 --- a/core/src/components/item/usage/javascript.md +++ b/core/src/components/item/usage/javascript.md @@ -306,6 +306,23 @@ + + Input (Fill: Solid) + + + + + Input (Fill: Outline) + + + + + Helper and Error Text + + Helper Text + Error Text + + Checkbox diff --git a/core/src/components/item/usage/react.md b/core/src/components/item/usage/react.md index 102ac5baaa..deae8f8ce1 100644 --- a/core/src/components/item/usage/react.md +++ b/core/src/components/item/usage/react.md @@ -1,6 +1,6 @@ ```tsx import React from 'react'; -import { IonContent, IonHeader, IonPage, IonTitle, IonToolbar, IonItem, IonLabel, IonList, IonText, IonAvatar, IonThumbnail, IonButton, IonIcon, IonDatetime, IonSelect, IonSelectOption, IonToggle, IonInput, IonCheckbox, IonRange } from '@ionic/react'; +import { IonContent, IonHeader, IonPage, IonTitle, IonToolbar, IonItem, IonLabel, IonList, IonText, IonAvatar, IonThumbnail, IonButton, IonIcon, IonDatetime, IonSelect, IonSelectOption, IonToggle, IonInput, IonCheckbox, IonRange, IonNote } from '@ionic/react'; import { closeCircle, home, star, navigate, informationCircle, checkmarkCircle, shuffle } from 'ionicons/icons'; export const ItemExamples: React.FC = () => { @@ -291,6 +291,23 @@ export const ItemExamples: React.FC = () => { + + Input (Fill: Solid) + + + + + Input (Fill: Outline) + + + + + Helper and Error Text + + Helper Text + Error Text + + Checkbox diff --git a/core/src/components/item/usage/stencil.md b/core/src/components/item/usage/stencil.md index b4e04f2602..13c3ec4bdd 100644 --- a/core/src/components/item/usage/stencil.md +++ b/core/src/components/item/usage/stencil.md @@ -409,6 +409,23 @@ export class ItemExample { , + + Input (Fill: Solid) + + , + + + Input (Fill: Outline) + + , + + + Helper and Error Text + + Helper Text + Error Text + , + Checkbox diff --git a/core/src/components/item/usage/vue.md b/core/src/components/item/usage/vue.md index 8c7de6c04a..7aeb2626a0 100644 --- a/core/src/components/item/usage/vue.md +++ b/core/src/components/item/usage/vue.md @@ -321,6 +321,23 @@ + + Input (Fill: Solid) + + + + + Input (Fill: Outline) + + + + + Helper and Error Text + + Helper Text + Error Text + + Checkbox @@ -338,6 +355,7 @@ import { IonButton, IonCheckbox, IonDatetime, + IonNote, IonIcon, IonInput, IonItem, @@ -366,6 +384,7 @@ export default defineComponent({ IonButton, IonCheckbox, IonDatetime, + IonNote, IonIcon, IonInput, IonItem, diff --git a/core/src/components/label/label.md.scss b/core/src/components/label/label.md.scss index 46ff3e2384..05bf329cef 100644 --- a/core/src/components/label/label.md.scss +++ b/core/src/components/label/label.md.scss @@ -17,8 +17,23 @@ * When translating the label, we need to use translateY * instead of translate3d due to a WebKit bug: https://bugs.webkit.org/show_bug.cgi?id=215731 */ + :host(.label-stacked), + :host(.label-floating) { + @include margin(0, 0, 0, 0); + /* stylelint-disable property-blacklist */ + transform-origin: top left; + /* stylelint-enable property-blacklist */ + z-index: 3; + } + + :host(.label-stacked.label-rtl), + :host(.label-floating.label-rtl) { + /* stylelint-disable property-blacklist */ + transform-origin: top right; + /* stylelint-enable property-blacklist */ + } + :host(.label-stacked) { - @include transform-origin(start, top); @include transform(translateY(50%), scale(.75)); transition: color 150ms $label-md-transition-timing-function; @@ -26,28 +41,73 @@ :host(.label-floating) { @include transform(translateY(96%)); - @include transform-origin(start, top); transition: color 150ms $label-md-transition-timing-function, transform 150ms $label-md-transition-timing-function; } -:host-context(.item-textarea).label-floating { - @include transform(translateY(185%)); -} - -:host(.label-stacked), -:host(.label-floating) { - @include margin(0, 0, 0, 0); -} - :host-context(.item-has-focus).label-floating, :host-context(.item-has-placeholder:not(.item-input)).label-floating, :host-context(.item-has-value).label-floating { @include transform(translateY(50%), scale(.75)); } +/** + * When translating the label inside of an ion-item with `fill="outline"`, + * add pseudo-elements to imitate fieldset-like padding without shifting the label + */ +:host-context(.item-fill-outline.item-has-focus).label-floating, +:host-context(.item-fill-outline.item-has-placeholder:not(.item-input)).label-floating, +:host-context(.item-fill-outline.item-has-value).label-floating { + @include transform(translateY(-6px), scale(.75)); + position: relative; + + max-width: min-content; + + background-color: $item-md-background; + + overflow: visible; + z-index: 3; + + &::before, + &::after { + position: absolute; + + width: $item-md-fill-outline-label-padding; + + height: 100%; + + background-color: $item-md-background; + + content: ""; + } + + &::before { + /* stylelint-disable property-blacklist */ + left: calc(-1 * #{$item-md-fill-outline-label-padding}); + /* stylelint-enable property-blacklist */ + } + + &::after { + /* stylelint-disable property-blacklist */ + right: calc(-1 * #{$item-md-fill-outline-label-padding}); + /* stylelint-enable property-blacklist */ + } +} + +:host-context(.item-fill-outline.item-has-focus.item-has-start-slot).label-floating, +:host-context(.item-fill-outline.item-has-placeholder:not(.item-input).item-has-start-slot).label-floating, +:host-context(.item-fill-outline.item-has-value.item-has-start-slot).label-floating { + @include transform(translateX(#{$item-md-fill-outline-label-translate-x}), translateY(-6px), scale(.75)); +} + +:host-context(.item-fill-outline.item-has-focus.item-has-start-slot).label-floating.label-rtl, +:host-context(.item-fill-outline.item-has-placeholder:not(.item-input).item-has-start-slot).label-floating.label-rtl, +:host-context(.item-fill-outline.item-has-value.item-has-start-slot).label-floating.label-rtl { + @include transform(translateX(calc(-1 * #{$item-md-fill-outline-label-translate-x})), translateY(-6px), scale(.75)); +} + :host-context(.item-has-focus).label-stacked:not(.ion-color), :host-context(.item-has-focus).label-floating:not(.ion-color) { color: $label-md-text-color-focused; @@ -58,6 +118,18 @@ color: #{current-color(contrast)}; } +:host-context(.item-fill-solid.item-has-focus.ion-color).label-stacked:not(.ion-color), +:host-context(.item-fill-solid.item-has-focus.ion-color).label-floating:not(.ion-color), +:host-context(.item-fill-outline.item-has-focus.ion-color).label-stacked:not(.ion-color), +:host-context(.item-fill-outline.item-has-focus.ion-color).label-floating:not(.ion-color) { + color: #{current-color(base)}; +} + +:host-context(.ion-invalid).label-stacked:not(.ion-color), +:host-context(.ion-invalid).label-floating:not(.ion-color) { + color: var(--highlight-color-invalid); +} + // MD Typography // -------------------------------------------------- diff --git a/core/src/components/label/label.tsx b/core/src/components/label/label.tsx index 3b08f6199c..5e4b6a034c 100644 --- a/core/src/components/label/label.tsx +++ b/core/src/components/label/label.tsx @@ -102,7 +102,8 @@ export class Label implements ComponentInterface { class={createColorClasses(this.color, { [mode]: true, [`label-${position}`]: position !== undefined, - [`label-no-animate`]: (this.noAnimate) + [`label-no-animate`]: (this.noAnimate), + 'label-rtl': document.dir === 'rtl' })} > diff --git a/core/src/components/note/readme.md b/core/src/components/note/readme.md index 86e2750ed0..74d5a5ddae 100644 --- a/core/src/components/note/readme.md +++ b/core/src/components/note/readme.md @@ -166,6 +166,19 @@ export default defineComponent({ | `--color` | Color of the note | +## Dependencies + +### Used by + + - [ion-item](../item) + +### Graph +```mermaid +graph TD; + ion-item --> ion-note + style ion-note fill:#f9f,stroke:#333,stroke-width:4px +``` + ---------------------------------------------- *Built with [StencilJS](https://stenciljs.com/)* diff --git a/core/src/themes/test/css-variables/index.html b/core/src/themes/test/css-variables/index.html index ccbc4c1dcc..4d0bc5cf89 100644 --- a/core/src/themes/test/css-variables/index.html +++ b/core/src/themes/test/css-variables/index.html @@ -191,6 +191,24 @@ Card Button Item 2 focused + + + + + + Standard + + Error Text + + + + Standard + + + Helper Text + + +
diff --git a/packages/vue/src/proxies.ts b/packages/vue/src/proxies.ts index 8f811df3a1..21afc25244 100644 --- a/packages/vue/src/proxies.ts +++ b/packages/vue/src/proxies.ts @@ -379,9 +379,12 @@ export const IonItem = /*@__PURE__*/ defineContainer('ion-item', Io 'detailIcon', 'disabled', 'download', + 'fill', + 'shape', 'href', 'rel', 'lines', + 'counter', 'routerAnimation', 'routerDirection', 'target',