From 41546c3da11f609a8cc7b8a5a23c92512dc10c60 Mon Sep 17 00:00:00 2001 From: Luke Vo Date: Wed, 3 Apr 2024 18:26:21 +0700 Subject: [PATCH 1/3] Fixed Button and Icon Button disability UI in fieldset. --- button/internal/_elevation.scss | 4 ++-- button/internal/_icon.scss | 2 +- button/internal/_outlined-button.scss | 6 +++--- button/internal/_shared.scss | 8 ++++---- iconbutton/internal/_outlined-icon-button.scss | 4 ++-- iconbutton/internal/_shared.scss | 4 ++-- 6 files changed, 14 insertions(+), 14 deletions(-) diff --git a/button/internal/_elevation.scss b/button/internal/_elevation.scss index 1899999e44..53dd28f7e2 100644 --- a/button/internal/_elevation.scss +++ b/button/internal/_elevation.scss @@ -20,7 +20,7 @@ $_md-sys-motion: tokens.md-sys-motion-values(); transition-timing-function: map.get($_md-sys-motion, 'emphasized-easing'); } - :host([disabled]) md-elevation { + :host(:disabled) md-elevation { transition: none; } @@ -59,7 +59,7 @@ $_md-sys-motion: tokens.md-sys-motion-values(); ); } - :host([disabled]) md-elevation { + :host(:disabled) md-elevation { @include elevation.theme( ( 'level': var(--_disabled-container-elevation), diff --git a/button/internal/_icon.scss b/button/internal/_icon.scss index e2156e5be5..f03eceb05c 100644 --- a/button/internal/_icon.scss +++ b/button/internal/_icon.scss @@ -31,7 +31,7 @@ color: var(--_pressed-icon-color); } - :host([disabled]) ::slotted([slot='icon']) { + :host(:disabled) ::slotted([slot='icon']) { color: var(--_disabled-icon-color); opacity: var(--_disabled-icon-opacity); } diff --git a/button/internal/_outlined-button.scss b/button/internal/_outlined-button.scss index 0cf7e0bcc9..3719764320 100644 --- a/button/internal/_outlined-button.scss +++ b/button/internal/_outlined-button.scss @@ -57,20 +57,20 @@ border-color: var(--_pressed-outline-color); } - :host([disabled]) .outline { + :host(:disabled) .outline { border-color: var(--_disabled-outline-color); opacity: var(--_disabled-outline-opacity); } @media (forced-colors: active) { - :host([disabled]) .background { + :host(:disabled) .background { // Only outlined buttons change their border when disabled to distinguish // them from other buttons that add a border for increased visibility in // HCM. border-color: GrayText; } - :host([disabled]) .outline { + :host(:disabled) .outline { opacity: 1; } } diff --git a/button/internal/_shared.scss b/button/internal/_shared.scss index 40e6b88899..af74a23007 100644 --- a/button/internal/_shared.scss +++ b/button/internal/_shared.scss @@ -71,7 +71,7 @@ ); } - :host([disabled]) { + :host(:disabled) { cursor: default; pointer-events: none; } @@ -139,12 +139,12 @@ text-overflow: inherit; } - :host([disabled]) .label { + :host(:disabled) .label { color: var(--_disabled-label-text-color); opacity: var(--_disabled-label-text-opacity); } - :host([disabled]) .background { + :host(:disabled) .background { background-color: var(--_disabled-container-color); opacity: var(--_disabled-container-opacity); } @@ -157,7 +157,7 @@ border: 1px solid CanvasText; } - :host([disabled]) { + :host(:disabled) { --_disabled-icon-color: GrayText; --_disabled-icon-opacity: 1; --_disabled-container-opacity: 1; diff --git a/iconbutton/internal/_outlined-icon-button.scss b/iconbutton/internal/_outlined-icon-button.scss index 0ecc5f4c59..44883494b2 100644 --- a/iconbutton/internal/_outlined-icon-button.scss +++ b/iconbutton/internal/_outlined-icon-button.scss @@ -79,7 +79,7 @@ } } - .outlined:disabled .icon { + :host(:disabled) .outlined .icon { opacity: var(--_disabled-icon-opacity); } @@ -139,7 +139,7 @@ } @media (forced-colors: active) { - :host([disabled]) { + :host(:disabled) { --_disabled-outline-opacity: 1; } diff --git a/iconbutton/internal/_shared.scss b/iconbutton/internal/_shared.scss index eea1b05aae..7ea3af85f5 100644 --- a/iconbutton/internal/_shared.scss +++ b/iconbutton/internal/_shared.scss @@ -41,7 +41,7 @@ ); } - :host([disabled]) { + :host(:disabled) { pointer-events: none; } @@ -109,7 +109,7 @@ } @media (forced-colors: active) { - :host([disabled]) { + :host(:disabled) { --_disabled-icon-opacity: 1; } } From eee37051714229ef2fd0ced39bb647b8d665647b Mon Sep 17 00:00:00 2001 From: Luke Vo Date: Wed, 3 Apr 2024 19:16:43 +0700 Subject: [PATCH 2/3] Fixed Checkbox, Radio, Select, Slider and Switch disabled state in fieldset. --- checkbox/internal/_checkbox.scss | 2 +- checkbox/internal/checkbox.ts | 11 ++++++--- radio/internal/_radio.scss | 10 ++++---- radio/internal/radio.ts | 12 +++++++--- select/internal/_shared.scss | 4 ++-- select/internal/select.ts | 18 +++++++++++---- slider/internal/_slider.scss | 12 +++++----- slider/internal/forced-colors-styles.scss | 4 ++-- slider/internal/slider.ts | 28 ++++++++++++++++------- switch/internal/_switch.scss | 2 +- switch/internal/switch.ts | 16 ++++++++++--- 11 files changed, 80 insertions(+), 39 deletions(-) diff --git a/checkbox/internal/_checkbox.scss b/checkbox/internal/_checkbox.scss index 71d039de71..1957320666 100644 --- a/checkbox/internal/_checkbox.scss +++ b/checkbox/internal/_checkbox.scss @@ -56,7 +56,7 @@ $_checkmark-bottom-left: 7px, -14px; cursor: pointer; } - :host([disabled]) { + :host(:disabled) { cursor: default; } diff --git a/checkbox/internal/checkbox.ts b/checkbox/internal/checkbox.ts index ce4942af06..82af4a411c 100644 --- a/checkbox/internal/checkbox.ts +++ b/checkbox/internal/checkbox.ts @@ -118,15 +118,20 @@ export class Checkbox extends checkboxBaseClass { super.update(changed); } + private get isControlDisabled() { + return this.disabled || this.matches(':disabled'); + } + protected override render() { const prevNone = !this.prevChecked && !this.prevIndeterminate; const prevChecked = this.prevChecked && !this.prevIndeterminate; const prevIndeterminate = this.prevIndeterminate; const isChecked = this.checked && !this.indeterminate; const isIndeterminate = this.indeterminate; + const disabled = this.isControlDisabled; const containerClasses = classMap({ - 'disabled': this.disabled, + 'disabled': disabled, 'selected': isChecked || isIndeterminate, 'unselected': !isChecked && !isIndeterminate, 'checked': isChecked, @@ -149,7 +154,7 @@ export class Checkbox extends checkboxBaseClass { aria-checked=${isIndeterminate ? 'mixed' : nothing} aria-label=${ariaLabel || nothing} aria-invalid=${ariaInvalid || nothing} - ?disabled=${this.disabled} + ?disabled=${disabled} ?required=${this.required} .indeterminate=${this.indeterminate} .checked=${this.checked} @@ -159,7 +164,7 @@ export class Checkbox extends checkboxBaseClass {
- + `; } @@ -135,7 +141,7 @@ export class Radio extends radioBaseClass { } private async handleClick(event: Event) { - if (this.disabled) { + if (this.isControlDisabled) { return; } diff --git a/select/internal/_shared.scss b/select/internal/_shared.scss index 60d7242701..c14fec9597 100644 --- a/select/internal/_shared.scss +++ b/select/internal/_shared.scss @@ -83,7 +83,7 @@ max-width: inherit; } - md-menu ::slotted(:not[disabled]) { + md-menu ::slotted(:not(:disabled)) { cursor: pointer; } @@ -96,7 +96,7 @@ display: inline-flex; } - :host([disabled]) { + :host(:disabled) { pointer-events: none; } } diff --git a/select/internal/select.ts b/select/internal/select.ts index 8f308baaf2..ed3daf3603 100644 --- a/select/internal/select.ts +++ b/select/internal/select.ts @@ -373,22 +373,30 @@ export abstract class Select extends selectBaseClass { super.firstUpdated(changed); } + private get isControlDisabled() { + return this.disabled || this.matches(':disabled'); + } + private getRenderClasses(): ClassInfo { + const disabled = this.isControlDisabled; + return { - 'disabled': this.disabled, + 'disabled': disabled, 'error': this.error, 'open': this.open, }; } private renderField() { + const disabled = this.isControlDisabled; + return staticHtml` <${this.fieldTag} aria-haspopup="listbox" role="combobox" part="field" id="field" - tabindex=${this.disabled ? '-1' : '0'} + tabindex=${disabled ? '-1' : '0'} aria-label=${(this as ARIAMixinStrict).ariaLabel || nothing} aria-describedby="description" aria-expanded=${this.open ? 'true' : 'false'} @@ -397,7 +405,7 @@ export abstract class Select extends selectBaseClass { label=${this.label} .focused=${this.focused || this.open} .populated=${!!this.displayText} - .disabled=${this.disabled} + .disabled=${disabled} .required=${this.required} .error=${this.hasError} ?has-start=${this.hasLeadingIcon} @@ -500,8 +508,8 @@ export abstract class Select extends selectBaseClass { * Handles opening the select on keydown and typahead selection when the menu * is closed. */ - private handleKeydown(event: KeyboardEvent) { - if (this.open || this.disabled || !this.menu) { + private handleKeydown(event: KeyboardEvent) { + if (this.open || this.isControlDisabled || !this.menu) { return; } diff --git a/slider/internal/_slider.scss b/slider/internal/_slider.scss index 558fb3753f..d37edef5c7 100644 --- a/slider/internal/_slider.scss +++ b/slider/internal/_slider.scss @@ -103,7 +103,7 @@ $_md-sys-shape: tokens.md-sys-shape-values(); // Note, opacity for active track and handle controlled via host. // This avoids bleed through from the handle to the track since they overlap. // It also means the inactive track opacity is calc'd to compensate. - :host([disabled]) { + :host(:disabled) { opacity: var(--_disabled-active-track-opacity); @include elevation.theme( @@ -172,7 +172,7 @@ $_md-sys-shape: tokens.md-sys-shape-values(); ); } - :host([disabled]) .track::before { + :host(:disabled) .track::before { // Note, the active track opacity is applied to the entire host, // so the inactive track is calc'd to compensate. opacity: calc( @@ -209,11 +209,11 @@ $_md-sys-shape: tokens.md-sys-shape-values(); clip-path: inset(0 $_active-track-start-clip 0 $_active-track-end-clip); } - :host([disabled]) .track::after { + :host(:disabled) .track::after { background: var(--_disabled-active-track-color); } - :host([disabled]) .tickmarks::before { + :host(:disabled) .tickmarks::before { background-image: _get-tick-image( var(--_with-tick-marks-disabled-container-color) ); @@ -265,7 +265,7 @@ $_md-sys-shape: tokens.md-sys-shape-values(); background: var(--_handle-color); } - :host([disabled]) .handleNub { + :host(:disabled) .handleNub { background: var(--_disabled-handle-color); } @@ -279,7 +279,7 @@ $_md-sys-shape: tokens.md-sys-shape-values(); background: var(--_hover-handle-color); } - :host(:not([disabled])) { + :host(:not(:disabled)) { input.end:active ~ .handleContainerPadded .handle.end > .handleNub, input.start:active ~ .handleContainerPadded .handle.start > .handleNub { background: var(--_pressed-handle-color); diff --git a/slider/internal/forced-colors-styles.scss b/slider/internal/forced-colors-styles.scss index 2d738f9675..9080bcca1e 100644 --- a/slider/internal/forced-colors-styles.scss +++ b/slider/internal/forced-colors-styles.scss @@ -44,7 +44,7 @@ } // inactive track - :host(:not([disabled])) .track::before { + :host(:not(:disabled)) .track::before { border: 1px solid var(--_active-track-color); } @@ -78,7 +78,7 @@ // stylelint-enable function-url-quotes } - :host([disabled]) .tickmarks::before { + :host(:disabled) .tickmarks::before { // TODO(b/298051946): Tick marks cannot be resized in HCM // stylelint-disable function-url-quotes -- SVG data URI // SVG is optimized for data URI (https://codepen.io/tigt/post/optimizing-svgs-in-data-uris) diff --git a/slider/internal/slider.ts b/slider/internal/slider.ts index c529a42872..77cc3f175b 100644 --- a/slider/internal/slider.ts +++ b/slider/internal/slider.ts @@ -331,6 +331,10 @@ export class Slider extends sliderBaseClass { this.performUpdate(); } + private get isControlDisabled() { + return this.disabled || this.matches(':disabled'); + } + protected override render() { const step = this.step === 0 ? 1 : this.step; const range = Math.max(this.max - this.min, step); @@ -425,8 +429,10 @@ export class Slider extends sliderBaseClass { hover: boolean; label: string; }) { - const onTop = !this.disabled && start === this.startOnTop; - const isOverlapping = !this.disabled && this.handlesOverlapping; + const disabled = this.isControlDisabled; + + const onTop = !disabled && start === this.startOnTop; + const isOverlapping = !disabled && this.handlesOverlapping; const name = start ? 'start' : 'end'; return html`
@@ -462,6 +468,8 @@ export class Slider extends sliderBaseClass { ariaMin: number; ariaMax: number; }) { + const disabled = this.isControlDisabled; + // Slider requires min/max set to the overall min/max for both inputs. // This is reported to screen readers, which is why we need aria-valuemin // and aria-valuemax. @@ -483,7 +491,7 @@ export class Slider extends sliderBaseClass { @input=${this.handleInput} @change=${this.handleChange} id=${name} - .disabled=${this.disabled} + .disabled=${disabled} .min=${String(this.min)} aria-valuemin=${ariaMin} .max=${String(this.max)} @@ -557,11 +565,13 @@ export class Slider extends sliderBaseClass { this.startAction(event); this.ripplePointerId = event.pointerId; const isStart = (event.target as HTMLInputElement) === this.inputStart; + + const disabled = this.isControlDisabled; // Since handle moves to pointer on down and there may not be a move, // it needs to be considered hovered.. this.handleStartHover = - !this.disabled && isStart && Boolean(this.handleStart); - this.handleEndHover = !this.disabled && !isStart && Boolean(this.handleEnd); + !disabled && isStart && Boolean(this.handleStart); + this.handleEndHover = !disabled && !isStart && Boolean(this.handleEnd); } private async handleUp(event: PointerEvent) { @@ -600,8 +610,10 @@ export class Slider extends sliderBaseClass { * slider is updated. */ private handleMove(event: PointerEvent) { - this.handleStartHover = !this.disabled && inBounds(event, this.handleStart); - this.handleEndHover = !this.disabled && inBounds(event, this.handleEnd); + const disabled = this.isControlDisabled; + + this.handleStartHover = !disabled && inBounds(event, this.handleStart); + this.handleEndHover = !disabled && inBounds(event, this.handleEnd); } private handleEnter(event: PointerEvent) { diff --git a/switch/internal/_switch.scss b/switch/internal/_switch.scss index 4148fd585e..24bd022729 100644 --- a/switch/internal/_switch.scss +++ b/switch/internal/_switch.scss @@ -43,7 +43,7 @@ cursor: pointer; } - :host([disabled]) { + :host(:disabled) { cursor: default; } diff --git a/switch/internal/switch.ts b/switch/internal/switch.ts index 570f256274..c2eb587cc4 100644 --- a/switch/internal/switch.ts +++ b/switch/internal/switch.ts @@ -99,7 +99,13 @@ export class Switch extends switchBaseClass { } } + private get isControlDisabled() { + return this.disabled || this.matches(':disabled'); + } + protected override render(): TemplateResult { + const disabled = this.isControlDisabled; + // NOTE: buttons must use only [phrasing // content](https://html.spec.whatwg.org/multipage/dom.html#phrasing-content) // children, which includes custom elements, but not `div`s @@ -112,7 +118,7 @@ export class Switch extends switchBaseClass { role="switch" aria-label=${(this as ARIAMixin).ariaLabel || nothing} ?checked=${this.selected} - ?disabled=${this.disabled} + ?disabled=${disabled} ?required=${this.required} @input=${this.handleInput} @change=${this.handleChange} /> @@ -124,21 +130,25 @@ export class Switch extends switchBaseClass { } private getRenderClasses(): ClassInfo { + const disabled = this.isControlDisabled; + return { 'selected': this.selected, 'unselected': !this.selected, - 'disabled': this.disabled, + 'disabled': disabled, }; } private renderHandle() { + const disabled = this.isControlDisabled; + const classes = { 'with-icon': this.showOnlySelectedIcon ? this.selected : this.icons, }; return html` ${this.renderTouchTarget()} - + ${this.shouldShowIcons() ? this.renderIcons() : html``} From 5ced25836423995b4f8aa552c037565c0c191695 Mon Sep 17 00:00:00 2001 From: Luke Vo Date: Wed, 3 Apr 2024 19:29:34 +0700 Subject: [PATCH 3/3] Fix formDisabledCallback modifying the control disabled property. --- labs/behaviors/form-associated.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/labs/behaviors/form-associated.ts b/labs/behaviors/form-associated.ts index 75f5efed34..e12be3b9d2 100644 --- a/labs/behaviors/form-associated.ts +++ b/labs/behaviors/form-associated.ts @@ -292,7 +292,7 @@ export function mixinFormAssociated< } formDisabledCallback(disabled: boolean) { - this.disabled = disabled; + this.requestUpdate(); } abstract formResetCallback(): void;