Compare commits

...
Author SHA1 Message Date
Bram Kragten d9732504b5 Merge remote-tracking branch 'origin/dev' into claude/modest-visvesvaraya-b0ff5e 2026-10-20 14:29:40 +02:00
Bram KragtenandClaude Opus 5.5 420bdf75da Stop the snap bottom sheet from blocking page scrolling
The sheet kept a non-passive touchmove listener, plus the other drag
listeners, on the document for as long as it existed, so every touch
scroll on the page had to wait for the main thread. It is always mounted
on the phone map dashboard and in the narrow automation editor sidebar.

Listen for touchmove, touchend and touchcancel on the sheet itself, next
to touchstart. A touch keeps sending its events to where it started, so
a drag that leaves the sheet still reaches it, and only the sheet's own
area, which its touchstart listener already covered, blocks scrolling.
Attaching document listeners at touchstart instead would be too late for
WebKit, which decides whether touchmoves can block when the touch starts.

A touch whose target leaves the DOM no longer reaches the sheet's
touchend, so each new touch clears what the previous one left behind.

Mouse drags still need the document; those listeners are now added on
mousedown and removed on mouseup or disconnect.

SortableJS keeps a non-passive document touchmove listener of its own
once loaded, as in the automation editor, so touch scrolling there still
waits on that one.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-20 14:29:33 +02:00
+17 -12
View File
@@ -145,6 +145,9 @@ export class HaSnapBottomSheet extends LitElement {
: undefined,
})}
@touchstart=${this._handleTouchStart}
@touchmove=${this._handleTouchMove}
@touchend=${this._handleTouchEnd}
@touchcancel=${this._handleTouchEnd}
>
<div class="handle-wrapper">
<div @mousedown=${this._handleMouseDown} class="handle"></div>
@@ -183,13 +186,6 @@ export class HaSnapBottomSheet extends LitElement {
this._reported = undefined;
this._reportWhenSettled();
}
document.addEventListener("mousemove", this._handleMouseMove);
document.addEventListener("mouseup", this._handleMouseUp);
document.addEventListener("touchmove", this._handleTouchMove, {
passive: false,
});
document.addEventListener("touchend", this._handleTouchEnd);
document.addEventListener("touchcancel", this._handleTouchEnd);
window.addEventListener("resize", this._handleViewportResize);
}
@@ -202,11 +198,7 @@ export class HaSnapBottomSheet extends LitElement {
this._pendingTouch = undefined;
this._dragHeight = undefined;
this._dismissOffset = undefined;
document.removeEventListener("mousemove", this._handleMouseMove);
document.removeEventListener("mouseup", this._handleMouseUp);
document.removeEventListener("touchmove", this._handleTouchMove);
document.removeEventListener("touchend", this._handleTouchEnd);
document.removeEventListener("touchcancel", this._handleTouchEnd);
this._removeMouseListeners();
window.removeEventListener("resize", this._handleViewportResize);
}
@@ -258,6 +250,8 @@ export class HaSnapBottomSheet extends LitElement {
private _handleMouseDown = (ev: MouseEvent) => {
document.body.style.setProperty("cursor", "grabbing");
document.addEventListener("mousemove", this._handleMouseMove);
document.addEventListener("mouseup", this._handleMouseUp);
this._startDrag(ev.clientY);
};
@@ -268,13 +262,22 @@ export class HaSnapBottomSheet extends LitElement {
};
private _handleMouseUp = () => {
this._removeMouseListeners();
this._endDrag();
};
private _removeMouseListeners() {
document.removeEventListener("mousemove", this._handleMouseMove);
document.removeEventListener("mouseup", this._handleMouseUp);
}
private _handleTouchStart = (ev: TouchEvent) => {
if (ev.touches.length !== 1) {
return;
}
// A touch whose target left the DOM never ended here
this._pendingTouch = undefined;
this._endDrag();
const touch = ev.touches[0];
const path = ev.composedPath();
if (path.includes(this._handle)) {
@@ -300,6 +303,8 @@ export class HaSnapBottomSheet extends LitElement {
this._pendingTouch = { x: touch.clientX, y: touch.clientY };
};
// On the sheet rather than the document, so touches elsewhere scroll without
// waiting for it; a touch's events keep going to where it started
private _handleTouchMove = (ev: TouchEvent) => {
const touch = ev.touches[0];
if (this._pendingTouch) {