Compare commits

...
17 changed files with 184 additions and 472 deletions

No files matched your search

-393
View File
@@ -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 };
}
}
+48 -8
View File
@@ -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",
});
};
+24 -2
View File
@@ -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
}
}
+1 -9
View File
@@ -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;
+1
View File
@@ -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",