mirror of
https://github.com/home-assistant/frontend.git
synced 2026-10-12 22:58:13 +00:00
Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f7df83600e | ||
|
|
1044476c25 | ||
|
|
f51ae10b69 | ||
|
|
c754b69ed7 |
No files matched your search
@@ -1,393 +0,0 @@
|
||||
import type { PropertyValues } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { BOTTOM_SHEET_ANIMATION_DURATION_MS } from "./ha-bottom-sheet";
|
||||
|
||||
/**
|
||||
* A bottom sheet component that slides up from the bottom of the screen.
|
||||
*
|
||||
* The bottom sheet provides a draggable interface that allows users to resize
|
||||
* the sheet by dragging the handle at the top. It supports both mouse and touch
|
||||
* interactions and automatically closes when dragged below a 20% of screen height.
|
||||
*
|
||||
* A persistent sheet never closes by dragging; it stops at its minimum height
|
||||
* instead, so a host can keep a collapsed strip of content reachable.
|
||||
*
|
||||
* @fires bottom-sheet-closed - Fired when the bottom sheet is closed
|
||||
* @fires bottom-sheet-resized - Fired with the sheet's height in pixels once
|
||||
* it has opened and whenever a drag ends
|
||||
*
|
||||
* @cssprop --ha-bottom-sheet-border-width - Border width for the sheet
|
||||
* @cssprop --ha-bottom-sheet-border-style - Border style for the sheet
|
||||
* @cssprop --ha-bottom-sheet-border-color - Border color for the sheet
|
||||
* @cssprop --ha-bottom-sheet-handle-padding - How far below the handle the
|
||||
* grab area reaches; shrink it when content sits right under the handle
|
||||
*/
|
||||
@customElement("ha-resizable-bottom-sheet")
|
||||
export class HaResizableBottomSheet extends LitElement {
|
||||
@query("dialog") private _dialog!: HTMLDialogElement;
|
||||
|
||||
/** Dragging down stops at the minimum height instead of closing the sheet */
|
||||
@property({ type: Boolean }) public persistent = false;
|
||||
|
||||
/**
|
||||
* The height in pixels the sheet cannot be dragged below, e.g. what a host
|
||||
* measures for the strip it wants to keep visible. Without it the sheet
|
||||
* stops at 20% of the viewport.
|
||||
*/
|
||||
@property({ type: Number, attribute: "min-height" })
|
||||
public minHeight?: number;
|
||||
|
||||
/**
|
||||
* The largest share of the viewport the sheet opens at, in percent. It
|
||||
* opens at its content height up to this, and can be dragged to 90 after.
|
||||
*/
|
||||
@property({ type: Number, attribute: "open-max-viewport-height" })
|
||||
public openMaxViewportHeight = 70;
|
||||
|
||||
/**
|
||||
* Whether the sheet may open smaller than 55% of the viewport, down to its
|
||||
* content height and the minimum it can be dragged to.
|
||||
*/
|
||||
@property({ type: Boolean, attribute: "open-at-content-height" })
|
||||
public openAtContentHeight = false;
|
||||
|
||||
private _dragging = false;
|
||||
|
||||
private _dragStartY = 0;
|
||||
|
||||
private _initialSize = 0;
|
||||
|
||||
private _opened = false;
|
||||
|
||||
@state() private _dialogMaxViewpointHeight?: number;
|
||||
|
||||
@state() private _dialogMinViewpointHeight?: number;
|
||||
|
||||
@state() private _dialogViewportHeight?: number;
|
||||
|
||||
render() {
|
||||
// Until it has opened, the sheet sizes to its content within the opening
|
||||
// bounds; afterwards it keeps the height it settled on or was dragged to
|
||||
const maxHeight =
|
||||
this._dialogMaxViewpointHeight ?? this.openMaxViewportHeight;
|
||||
const minHeight =
|
||||
this._dialogMinViewpointHeight ??
|
||||
(this.openAtContentHeight ? this._minViewportHeight() : 55);
|
||||
return html`<dialog
|
||||
open
|
||||
@transitionend=${this._handleTransitionEnd}
|
||||
style=${`
|
||||
--height: ${this._dialogViewportHeight}vh;
|
||||
--height: ${this._dialogViewportHeight}dvh;
|
||||
--max-height: ${maxHeight}vh;
|
||||
--max-height: ${maxHeight}dvh;
|
||||
--min-height: ${minHeight}vh;
|
||||
--min-height: ${minHeight}dvh;
|
||||
`}
|
||||
>
|
||||
<div class="handle-wrapper">
|
||||
<div
|
||||
@mousedown=${this._handleMouseDown}
|
||||
@touchstart=${this._handleTouchStart}
|
||||
class="handle"
|
||||
></div>
|
||||
</div>
|
||||
<slot></slot>
|
||||
</dialog>`;
|
||||
}
|
||||
|
||||
protected firstUpdated(changedProperties: PropertyValues<this>) {
|
||||
super.firstUpdated(changedProperties);
|
||||
this._openSheet();
|
||||
}
|
||||
|
||||
private _openSheet() {
|
||||
requestAnimationFrame(() => {
|
||||
// trigger opening animation
|
||||
this._dialog.classList.add("show");
|
||||
});
|
||||
}
|
||||
|
||||
public closeSheet() {
|
||||
requestAnimationFrame(() => {
|
||||
this._dialog.classList.remove("show");
|
||||
});
|
||||
}
|
||||
|
||||
private _handleTransitionEnd() {
|
||||
if (this._dialog.classList.contains("show")) {
|
||||
// after show animation is done
|
||||
// - set the height to the natural height, to prevent content shift when switch content
|
||||
// - set max height to 90vh, so it opens at max 70vh but can be resized to 90vh
|
||||
this._dialogViewportHeight =
|
||||
(this._dialog.offsetHeight / window.innerHeight) * 100;
|
||||
this._dialogMaxViewpointHeight = 90;
|
||||
this._dialogMinViewpointHeight = this._minViewportHeight();
|
||||
this._opened = true;
|
||||
this._fireResized();
|
||||
} else {
|
||||
// after close animation is done close dialog element and fire closed event
|
||||
this._dialog.close();
|
||||
fireEvent(this, "bottom-sheet-closed");
|
||||
}
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
|
||||
// register event listeners for drag handling
|
||||
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);
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
|
||||
// unregister event listeners for drag handling
|
||||
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);
|
||||
window.removeEventListener("resize", this._handleViewportResize);
|
||||
}
|
||||
|
||||
// minHeight is a pixel value, so its viewport-relative minimum depends on the
|
||||
// window height; recompute it when the viewport changes (for example on
|
||||
// rotation) so the cached percentage cannot clip the persistent peek content.
|
||||
private _handleViewportResize = () => {
|
||||
this._applyMinViewportHeight();
|
||||
};
|
||||
|
||||
private _handleMouseDown = (ev: MouseEvent) => {
|
||||
this._startDrag(ev.clientY);
|
||||
};
|
||||
|
||||
private _handleTouchStart = (ev: TouchEvent) => {
|
||||
// Prevent the browser from interpreting this as a scroll/PTR gesture.
|
||||
ev.preventDefault();
|
||||
this._startDrag(ev.touches[0].clientY);
|
||||
};
|
||||
|
||||
private _startDrag(clientY: number) {
|
||||
this._dragging = true;
|
||||
this._dragStartY = clientY;
|
||||
this._initialSize = (this._dialog.offsetHeight / window.innerHeight) * 100;
|
||||
document.body.style.setProperty("cursor", "grabbing");
|
||||
}
|
||||
|
||||
private _handleMouseMove = (ev: MouseEvent) => {
|
||||
if (!this._dragging) {
|
||||
return;
|
||||
}
|
||||
this._updateSize(ev.clientY);
|
||||
};
|
||||
|
||||
private _handleTouchMove = (ev: TouchEvent) => {
|
||||
if (!this._dragging) {
|
||||
return;
|
||||
}
|
||||
ev.preventDefault(); // Prevent scrolling
|
||||
this._updateSize(ev.touches[0].clientY);
|
||||
};
|
||||
|
||||
private _updateSize(clientY: number) {
|
||||
const deltaY = this._dragStartY - clientY;
|
||||
const viewportHeight = window.innerHeight;
|
||||
const deltaVh = (deltaY / viewportHeight) * 100;
|
||||
|
||||
// Calculate new size and clamp between 10vh and 90vh
|
||||
let newSize = this._initialSize + deltaVh;
|
||||
newSize = Math.max(10, Math.min(90, newSize));
|
||||
|
||||
if (this.persistent) {
|
||||
newSize = Math.max(this._minViewportHeight(), newSize);
|
||||
} else if (newSize < 20 && deltaY < 0) {
|
||||
// on drag down and below 20vh
|
||||
this._endDrag();
|
||||
this.closeSheet();
|
||||
return;
|
||||
}
|
||||
|
||||
this._dialogViewportHeight = newSize;
|
||||
}
|
||||
|
||||
private _minViewportHeight(): number {
|
||||
return this.minHeight === undefined
|
||||
? 20
|
||||
: (this.minHeight / window.innerHeight) * 100;
|
||||
}
|
||||
|
||||
protected updated(changedProperties: PropertyValues<this>) {
|
||||
super.updated(changedProperties);
|
||||
// A minimum set after opening applies right away
|
||||
if (changedProperties.has("minHeight") && this._opened) {
|
||||
this._applyMinViewportHeight();
|
||||
}
|
||||
}
|
||||
|
||||
// Applies the current pixel minimum as a viewport percentage and, once it has
|
||||
// rendered, reports the size if it changed: raising the minimum can grow a
|
||||
// sheet resting at the old one, and the host needs the new height to keep its
|
||||
// overlay padding in step.
|
||||
private _applyMinViewportHeight() {
|
||||
if (!this._opened) {
|
||||
return;
|
||||
}
|
||||
this._dialogMinViewpointHeight = this._minViewportHeight();
|
||||
this.updateComplete.then(() => {
|
||||
if (this._dialog.offsetHeight !== this._lastReportedHeight) {
|
||||
this._fireResized();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private _lastReportedHeight?: number;
|
||||
|
||||
private _fireResized() {
|
||||
this._lastReportedHeight = this._dialog.offsetHeight;
|
||||
fireEvent(this, "bottom-sheet-resized", {
|
||||
height: this._lastReportedHeight,
|
||||
});
|
||||
}
|
||||
|
||||
private _handleMouseUp = () => {
|
||||
this._endDrag();
|
||||
};
|
||||
|
||||
private _handleTouchEnd = () => {
|
||||
this._endDrag();
|
||||
};
|
||||
|
||||
private _endDrag() {
|
||||
if (!this._dragging) {
|
||||
return;
|
||||
}
|
||||
this._dragging = false;
|
||||
document.body.style.removeProperty("cursor");
|
||||
// Hosts lay out around the sheet once, not on every move
|
||||
this.updateComplete.then(() => this._fireResized());
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
.handle-wrapper {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
padding-bottom: 2px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
cursor: grab;
|
||||
touch-action: none;
|
||||
}
|
||||
.handle-wrapper .handle {
|
||||
height: 20px;
|
||||
width: 200px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 7;
|
||||
/* Extends the grab area over the content below the handle */
|
||||
padding-bottom: var(--ha-bottom-sheet-handle-padding, 76px);
|
||||
}
|
||||
.handle-wrapper .handle::after {
|
||||
content: "";
|
||||
border-radius: var(--ha-border-radius-md);
|
||||
height: 4px;
|
||||
background: var(--divider-color, #e0e0e0);
|
||||
width: 80px;
|
||||
}
|
||||
.handle-wrapper .handle:active::after {
|
||||
cursor: grabbing;
|
||||
}
|
||||
dialog {
|
||||
height: var(--height, auto);
|
||||
max-height: min(
|
||||
var(--max-height, 70vh),
|
||||
calc(100vh - var(--safe-area-inset-top))
|
||||
);
|
||||
max-height: min(
|
||||
var(--max-height, 70dvh),
|
||||
calc(100dvh - var(--safe-area-inset-top))
|
||||
);
|
||||
min-height: var(--min-height, 30vh);
|
||||
min-height: var(--min-height, 30dvh);
|
||||
background-color: var(
|
||||
--ha-bottom-sheet-surface-background,
|
||||
var(--ha-color-surface-default)
|
||||
);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
top: 0;
|
||||
inset-inline-start: 0;
|
||||
position: fixed;
|
||||
--sheet-inset-left: var(
|
||||
--ha-bottom-sheet-inset-left,
|
||||
var(--safe-area-inset-left)
|
||||
);
|
||||
--sheet-inset-right: var(
|
||||
--ha-bottom-sheet-inset-right,
|
||||
var(--safe-area-inset-right)
|
||||
);
|
||||
--sheet-border-width: var(--ha-bottom-sheet-border-width, 0px);
|
||||
--sheet-side-borders: calc(2 * var(--sheet-border-width));
|
||||
width: calc(
|
||||
100% - var(--sheet-side-borders) - var(--sheet-inset-left) - var(
|
||||
--sheet-inset-right
|
||||
)
|
||||
);
|
||||
max-width: 100%;
|
||||
border: none;
|
||||
box-shadow: var(--wa-shadow-l);
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
top: auto;
|
||||
inset-inline-end: auto;
|
||||
bottom: 0;
|
||||
inset-inline-start: 0;
|
||||
box-shadow: 0px -8px 16px rgba(0, 0, 0, 0.2);
|
||||
border-top-left-radius: var(
|
||||
--ha-bottom-sheet-border-radius,
|
||||
var(--ha-dialog-border-radius, var(--ha-border-radius-2xl))
|
||||
);
|
||||
border-top-right-radius: var(
|
||||
--ha-bottom-sheet-border-radius,
|
||||
var(--ha-dialog-border-radius, var(--ha-border-radius-2xl))
|
||||
);
|
||||
transform: translateY(100%);
|
||||
transition: transform ${BOTTOM_SHEET_ANIMATION_DURATION_MS}ms ease;
|
||||
border-top-width: var(--sheet-border-width);
|
||||
border-right-width: var(--sheet-border-width);
|
||||
border-left-width: var(--sheet-border-width);
|
||||
border-bottom-width: 0;
|
||||
border-style: var(--ha-bottom-sheet-border-style);
|
||||
border-color: var(--ha-bottom-sheet-border-color);
|
||||
margin-left: var(--sheet-inset-left);
|
||||
margin-right: var(--sheet-inset-right);
|
||||
}
|
||||
|
||||
dialog.show {
|
||||
transform: translateY(0);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-resizable-bottom-sheet": HaResizableBottomSheet;
|
||||
}
|
||||
|
||||
interface HASSDomEvents {
|
||||
"bottom-sheet-closed": undefined;
|
||||
"bottom-sheet-resized": { height: number };
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,9 @@ const HALF_VIEWPORT_SHARE = 0.5;
|
||||
// How far a touch moves before its direction decides what it drags
|
||||
const TOUCH_SLOP_PX = 4;
|
||||
|
||||
// How much of the lowest stop a drag hides before it closes the sheet
|
||||
const DISMISS_DRAG_SHARE = 0.75;
|
||||
|
||||
interface Stop {
|
||||
position: BottomSheetPosition;
|
||||
height: number;
|
||||
@@ -31,9 +34,11 @@ interface Stop {
|
||||
* them when a drag ends, or to the next one in the direction of a quick swipe.
|
||||
* The handle drags with a mouse or a finger. On touch the whole sheet drags
|
||||
* too, until it is at full height: then its content scrolls, and pulling down
|
||||
* from the top brings the sheet down again. Dragging down never closes it; it
|
||||
* collapses to `minHeight` so a host keeps a strip of content reachable, and
|
||||
* only `closeSheet` closes it.
|
||||
* from the top brings the sheet down again. Dragging down collapses it to
|
||||
* `minHeight` so a host keeps a strip of content reachable. Only `closeSheet`
|
||||
* closes it, unless it is `dismissable`: then a drag that pulls it below its
|
||||
* lowest stop closes it with a downward swipe, or once it is almost out of the
|
||||
* screen. A quick swipe that ends above the lowest stop still stops there.
|
||||
*
|
||||
* @fires bottom-sheet-closed - Fired when the bottom sheet is closed
|
||||
* @fires bottom-sheet-resized - Fired with the sheet's height in pixels
|
||||
@@ -61,12 +66,17 @@ export class HaSnapBottomSheet extends LitElement {
|
||||
/** Names the sheet for assistive technologies */
|
||||
@property() public label?: string;
|
||||
|
||||
@property({ type: Boolean }) public dismissable = false;
|
||||
|
||||
@state() private _shown = false;
|
||||
|
||||
@state() private _position: BottomSheetPosition = "half";
|
||||
|
||||
@state() private _dragHeight?: number;
|
||||
|
||||
// How far below its lowest stop a drag has pulled the sheet
|
||||
@state() private _dismissOffset?: number;
|
||||
|
||||
private _gesture = new SwipeGestureRecognizer();
|
||||
|
||||
private _dragging = false;
|
||||
@@ -118,13 +128,21 @@ export class HaSnapBottomSheet extends LitElement {
|
||||
@click=${this._handleBackdropClick}
|
||||
></div>
|
||||
<div
|
||||
class="sheet ${classMap({ show: this._shown, full, dragging })}"
|
||||
class="sheet ${classMap({
|
||||
show: this._shown,
|
||||
full,
|
||||
dragging,
|
||||
dismissing: Boolean(this._dismissOffset),
|
||||
})}"
|
||||
popover=${popover}
|
||||
role="region"
|
||||
aria-label=${ifDefined(this.label)}
|
||||
style=${styleMap({
|
||||
height: this._height(),
|
||||
"min-height": this._collapsedHeight(),
|
||||
transform: this._dismissOffset
|
||||
? `translateY(${this._dismissOffset}px)`
|
||||
: undefined,
|
||||
})}
|
||||
@touchstart=${this._handleTouchStart}
|
||||
>
|
||||
@@ -183,6 +201,7 @@ export class HaSnapBottomSheet extends LitElement {
|
||||
this._dragging = false;
|
||||
this._pendingTouch = undefined;
|
||||
this._dragHeight = undefined;
|
||||
this._dismissOffset = undefined;
|
||||
document.removeEventListener("mousemove", this._handleMouseMove);
|
||||
document.removeEventListener("mouseup", this._handleMouseUp);
|
||||
document.removeEventListener("touchmove", this._handleTouchMove);
|
||||
@@ -317,15 +336,20 @@ export class HaSnapBottomSheet extends LitElement {
|
||||
private _startDrag(clientY: number) {
|
||||
this._dragging = true;
|
||||
this._dragStartHeight = this._sheet.offsetHeight;
|
||||
if (this.dismissable) {
|
||||
this._dismissOffset = 0;
|
||||
}
|
||||
this._gesture.start(clientY);
|
||||
}
|
||||
|
||||
private _moveDrag(clientY: number) {
|
||||
const delta = this._gesture.move(clientY);
|
||||
this._dragHeight = Math.max(
|
||||
this._stops()[0].height,
|
||||
this._dragStartHeight + delta
|
||||
);
|
||||
const lowestHeight = this._stops()[0].height;
|
||||
const height = this._dragStartHeight + delta;
|
||||
this._dragHeight = Math.max(lowestHeight, height);
|
||||
if (this._dismissOffset !== undefined) {
|
||||
this._dismissOffset = Math.max(0, lowestHeight - height);
|
||||
}
|
||||
}
|
||||
|
||||
private _endDrag() {
|
||||
@@ -335,6 +359,18 @@ export class HaSnapBottomSheet extends LitElement {
|
||||
this._dragging = false;
|
||||
document.body.style.removeProperty("cursor");
|
||||
const { isSwipe, isDownwardSwipe } = this._gesture.end();
|
||||
const dismissOffset = this._dismissOffset;
|
||||
this._dismissOffset = undefined;
|
||||
if (
|
||||
dismissOffset &&
|
||||
((isSwipe && isDownwardSwipe) ||
|
||||
dismissOffset > this._stops()[0].height * DISMISS_DRAG_SHARE)
|
||||
) {
|
||||
this._position = this._stops()[0].position;
|
||||
this._dragHeight = undefined;
|
||||
this.closeSheet();
|
||||
return;
|
||||
}
|
||||
this._position = this._stopAfterDrag(
|
||||
this._sheet.offsetHeight,
|
||||
isSwipe ? (isDownwardSwipe ? "down" : "up") : undefined
|
||||
@@ -556,6 +592,10 @@ export class HaSnapBottomSheet extends LitElement {
|
||||
.sheet.show {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.sheet.dismissing {
|
||||
transition: none;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
@@ -115,6 +115,7 @@ import "./types/ha-automation-action-stop";
|
||||
import "./types/ha-automation-action-wait_for_trigger";
|
||||
import "./types/ha-automation-action-wait_template";
|
||||
import { renderCtrlOrCmd } from "../../../../common/keyboard/ctrl-or-cmd";
|
||||
import { scrollRowIntoView } from "../scroll-row-into-view";
|
||||
|
||||
export interface ActionElement extends LitElement {
|
||||
action: Action;
|
||||
@@ -1233,10 +1234,7 @@ export default class HaAutomationActionRow extends LitElement {
|
||||
|
||||
if (this.narrow) {
|
||||
window.setTimeout(() => {
|
||||
this.scrollIntoView({
|
||||
block: "start",
|
||||
behavior: "smooth",
|
||||
});
|
||||
scrollRowIntoView(this);
|
||||
}, 180); // duration of transition of added padding for bottom sheet
|
||||
}
|
||||
}
|
||||
|
||||
@@ -189,12 +189,6 @@ export default class HaAutomationAction extends AutomationSortableListMixin<Acti
|
||||
(!ACTION_BUILDING_BLOCKS.includes(type) || mode === "moved")
|
||||
) {
|
||||
row.openSidebar();
|
||||
if (this.narrow) {
|
||||
row.scrollIntoView({
|
||||
block: "start",
|
||||
behavior: "smooth",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (mode === "new") {
|
||||
|
||||
@@ -86,6 +86,7 @@ import "./types/ha-automation-condition-time";
|
||||
import "./types/ha-automation-condition-trigger";
|
||||
import "./types/ha-automation-condition-zone";
|
||||
import { renderCtrlOrCmd } from "../../../../common/keyboard/ctrl-or-cmd";
|
||||
import { scrollRowIntoView } from "../scroll-row-into-view";
|
||||
|
||||
export interface ConditionElement extends LitElement {
|
||||
condition: Condition;
|
||||
@@ -935,10 +936,7 @@ export default class HaAutomationConditionRow extends LitElement {
|
||||
|
||||
if (this.narrow) {
|
||||
window.setTimeout(() => {
|
||||
this.scrollIntoView({
|
||||
block: "start",
|
||||
behavior: "smooth",
|
||||
});
|
||||
scrollRowIntoView(this);
|
||||
}, 180); // duration of transition of added padding for bottom sheet
|
||||
}
|
||||
}
|
||||
|
||||
@@ -141,12 +141,6 @@ export default class HaAutomationCondition extends AutomationSortableListMixin<C
|
||||
mode === "moved")
|
||||
) {
|
||||
row.openSidebar();
|
||||
if (this.narrow) {
|
||||
row.scrollIntoView({
|
||||
block: "start",
|
||||
behavior: "smooth",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (mode === "new") {
|
||||
|
||||
@@ -4,8 +4,8 @@ import { tinykeys } from "tinykeys";
|
||||
import { consume } from "../../../common/decorators/consume";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import { computeRTL } from "../../../common/util/compute_rtl";
|
||||
import "../../../components/ha-resizable-bottom-sheet";
|
||||
import type { HaResizableBottomSheet } from "../../../components/ha-resizable-bottom-sheet";
|
||||
import "../../../components/ha-snap-bottom-sheet";
|
||||
import type { HaSnapBottomSheet } from "../../../components/ha-snap-bottom-sheet";
|
||||
import {
|
||||
isCondition,
|
||||
isScriptField,
|
||||
@@ -27,6 +27,9 @@ import "./sidebar/ha-automation-sidebar-script-field";
|
||||
import "./sidebar/ha-automation-sidebar-script-field-selector";
|
||||
import "./sidebar/ha-automation-sidebar-trigger";
|
||||
|
||||
// The sidebar card header: the dialog header's 48px minimum plus its padding
|
||||
const COLLAPSED_SHEET_HEIGHT = 68;
|
||||
|
||||
@customElement("ha-automation-sidebar")
|
||||
export default class HaAutomationSidebar extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
@@ -50,8 +53,8 @@ export default class HaAutomationSidebar extends LitElement {
|
||||
@consume({ context: manifestsContext, subscribe: true })
|
||||
private _manifests?: DomainManifestLookup;
|
||||
|
||||
@query("ha-resizable-bottom-sheet")
|
||||
private _bottomSheetElement?: HaResizableBottomSheet;
|
||||
@query("ha-snap-bottom-sheet")
|
||||
private _bottomSheetElement?: HaSnapBottomSheet;
|
||||
|
||||
@query(".handle")
|
||||
private _handleElement?: HTMLDivElement;
|
||||
@@ -62,6 +65,12 @@ export default class HaAutomationSidebar extends LitElement {
|
||||
|
||||
protected updated(changedProperties: PropertyValues<this>) {
|
||||
super.updated(changedProperties);
|
||||
if (
|
||||
changedProperties.has("sidebarKey") &&
|
||||
this._bottomSheetElement?.position === "collapsed"
|
||||
) {
|
||||
this._bottomSheetElement.snapTo("half");
|
||||
}
|
||||
if (changedProperties.has("config") || changedProperties.has("narrow")) {
|
||||
if (!this.config || this.narrow) {
|
||||
this._tinykeysUnsub?.();
|
||||
@@ -185,9 +194,16 @@ export default class HaAutomationSidebar extends LitElement {
|
||||
|
||||
if (this.narrow) {
|
||||
return html`
|
||||
<ha-resizable-bottom-sheet @bottom-sheet-closed=${this._closeSidebar}>
|
||||
<ha-snap-bottom-sheet
|
||||
dismissable
|
||||
.minHeight=${COLLAPSED_SHEET_HEIGHT}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.sidebar_label"
|
||||
)}
|
||||
@bottom-sheet-closed=${this._closeSidebar}
|
||||
>
|
||||
${this._renderContent()}
|
||||
</ha-resizable-bottom-sheet>
|
||||
</ha-snap-bottom-sheet>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -389,9 +405,6 @@ export default class HaAutomationSidebar extends LitElement {
|
||||
var(--ha-border-radius-2xl)
|
||||
);
|
||||
border-radius: var(--ha-card-border-radius);
|
||||
--ha-bottom-sheet-border-width: 2px;
|
||||
--ha-bottom-sheet-border-style: solid;
|
||||
--ha-bottom-sheet-border-color: var(--primary-color);
|
||||
margin-top: var(--safe-area-inset-top);
|
||||
|
||||
--ha-bottom-sheet-surface-background: var(--card-background-color);
|
||||
|
||||
@@ -8,8 +8,10 @@ import {
|
||||
} from "lit";
|
||||
import { property, query, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import { consume } from "../../../common/decorators/consume";
|
||||
import { storage } from "../../../common/decorators/storage";
|
||||
import type { HASSDomEvent } from "../../../common/dom/fire_event";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import { replaceCurrentUrl } from "../../../common/navigate";
|
||||
import { constructUrlCurrentPath } from "../../../common/url/construct-url";
|
||||
@@ -68,6 +70,8 @@ export const ManualEditorMixin = <TConfig>(
|
||||
|
||||
@state() protected sidebarKey = 0;
|
||||
|
||||
@state() private _bottomSheetHeight?: number;
|
||||
|
||||
@storage({
|
||||
key: "automation-sidebar-width",
|
||||
state: false,
|
||||
@@ -116,12 +120,18 @@ export const ManualEditorMixin = <TConfig>(
|
||||
"has-sidebar": this.sidebarConfig && !this.narrow,
|
||||
})}
|
||||
>
|
||||
<div class="content-wrapper">
|
||||
<div
|
||||
class="content ${
|
||||
this.sidebarConfig && this.narrow ? "has-bottom-sheet" : ""
|
||||
}"
|
||||
>
|
||||
<div
|
||||
class="content-wrapper ${
|
||||
this.sidebarConfig && this.narrow ? "has-bottom-sheet" : ""
|
||||
}"
|
||||
style=${styleMap({
|
||||
"--automation-bottom-sheet-settled-height":
|
||||
this._bottomSheetHeight === undefined
|
||||
? undefined
|
||||
: `${this._bottomSheetHeight}px`,
|
||||
})}
|
||||
>
|
||||
<div class="content">
|
||||
<slot name="alerts"></slot>
|
||||
${this.renderContent()}
|
||||
</div>
|
||||
@@ -152,6 +162,7 @@ export const ManualEditorMixin = <TConfig>(
|
||||
@sidebar-resized=${this.resizeSidebar}
|
||||
@sidebar-resizing-stopped=${this.stopResizeSidebar}
|
||||
@sidebar-reset-size=${this.resetSidebarWidth}
|
||||
@bottom-sheet-resized=${this._handleBottomSheetResized}
|
||||
></ha-automation-sidebar>
|
||||
</div>
|
||||
</div>
|
||||
@@ -181,6 +192,8 @@ export const ManualEditorMixin = <TConfig>(
|
||||
// deselect previous selected row
|
||||
this.sidebarConfig?.close?.();
|
||||
this.sidebarConfig = ev.detail;
|
||||
// The sheet opens items at half height, so make room before rows scroll
|
||||
this._bottomSheetHeight = undefined;
|
||||
this.dispatchEvent(
|
||||
new CustomEvent("sidebar-config-changed", {
|
||||
detail: { value: this.sidebarConfig },
|
||||
@@ -226,8 +239,15 @@ export const ManualEditorMixin = <TConfig>(
|
||||
}
|
||||
}
|
||||
|
||||
private _handleBottomSheetResized(
|
||||
ev: HASSDomEvent<HASSDomEvents["bottom-sheet-resized"]>
|
||||
) {
|
||||
this._bottomSheetHeight = ev.detail.height;
|
||||
}
|
||||
|
||||
protected handleCloseSidebar() {
|
||||
this.sidebarConfig = undefined;
|
||||
this._bottomSheetHeight = undefined;
|
||||
this.dispatchEvent(
|
||||
new CustomEvent("sidebar-config-changed", {
|
||||
detail: { value: undefined },
|
||||
|
||||
@@ -53,6 +53,7 @@ import {
|
||||
rowStyles,
|
||||
} from "../styles";
|
||||
import { renderCtrlOrCmd } from "../../../../common/keyboard/ctrl-or-cmd";
|
||||
import { scrollRowIntoView } from "../scroll-row-into-view";
|
||||
|
||||
@customElement("ha-automation-option-row")
|
||||
export default class HaAutomationOptionRow extends LitElement {
|
||||
@@ -590,10 +591,7 @@ export default class HaAutomationOptionRow extends LitElement {
|
||||
|
||||
if (this.narrow) {
|
||||
window.setTimeout(() => {
|
||||
this.scrollIntoView({
|
||||
block: "start",
|
||||
behavior: "smooth",
|
||||
});
|
||||
scrollRowIntoView(this);
|
||||
}, 180); // duration of transition of added padding for bottom sheet
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import { AutomationSortableListMixin } from "../ha-automation-sortable-list-mixi
|
||||
import { automationRowsStyles } from "../styles";
|
||||
import "./ha-automation-option-row";
|
||||
import type HaAutomationOptionRow from "./ha-automation-option-row";
|
||||
import { scrollRowIntoView } from "../scroll-row-into-view";
|
||||
|
||||
@customElement("ha-automation-option")
|
||||
export default class HaAutomationOption extends AutomationSortableListMixin<Option>(
|
||||
@@ -141,13 +142,6 @@ export default class HaAutomationOption extends AutomationSortableListMixin<Opti
|
||||
this.focusItemIndexOnChange = undefined;
|
||||
|
||||
row.updateComplete.then(() => {
|
||||
if (this.narrow) {
|
||||
row.scrollIntoView({
|
||||
block: "start",
|
||||
behavior: "smooth",
|
||||
});
|
||||
}
|
||||
|
||||
if (mode === "new") {
|
||||
row.expand();
|
||||
}
|
||||
@@ -155,6 +149,9 @@ export default class HaAutomationOption extends AutomationSortableListMixin<Opti
|
||||
if (this.optionsInSidebar) {
|
||||
row.openSidebar();
|
||||
} else {
|
||||
if (this.narrow) {
|
||||
scrollRowIntoView(row);
|
||||
}
|
||||
row.focus();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
const parentInFlatTree = (element: Element): Element | null => {
|
||||
if (element.assignedSlot) {
|
||||
return element.assignedSlot;
|
||||
}
|
||||
if (element.parentElement) {
|
||||
return element.parentElement;
|
||||
}
|
||||
const root = element.getRootNode();
|
||||
return root instanceof ShadowRoot ? root.host : null;
|
||||
};
|
||||
|
||||
const scrollContainer = (element: Element): Element | null => {
|
||||
for (
|
||||
let parent = parentInFlatTree(element);
|
||||
parent;
|
||||
parent = parentInFlatTree(parent)
|
||||
) {
|
||||
const { overflowY } = getComputedStyle(parent);
|
||||
if (overflowY === "auto" || overflowY === "scroll") {
|
||||
return parent;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Brings a row into the part of the editor that is visible above the bottom
|
||||
* sheet, which covers as much as the row's scroll-margin-bottom. A row already
|
||||
* there stays put; otherwise it is centered there, or aligned to the top when
|
||||
* it is taller than that space.
|
||||
*/
|
||||
export const scrollRowIntoView = (row: HTMLElement) => {
|
||||
const container = scrollContainer(row)?.getBoundingClientRect() ?? {
|
||||
top: 0,
|
||||
bottom: window.innerHeight,
|
||||
};
|
||||
const style = getComputedStyle(row);
|
||||
const visibleTop = container.top + parseFloat(style.scrollMarginTop);
|
||||
const visibleBottom = container.bottom - parseFloat(style.scrollMarginBottom);
|
||||
const { top, bottom } = row.getBoundingClientRect();
|
||||
if (top >= visibleTop && bottom <= visibleBottom) {
|
||||
return;
|
||||
}
|
||||
row.scrollIntoView({
|
||||
block: bottom - top > visibleBottom - visibleTop ? "start" : "center",
|
||||
behavior: "smooth",
|
||||
});
|
||||
};
|
||||
@@ -231,8 +231,23 @@ export const manualEditorStyles = css`
|
||||
transition: padding-bottom 180ms ease-in-out;
|
||||
}
|
||||
|
||||
.content.has-bottom-sheet {
|
||||
padding-bottom: calc(90vh - max(var(--safe-area-inset-bottom), 32px));
|
||||
.has-bottom-sheet {
|
||||
--automation-bottom-sheet-height: 50dvh;
|
||||
/* The backdrop covers the editor when the sheet is at full height */
|
||||
--automation-bottom-sheet-covered-height: min(
|
||||
var(--automation-bottom-sheet-settled-height, 100dvh),
|
||||
var(--automation-bottom-sheet-height)
|
||||
);
|
||||
}
|
||||
.has-bottom-sheet .content {
|
||||
padding-bottom: calc(
|
||||
var(--automation-bottom-sheet-covered-height) + var(--ha-space-4)
|
||||
);
|
||||
}
|
||||
.has-bottom-sheet .fab-positioner ha-button[slot="fab"].dirty {
|
||||
bottom: calc(
|
||||
var(--automation-bottom-sheet-covered-height) + var(--ha-space-4)
|
||||
);
|
||||
}
|
||||
|
||||
ha-automation-sidebar {
|
||||
@@ -281,6 +296,13 @@ export const automationRowsStyles = css`
|
||||
display: block;
|
||||
scroll-margin-top: 48px;
|
||||
}
|
||||
/* The part the open bottom sheet covers, see scrollRowIntoView */
|
||||
ha-automation-trigger-row,
|
||||
ha-automation-condition-row,
|
||||
ha-automation-action-row,
|
||||
ha-automation-option-row {
|
||||
scroll-margin-bottom: var(--automation-bottom-sheet-height, 0px);
|
||||
}
|
||||
.handle {
|
||||
padding: 4px;
|
||||
cursor: move; /* fallback if grab cursor is unsupported */
|
||||
|
||||
@@ -103,6 +103,7 @@ import "./types/ha-automation-trigger-time_pattern";
|
||||
import "./types/ha-automation-trigger-webhook";
|
||||
import "./types/ha-automation-trigger-zone";
|
||||
import { renderCtrlOrCmd } from "../../../../common/keyboard/ctrl-or-cmd";
|
||||
import { scrollRowIntoView } from "../scroll-row-into-view";
|
||||
|
||||
export interface TriggerElement extends LitElement {
|
||||
trigger: Trigger;
|
||||
@@ -816,10 +817,7 @@ export default class HaAutomationTriggerRow extends LitElement {
|
||||
|
||||
if (this.narrow) {
|
||||
window.setTimeout(() => {
|
||||
this.scrollIntoView({
|
||||
block: "start",
|
||||
behavior: "smooth",
|
||||
});
|
||||
scrollRowIntoView(this);
|
||||
}, 180);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -237,12 +237,6 @@ export default class HaAutomationTrigger extends AutomationSortableListMixin<Tri
|
||||
row.updateComplete.then(() => {
|
||||
if (this.optionsInSidebar) {
|
||||
row.openSidebar();
|
||||
if (this.narrow) {
|
||||
row.scrollIntoView({
|
||||
block: "start",
|
||||
behavior: "smooth",
|
||||
});
|
||||
}
|
||||
} else {
|
||||
row.expand();
|
||||
row.focus();
|
||||
|
||||
@@ -27,6 +27,7 @@ import { showPromptDialog } from "../../../dialogs/generic/show-dialog-box";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import { isMac } from "../../../util/is_mac";
|
||||
import { showEditorToast } from "../automation/editor-toast";
|
||||
import { scrollRowIntoView } from "../automation/scroll-row-into-view";
|
||||
import { indentStyle, overflowStyles, rowStyles } from "../automation/styles";
|
||||
import "./ha-script-field-selector-editor";
|
||||
import type HaScriptFieldSelectorEditor from "./ha-script-field-selector-editor";
|
||||
@@ -438,10 +439,7 @@ export default class HaScriptFieldRow extends LitElement {
|
||||
|
||||
if (this.narrow) {
|
||||
window.setTimeout(() => {
|
||||
this.scrollIntoView({
|
||||
block: "start",
|
||||
behavior: "smooth",
|
||||
});
|
||||
scrollRowIntoView(this);
|
||||
}, 180); // duration of transition of added padding for bottom sheet
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,15 +75,6 @@ export default class HaScriptFields extends LitElement {
|
||||
row.updateComplete.then(() => {
|
||||
row.openSidebar();
|
||||
row.focus();
|
||||
|
||||
if (this.narrow) {
|
||||
window.setTimeout(() => {
|
||||
row.scrollIntoView({
|
||||
block: "start",
|
||||
behavior: "smooth",
|
||||
});
|
||||
}, 180); // duration of transition of added padding for bottom sheet
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -162,6 +153,7 @@ export default class HaScriptFields extends LitElement {
|
||||
display: block;
|
||||
margin-bottom: 16px;
|
||||
scroll-margin-top: 48px;
|
||||
scroll-margin-bottom: var(--automation-bottom-sheet-height, 0px);
|
||||
}
|
||||
ha-svg-icon {
|
||||
height: 20px;
|
||||
|
||||
@@ -5400,6 +5400,7 @@
|
||||
},
|
||||
"edit_yaml": "Edit in YAML",
|
||||
"edit_ui": "Edit in visual editor",
|
||||
"sidebar_label": "Item editor",
|
||||
"copy_to_clipboard": "Copy to clipboard",
|
||||
"unknown_entity": "unknown entity",
|
||||
"edit_unknown_device": "Editor not available for unknown device",
|
||||
|
||||
Reference in new issue
Block a user