diff --git a/etc/lime-elements.api.md b/etc/lime-elements.api.md index ea2b05b180..783015abef 100644 --- a/etc/lime-elements.api.md +++ b/etc/lime-elements.api.md @@ -555,6 +555,17 @@ export namespace Components { "allowClicksElement": HTMLElement; "open": boolean; } + export interface LimelNotchedOutline { + "disabled": boolean; + "hasFloatingLabel": boolean; + "hasLeadingIcon": boolean; + "hasValue": boolean; + "invalid": boolean; + "label"?: string; + "labelId"?: string; + "readonly": boolean; + "required": boolean; + } // (undocumented) export interface LimelPicker { "actionPosition": ActionPosition; @@ -1119,6 +1130,8 @@ export namespace JSX { // (undocumented) "limel-menu-surface": LimelMenuSurface; // (undocumented) + "limel-notched-outline": LimelNotchedOutline; + // (undocumented) "limel-picker": LimelPicker; // (undocumented) "limel-popover": LimelPopover; @@ -1618,6 +1631,17 @@ export namespace JSX { "onDismiss"?: (event: LimelMenuSurfaceCustomEvent) => void; "open"?: boolean; } + export interface LimelNotchedOutline { + "disabled"?: boolean; + "hasFloatingLabel"?: boolean; + "hasLeadingIcon"?: boolean; + "hasValue"?: boolean; + "invalid"?: boolean; + "label"?: string; + "labelId"?: string; + "readonly"?: boolean; + "required"?: boolean; + } // (undocumented) export interface LimelPicker { "actionPosition"?: ActionPosition; diff --git a/src/components/chip-set/chip-set.e2e.ts b/src/components/chip-set/chip-set.e2e.ts index 60651b9f56..6317973037 100644 --- a/src/components/chip-set/chip-set.e2e.ts +++ b/src/components/chip-set/chip-set.e2e.ts @@ -25,7 +25,9 @@ describe('limel-chip-set', () => { ]); await page.waitForChanges(); - label = await page.find('limel-chip-set >>> .chip-set__label'); + label = await page.find( + 'limel-chip-set >>> .limel-notched-outline--notch label', + ); chips = await page.findAll('limel-chip-set >>> limel-chip'); spy = await chipSet.spyOnEvent('interact'); diff --git a/src/components/chip-set/chip-set.scss b/src/components/chip-set/chip-set.scss index 85cc8e71b8..ed93941943 100644 --- a/src/components/chip-set/chip-set.scss +++ b/src/components/chip-set/chip-set.scss @@ -6,9 +6,6 @@ @use '@material/textfield'; @use '@material/textfield/icon'; -@use '@material/notched-outline/mdc-notched-outline'; -@use '@material/floating-label'; -@use '@material/floating-label/mdc-floating-label'; /** * @prop --icon-background-color: Background color of the icon. Defaults to transparent. @@ -21,14 +18,7 @@ @include textfield.core-styles; @include icon.icon-core-styles; -@include shared_input-select-picker.outlined-style-overrides; -@include shared_input-select-picker.floating-label-overrides; -@include shared_input-select-picker.cropped-label-hack; -@include shared_input-select-picker.disabled-overrides; -@include shared_input-select-picker.readonly-overrides; @include shared_input-select-picker.leading-icon; -@include shared_input-select-picker.lime-empty-value-for-readonly; -@include shared_input-select-picker.lime-looks-like-input-value; $height-of-chip-set-input: functions.pxToRem(36); $leading-icon-space: 1.5rem; @@ -37,6 +27,21 @@ $leading-icon-space: 1.5rem; isolation: isolate; } +:host(limel-chip-set[type='input']) { + limel-notched-outline { + [slot='content'] { + min-height: shared_input-select-picker.$height-of-mdc-text-field; + } + } +} + +:host(limel-chip-set:not([type='input'])) { + .limel-notched-outline { + --limel-notched-outline-border-color: transparent; + --limel-notched-outline-background-color: transparent; + } +} + .mdc-chip-set { display: flex; flex-wrap: wrap; @@ -45,12 +50,6 @@ $leading-icon-space: 1.5rem; min-height: shared_input-select-picker.$height-of-mdc-text-field; position: relative; - &.chip-set--with-label { - .lime-floating-label--float-above { - padding-left: functions.pxToRem(4); - } - } - &.mdc-chip-set--input { padding: 0.4rem 0.5rem; width: 100%; @@ -75,7 +74,7 @@ $leading-icon-space: 1.5rem; @include shared_input-select-picker.input-field-placeholder; width: auto; - padding: 0 functions.pxToRem(12); + padding: 0 0.5rem; flex-grow: 1; flex-shrink: 0; @@ -100,39 +99,6 @@ $leading-icon-space: 1.5rem; } } -// used only in chipsets that do not have input -.chip-set__label { - @include mixins.truncate-text; - width: 120%; // `120%` instead of `100%`, - // because this class is always together with `mdc-floating-label--float-above`, - // which scales the label down. So there is more horizontal space to display the label in. - top: functions.pxToRem(13); - padding-left: functions.pxToRem(20); -} - -// Because MDC removes some classes in chipset, we add custom -// classes with similar names and expected behavior & styles. -// These class names start with `lime-`, instead of `mdc-`. -.lime-notched-outline--notched { - .mdc-notched-outline__notch { - border-top: 1px solid transparent !important; - - .lime-floating-label--float-above { - // This overrides MDC's original top value which is `top: 50%`. - // The reason is that a % value aligns the label in a wrong position - // vertically, when there are multiple rows of chips. - top: functions.pxToRem(27); - - transform: translateY(-34.75px) scale(0.75) !important; - font-size: shared_input-select-picker.$cropped-label-hack--font-size; - } - } -} - -.force-invalid { - @extend .mdc-text-field--invalid; -} - .clear-all-button { @include mixins.clear-all-button; @include mixins.visualize-keyboard-focus; @@ -144,14 +110,14 @@ $leading-icon-space: 1.5rem; opacity: 0; // Is hidden, but can receive focus (such as when navigating through tab indexes). &:focus, - .has-chips:not(.mdc-text-field--disabled):hover &, - .has-chips:not(.mdc-text-field--disabled).mdc-text-field--focused & { + .has-chips:not(.disabled):hover &, + .has-chips:not(.disabled).mdc-text-field--focused & { opacity: 1; outline: none; } .mdc-chip-set:not(.has-chips) &, - .has-chips.mdc-text-field--disabled & { + .has-chips.disabled & { display: none; // Won't receive focus when disabled } } @@ -161,15 +127,6 @@ $leading-icon-space: 1.5rem; .mdc-text-field__input { padding-left: $leading-icon-space; } - - .mdc-floating-label { - &:not(.lime-floating-label--float-above) { - left: $leading-icon-space; - } - &.mdc-floating-label--float-above { - left: functions.pxToRem(4); - } - } } limel-chip { @@ -207,10 +164,3 @@ limel-chip { @import './partial-styles/_readonly'; @import './partial-styles/_helper-text'; - -// To make the input field render smaller than the default MDC UI -.mdc-text-field { - &.mdc-text-field--outlined { - min-height: shared_input-select-picker.$height-of-mdc-text-field; - } -} diff --git a/src/components/chip-set/chip-set.tsx b/src/components/chip-set/chip-set.tsx index 680c4ce493..5887692d8a 100644 --- a/src/components/chip-set/chip-set.tsx +++ b/src/components/chip-set/chip-set.tsx @@ -7,7 +7,6 @@ import { Event, EventEmitter, h, - Host, Method, Prop, State, @@ -18,6 +17,7 @@ import translate from '../../global/translations'; import { getHref, getTarget } from '../../util/link-helper'; import { isEqual } from 'lodash-es'; import { LimelChipCustomEvent } from '../../components'; +import { createRandomString } from '../../util/random-string'; const INPUT_FIELD_TABINDEX = 1; @@ -243,8 +243,10 @@ export class ChipSet { private mdcTextField: MDCTextField; private readonly handleKeyDown = handleKeyboardEvent; + private labelId: string; constructor() { + this.labelId = createRandomString(); this.renderChip = this.renderChip.bind(this); this.renderInputChip = this.renderInputChip.bind(this); this.isFull = this.isFull.bind(this); @@ -337,35 +339,70 @@ export class ChipSet { } public render() { - if (this.type === 'input') { - return this.renderInputChips(); - } - const classes = { 'mdc-chip-set': true, - disabled: this.disabled || this.readonly, 'mdc-text-field--with-trailing-icon': true, + disabled: this.disabled || this.readonly, }; + if (this.type) { classes[`mdc-chip-set--${this.type}`] = true; } - const chipSetLabel = this.renderChipSetLabel(); - if (chipSetLabel) { - classes['chip-set--with-label'] = true; + if (this.type === 'input') { + Object.assign(classes, { + 'mdc-text-field': true, + 'mdc-text-field--outlined': true, + 'mdc-chip-set--input': true, + 'lime-text-field--readonly': this.readonly, + 'has-chips': this.value.length !== 0, + 'has-leading-icon': this.leadingIcon !== null, + 'has-clear-all-button': this.clearAllButton, + }); } const value = this.getValue(); - return ( -
- {chipSetLabel} - {value.map(this.renderChip)} - {this.renderHelperLine()} -
- ); + return [ + +
+ {this.renderContent(value)} +
+
, + this.renderHelperLine(), + ]; } + private getContentProps() { + if (this.type === 'input') { + return { + onClick: this.handleTextFieldFocus, + }; + } + + return { + role: 'grid', + }; + } + + private renderContent = (value: Chip[]) => { + if (this.type === 'input') { + return this.renderInputChips(); + } + + return value.map(this.renderChip); + }; + private readonly getValue = () => { return this.value.map((chip) => ({ ...chip, @@ -385,113 +422,42 @@ export class ChipSet { this.initialize(); } - private renderChipSetLabel() { - if (!this.label) { - return; - } - - return ( - - ); - } - private renderInputChips() { - return ( - -
- {this.value.map(this.renderInputChip)} - -
-
- {this.renderLabel()} -
-
- {this.renderLeadingIcon()} - {this.renderEmptyValueForReadonly()} - {this.renderClearAllChipsButton()} -
- {this.renderHelperLine()} - - ); - } - - private readonly renderEmptyValueForReadonly = () => { - if (this.readonly && this.value.length === 0) { - return ( - - – - - ); - } - }; - - private renderLabel() { - const labelClassList = { - 'mdc-floating-label': true, - 'mdc-text-field--disabled': this.readonly || this.disabled, - 'mdc-floating-label--required': this.required, - 'lime-floating-label--float-above': this.floatLabelAbove(), - }; - - if (!this.label) { - return; - } - - return ( -
- -
- ); + return [ + this.value.map(this.renderInputChip), + , + this.renderLeadingIcon(), + this.renderClearAllChipsButton(), + ]; } private readonly floatLabelAbove = () => { - if (!!this.value.length || this.editMode || this.readonly) { + if ( + !!this.value.length || + this.editMode || + this.readonly || + this.textValue + ) { return true; } }; diff --git a/src/components/chip-set/partial-styles/_readonly.scss b/src/components/chip-set/partial-styles/_readonly.scss index a59794ef4e..836319e928 100644 --- a/src/components/chip-set/partial-styles/_readonly.scss +++ b/src/components/chip-set/partial-styles/_readonly.scss @@ -1,6 +1,6 @@ :host(limel-chip-set[readonly]) { .mdc-text-field { - &.mdc-text-field--disabled { + &.disabled { pointer-events: auto; } } diff --git a/src/components/input-field/input-field.e2e.ts b/src/components/input-field/input-field.e2e.ts index bf96fa12d8..f7290fc43f 100644 --- a/src/components/input-field/input-field.e2e.ts +++ b/src/components/input-field/input-field.e2e.ts @@ -5,8 +5,6 @@ describe('limel-input-field', () => { let page: E2EPage; let limelInput: E2EElement; let inputContainer: E2EElement; - let label: E2EElement; - let outline: E2EElement; const types: Array<{ name: InputType; [key: string]: any }> = [ { name: 'email' }, @@ -97,42 +95,6 @@ describe('limel-input-field', () => { expect(nativeInput).toHaveClass('mdc-text-field__input'); }); }); - describe('the label', () => { - beforeEach(async () => { - label = await page.find( - 'limel-input-field>>>.mdc-floating-label', - ); - }); - it('is NOT floating', () => { - expect(label).not.toHaveClass( - 'mdc-floating-label--float-above', - ); - }); - describe('after focusing', () => { - beforeEach(async () => { - label.click(); - await page.waitForEvent('click'); - await page.waitForChanges(); - }); - it('IS floating', () => { - expect(label).toHaveClass( - 'mdc-floating-label--float-above', - ); - }); - }); - }); - describe('the outline', () => { - beforeEach(async () => { - outline = await page.find( - 'limel-input-field>>>.mdc-notched-outline', - ); - }); - it('has the expected structure', () => { - expect(replaceLabelId(outline.outerHTML)).toEqual( - 'Test', - ); - }); - }); describe('when invalid is set to true', () => { beforeEach(async () => { limelInput.setAttribute('invalid', true); @@ -247,21 +209,11 @@ describe('limel-input-field', () => { page = await createPage(` `); - await page.evaluate(() => { - const elements = document.querySelectorAll( - '.mdc-floating-label', - ); - - elements.forEach((el) => { - el.id = 'test'; - }); - }); - limelInput = await page.find('limel-input-field'); inputContainer = await page.find( 'limel-input-field>>>label.mdc-text-field', @@ -354,10 +306,3 @@ describe('limel-input-field', () => { async function createPage(content: string) { return newE2EPage({ html: content }); } - -function replaceLabelId(HTML: string) { - return HTML.replace( - /"a_(\d|[a-f]){8}-(\d|[a-f]){4}-(\d|[a-f]){4}-(\d|[a-f]){4}-(\d|[a-f]){12}"/g, - '"tf-input-label"', - ); -} diff --git a/src/components/input-field/input-field.scss b/src/components/input-field/input-field.scss index 855877f131..a4ef9a38f0 100644 --- a/src/components/input-field/input-field.scss +++ b/src/components/input-field/input-field.scss @@ -7,10 +7,6 @@ @use '@material/textfield'; @use '@material/textfield/icon'; -@use '@material/notched-outline/mdc-notched-outline'; -@use '@material/floating-label'; -@use '@material/floating-label/mdc-floating-label'; -@use '@material/ripple'; @use '@material/list'; @use '@material/elevation'; @use '@material/menu-surface'; @@ -47,14 +43,9 @@ @include textfield.core-styles; @include icon.icon-core-styles; -@include shared_input-select-picker.outlined-style-overrides; @include shared_input-select-picker.readonly-overrides; -@include shared_input-select-picker.cropped-label-hack; -@include shared_input-select-picker.disabled-overrides; @include shared_input-select-picker.leading-icon; @include shared_input-select-picker.trailing-icon; -@include shared_input-select-picker.floating-label-overrides; -@include shared_input-select-picker.lime-empty-value-for-readonly; @include shared_input-select-picker.lime-looks-like-input-value; .lime-text-field--empty { @@ -65,6 +56,15 @@ } } +limel-notched-outline { + &[has-value], + &:focus-within { + .mdc-text-field__affix { + opacity: 1; + } + } +} + .mdc-text-field { width: 100%; diff --git a/src/components/input-field/input-field.tsx b/src/components/input-field/input-field.tsx index c5e1af28dd..e9b5a9392a 100644 --- a/src/components/input-field/input-field.tsx +++ b/src/components/input-field/input-field.tsx @@ -321,21 +321,26 @@ export class InputField { } return [ - , + + + , this.renderHelperLine(), this.renderAutocompleteList(), ]; @@ -408,7 +413,6 @@ export class InputField { private getContainerClassList = () => { const classList = { 'mdc-text-field': true, - 'mdc-text-field--no-label': !this.label, 'mdc-text-field--outlined': true, 'mdc-text-field--invalid': this.isInvalid(), 'mdc-text-field--disabled': this.disabled || this.readonly, @@ -569,16 +573,6 @@ export class InputField { ); }; - private renderEmptyValueForReadonly = () => { - if (this.readonly && this.isEmpty()) { - return ( - - – - - ); - } - }; - private renderSuffix = () => { if (!this.hasSuffix() || this.type === 'textarea') { return; @@ -650,26 +644,6 @@ export class InputField { } }; - private renderLabel = () => { - const labelClassList = { - 'mdc-floating-label': true, - 'mdc-floating-label--float-above': - !this.isEmpty() || this.isFocused || this.readonly, - }; - - if (!this.label) { - return; - } - - return ( - - - {this.label} - - - ); - }; - private renderLeadingIcon = () => { if (this.type === 'textarea') { return; diff --git a/src/components/notched-outline/examples/notched-outline-basic.scss b/src/components/notched-outline/examples/notched-outline-basic.scss new file mode 100644 index 0000000000..d1712b75b4 --- /dev/null +++ b/src/components/notched-outline/examples/notched-outline-basic.scss @@ -0,0 +1,22 @@ +* { + box-sizing: border-box; +} + +section { + padding: 1rem; + background: url("data:image/svg+xml;charset=utf-8, "); + background-size: 0.5rem; +} + +input[type='text'] { + // Overriding some of the native input styles for the + // sake of this example. + border: none; + outline: none; + + height: 2.5rem; + width: 100%; + padding: 0 1rem; + + background-color: transparent; +} diff --git a/src/components/notched-outline/examples/notched-outline-basic.tsx b/src/components/notched-outline/examples/notched-outline-basic.tsx new file mode 100644 index 0000000000..b5d0f68d49 --- /dev/null +++ b/src/components/notched-outline/examples/notched-outline-basic.tsx @@ -0,0 +1,154 @@ +import { Component, h, State } from '@stencil/core'; + +/** + * Basic example + * + * Notice that the wrapping div has a hardcoded height, + * which results in the notched outline to wrap around + * the div. + */ +@Component({ + tag: 'limel-example-notched-outline-basic', + shadow: true, + styleUrl: 'notched-outline-basic.scss', +}) +export class NotchedOutlineBasicExample { + @State() + private required = false; + + @State() + private disabled = false; + + @State() + private readonly = false; + + @State() + private invalid = false; + + @State() + private hasValue = false; + + @State() + private hasLeadingIcon = false; + + @State() + private hasFloatingLabel = false; + + @State() + private inputValue: string; + + public render() { + const id = 'abcd'; + + return [ +
+ + + +
, + + + + + +
+ + + +
, + , + ]; + } + + private handleInput = (event: Event) => { + const input = event.target as HTMLInputElement; + this.inputValue = input.value; + this.hasValue = !!input.value; + }; + + private setDisabled = (event: CustomEvent) => { + event.stopPropagation(); + this.disabled = event.detail; + }; + + private setReadonly = (event: CustomEvent) => { + event.stopPropagation(); + this.readonly = event.detail; + }; + + private setRequired = (event: CustomEvent) => { + event.stopPropagation(); + this.required = event.detail; + }; + + private setInvalid = (event: CustomEvent) => { + event.stopPropagation(); + this.invalid = event.detail; + }; + + private setHasValue = (event: CustomEvent) => { + event.stopPropagation(); + this.hasValue = event.detail; + }; + + private setHasLeadingIcon = (event: CustomEvent) => { + event.stopPropagation(); + this.hasLeadingIcon = event.detail; + }; + + private setHasFloatingLabel = (event: CustomEvent) => { + event.stopPropagation(); + this.hasFloatingLabel = event.detail; + }; +} diff --git a/src/components/notched-outline/notched-outline.scss b/src/components/notched-outline/notched-outline.scss new file mode 100644 index 0000000000..a12953e0b8 --- /dev/null +++ b/src/components/notched-outline/notched-outline.scss @@ -0,0 +1,249 @@ +@use '../../style/mixins.scss'; +@use '../../style/internal/shared_input-select-picker'; +/** + * @prop --limel-notched-outline-z-index: Defines the `z-index` of the outlines & the label, since they are absolutely positioned. Useful if there are other elements with z-indexes in the consumer. + */ + +$border-radius: 0.25rem; +$value-top: 0.62rem; + +limel-notched-outline { + --limel-notched-outline-border-color: #{shared_input-select-picker.$lime-text-field-outline-color}; + --limel-notched-outline-background-color: #{shared_input-select-picker.$background-color-normal}; + + display: block; + width: 100%; + height: 100%; + + *, + *:before, + *:after { + box-sizing: border-box; + } +} + +.limel-notched-outline { + position: relative; + width: 100%; + height: 100%; + + [slot='content'] { + background-color: var(--limel-notched-outline-background-color); + border-radius: var( + --limel-notched-outline-border-radius, + $border-radius + ); + } + + // Why is everything prefixed? + // Because the component has `shadow: false;` + // and this ensures that we are not inheriting styles. + &--outlines { + pointer-events: none; + position: absolute; + inset: 0; + z-index: var(--limel-notched-outline-z-index, 0); + display: flex; + } + + &--leading-outline, + &--notch, + &--trailing-outline { + transition: border-color 0.2s ease; + border-width: 1px; + border-style: solid; + border-color: var(--limel-notched-outline-border-color); + } + + &--leading-outline { + flex-shrink: 0; + width: 0.75rem; + border-right-width: 0; + border-top-left-radius: var( + --limel-notched-outline-border-radius, + $border-radius + ); + border-bottom-left-radius: var( + --limel-notched-outline-border-radius, + $border-radius + ); + } + + &--notch { + flex-shrink: 0; + + position: relative; + z-index: 2; + + border-top-color: var( + --limel-notched-outline-notch-border-top-color, + var(--limel-notched-outline-border-color) + ); + border-right-width: 0; + border-left-width: 0; + + max-width: calc(100% - 1.5rem); + + label { + all: unset; + @include mixins.truncate-text; + position: relative; + transition: + color 0.2s ease, + font-size 0.2s ease, + transform 0.12s cubic-bezier(0.4, 0, 0.2, 1); + + transform: translate3d( + var(--limel-notched-outline-label-transform-x, 0), + var(--limel-notched-outline-label-transform-y, $value-top), + 0 + ); + display: block; + padding: 0 0.25rem; + + color: var( + --limel-notched-outline-label-color, + #{shared_input-select-picker.$label-color} + ); + font-size: var( + --limel-notched-outline-label-font-size, + #{shared_input-select-picker.$cropped-label-hack--font-size} + ); + letter-spacing: var( + --mdc-typography-subtitle1-letter-spacing, + 0.009375em + ); + + &:after { + position: absolute; + right: 0; + padding: 0 0.25rem; + } + } + } + + &--trailing-outline { + flex-grow: 1; + border-left-width: 0; + border-top-right-radius: var( + --limel-notched-outline-border-radius, + $border-radius + ); + border-bottom-right-radius: var( + --limel-notched-outline-border-radius, + $border-radius + ); + } + + &--empty-readonly-value { + @include shared_input-select-picker.lime-looks-like-input-value; + position: absolute; + top: $value-top; + left: 1rem; + } +} + +limel-notched-outline { + &:not([disabled]:not([disabled='false'])) { + &:hover { + --limel-notched-outline-border-color: #{shared_input-select-picker.$lime-text-field-outline-color--hovered}; + --limel-notched-outline-background-color: #{shared_input-select-picker.$background-color-hovered}; + } + + &:has([slot='content']:focus-visible), + &:has([slot='content']:focus-within) { + --limel-notched-outline-border-color: #{shared_input-select-picker.$lime-text-field-outline-color--focused}; + --limel-notched-outline-background-color: #{shared_input-select-picker.$background-color-focused}; + } + } + + &[disabled]:not([disabled='false']) { + --limel-notched-outline-label-color: #{shared_input-select-picker.$label-color-disabled}; + } + + &[required]:not([required='false']) { + .limel-notched-outline--notch { + label { + padding-right: 0.75rem; + + &:after { + content: '*'; + scale: 1.3; + } + } + } + } + + &[invalid]:not([invalid='false']) { + &:not([disabled]:not([disabled='false'])) { + --limel-notched-outline-border-color: var(--lime-error-text-color); + &:hover { + --limel-notched-outline-border-color: var( + --lime-error-text-color + ); + } + } + + .limel-notched-outline--notch { + label { + &:after { + color: var(--lime-error-text-color); + } + } + } + } + + &[readonly]:not([readonly='false']) { + --limel-notched-outline-border-color: transparent !important; + --limel-notched-outline-background-color: transparent !important; + + .limel-notched-outline--notch { + label { + transition-duration: 0s; + } + } + } + + &[has-leading-icon] { + &:not([has-leading-icon='false']):not([has-floating-label]):not( + [has-value] + ) { + --limel-notched-outline-label-transform-x: 1.25rem; + } + + .limel-notched-outline--empty-readonly-value { + left: 2.25rem; + } + } +} + +// Transitioning the floating label +@mixin float-label { + --limel-notched-outline-label-font-size: 0.65rem; // `10.4px` similar to MDC's floating label + --limel-notched-outline-label-transform-x: 0; + --limel-notched-outline-label-transform-y: calc(-50% - 0.09375rem); + --limel-notched-outline-notch-border-top-color: transparent; +} + +limel-notched-outline { + &:not([disabled]:not([disabled='false'])) { + &:hover, + &:focus, + &:focus-within { + label { + will-change: color, transform, font-size; + } + } + + &:has([slot='content']:focus-visible), + &:has([slot='content']:focus-within) { + @include float-label; + } + } + + &[has-floating-label], + &[has-value]:not([has-value='false']), + &[readonly]:not([has-value]:not([has-value='true'])) { + @include float-label; + } +} diff --git a/src/components/notched-outline/notched-outline.tsx b/src/components/notched-outline/notched-outline.tsx new file mode 100644 index 0000000000..bdd0fb0f07 --- /dev/null +++ b/src/components/notched-outline/notched-outline.tsx @@ -0,0 +1,139 @@ +import { Component, Prop, h } from '@stencil/core'; + +/** + * This is a private component, used to render a notched outline + * around all input elements that can have a floating label. + * Inspired by Material Design's styles for input fields. + * We use it in various components to unify styles and avoid + * repeating code. + * + * :::note + * The component has `shadow: false`. This is to improve performance, + * and ensure that its internal elements are considered as internal parts + * of the consumer's DOM. This way, the value `for` in `