From 3a0c367f7615b0bbd1d905a535c294334b7e76d0 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Fri, 25 Apr 2025 07:51:35 +0200 Subject: [PATCH] Add a drag-scroll controller (#25159) * Add a drag-scroll controller * simplify and fix --- package.json | 1 + .../controllers/drag-scroll-controller.ts | 102 ++++++++++++++++++ src/components/sl-tab-group.ts | 84 ++------------- .../more-info/controls/more-info-weather.ts | 7 ++ src/panels/lovelace/views/hui-view-header.ts | 16 ++- yarn.lock | 1 + 6 files changed, 132 insertions(+), 79 deletions(-) create mode 100644 src/common/controllers/drag-scroll-controller.ts diff --git a/package.json b/package.json index 58b2a1526b..c60936fe62 100644 --- a/package.json +++ b/package.json @@ -56,6 +56,7 @@ "@lit-labs/observers": "2.0.5", "@lit-labs/virtualizer": "2.1.0", "@lit/context": "1.1.5", + "@lit/reactive-element": "2.1.0", "@material/chips": "=14.0.0-canary.53b3cad2f.0", "@material/data-table": "=14.0.0-canary.53b3cad2f.0", "@material/mwc-base": "0.27.0", diff --git a/src/common/controllers/drag-scroll-controller.ts b/src/common/controllers/drag-scroll-controller.ts new file mode 100644 index 0000000000..cefb9adad5 --- /dev/null +++ b/src/common/controllers/drag-scroll-controller.ts @@ -0,0 +1,102 @@ +import type { + ReactiveController, + ReactiveControllerHost, +} from "@lit/reactive-element/reactive-controller"; +import type { LitElement } from "lit"; + +/** + * The config options for a DragScrollController. + */ +export interface DragScrollControllerConfig { + selector: string; +} + +export class DragScrollController implements ReactiveController { + public mouseIsDown = false; + + public scrolled = false; + + public scrolling = false; + + public scrollStartX = 0; + + public scrollLeft = 0; + + private _host: ReactiveControllerHost & LitElement; + + private _selector: string; + + private _scrollContainer?: HTMLElement | null; + + constructor( + host: ReactiveControllerHost & LitElement, + { selector }: DragScrollControllerConfig + ) { + this._selector = selector; + this._host = host; + host.addController(this); + } + + hostUpdated() { + if (this._scrollContainer) { + return; + } + this._scrollContainer = this._host.renderRoot?.querySelector( + this._selector + ); + if (this._scrollContainer) { + this._scrollContainer.addEventListener("mousedown", this._mouseDown); + } + } + + hostDisconnected() { + window.removeEventListener("mousemove", this._mouseMove); + window.removeEventListener("mouseup", this._mouseUp); + } + + private _mouseDown = (event: MouseEvent) => { + const scrollContainer = this._scrollContainer; + + if (!scrollContainer) { + return; + } + + this.scrollStartX = event.pageX - scrollContainer.offsetLeft; + this.scrollLeft = scrollContainer.scrollLeft; + this.mouseIsDown = true; + this.scrolled = false; + + window.addEventListener("mousemove", this._mouseMove); + window.addEventListener("mouseup", this._mouseUp, { once: true }); + }; + + private _mouseUp = () => { + this.mouseIsDown = false; + this.scrolling = false; + this._host.requestUpdate(); + window.removeEventListener("mousemove", this._mouseMove); + }; + + private _mouseMove = (event: MouseEvent) => { + if (!this.mouseIsDown) { + return; + } + + const scrollContainer = this._scrollContainer; + + if (!scrollContainer) { + return; + } + + const x = event.pageX - scrollContainer.offsetLeft; + const scroll = x - this.scrollStartX; + + if (!this.scrolled) { + this.scrolled = Math.abs(scroll) > 1; + this.scrolling = this.scrolled; + this._host.requestUpdate(); + } + + scrollContainer.scrollLeft = this.scrollLeft - scroll; + }; +} diff --git a/src/components/sl-tab-group.ts b/src/components/sl-tab-group.ts index df24acbedb..cab5ea0f22 100644 --- a/src/components/sl-tab-group.ts +++ b/src/components/sl-tab-group.ts @@ -1,30 +1,16 @@ import TabGroup from "@shoelace-style/shoelace/dist/components/tab-group/tab-group.component"; import TabGroupStyles from "@shoelace-style/shoelace/dist/components/tab-group/tab-group.styles"; import "@shoelace-style/shoelace/dist/components/tab/tab"; -import type { PropertyValues } from "lit"; import { css } from "lit"; -import { customElement, query } from "lit/decorators"; +import { customElement } from "lit/decorators"; +import { DragScrollController } from "../common/controllers/drag-scroll-controller"; @customElement("sl-tab-group") // @ts-ignore export class HaSlTabGroup extends TabGroup { - private _mouseIsDown = false; - - private _scrolled = false; - - private _mouseReleasedAt?: number; - - private _scrollStartX = 0; - - private _scrollLeft = 0; - - @query(".tab-group__nav", true) private _scrollContainer?: HTMLElement; - - public disconnectedCallback(): void { - super.disconnectedCallback(); - window.removeEventListener("mousemove", this._mouseMove); - window.removeEventListener("mouseup", this._mouseUp); - } + private _dragScrollController = new DragScrollController(this, { + selector: ".tab-group__nav", + }); override setAriaLabels() { // Override the method to prevent setting aria-labels, as we don't use panels @@ -38,73 +24,15 @@ export class HaSlTabGroup extends TabGroup { return []; } - protected override firstUpdated(_changedProperties: PropertyValues): void { - super.firstUpdated(_changedProperties); - - const scrollContainer = this._scrollContainer; - - if (scrollContainer) { - scrollContainer.addEventListener("mousedown", this._mouseDown); - } - } - // @ts-ignore protected override handleClick(event: MouseEvent) { - if ( - this._mouseReleasedAt && - new Date().getTime() - this._mouseReleasedAt < 100 - ) { + if (this._dragScrollController.scrolled) { return; } // @ts-ignore super.handleClick(event); } - private _mouseDown = (event: MouseEvent) => { - const scrollContainer = this._scrollContainer; - - if (!scrollContainer) { - return; - } - - this._scrollStartX = event.pageX - scrollContainer.offsetLeft; - this._scrollLeft = scrollContainer.scrollLeft; - this._mouseIsDown = true; - this._scrolled = false; - - window.addEventListener("mousemove", this._mouseMove); - window.addEventListener("mouseup", this._mouseUp, { once: true }); - }; - - private _mouseUp = () => { - this._mouseIsDown = false; - if (this._scrolled) { - this._mouseReleasedAt = new Date().getTime(); - } - window.removeEventListener("mousemove", this._mouseMove); - }; - - private _mouseMove = (event: MouseEvent) => { - if (!this._mouseIsDown) { - return; - } - - const scrollContainer = this._scrollContainer; - - if (!scrollContainer) { - return; - } - - const x = event.pageX - scrollContainer.offsetLeft; - const scroll = x - this._scrollStartX; - - if (!this._scrolled) { - this._scrolled = Math.abs(scroll) > 1; - } - - scrollContainer.scrollLeft = this._scrollLeft - scroll; - }; - static override styles = [ TabGroupStyles, css` diff --git a/src/dialogs/more-info/controls/more-info-weather.ts b/src/dialogs/more-info/controls/more-info-weather.ts index 2ccd6c22d3..7fcffd0f3a 100644 --- a/src/dialogs/more-info/controls/more-info-weather.ts +++ b/src/dialogs/more-info/controls/more-info-weather.ts @@ -29,6 +29,7 @@ import { import type { HomeAssistant } from "../../../types"; import "../../../components/ha-relative-time"; import "../../../components/ha-state-icon"; +import { DragScrollController } from "../../../common/controllers/drag-scroll-controller"; @customElement("more-info-weather") class MoreInfoWeather extends LitElement { @@ -42,6 +43,11 @@ class MoreInfoWeather extends LitElement { @state() private _subscribed?: Promise<() => void>; + // @ts-ignore + private _dragScrollController = new DragScrollController(this, { + selector: ".forecast", + }); + private _unsubscribeForecastEvents() { if (this._subscribed) { this._subscribed.then((unsub) => unsub()); @@ -547,6 +553,7 @@ class MoreInfoWeather extends LitElement { black 94%, transparent 100% ); + user-select: none; } .forecast > div { diff --git a/src/panels/lovelace/views/hui-view-header.ts b/src/panels/lovelace/views/hui-view-header.ts index e4fd00c847..b4ef30037a 100644 --- a/src/panels/lovelace/views/hui-view-header.ts +++ b/src/panels/lovelace/views/hui-view-header.ts @@ -20,6 +20,7 @@ import { replaceView } from "../editor/config-util"; import { showEditViewHeaderDialog } from "../editor/view-header/show-edit-view-header-dialog"; import type { Lovelace } from "../types"; import { showEditCardDialog } from "../editor/card-editor/show-edit-card-dialog"; +import { DragScrollController } from "../../../common/controllers/drag-scroll-controller"; export const DEFAULT_VIEW_HEADER_LAYOUT = "center"; export const DEFAULT_VIEW_HEADER_BADGES_POSITION = "bottom"; @@ -51,6 +52,10 @@ export class HuiViewHeader extends LitElement { this._checkHidden(); }; + private _dragScrollController = new DragScrollController(this, { + selector: ".scroll", + }); + connectedCallback(): void { super.connectedCallback(); this.addEventListener( @@ -252,7 +257,12 @@ export class HuiViewHeader extends LitElement { : nothing} ${this.lovelace && (editMode || this.badges.length > 0) ? html` -
+