From c925053bb81b3752d28f64b8a42ba2f453e6e2d3 Mon Sep 17 00:00:00 2001 From: uptimeZERO_ Date: Tue, 3 Feb 2026 14:55:10 +0000 Subject: [PATCH] Animate app side bar (#29026) --- src/components/chart/ha-chart-base.ts | 69 ++++++++++++--- src/components/ha-drawer.ts | 87 +++++++++++++++++++ src/components/ha-sidebar.ts | 77 ++++++++++++---- src/components/ha-top-app-bar-fixed.ts | 9 ++ .../ha-two-pane-top-app-bar-fixed.ts | 9 ++ src/html/index.html.template | 4 +- src/panels/climate/ha-panel-climate.ts | 1 - .../config/storage/storage-breakdown-chart.ts | 4 +- src/panels/energy/ha-panel-energy.ts | 5 ++ src/panels/light/ha-panel-light.ts | 1 - .../components/hui-energy-period-selector.ts | 2 +- src/panels/lovelace/hui-root.ts | 1 - .../media-browser/ha-bar-media-player.ts | 9 ++ src/panels/security/ha-panel-security.ts | 1 - src/resources/styles.ts | 14 +++ src/resources/theme/core.globals.ts | 8 +- src/util/launch-screen.ts | 2 +- 17 files changed, 264 insertions(+), 39 deletions(-) diff --git a/src/components/chart/ha-chart-base.ts b/src/components/chart/ha-chart-base.ts index 3388e8376a..6eaf002077 100644 --- a/src/components/chart/ha-chart-base.ts +++ b/src/components/chart/ha-chart-base.ts @@ -19,6 +19,7 @@ import { styleMap } from "lit/directives/style-map"; import { ensureArray } from "../../common/array/ensure-array"; import { getAllGraphColors } from "../../common/color/colors"; import { fireEvent } from "../../common/dom/fire_event"; +import type { HASSDomEvent } from "../../common/dom/fire_event"; import { listenMediaQuery } from "../../common/dom/media_query"; import { themesContext } from "../../data/context"; import type { Themes } from "../../data/ws-themes"; @@ -93,10 +94,18 @@ export class HaChartBase extends LitElement { private _resizeAnimationDuration?: number; + private _suspendResize = false; + + private _layoutTransitionActive = false; + // @ts-ignore private _resizeController = new ResizeController(this, { callback: () => { if (this.chart) { + if (this._suspendResize) { + this._shouldResizeChart = true; + return; + } if (!this.chart.getZr().animation.isFinished()) { this._shouldResizeChart = true; } else { @@ -191,6 +200,26 @@ export class HaChartBase extends LitElement { () => window.removeEventListener("keyup", handleKeyUp) ); } + + const handleLayoutTransition: EventListener = (ev) => { + const event = ev as HASSDomEvent; + this._layoutTransitionActive = Boolean(event.detail?.active); + this.toggleAttribute( + "layout-transition-active", + this._layoutTransitionActive + ); + this._suspendResize = this._layoutTransitionActive; + if (!this._suspendResize) { + this._resizeChartIfNeeded(); + } + }; + window.addEventListener("hass-layout-transition", handleLayoutTransition); + this._listeners.push(() => + window.removeEventListener( + "hass-layout-transition", + handleLayoutTransition + ) + ); } protected firstUpdated() { @@ -998,19 +1027,29 @@ export class HaChartBase extends LitElement { } private _handleChartRenderFinished = () => { - if (this._shouldResizeChart) { - this.chart?.resize({ - animation: - this._reducedMotion || - typeof this._resizeAnimationDuration !== "number" - ? undefined - : { duration: this._resizeAnimationDuration }, - }); - this._shouldResizeChart = false; - this._resizeAnimationDuration = undefined; - } + this._resizeChartIfNeeded(); }; + private _resizeChartIfNeeded() { + if (!this.chart || !this._shouldResizeChart) { + return; + } + if (this._suspendResize) { + return; + } + if (!this.chart.getZr().animation.isFinished()) { + return; + } + this.chart.resize({ + animation: + this._reducedMotion || typeof this._resizeAnimationDuration !== "number" + ? undefined + : { duration: this._resizeAnimationDuration }, + }); + this._shouldResizeChart = false; + this._resizeAnimationDuration = undefined; + } + private _compareCustomLegendOptions( oldOptions: ECOption | undefined, newOptions: ECOption | undefined @@ -1032,11 +1071,18 @@ export class HaChartBase extends LitElement { display: block; position: relative; letter-spacing: normal; + overflow: visible; + } + :host([layout-transition-active]), + :host([layout-transition-active]) .container, + :host([layout-transition-active]) .chart-container { + overflow: hidden; } .container { display: flex; flex-direction: column; position: relative; + overflow: visible; } .container.has-height { max-height: var(--chart-max-height, 350px); @@ -1044,6 +1090,7 @@ export class HaChartBase extends LitElement { .chart-container { width: 100%; max-height: var(--chart-max-height, 350px); + overflow: visible; } .has-height .chart-container { flex: 1; diff --git a/src/components/ha-drawer.ts b/src/components/ha-drawer.ts index 3e563cd60b..72b69ca8b5 100644 --- a/src/components/ha-drawer.ts +++ b/src/components/ha-drawer.ts @@ -4,6 +4,18 @@ import type { PropertyValues } from "lit"; import { css } from "lit"; import { customElement, property } from "lit/decorators"; import { fireEvent } from "../common/dom/fire_event"; +import type { HASSDomEvent } from "../common/dom/fire_event"; + +declare global { + interface HASSDomEvents { + "hass-layout-transition": { active: boolean; reason?: string }; + } + interface HTMLElementEventMap { + "hass-layout-transition": HASSDomEvent< + HASSDomEvents["hass-layout-transition"] + >; + } +} const blockingElements = (document as any).$blockingElements; @@ -15,6 +27,30 @@ export class HaDrawer extends DrawerBase { private _rtlStyle?: HTMLElement; + private _sidebarTransitionActive = false; + + private _handleDrawerTransitionStart = (ev: TransitionEvent) => { + if (ev.propertyName !== "width" || this._sidebarTransitionActive) { + return; + } + this._sidebarTransitionActive = true; + fireEvent(window, "hass-layout-transition", { + active: true, + reason: "sidebar", + }); + }; + + private _handleDrawerTransitionEnd = (ev: TransitionEvent) => { + if (ev.propertyName !== "width" || !this._sidebarTransitionActive) { + return; + } + this._sidebarTransitionActive = false; + fireEvent(window, "hass-layout-transition", { + active: false, + reason: "sidebar", + }); + }; + protected createAdapter() { return { ...super.createAdapter(), @@ -63,6 +99,38 @@ export class HaDrawer extends DrawerBase { } } + protected firstUpdated() { + super.firstUpdated(); + this.mdcRoot?.addEventListener( + "transitionstart", + this._handleDrawerTransitionStart + ); + this.mdcRoot?.addEventListener( + "transitionend", + this._handleDrawerTransitionEnd + ); + this.mdcRoot?.addEventListener( + "transitioncancel", + this._handleDrawerTransitionEnd + ); + } + + public disconnectedCallback() { + super.disconnectedCallback(); + this.mdcRoot?.removeEventListener( + "transitionstart", + this._handleDrawerTransitionStart + ); + this.mdcRoot?.removeEventListener( + "transitionend", + this._handleDrawerTransitionEnd + ); + this.mdcRoot?.removeEventListener( + "transitioncancel", + this._handleDrawerTransitionEnd + ); + } + private async _setupSwipe() { const hammer = await import("../resources/hammer"); this._mc = new hammer.Manager(document, { @@ -90,6 +158,16 @@ export class HaDrawer extends DrawerBase { border-color: var(--divider-color, rgba(0, 0, 0, 0.12)); inset-inline-start: 0 !important; inset-inline-end: initial !important; + transition-property: transform, width; + transition-duration: + var(--mdc-drawer-transition-duration, 0.2s), + var(--ha-animation-duration-normal); + transition-timing-function: + var( + --mdc-drawer-transition-timing-function, + cubic-bezier(0.4, 0, 0.2, 1) + ), + ease; } .mdc-drawer.mdc-drawer--modal.mdc-drawer--open { z-index: 200; @@ -103,6 +181,15 @@ export class HaDrawer extends DrawerBase { direction: var(--direction); width: 100%; box-sizing: border-box; + transition: + padding-left var(--ha-animation-duration-normal) ease, + padding-inline-start var(--ha-animation-duration-normal) ease; + } + @media (prefers-reduced-motion: reduce) { + .mdc-drawer, + .mdc-drawer-app-content { + transition: none; + } } `, ]; diff --git a/src/components/ha-sidebar.ts b/src/components/ha-sidebar.ts index 356d0a3af1..a50500dd1b 100644 --- a/src/components/ha-sidebar.ts +++ b/src/components/ha-sidebar.ts @@ -492,19 +492,22 @@ class HaSidebar extends SubscribeMixin(ScrollableFadeMixin(LitElement)) { @mouseleave=${this._itemMouseLeave} > - ${!this.alwaysExpand && - (this._updatesCount > 0 || this._issuesCount > 0) - ? html`${this._updatesCount + this._issuesCount}` + ${this._updatesCount > 0 || this._issuesCount > 0 + ? html` + + ${this._updatesCount + this._issuesCount} + + ` : nothing} ${this.hass.localize("panel.config")} - ${this.alwaysExpand && (this._updatesCount > 0 || this._issuesCount > 0) - ? html`${this._updatesCount + this._issuesCount}` + ${this._updatesCount > 0 || this._issuesCount > 0 + ? html` + ${this._updatesCount + this._issuesCount} + ` : nothing} `; @@ -524,13 +527,15 @@ class HaSidebar extends SubscribeMixin(ScrollableFadeMixin(LitElement)) { type="button" > - ${!this.alwaysExpand && notificationCount > 0 - ? html`${notificationCount}` + ${notificationCount > 0 + ? html` + ${notificationCount} + ` : nothing} ${this.hass.localize("ui.notification_drawer.title")} - ${this.alwaysExpand && notificationCount > 0 + ${notificationCount > 0 ? html`${notificationCount}` : nothing} @@ -739,6 +744,8 @@ class HaSidebar extends SubscribeMixin(ScrollableFadeMixin(LitElement)) { ); font-size: var(--ha-font-size-xl); align-items: center; + overflow: hidden; + width: calc(56px + var(--safe-area-inset-left, 0px)); padding-left: calc( var(--ha-space-1) + var(--safe-area-inset-left, 0px) ); @@ -747,6 +754,7 @@ class HaSidebar extends SubscribeMixin(ScrollableFadeMixin(LitElement)) { ); padding-inline-end: initial; padding-top: var(--safe-area-inset-top, 0px); + transition: width var(--ha-animation-duration-normal) ease; } :host([expanded]) .menu { width: calc(256px + var(--safe-area-inset-left, 0px)); @@ -761,15 +769,22 @@ class HaSidebar extends SubscribeMixin(ScrollableFadeMixin(LitElement)) { margin-left: 3px; margin-inline-start: 3px; margin-inline-end: initial; - width: 100%; - display: none; + flex: 1; + min-width: 0; + max-width: 0; + opacity: 0; + transition: + max-width var(--ha-animation-duration-normal) ease, + opacity var(--ha-animation-duration-normal) ease; } :host([narrow]) .title { margin: 0; padding: 0 var(--ha-space-4); } :host([expanded]) .title { - display: initial; + max-width: 100%; + opacity: 1; + transition-delay: 0ms, 80ms; } .panels-list { @@ -827,6 +842,7 @@ class HaSidebar extends SubscribeMixin(ScrollableFadeMixin(LitElement)) { --md-list-item-leading-space: var(--ha-space-3); --md-list-item-trailing-space: var(--ha-space-3); --md-list-item-leading-icon-size: var(--ha-space-6); + transition: width var(--ha-animation-duration-normal) ease; } :host([expanded]) ha-md-list-item { width: 248px; @@ -867,11 +883,22 @@ class HaSidebar extends SubscribeMixin(ScrollableFadeMixin(LitElement)) { } ha-md-list-item .item-text { - display: none; + display: block; + max-width: 0; + opacity: 0; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; font-size: var(--ha-font-size-m); font-weight: var(--ha-font-weight-medium); + transition: + max-width var(--ha-animation-duration-normal) ease, + opacity var(--ha-animation-duration-normal) ease; } :host([expanded]) ha-md-list-item .item-text { + max-width: 100%; + opacity: 1; + transition-delay: 0ms, 80ms; display: block; overflow: hidden; text-overflow: ellipsis; @@ -889,6 +916,9 @@ class HaSidebar extends SubscribeMixin(ScrollableFadeMixin(LitElement)) { background-color: var(--accent-color); padding: 2px 6px; color: var(--text-accent-color, var(--text-primary-color)); + transition: + opacity var(--ha-animation-duration-normal) ease, + transform var(--ha-animation-duration-normal) ease; } ha-svg-icon + .badge { @@ -900,6 +930,12 @@ class HaSidebar extends SubscribeMixin(ScrollableFadeMixin(LitElement)) { line-height: var(--ha-line-height-expanded); padding: 0 var(--ha-space-1); } + :host([expanded]) .badge[slot="start"], + :host(:not([expanded])) .badge[slot="end"] { + opacity: 0; + transform: scale(0.8); + pointer-events: none; + } ha-md-list-item.user { --md-list-item-leading-icon-size: var(--ha-space-10); @@ -938,6 +974,15 @@ class HaSidebar extends SubscribeMixin(ScrollableFadeMixin(LitElement)) { -webkit-transform: scaleX(var(--scale-direction)); transform: scaleX(var(--scale-direction)); } + + @media (prefers-reduced-motion: reduce) { + .menu, + ha-md-list-item, + ha-md-list-item .item-text, + .title { + transition: none; + } + } `, ]; } diff --git a/src/components/ha-top-app-bar-fixed.ts b/src/components/ha-top-app-bar-fixed.ts index 4481c42f54..1f7a650dd9 100644 --- a/src/components/ha-top-app-bar-fixed.ts +++ b/src/components/ha-top-app-bar-fixed.ts @@ -36,10 +36,19 @@ export class HaTopAppBarFixed extends TopAppBarFixedBase { ); padding-top: var(--safe-area-inset-top); padding-right: var(--safe-area-inset-right); + transition: + width var(--ha-animation-duration-normal) ease, + padding-left var(--ha-animation-duration-normal) ease, + padding-right var(--ha-animation-duration-normal) ease; } :host([narrow]) .mdc-top-app-bar { padding-left: var(--safe-area-inset-left); } + @media (prefers-reduced-motion: reduce) { + .mdc-top-app-bar { + transition: none; + } + } .mdc-top-app-bar__title { font-size: var(--ha-font-size-xl); padding-inline-start: var(--ha-space-6); diff --git a/src/components/ha-two-pane-top-app-bar-fixed.ts b/src/components/ha-two-pane-top-app-bar-fixed.ts index eca2775e2a..52a560b5d1 100644 --- a/src/components/ha-two-pane-top-app-bar-fixed.ts +++ b/src/components/ha-two-pane-top-app-bar-fixed.ts @@ -288,10 +288,19 @@ export class TopAppBarBaseBase extends BaseElement { ); padding-top: var(--safe-area-inset-top); padding-right: var(--safe-area-inset-right); + transition: + width var(--ha-animation-duration-normal) ease, + padding-left var(--ha-animation-duration-normal) ease, + padding-right var(--ha-animation-duration-normal) ease; } :host([narrow]) .mdc-top-app-bar { padding-left: var(--safe-area-inset-left); } + @media (prefers-reduced-motion: reduce) { + .mdc-top-app-bar { + transition: none; + } + } .mdc-top-app-bar--pane.mdc-top-app-bar--fixed-scrolled { box-shadow: none; } diff --git a/src/html/index.html.template b/src/html/index.html.template index 76198db310..029f8c7047 100644 --- a/src/html/index.html.template +++ b/src/html/index.html.template @@ -29,11 +29,11 @@ } } ::view-transition-group(launch-screen) { - animation-duration: var(--ha-animation-base-duration, 350ms); + animation-duration: var(--ha-animation-duration-slow, 350ms); animation-timing-function: ease-out; } ::view-transition-old(launch-screen) { - animation: fade-out var(--ha-animation-base-duration, 350ms) ease-out; + animation: fade-out var(--ha-animation-duration-slow, 350ms) ease-out; } html { background-color: var(--primary-background-color, #fafafa); diff --git a/src/panels/climate/ha-panel-climate.ts b/src/panels/climate/ha-panel-climate.ts index 9b162239a2..f61e98838c 100644 --- a/src/panels/climate/ha-panel-climate.ts +++ b/src/panels/climate/ha-panel-climate.ts @@ -186,7 +186,6 @@ class PanelClimate extends LitElement { ); padding-top: var(--safe-area-inset-top); z-index: 4; - transition: box-shadow 200ms linear; display: flex; flex-direction: row; -webkit-backdrop-filter: var(--app-header-backdrop-filter, none); diff --git a/src/panels/config/storage/storage-breakdown-chart.ts b/src/panels/config/storage/storage-breakdown-chart.ts index 230b3dc2c7..a969fd4db4 100644 --- a/src/panels/config/storage/storage-breakdown-chart.ts +++ b/src/panels/config/storage/storage-breakdown-chart.ts @@ -242,7 +242,7 @@ export class StorageBreakdownChart extends LitElement { } .chart-container { - transition: height var(--ha-animation-base-duration) ease; + transition: height var(--ha-animation-duration-slow) ease; overflow: hidden; } @@ -264,7 +264,7 @@ export class StorageBreakdownChart extends LitElement { ha-segmented-bar, ha-sunburst-chart { - animation: fade-in var(--ha-animation-base-duration) ease; + animation: fade-in var(--ha-animation-duration-slow) ease; } @keyframes fade-in { diff --git a/src/panels/energy/ha-panel-energy.ts b/src/panels/energy/ha-panel-energy.ts index 4236ff308d..25ea603d04 100644 --- a/src/panels/energy/ha-panel-energy.ts +++ b/src/panels/energy/ha-panel-energy.ts @@ -699,6 +699,11 @@ class PanelEnergy extends LitElement { var(--safe-area-inset-left, 0px) ); inset-inline-end: var(--safe-area-inset-right, 0); + transition: + left var(--ha-animation-duration-normal) ease, + right var(--ha-animation-duration-normal) ease, + inset-inline-start var(--ha-animation-duration-normal) ease, + inset-inline-end var(--ha-animation-duration-normal) ease; margin: 0 auto; max-width: calc(min(470px, 100% - var(--ha-space-4))); box-sizing: border-box; diff --git a/src/panels/light/ha-panel-light.ts b/src/panels/light/ha-panel-light.ts index 58dfe00424..f2036e3415 100644 --- a/src/panels/light/ha-panel-light.ts +++ b/src/panels/light/ha-panel-light.ts @@ -186,7 +186,6 @@ class PanelLight extends LitElement { ); padding-top: var(--safe-area-inset-top); z-index: 4; - transition: box-shadow 200ms linear; display: flex; flex-direction: row; -webkit-backdrop-filter: var(--app-header-backdrop-filter, none); diff --git a/src/panels/lovelace/components/hui-energy-period-selector.ts b/src/panels/lovelace/components/hui-energy-period-selector.ts index 929395289d..9f484cfadd 100644 --- a/src/panels/lovelace/components/hui-energy-period-selector.ts +++ b/src/panels/lovelace/components/hui-energy-period-selector.ts @@ -545,7 +545,7 @@ export class HuiEnergyPeriodSelector extends SubscribeMixin(LitElement) { ); pointer-events: none; opacity: 0; - transition: opacity var(--ha-animation-base-duration) ease-in-out; + transition: opacity var(--ha-animation-duration-slow) ease-in-out; } .datepicker-open .backdrop { opacity: 1; diff --git a/src/panels/lovelace/hui-root.ts b/src/panels/lovelace/hui-root.ts index 9165e52861..2959433cef 100644 --- a/src/panels/lovelace/hui-root.ts +++ b/src/panels/lovelace/hui-root.ts @@ -1299,7 +1299,6 @@ class HUIRoot extends LitElement { padding-top: var(--safe-area-inset-top); padding-right: var(--safe-area-inset-right); z-index: 4; - transition: box-shadow 200ms linear; } .narrow .header { width: calc( diff --git a/src/panels/media-browser/ha-bar-media-player.ts b/src/panels/media-browser/ha-bar-media-player.ts index fc4c8b6186..0a5807f021 100644 --- a/src/panels/media-browser/ha-bar-media-player.ts +++ b/src/panels/media-browser/ha-bar-media-player.ts @@ -669,10 +669,19 @@ export class BarMediaPlayer extends SubscribeMixin(LitElement) { ); border-top: 1px solid var(--divider-color); margin-right: var(--safe-area-inset-right); + transition: + width var(--ha-animation-duration-normal) ease, + margin-left var(--ha-animation-duration-normal) ease, + margin-right var(--ha-animation-duration-normal) ease; } :host([narrow]) { margin-left: var(--safe-area-inset-left); } + @media (prefers-reduced-motion: reduce) { + :host { + transition: none; + } + } ha-slider { width: 100%; diff --git a/src/panels/security/ha-panel-security.ts b/src/panels/security/ha-panel-security.ts index 0d071374ff..baeff65736 100644 --- a/src/panels/security/ha-panel-security.ts +++ b/src/panels/security/ha-panel-security.ts @@ -186,7 +186,6 @@ class PanelSecurity extends LitElement { ); padding-top: var(--safe-area-inset-top); z-index: 4; - transition: box-shadow 200ms linear; display: flex; flex-direction: row; -webkit-backdrop-filter: var(--app-header-backdrop-filter, none); diff --git a/src/resources/styles.ts b/src/resources/styles.ts index 047f94b4ad..e9276eb610 100644 --- a/src/resources/styles.ts +++ b/src/resources/styles.ts @@ -34,6 +34,20 @@ export const haStyle = css` margin-inline-end: initial; } + .header { + transition: + box-shadow 200ms linear, + width var(--ha-animation-duration-normal) ease, + padding-left var(--ha-animation-duration-normal) ease, + padding-right var(--ha-animation-duration-normal) ease; + } + + @media (prefers-reduced-motion: reduce) { + .header { + transition: box-shadow 200ms linear; + } + } + h1 { font-family: var(--ha-font-family-heading); -webkit-font-smoothing: var(--ha-font-smoothing); diff --git a/src/resources/theme/core.globals.ts b/src/resources/theme/core.globals.ts index 8ecf898cbf..b746d79656 100644 --- a/src/resources/theme/core.globals.ts +++ b/src/resources/theme/core.globals.ts @@ -55,12 +55,16 @@ export const coreStyles = css` --ha-shadow-spread-md: 0; --ha-shadow-spread-lg: 0; - --ha-animation-base-duration: 350ms; + --ha-animation-duration-fast: 150ms; + --ha-animation-duration-normal: 250ms; + --ha-animation-duration-slow: 350ms; } @media (prefers-reduced-motion: reduce) { html { - --ha-animation-base-duration: 0ms; + --ha-animation-duration-fast: 0ms; + --ha-animation-duration-normal: 0ms; + --ha-animation-duration-slow: 0ms; } } `; diff --git a/src/util/launch-screen.ts b/src/util/launch-screen.ts index 1abc5b9e9f..3bcebb5a47 100644 --- a/src/util/launch-screen.ts +++ b/src/util/launch-screen.ts @@ -18,7 +18,7 @@ export const removeLaunchScreen = () => { launchScreenElement.classList.add("removing"); const durationFromCss = getComputedStyle(document.documentElement) - .getPropertyValue("--ha-animation-base-duration") + .getPropertyValue("--ha-animation-duration-slow") .trim(); setTimeout(() => {