From 576f68be36a9d8b04ef86fd6c3d8d55bf7daa898 Mon Sep 17 00:00:00 2001 From: MariaLStefan <103122411+MariaLStefan@users.noreply.github.com> Date: Thu, 14 Mar 2024 15:04:07 +0100 Subject: [PATCH] fix(elements/ino-input): prevent label cut-off in outline variant (#1189) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * style(elements|ino-input): add padding to label * chore: add outline runtime change warning * fix: ino-input story type errors * use MDCNotchedOutline * fix notch of input * push build changes * revert to minimal changes * improve spacing * remove redundant css * feat: add mac special font scaling * fix mac issue * move label logic to ino-label * use correct lifecycle --------- Co-authored-by: Jan-Niklas Voß Co-authored-by: Benjamin Pagelsdorf --- .../src/directives/proxies.ts | 3 +- packages/elements/package.json | 1 + packages/elements/src/components.d.ts | 10 +++- .../src/components/ino-input/ino-input.tsx | 49 ++++++++++++----- .../src/components/ino-input/readme.md | 50 +++++++++--------- .../ino-label/createMDCNotchedOutline.ts | 52 +++++++++++++++++++ .../src/components/ino-label/ino-label.tsx | 27 +++++++++- .../src/components/ino-label/readme.md | 13 +++++ packages/storybook/elements-stencil-docs.json | 37 ++++++++++++- .../stories/ino-input/ino-input.stories.ts | 2 - 10 files changed, 199 insertions(+), 45 deletions(-) create mode 100644 packages/elements/src/components/ino-label/createMDCNotchedOutline.ts diff --git a/packages/elements-angular/src/directives/proxies.ts b/packages/elements-angular/src/directives/proxies.ts index 16c601bc74..526c8593ce 100644 --- a/packages/elements-angular/src/directives/proxies.ts +++ b/packages/elements-angular/src/directives/proxies.ts @@ -585,7 +585,8 @@ export declare interface InoInputFile extends Components.InoInputFile { @ProxyCmp({ - inputs: ['disabled', 'for', 'outline', 'required', 'showHint', 'text'] + inputs: ['disabled', 'for', 'outline', 'required', 'showHint', 'text'], + methods: ['getMdcNotchedOutlineInstance'] }) @Component({ selector: 'ino-label', diff --git a/packages/elements/package.json b/packages/elements/package.json index 1eba291100..52c8ed168c 100644 --- a/packages/elements/package.json +++ b/packages/elements/package.json @@ -71,6 +71,7 @@ "@material/tab-indicator": "^13.0.0", "@material/tab-scroller": "^13.0.0", "@material/textfield": "^13.0.0", + "@material/notched-outline": "^13.0.0", "@material/typography": "^13.0.0", "@stencil/core": "^4.12.2", "@tarekraafat/autocomplete.js": "^10.2.7", diff --git a/packages/elements/src/components.d.ts b/packages/elements/src/components.d.ts index 8cfafb6186..10783832df 100644 --- a/packages/elements/src/components.d.ts +++ b/packages/elements/src/components.d.ts @@ -8,10 +8,12 @@ import { HTMLStencilElement, JSXBase } from "@stencil/core/internal"; import { Alignment, ButtonType, ButtonVariants, ChipSurface, DialogCloseAction, DialogSubmitAction, HorizontalLocation, ImageDecodingTypes, InputType, KeyValue, Locations, NavDrawerAnchor, NavDrawerLabels, NavDrawerVariant, SnackbarLabels, SnackbarType, SpinnerType, TippyThemes, TooltipTrigger, UserInputInterceptor, VerticalLocation, ViewModeUnion } from "./components/types"; import { PickerTypeKeys } from "./components/ino-datepicker/picker-factory"; import { Placement, Props } from "tippy.js"; +import { MDCNotchedOutline } from "@material/notched-outline"; import { SortDirection, SortDirectionChangeDetails } from "./interface"; export { Alignment, ButtonType, ButtonVariants, ChipSurface, DialogCloseAction, DialogSubmitAction, HorizontalLocation, ImageDecodingTypes, InputType, KeyValue, Locations, NavDrawerAnchor, NavDrawerLabels, NavDrawerVariant, SnackbarLabels, SnackbarType, SpinnerType, TippyThemes, TooltipTrigger, UserInputInterceptor, VerticalLocation, ViewModeUnion } from "./components/types"; export { PickerTypeKeys } from "./components/ino-datepicker/picker-factory"; export { Placement, Props } from "tippy.js"; +export { MDCNotchedOutline } from "@material/notched-outline"; export { SortDirection, SortDirectionChangeDetails } from "./interface"; export namespace Components { interface InoAccordion { @@ -796,7 +798,7 @@ export namespace Components { */ "name"?: string; /** - * Styles the input field as outlined element. + * Styles the input field as outlined element. This property is immutable which means that it should not be changed after its first initialization. Changing this property at runtime causes problems in combination with the floating label. You can read more about this issue [here](https://github.com/inovex/elements/issues/1216). */ "outline"?: boolean; /** @@ -906,6 +908,10 @@ export namespace Components { * Id of the associated form control */ "for": string; + /** + * Returns internal mdcNotchedOutline instance + */ + "getMdcNotchedOutlineInstance": () => Promise; /** * Styles the label in an outlined style */ @@ -3598,7 +3604,7 @@ declare namespace LocalJSX { */ "onValueChange"?: (event: InoInputCustomEvent) => void; /** - * Styles the input field as outlined element. + * Styles the input field as outlined element. This property is immutable which means that it should not be changed after its first initialization. Changing this property at runtime causes problems in combination with the floating label. You can read more about this issue [here](https://github.com/inovex/elements/issues/1216). */ "outline"?: boolean; /** diff --git a/packages/elements/src/components/ino-input/ino-input.tsx b/packages/elements/src/components/ino-input/ino-input.tsx index b83589fb99..82fd189507 100644 --- a/packages/elements/src/components/ino-input/ino-input.tsx +++ b/packages/elements/src/components/ino-input/ino-input.tsx @@ -18,6 +18,7 @@ import classNames from 'classnames'; import { generateUniqueId, hasSlotContent } from '../../util/component-utils'; import { getPrecision } from '../../util/math-utils'; import { InputType, UserInputInterceptor } from '../types'; +import { MDCNotchedOutline } from '@material/notched-outline'; /** * An input component with styles. It functions as a wrapper around the material [textfield](https://github.com/material-components/material-components-web/tree/master/packages/mdc-textfield) component. @@ -36,6 +37,7 @@ export class Input implements ComponentInterface { @Element() el!: HTMLInoInputElement; private nativeInputEl?: HTMLInputElement; + private inoLabelElement?: HTMLInoLabelElement; private cursorPosition = 0; /** @@ -54,12 +56,12 @@ export class Input implements ComponentInterface { private mdcTextfield: MDCTextField; /** - * An internal instance of an textfield helper text instance (if neccessary). + * An internal instance of a textfield helper text instance (if necessary). */ private mdcHelperText: MDCTextFieldHelperText; /** - * An internal instance of an textfield icon instance (if neccessary). + * An internal instance of a textfield icon instance (if necessary). */ private mdcTextfieldIcon: MDCTextFieldIcon; @@ -163,9 +165,21 @@ export class Input implements ComponentInterface { /** * Styles the input field as outlined element. + * + * This property is immutable which means that it should not be changed after its first initialization. + * Changing this property at runtime causes problems in combination with the floating label. + * You can read more about this issue [here](https://github.com/inovex/elements/issues/1216). */ @Prop() outline?: boolean; + @Watch('outline') + handleOutlineChange(oldVal: boolean, newVal: boolean) { + if (oldVal !== newVal) + console.warn( + `Changing the 'outline' property at runtime is not recommended. Read more about it here: https://github.com/inovex/elements/issues/1216`, + ); + } + /** * The validation pattern of this element. */ @@ -260,20 +274,15 @@ export class Input implements ComponentInterface { // Lifecycle methods // ---- - componentDidLoad() { + async componentDidLoad() { this.mdcTextfield = new MDCTextField( this.el.querySelector('.mdc-text-field'), ); - if (this.type === 'email') { - this.mdcTextfield.useNativeValidation = false; - } - if (this.helper) { - const helperTextEl = document.querySelector( - '.mdc-text-field-helper-text', + this.mdcHelperText = new MDCTextFieldHelperText( + this.el.querySelector('.mdc-text-field-helper-text'), ); - this.mdcHelperText = new MDCTextFieldHelperText(helperTextEl); } if ( @@ -284,6 +293,23 @@ export class Input implements ComponentInterface { this.el.querySelector('.mdc-text-field__icon'), ); } + + const mdcNotchedOutline = + await this.inoLabelElement.getMdcNotchedOutlineInstance(); + this.mdcTextfield.initialize( + undefined, + undefined, + (el) => this.mdcHelperText ?? new MDCTextFieldHelperText(el), + undefined, + (el) => this.mdcTextfieldIcon ?? new MDCTextFieldIcon(el), + undefined, + (el) => mdcNotchedOutline ?? new MDCNotchedOutline(el), + ); + + if (this.type === 'email') { + this.mdcTextfield.useNativeValidation = false; + } + this.textfieldValue = this.value || ''; if (this.autoFocus) { @@ -306,8 +332,6 @@ export class Input implements ComponentInterface { disconnectedCallback() { this.mdcTextfield?.destroy(); - this.mdcHelperText?.destroy(); - this.mdcTextfieldIcon?.destroy(); } // ---- @@ -492,6 +516,7 @@ export class Input implements ComponentInterface { (this.inoLabelElement = el)} for={this.inputID} outline={this.outline} text={this.label} diff --git a/packages/elements/src/components/ino-input/readme.md b/packages/elements/src/components/ino-input/readme.md index 4689abab2d..5bb83f7411 100644 --- a/packages/elements/src/components/ino-input/readme.md +++ b/packages/elements/src/components/ino-input/readme.md @@ -12,31 +12,31 @@ Use this element for **simple types** like `text`, `password`, `number` or `emai ## Properties -| Property | Attribute | Description | Type | Default | -| ------------------------ | -------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------ | ----------- | -| `autoFocus` | `auto-focus` | The autofocus of this element. | `boolean` | `undefined` | -| `autocomplete` | `autocomplete` | The autocomplete property of this element. | `string` | `undefined` | -| `dataList` | `data-list` | The id of the datalist child | `string` | `undefined` | -| `disabled` | `disabled` | Disables this element. | `boolean` | `undefined` | -| `error` | `error` | Displays the input field as invalid if set to true. If the property is not set or set to false, the validation is handled by the `pattern` property. This functionality might be useful if the input validation is (additionally) handled by the backend. | `boolean` | `undefined` | -| `helper` | `helper` | The optional helper text. | `string` | `undefined` | -| `helperCharacterCounter` | `helper-character-counter` | Displays the number of characters. The maxlength-property must be set. This helper text will be displayed persistently. | `boolean` | `undefined` | -| `helperPersistent` | `helper-persistent` | Displays the helper permanently. | `boolean` | `undefined` | -| `helperValidation` | `helper-validation` | Styles the helper text as a validation message. | `boolean` | `undefined` | -| `label` | `label` | The optional floating label of this input field. | `string` | `undefined` | -| `max` | `max` | The max value of this element. | `string` | `undefined` | -| `maxlength` | `maxlength` | Limits the number of possible characters to the given number | `number` | `undefined` | -| `min` | `min` | The min value of this element. | `string` | `undefined` | -| `name` | `name` | The name of this element. | `string` | `undefined` | -| `outline` | `outline` | Styles the input field as outlined element. | `boolean` | `undefined` | -| `pattern` | `pattern` | The validation pattern of this element. | `string` | `undefined` | -| `placeholder` | `placeholder` | The placeholder of this element. | `string` | `undefined` | -| `required` | `required` | Marks this element as required. | `boolean` | `undefined` | -| `showLabelHint` | `show-label-hint` | If true, an *optional* message is displayed if not required, otherwise a * marker is displayed if required | `boolean` | `undefined` | -| `step` | `step` | The step value of this element. Use `any` for decimal numbers | `"any" \| number` | `1` | -| `type` | `type` | The type of this element (default = text). | `"color" \| "email" \| "number" \| "password" \| "search" \| "tel" \| "text" \| "url" \| "week"` | `'text'` | -| `unit` | `unit` | Displays the given unit at the end of the input field. | `string` | `undefined` | -| `value` | `value` | The value of this element. (**unmanaged**) | `string` | `''` | +| Property | Attribute | Description | Type | Default | +| ------------------------ | -------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------ | ----------- | +| `autoFocus` | `auto-focus` | The autofocus of this element. | `boolean` | `undefined` | +| `autocomplete` | `autocomplete` | The autocomplete property of this element. | `string` | `undefined` | +| `dataList` | `data-list` | The id of the datalist child | `string` | `undefined` | +| `disabled` | `disabled` | Disables this element. | `boolean` | `undefined` | +| `error` | `error` | Displays the input field as invalid if set to true. If the property is not set or set to false, the validation is handled by the `pattern` property. This functionality might be useful if the input validation is (additionally) handled by the backend. | `boolean` | `undefined` | +| `helper` | `helper` | The optional helper text. | `string` | `undefined` | +| `helperCharacterCounter` | `helper-character-counter` | Displays the number of characters. The maxlength-property must be set. This helper text will be displayed persistently. | `boolean` | `undefined` | +| `helperPersistent` | `helper-persistent` | Displays the helper permanently. | `boolean` | `undefined` | +| `helperValidation` | `helper-validation` | Styles the helper text as a validation message. | `boolean` | `undefined` | +| `label` | `label` | The optional floating label of this input field. | `string` | `undefined` | +| `max` | `max` | The max value of this element. | `string` | `undefined` | +| `maxlength` | `maxlength` | Limits the number of possible characters to the given number | `number` | `undefined` | +| `min` | `min` | The min value of this element. | `string` | `undefined` | +| `name` | `name` | The name of this element. | `string` | `undefined` | +| `outline` | `outline` | Styles the input field as outlined element. This property is immutable which means that it should not be changed after its first initialization. Changing this property at runtime causes problems in combination with the floating label. You can read more about this issue [here](https://github.com/inovex/elements/issues/1216). | `boolean` | `undefined` | +| `pattern` | `pattern` | The validation pattern of this element. | `string` | `undefined` | +| `placeholder` | `placeholder` | The placeholder of this element. | `string` | `undefined` | +| `required` | `required` | Marks this element as required. | `boolean` | `undefined` | +| `showLabelHint` | `show-label-hint` | If true, an *optional* message is displayed if not required, otherwise a * marker is displayed if required | `boolean` | `undefined` | +| `step` | `step` | The step value of this element. Use `any` for decimal numbers | `"any" \| number` | `1` | +| `type` | `type` | The type of this element (default = text). | `"color" \| "email" \| "number" \| "password" \| "search" \| "tel" \| "text" \| "url" \| "week"` | `'text'` | +| `unit` | `unit` | Displays the given unit at the end of the input field. | `string` | `undefined` | +| `value` | `value` | The value of this element. (**unmanaged**) | `string` | `''` | ## Events diff --git a/packages/elements/src/components/ino-label/createMDCNotchedOutline.ts b/packages/elements/src/components/ino-label/createMDCNotchedOutline.ts new file mode 100644 index 0000000000..fb8ca366bc --- /dev/null +++ b/packages/elements/src/components/ino-label/createMDCNotchedOutline.ts @@ -0,0 +1,52 @@ +import { + MDCNotchedOutline, + MDCNotchedOutlineFoundation, + strings, + numbers, +} from '@material/notched-outline'; + +const NOTCH_PADDING = numbers.NOTCH_ELEMENT_PADDING + 2; + +// is required because Mac and Windows do render bold fonts differently +const isMac = navigator.userAgent.toUpperCase().includes('MAC'); +const extraScaleFactor = isMac ? 0.1 : 0; + +export function createMDCNotchedOutline(el: HTMLElement, isDense?: boolean) { + const notchElement: HTMLElement = el.querySelector( + strings.NOTCH_ELEMENT_SELECTOR, + ); + const textLabel: HTMLLabelElement = notchElement.querySelector( + '.mdc-floating-label', + ); + const textLabelWidth = textLabel?.clientWidth ?? 0; + const scaleFactor = (isDense ? 0.66 : 0.76) + extraScaleFactor; + let lastNotchWidth = 0; + + return new MDCNotchedOutline( + el, + new MDCNotchedOutlineFoundation({ + addClass(className: string) { + el.classList.add(className); + }, + removeClass(className: string) { + el.classList.remove(className); + }, + setNotchWidthProperty(width: number) { + const isActive = textLabel.control === document.activeElement; + let newWidth = width; + if (textLabelWidth > 0 && isActive) { + newWidth = textLabelWidth * scaleFactor + NOTCH_PADDING; + } + + if (lastNotchWidth !== newWidth) { + notchElement.style.setProperty('width', newWidth + 'px'); + lastNotchWidth = newWidth; + } + }, + removeNotchWidthProperty() { + notchElement.style.removeProperty('width'); + lastNotchWidth = 0; + }, + }), + ); +} diff --git a/packages/elements/src/components/ino-label/ino-label.tsx b/packages/elements/src/components/ino-label/ino-label.tsx index 9c4fa20dfb..58b83e0e4c 100644 --- a/packages/elements/src/components/ino-label/ino-label.tsx +++ b/packages/elements/src/components/ino-label/ino-label.tsx @@ -1,5 +1,7 @@ -import { Component, Prop, h, Host } from '@stencil/core'; +import { Component, Prop, h, Host, Element, Method } from '@stencil/core'; import classNames from 'classnames'; +import { MDCNotchedOutline } from '@material/notched-outline'; +import { createMDCNotchedOutline } from './createMDCNotchedOutline'; /** * This is an internally used component for various sorts of inputs like `ino-input`, `ino-select` and `ino-textarea`. It is used to display the label for each respective component. @@ -10,6 +12,13 @@ import classNames from 'classnames'; shadow: false, }) export class Label { + @Element() el!: HTMLInoLabelElement; + + /** + * An instance of the material design outline notch. + */ + private mdcNotchedOutline?: MDCNotchedOutline; + /** * Styles the label in an outlined style */ @@ -41,6 +50,22 @@ export class Label { */ @Prop() for: string; + /** + * Returns internal mdcNotchedOutline instance + */ + @Method() + async getMdcNotchedOutlineInstance() { + return this.mdcNotchedOutline; + } + + componentDidLoad() { + if (this.outline && !this.mdcNotchedOutline) { + this.mdcNotchedOutline = createMDCNotchedOutline( + this.el.querySelector('.mdc-notched-outline'), + ); + } + } + private filledTemplate = (label: HTMLElement) => [
, label, diff --git a/packages/elements/src/components/ino-label/readme.md b/packages/elements/src/components/ino-label/readme.md index beeaadcc0c..0d7da0f052 100644 --- a/packages/elements/src/components/ino-label/readme.md +++ b/packages/elements/src/components/ino-label/readme.md @@ -21,6 +21,19 @@ This is an internally used component for various sorts of inputs like `ino-input | `text` | `text` | The text of the label itself | `string` | `undefined` | +## Methods + +### `getMdcNotchedOutlineInstance() => Promise` + +Returns internal mdcNotchedOutline instance + +#### Returns + +Type: `Promise` + + + + ## Dependencies ### Used by diff --git a/packages/storybook/elements-stencil-docs.json b/packages/storybook/elements-stencil-docs.json index 0a0befda6b..fd69da5b4b 100644 --- a/packages/storybook/elements-stencil-docs.json +++ b/packages/storybook/elements-stencil-docs.json @@ -4981,7 +4981,7 @@ "mutable": false, "attr": "outline", "reflectToAttr": false, - "docs": "Styles the input field as outlined element.", + "docs": "Styles the input field as outlined element.\n\nThis property is immutable which means that it should not be changed after its first initialization.\nChanging this property at runtime causes problems in combination with the floating label.\nYou can read more about this issue [here](https://github.com/inovex/elements/issues/1216).", "docsTags": [], "values": [ { @@ -5811,7 +5811,35 @@ "required": false } ], - "methods": [], + "methods": [ + { + "name": "getMdcNotchedOutlineInstance", + "returns": { + "type": "Promise", + "docs": "" + }, + "complexType": { + "signature": "() => Promise", + "parameters": [], + "references": { + "Promise": { + "location": "global", + "id": "global::Promise" + }, + "MDCNotchedOutline": { + "location": "import", + "path": "@material/notched-outline", + "id": "../../node_modules/@material/notched-outline/index.d.ts::MDCNotchedOutline" + } + }, + "return": "Promise" + }, + "signature": "getMdcNotchedOutlineInstance() => Promise", + "parameters": [], + "docs": "Returns internal mdcNotchedOutline instance", + "docsTags": [] + } + ], "events": [], "styles": [], "slots": [], @@ -11374,6 +11402,11 @@ "docstring": "", "path": "src/components/types.ts" }, + "../../node_modules/@material/notched-outline/index.d.ts::MDCNotchedOutline": { + "declaration": "any", + "docstring": "", + "path": "../../node_modules/@material/notched-outline/index.d.ts" + }, "src/components/types.ts::ViewModeUnion": { "declaration": "\"markdown\" | \"preview\" | \"readonly\"", "docstring": "", diff --git a/packages/storybook/src/stories/ino-input/ino-input.stories.ts b/packages/storybook/src/stories/ino-input/ino-input.stories.ts index 29e9b97f39..cae28ee765 100644 --- a/packages/storybook/src/stories/ino-input/ino-input.stories.ts +++ b/packages/storybook/src/stories/ino-input/ino-input.stories.ts @@ -45,7 +45,6 @@ const InoInputMeta = { placeholder="${args.placeholder}" required="${args.required}" show-label-hint="${args.showLabelHint}" - size=${args.size} step="${args.step}" type="${args.type}" unit="${args.unit}" @@ -89,7 +88,6 @@ const InoInputMeta = { placeholder: '', required: false, showLabelHint: false, - size: 0, step: 5, type: 'text', unit: '',