mirror of
https://github.com/home-assistant/frontend.git
synced 2025-11-08 18:39:40 +00:00
Automation editor mobile bottom sheet (#26680)
This commit is contained in:
268
src/components/ha-bottom-sheet.ts
Normal file
268
src/components/ha-bottom-sheet.ts
Normal file
@@ -0,0 +1,268 @@
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, query, state } from "lit/decorators";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
|
||||
const ANIMATION_DURATION_MS = 300;
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @fires bottom-sheet-closed - Fired when the bottom sheet is closed
|
||||
*
|
||||
* @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
|
||||
*/
|
||||
@customElement("ha-bottom-sheet")
|
||||
export class HaBottomSheet extends LitElement {
|
||||
@query("dialog") private _dialog!: HTMLDialogElement;
|
||||
|
||||
private _dragging = false;
|
||||
|
||||
private _dragStartY = 0;
|
||||
|
||||
private _initialSize = 0;
|
||||
|
||||
@state() private _dialogMaxViewpointHeight = 70;
|
||||
|
||||
@state() private _dialogViewportHeight?: number;
|
||||
|
||||
render() {
|
||||
return html`<dialog
|
||||
open
|
||||
@transitionend=${this._handleTransitionEnd}
|
||||
style=${styleMap({
|
||||
height: this._dialogViewportHeight
|
||||
? `${this._dialogViewportHeight}vh`
|
||||
: "auto",
|
||||
maxHeight: `${this._dialogMaxViewpointHeight}vh`,
|
||||
})}
|
||||
>
|
||||
<div class="handle-wrapper">
|
||||
<div
|
||||
@mousedown=${this._handleMouseDown}
|
||||
@touchstart=${this._handleTouchStart}
|
||||
class="handle"
|
||||
></div>
|
||||
</div>
|
||||
<slot></slot>
|
||||
</dialog>`;
|
||||
}
|
||||
|
||||
protected firstUpdated(changedProperties) {
|
||||
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;
|
||||
} 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);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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));
|
||||
|
||||
// on drag down and below 20vh
|
||||
if (newSize < 20 && deltaY < 0) {
|
||||
this._endDrag();
|
||||
this.closeSheet();
|
||||
return;
|
||||
}
|
||||
|
||||
this._dialogViewportHeight = newSize;
|
||||
}
|
||||
|
||||
private _handleMouseUp = () => {
|
||||
this._endDrag();
|
||||
};
|
||||
|
||||
private _handleTouchEnd = () => {
|
||||
this._endDrag();
|
||||
};
|
||||
|
||||
private _endDrag() {
|
||||
if (!this._dragging) {
|
||||
return;
|
||||
}
|
||||
this._dragging = false;
|
||||
document.body.style.removeProperty("cursor");
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
.handle-wrapper .handle::after {
|
||||
content: "";
|
||||
border-radius: 8px;
|
||||
height: 4px;
|
||||
background: var(--divider-color, #e0e0e0);
|
||||
width: 80px;
|
||||
}
|
||||
.handle-wrapper .handle:active::after {
|
||||
cursor: grabbing;
|
||||
}
|
||||
dialog {
|
||||
height: auto;
|
||||
max-height: 70vh;
|
||||
min-height: 30vh;
|
||||
background-color: var(
|
||||
--ha-dialog-surface-background,
|
||||
var(--mdc-theme-surface, #fff)
|
||||
);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
top: 0;
|
||||
inset-inline-start: 0;
|
||||
position: fixed;
|
||||
width: calc(100% - 4px);
|
||||
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-dialog-border-radius,
|
||||
var(--ha-border-radius-2xl)
|
||||
);
|
||||
border-top-right-radius: var(
|
||||
--ha-dialog-border-radius,
|
||||
var(--ha-border-radius-2xl)
|
||||
);
|
||||
transform: translateY(100%);
|
||||
transition: transform ${ANIMATION_DURATION_MS}ms ease;
|
||||
border-top-width: var(--ha-bottom-sheet-border-width);
|
||||
border-right-width: var(--ha-bottom-sheet-border-width);
|
||||
border-left-width: var(--ha-bottom-sheet-border-width);
|
||||
border-bottom-width: 0;
|
||||
border-style: var(--ha-bottom-sheet-border-style);
|
||||
border-color: var(--ha-bottom-sheet-border-color);
|
||||
}
|
||||
|
||||
dialog.show {
|
||||
transform: translateY(0);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-bottom-sheet": HaBottomSheet;
|
||||
}
|
||||
|
||||
interface HASSDomEvents {
|
||||
"bottom-sheet-closed": undefined;
|
||||
}
|
||||
}
|
||||
@@ -642,9 +642,6 @@ export default class HaAutomationActionRow extends LitElement {
|
||||
}
|
||||
|
||||
public openSidebar(action?: Action): void {
|
||||
if (this.narrow) {
|
||||
this.scrollIntoView();
|
||||
}
|
||||
const sidebarAction = action ?? this.action;
|
||||
const actionType = getAutomationActionType(sidebarAction);
|
||||
|
||||
@@ -670,6 +667,13 @@ export default class HaAutomationActionRow extends LitElement {
|
||||
yamlMode: this._yamlMode,
|
||||
} satisfies ActionSidebarConfig);
|
||||
this._selected = true;
|
||||
|
||||
if (this.narrow) {
|
||||
this.scrollIntoView({
|
||||
block: "start",
|
||||
behavior: "smooth",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public expand() {
|
||||
|
||||
@@ -164,12 +164,15 @@ export default class HaAutomationAction extends LitElement {
|
||||
!ACTION_BUILDING_BLOCKS.includes(type)
|
||||
) {
|
||||
row.openSidebar();
|
||||
if (this.narrow) {
|
||||
row.scrollIntoView({
|
||||
block: "start",
|
||||
behavior: "smooth",
|
||||
});
|
||||
}
|
||||
} else if (!this.optionsInSidebar) {
|
||||
row.expand();
|
||||
}
|
||||
if (this.narrow) {
|
||||
row.scrollIntoView();
|
||||
}
|
||||
row.focus();
|
||||
});
|
||||
}
|
||||
@@ -185,6 +188,10 @@ export default class HaAutomationAction extends LitElement {
|
||||
}
|
||||
|
||||
private _addActionDialog() {
|
||||
if (this.narrow) {
|
||||
fireEvent(this, "close-sidebar");
|
||||
}
|
||||
|
||||
showAddAutomationElementDialog(this, {
|
||||
type: "action",
|
||||
add: this._addAction,
|
||||
|
||||
@@ -599,10 +599,6 @@ export default class HaAutomationConditionRow extends LitElement {
|
||||
}
|
||||
|
||||
public openSidebar(condition?: Condition): void {
|
||||
if (this.narrow) {
|
||||
this.scrollIntoView();
|
||||
}
|
||||
|
||||
const sidebarCondition = condition || this.condition;
|
||||
fireEvent(this, "open-sidebar", {
|
||||
save: (value) => {
|
||||
@@ -626,6 +622,13 @@ export default class HaAutomationConditionRow extends LitElement {
|
||||
yamlMode: this._yamlMode,
|
||||
} satisfies ConditionSidebarConfig);
|
||||
this._selected = true;
|
||||
|
||||
if (this.narrow) {
|
||||
this.scrollIntoView({
|
||||
block: "start",
|
||||
behavior: "smooth",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private _uiSupported = memoizeOne(
|
||||
|
||||
@@ -108,12 +108,15 @@ export default class HaAutomationCondition extends LitElement {
|
||||
!CONDITION_BUILDING_BLOCKS.includes(row.condition.condition)
|
||||
) {
|
||||
row.openSidebar();
|
||||
if (this.narrow) {
|
||||
row.scrollIntoView({
|
||||
block: "start",
|
||||
behavior: "smooth",
|
||||
});
|
||||
}
|
||||
} else if (!this.optionsInSidebar) {
|
||||
row.expand();
|
||||
}
|
||||
if (this.narrow) {
|
||||
row.scrollIntoView();
|
||||
}
|
||||
row.focus();
|
||||
});
|
||||
}
|
||||
@@ -205,6 +208,9 @@ export default class HaAutomationCondition extends LitElement {
|
||||
}
|
||||
|
||||
private _addConditionDialog() {
|
||||
if (this.narrow) {
|
||||
fireEvent(this, "close-sidebar");
|
||||
}
|
||||
showAddAutomationElementDialog(this, {
|
||||
type: "condition",
|
||||
add: this._addCondition,
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import "../../../components/ha-bottom-sheet";
|
||||
import type { HaBottomSheet } from "../../../components/ha-bottom-sheet";
|
||||
import {
|
||||
isCondition,
|
||||
isScriptField,
|
||||
@@ -13,14 +15,12 @@ import {
|
||||
} from "../../../data/automation";
|
||||
import { isTriggerList } from "../../../data/trigger";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import type HaAutomationConditionEditor from "./condition/ha-automation-condition-editor";
|
||||
import "./sidebar/ha-automation-sidebar-action";
|
||||
import "./sidebar/ha-automation-sidebar-condition";
|
||||
import "./sidebar/ha-automation-sidebar-option";
|
||||
import "./sidebar/ha-automation-sidebar-script-field";
|
||||
import "./sidebar/ha-automation-sidebar-script-field-selector";
|
||||
import "./sidebar/ha-automation-sidebar-trigger";
|
||||
import type HaAutomationTriggerEditor from "./trigger/ha-automation-trigger-editor";
|
||||
|
||||
@customElement("ha-automation-sidebar")
|
||||
export default class HaAutomationSidebar extends LitElement {
|
||||
@@ -32,92 +32,101 @@ export default class HaAutomationSidebar extends LitElement {
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
|
||||
@state() private _yamlMode = false;
|
||||
|
||||
@query(".sidebar-editor")
|
||||
public editor?: HaAutomationTriggerEditor | HaAutomationConditionEditor;
|
||||
|
||||
protected render() {
|
||||
if (!this.config) {
|
||||
return nothing;
|
||||
}
|
||||
@query("ha-bottom-sheet") private _bottomSheetElement?: HaBottomSheet;
|
||||
|
||||
private _renderContent() {
|
||||
// get config type
|
||||
const type = this._getType();
|
||||
|
||||
if (type === "trigger") {
|
||||
return html`
|
||||
<ha-automation-sidebar-trigger
|
||||
class="sidebar-content"
|
||||
.hass=${this.hass}
|
||||
.config=${this.config}
|
||||
.isWide=${this.isWide}
|
||||
.narrow=${this.narrow}
|
||||
.disabled=${this.disabled}
|
||||
.yamlMode=${this._yamlMode}
|
||||
@toggle-yaml-mode=${this._toggleYamlMode}
|
||||
@close-sidebar=${this._closeSidebar}
|
||||
@close-sidebar=${this._handleCloseSidebar}
|
||||
></ha-automation-sidebar-trigger>
|
||||
`;
|
||||
}
|
||||
if (type === "condition") {
|
||||
return html`
|
||||
<ha-automation-sidebar-condition
|
||||
class="sidebar-content"
|
||||
.hass=${this.hass}
|
||||
.config=${this.config}
|
||||
.isWide=${this.isWide}
|
||||
.narrow=${this.narrow}
|
||||
.disabled=${this.disabled}
|
||||
.yamlMode=${this._yamlMode}
|
||||
@toggle-yaml-mode=${this._toggleYamlMode}
|
||||
@close-sidebar=${this._closeSidebar}
|
||||
@close-sidebar=${this._handleCloseSidebar}
|
||||
></ha-automation-sidebar-condition>
|
||||
`;
|
||||
}
|
||||
if (type === "action") {
|
||||
return html`
|
||||
<ha-automation-sidebar-action
|
||||
class="sidebar-content"
|
||||
.hass=${this.hass}
|
||||
.config=${this.config}
|
||||
.isWide=${this.isWide}
|
||||
.narrow=${this.narrow}
|
||||
.disabled=${this.disabled}
|
||||
.yamlMode=${this._yamlMode}
|
||||
@toggle-yaml-mode=${this._toggleYamlMode}
|
||||
@close-sidebar=${this._closeSidebar}
|
||||
@close-sidebar=${this._handleCloseSidebar}
|
||||
></ha-automation-sidebar-action>
|
||||
`;
|
||||
}
|
||||
if (type === "option") {
|
||||
return html`
|
||||
<ha-automation-sidebar-option
|
||||
class="sidebar-content"
|
||||
.hass=${this.hass}
|
||||
.config=${this.config}
|
||||
.isWide=${this.isWide}
|
||||
.narrow=${this.narrow}
|
||||
.disabled=${this.disabled}
|
||||
@close-sidebar=${this._closeSidebar}
|
||||
@close-sidebar=${this._handleCloseSidebar}
|
||||
></ha-automation-sidebar-option>
|
||||
`;
|
||||
}
|
||||
if (type === "script-field-selector") {
|
||||
return html`
|
||||
<ha-automation-sidebar-script-field-selector
|
||||
class="sidebar-content"
|
||||
.hass=${this.hass}
|
||||
.config=${this.config}
|
||||
.isWide=${this.isWide}
|
||||
.narrow=${this.narrow}
|
||||
.disabled=${this.disabled}
|
||||
.yamlMode=${this._yamlMode}
|
||||
@toggle-yaml-mode=${this._toggleYamlMode}
|
||||
@close-sidebar=${this._closeSidebar}
|
||||
@close-sidebar=${this._handleCloseSidebar}
|
||||
></ha-automation-sidebar-script-field-selector>
|
||||
`;
|
||||
}
|
||||
if (type === "script-field") {
|
||||
return html`
|
||||
<ha-automation-sidebar-script-field
|
||||
class="sidebar-content"
|
||||
.hass=${this.hass}
|
||||
.config=${this.config}
|
||||
.isWide=${this.isWide}
|
||||
.narrow=${this.narrow}
|
||||
.disabled=${this.disabled}
|
||||
.yamlMode=${this._yamlMode}
|
||||
@toggle-yaml-mode=${this._toggleYamlMode}
|
||||
@close-sidebar=${this._closeSidebar}
|
||||
@close-sidebar=${this._handleCloseSidebar}
|
||||
></ha-automation-sidebar-script-field>
|
||||
`;
|
||||
}
|
||||
@@ -125,6 +134,22 @@ export default class HaAutomationSidebar extends LitElement {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this.config) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
if (this.narrow) {
|
||||
return html`
|
||||
<ha-bottom-sheet @bottom-sheet-closed=${this._closeSidebar}>
|
||||
${this._renderContent()}
|
||||
</ha-bottom-sheet>
|
||||
`;
|
||||
}
|
||||
|
||||
return this._renderContent();
|
||||
}
|
||||
|
||||
private _getType() {
|
||||
if (
|
||||
(this.config as TriggerSidebarConfig)?.config &&
|
||||
@@ -157,8 +182,17 @@ export default class HaAutomationSidebar extends LitElement {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private _closeSidebar(ev: CustomEvent) {
|
||||
private _handleCloseSidebar(ev: CustomEvent) {
|
||||
ev.stopPropagation();
|
||||
if (this.narrow) {
|
||||
this._bottomSheetElement?.closeSheet();
|
||||
return;
|
||||
}
|
||||
|
||||
this._closeSidebar();
|
||||
}
|
||||
|
||||
private _closeSidebar() {
|
||||
this.config?.close();
|
||||
}
|
||||
|
||||
@@ -180,6 +214,15 @@ 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);
|
||||
}
|
||||
|
||||
@media all and (max-width: 870px) {
|
||||
.sidebar-content {
|
||||
max-height: 100%;
|
||||
}
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import { load } from "js-yaml";
|
||||
import type { CSSResultGroup } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import {
|
||||
any,
|
||||
@@ -84,6 +84,9 @@ export class HaManualAutomationEditor extends LitElement {
|
||||
|
||||
@state() private _sidebarConfig?: SidebarConfig;
|
||||
|
||||
@query(".content")
|
||||
private _contentElement?: HTMLDivElement;
|
||||
|
||||
private _previousConfig?: ManualAutomationConfig;
|
||||
|
||||
public connectedCallback() {
|
||||
@@ -273,10 +276,11 @@ export class HaManualAutomationEditor extends LitElement {
|
||||
class=${classMap({
|
||||
sidebar: true,
|
||||
hidden: !this._sidebarConfig,
|
||||
overlay: !this.isWide,
|
||||
overlay: !this.isWide && !this.narrow,
|
||||
})}
|
||||
.isWide=${this.isWide}
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.config=${this._sidebarConfig}
|
||||
@value-changed=${this._sidebarConfigChanged}
|
||||
.disabled=${this.disabled}
|
||||
@@ -313,6 +317,8 @@ export class HaManualAutomationEditor extends LitElement {
|
||||
|
||||
private _handleCloseSidebar() {
|
||||
this._sidebarConfig = undefined;
|
||||
// fix content shift when bottom rows are scrolled into view
|
||||
this._contentElement?.scrollIntoView();
|
||||
}
|
||||
|
||||
private _triggerChanged(ev: CustomEvent): void {
|
||||
@@ -612,26 +618,23 @@ export class HaManualAutomationEditor extends LitElement {
|
||||
|
||||
.sidebar.overlay {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
height: calc(100% - 64px);
|
||||
bottom: 8px;
|
||||
right: 8px;
|
||||
height: calc(100% - 70px);
|
||||
padding: 0;
|
||||
z-index: 5;
|
||||
box-shadow: -8px 0 16px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
@media all and (max-width: 870px) {
|
||||
.sidebar.overlay {
|
||||
max-height: 70vh;
|
||||
max-height: 70dvh;
|
||||
height: auto;
|
||||
width: 100%;
|
||||
box-shadow: 0px -8px 16px rgba(0, 0, 0, 0.2);
|
||||
.split-view {
|
||||
gap: 0;
|
||||
margin-right: -8px;
|
||||
}
|
||||
}
|
||||
|
||||
@media all and (max-width: 870px) {
|
||||
.sidebar.overlay.hidden {
|
||||
.sidebar {
|
||||
height: 0;
|
||||
width: 0;
|
||||
flex: 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -343,10 +343,6 @@ export default class HaAutomationOptionRow extends LitElement {
|
||||
}
|
||||
|
||||
public openSidebar(): void {
|
||||
if (this.narrow) {
|
||||
this.scrollIntoView();
|
||||
}
|
||||
|
||||
fireEvent(this, "open-sidebar", {
|
||||
close: () => {
|
||||
this._selected = false;
|
||||
@@ -359,6 +355,13 @@ export default class HaAutomationOptionRow extends LitElement {
|
||||
delete: this._removeOption,
|
||||
} satisfies OptionSidebarConfig);
|
||||
this._selected = true;
|
||||
|
||||
if (this.narrow) {
|
||||
this.scrollIntoView({
|
||||
block: "start",
|
||||
behavior: "smooth",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public expand() {
|
||||
|
||||
@@ -132,10 +132,11 @@ export default class HaAutomationOption extends LitElement {
|
||||
row.updateComplete.then(() => {
|
||||
if (!this.optionsInSidebar) {
|
||||
row.expand();
|
||||
}
|
||||
|
||||
if (this.narrow) {
|
||||
row.scrollIntoView();
|
||||
} else if (this.narrow) {
|
||||
row.scrollIntoView({
|
||||
block: "start",
|
||||
behavior: "smooth",
|
||||
});
|
||||
}
|
||||
row.focus();
|
||||
});
|
||||
|
||||
@@ -34,6 +34,8 @@ export default class HaAutomationSidebarAction extends LitElement {
|
||||
|
||||
@property({ type: Boolean, attribute: "yaml-mode" }) public yamlMode = false;
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
|
||||
@state() private _warnings?: string[];
|
||||
|
||||
@query(".sidebar-editor")
|
||||
@@ -85,6 +87,7 @@ export default class HaAutomationSidebarAction extends LitElement {
|
||||
.isWide=${this.isWide}
|
||||
.yamlMode=${this.yamlMode}
|
||||
.warnings=${this._warnings}
|
||||
.narrow=${this.narrow}
|
||||
>
|
||||
<span slot="title">${title}</span>
|
||||
<span slot="subtitle">${subtitle}</span>
|
||||
@@ -133,8 +136,9 @@ export default class HaAutomationSidebarAction extends LitElement {
|
||||
)}
|
||||
<ha-svg-icon slot="start" .path=${mdiDelete}></ha-svg-icon>
|
||||
</ha-md-menu-item>
|
||||
${description ||
|
||||
html`<ha-automation-action-editor
|
||||
${description && !this.yamlMode
|
||||
? html`<div class="description">${description}</div>`
|
||||
: html`<ha-automation-action-editor
|
||||
class="sidebar-editor"
|
||||
.hass=${this.hass}
|
||||
.action=${this.config.config}
|
||||
@@ -179,6 +183,9 @@ export default class HaAutomationSidebarAction extends LitElement {
|
||||
.sidebar-editor {
|
||||
padding-top: 64px;
|
||||
}
|
||||
.description {
|
||||
padding-top: 16px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
import { mdiClose, mdiDotsVertical } from "@mdi/js";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import {
|
||||
customElement,
|
||||
eventOptions,
|
||||
property,
|
||||
query,
|
||||
state,
|
||||
} from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import { stopPropagation } from "../../../../common/dom/stop_propagation";
|
||||
@@ -33,6 +39,12 @@ export default class HaAutomationSidebarCard extends LitElement {
|
||||
|
||||
@property({ attribute: false }) public warnings?: string[];
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
|
||||
@state() private _contentScrolled = false;
|
||||
|
||||
@query(".card-content") private _contentElement?: HTMLDivElement;
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
<ha-card
|
||||
@@ -42,7 +54,9 @@ export default class HaAutomationSidebarCard extends LitElement {
|
||||
yaml: this.yamlMode,
|
||||
})}
|
||||
>
|
||||
<ha-dialog-header>
|
||||
<ha-dialog-header
|
||||
class=${classMap({ scrolled: this._contentScrolled })}
|
||||
>
|
||||
<ha-icon-button
|
||||
slot="navigationIcon"
|
||||
.label=${this.hass.localize("ui.common.close")}
|
||||
@@ -56,7 +70,7 @@ export default class HaAutomationSidebarCard extends LitElement {
|
||||
@click=${this._openOverflowMenu}
|
||||
@keydown=${stopPropagation}
|
||||
@closed=${stopPropagation}
|
||||
positioning="fixed"
|
||||
.positioning=${this.narrow ? "absolute" : "fixed"}
|
||||
>
|
||||
<ha-icon-button
|
||||
slot="trigger"
|
||||
@@ -74,13 +88,19 @@ export default class HaAutomationSidebarCard extends LitElement {
|
||||
>
|
||||
</ha-automation-editor-warning>`
|
||||
: nothing}
|
||||
<div class="card-content">
|
||||
<div class="card-content" @scroll=${this._onScroll}>
|
||||
<slot></slot>
|
||||
</div>
|
||||
</ha-card>
|
||||
`;
|
||||
}
|
||||
|
||||
@eventOptions({ passive: true })
|
||||
private _onScroll() {
|
||||
const top = this._contentElement?.scrollTop ?? 0;
|
||||
this._contentScrolled = top > 0;
|
||||
}
|
||||
|
||||
private _closeSidebar() {
|
||||
fireEvent(this, "close-sidebar");
|
||||
}
|
||||
@@ -98,25 +118,33 @@ export default class HaAutomationSidebarCard extends LitElement {
|
||||
border-width: 2px;
|
||||
display: block;
|
||||
}
|
||||
ha-card.mobile {
|
||||
border-bottom-right-radius: var(--ha-border-radius-square);
|
||||
border-bottom-left-radius: var(--ha-border-radius-square);
|
||||
}
|
||||
|
||||
@media all and (max-width: 870px) {
|
||||
ha-card.mobile {
|
||||
max-height: 70vh;
|
||||
max-height: 70dvh;
|
||||
border-width: 2px 2px 0;
|
||||
border: none;
|
||||
}
|
||||
ha-card.mobile.yaml {
|
||||
height: 70vh;
|
||||
height: 70dvh;
|
||||
ha-card.mobile {
|
||||
border-bottom-right-radius: var(--ha-border-radius-square);
|
||||
border-bottom-left-radius: var(--ha-border-radius-square);
|
||||
}
|
||||
}
|
||||
|
||||
ha-dialog-header {
|
||||
border-radius: var(--ha-card-border-radius);
|
||||
box-shadow: none;
|
||||
transition: box-shadow 180ms ease-in-out;
|
||||
border-bottom-left-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
z-index: 6;
|
||||
position: relative;
|
||||
background-color: var(
|
||||
--ha-dialog-surface-background,
|
||||
var(--mdc-theme-surface, #fff)
|
||||
);
|
||||
}
|
||||
|
||||
ha-dialog-header.scrolled {
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
|
||||
.card-content {
|
||||
@@ -130,17 +158,6 @@ export default class HaAutomationSidebarCard extends LitElement {
|
||||
overflow: auto;
|
||||
}
|
||||
}
|
||||
|
||||
@media all and (max-width: 870px) {
|
||||
ha-card.mobile .card-content {
|
||||
max-height: calc(
|
||||
70vh - 88px - max(var(--safe-area-inset-bottom), 16px)
|
||||
);
|
||||
max-height: calc(
|
||||
70dvh - 88px - max(var(--safe-area-inset-bottom), 16px)
|
||||
);
|
||||
}
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
@@ -28,6 +28,8 @@ export default class HaAutomationSidebarCondition extends LitElement {
|
||||
|
||||
@property({ type: Boolean, attribute: "yaml-mode" }) public yamlMode = false;
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
|
||||
@state() private _warnings?: string[];
|
||||
|
||||
@query(".sidebar-editor")
|
||||
@@ -74,6 +76,7 @@ export default class HaAutomationSidebarCondition extends LitElement {
|
||||
.isWide=${this.isWide}
|
||||
.yamlMode=${this.yamlMode}
|
||||
.warnings=${this._warnings}
|
||||
.narrow=${this.narrow}
|
||||
>
|
||||
<span slot="title">${title}</span>
|
||||
<span slot="subtitle">${subtitle}</span>
|
||||
@@ -122,8 +125,9 @@ export default class HaAutomationSidebarCondition extends LitElement {
|
||||
)}
|
||||
<ha-svg-icon slot="start" .path=${mdiDelete}></ha-svg-icon>
|
||||
</ha-md-menu-item>
|
||||
${description ||
|
||||
html`<ha-automation-condition-editor
|
||||
${description && !this.yamlMode
|
||||
? html`<div class="description">${description}</div>`
|
||||
: html`<ha-automation-condition-editor
|
||||
class="sidebar-editor"
|
||||
.hass=${this.hass}
|
||||
.condition=${this.config.config}
|
||||
@@ -166,6 +170,9 @@ export default class HaAutomationSidebarCondition extends LitElement {
|
||||
.sidebar-editor {
|
||||
padding-top: 64px;
|
||||
}
|
||||
.description {
|
||||
padding-top: 16px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,8 @@ export default class HaAutomationSidebarOption extends LitElement {
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
|
||||
@query(".sidebar-editor")
|
||||
public editor?: HaAutomationConditionEditor;
|
||||
|
||||
@@ -37,6 +39,7 @@ export default class HaAutomationSidebarOption extends LitElement {
|
||||
return html`<ha-automation-sidebar-card
|
||||
.hass=${this.hass}
|
||||
.isWide=${this.isWide}
|
||||
.narrow=${this.narrow}
|
||||
>
|
||||
<span slot="title">${title}</span>
|
||||
<span slot="subtitle">${subtitle}</span>
|
||||
@@ -66,7 +69,7 @@ export default class HaAutomationSidebarOption extends LitElement {
|
||||
)}
|
||||
<ha-svg-icon slot="start" .path=${mdiDelete}></ha-svg-icon>
|
||||
</ha-md-menu-item>
|
||||
${description}
|
||||
<div class="description">${description}</div>
|
||||
</ha-automation-sidebar-card>`;
|
||||
}
|
||||
|
||||
@@ -74,6 +77,9 @@ export default class HaAutomationSidebarOption extends LitElement {
|
||||
.sidebar-editor {
|
||||
padding-top: 64px;
|
||||
}
|
||||
.description {
|
||||
padding-top: 16px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
@@ -21,6 +21,8 @@ export default class HaAutomationSidebarScriptFieldSelector extends LitElement {
|
||||
|
||||
@property({ type: Boolean, attribute: "yaml-mode" }) public yamlMode = false;
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
|
||||
@state() private _warnings?: string[];
|
||||
|
||||
@query(".sidebar-editor")
|
||||
@@ -53,6 +55,7 @@ export default class HaAutomationSidebarScriptFieldSelector extends LitElement {
|
||||
.isWide=${this.isWide}
|
||||
.yamlMode=${this.yamlMode}
|
||||
.warnings=${this._warnings}
|
||||
.narrow=${this.narrow}
|
||||
>
|
||||
<span slot="title">${title}</span>
|
||||
<span slot="subtitle">${subtitle}</span>
|
||||
|
||||
@@ -20,6 +20,8 @@ export default class HaAutomationSidebarScriptField extends LitElement {
|
||||
|
||||
@property({ type: Boolean, attribute: "yaml-mode" }) public yamlMode = false;
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
|
||||
@state() private _warnings?: string[];
|
||||
|
||||
@query(".sidebar-editor")
|
||||
@@ -47,6 +49,7 @@ export default class HaAutomationSidebarScriptField extends LitElement {
|
||||
.isWide=${this.isWide}
|
||||
.yamlMode=${this.yamlMode}
|
||||
.warnings=${this._warnings}
|
||||
.narrow=${this.narrow}
|
||||
>
|
||||
<span slot="title">${title}</span>
|
||||
<ha-md-menu-item
|
||||
|
||||
@@ -29,6 +29,8 @@ export default class HaAutomationSidebarTrigger extends LitElement {
|
||||
|
||||
@property({ type: Boolean, attribute: "yaml-mode" }) public yamlMode = false;
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
|
||||
@state() private _requestShowId = false;
|
||||
|
||||
@state() private _warnings?: string[];
|
||||
@@ -71,6 +73,7 @@ export default class HaAutomationSidebarTrigger extends LitElement {
|
||||
.isWide=${this.isWide}
|
||||
.yamlMode=${this.yamlMode}
|
||||
.warnings=${this._warnings}
|
||||
.narrow=${this.narrow}
|
||||
>
|
||||
<span slot="title">${title}</span>
|
||||
<span slot="subtitle">${subtitle}</span>
|
||||
|
||||
@@ -469,9 +469,6 @@ export default class HaAutomationTriggerRow extends LitElement {
|
||||
}
|
||||
|
||||
public openSidebar(trigger?: Trigger): void {
|
||||
if (this.narrow) {
|
||||
this.scrollIntoView();
|
||||
}
|
||||
fireEvent(this, "open-sidebar", {
|
||||
save: (value) => {
|
||||
fireEvent(this, "value-changed", { value });
|
||||
@@ -494,6 +491,13 @@ export default class HaAutomationTriggerRow extends LitElement {
|
||||
yamlMode: this._yamlMode,
|
||||
} satisfies TriggerSidebarConfig);
|
||||
this._selected = true;
|
||||
|
||||
if (this.narrow) {
|
||||
this.scrollIntoView({
|
||||
block: "start",
|
||||
behavior: "smooth",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private _setClipboard() {
|
||||
|
||||
@@ -135,6 +135,9 @@ export default class HaAutomationTrigger extends LitElement {
|
||||
}
|
||||
|
||||
private _addTriggerDialog() {
|
||||
if (this.narrow) {
|
||||
fireEvent(this, "close-sidebar");
|
||||
}
|
||||
showAddAutomationElementDialog(this, {
|
||||
type: "trigger",
|
||||
add: this._addTrigger,
|
||||
@@ -177,12 +180,15 @@ export default class HaAutomationTrigger extends LitElement {
|
||||
row.updateComplete.then(() => {
|
||||
if (this.optionsInSidebar) {
|
||||
row.openSidebar();
|
||||
if (this.narrow) {
|
||||
row.scrollIntoView({
|
||||
block: "start",
|
||||
behavior: "smooth",
|
||||
});
|
||||
}
|
||||
} else {
|
||||
row.expand();
|
||||
}
|
||||
if (this.narrow) {
|
||||
row.scrollIntoView();
|
||||
}
|
||||
row.focus();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -187,9 +187,6 @@ export default class HaScriptFieldRow extends LitElement {
|
||||
if (!selectorEditor) {
|
||||
this._selected = true;
|
||||
}
|
||||
if (this.narrow) {
|
||||
this.scrollIntoView();
|
||||
}
|
||||
|
||||
fireEvent(this, "open-sidebar", {
|
||||
save: (value) => {
|
||||
@@ -216,6 +213,13 @@ export default class HaScriptFieldRow extends LitElement {
|
||||
},
|
||||
yamlMode: this._yamlMode,
|
||||
} satisfies ScriptFieldSidebarConfig);
|
||||
|
||||
if (this.narrow) {
|
||||
this.scrollIntoView({
|
||||
block: "start",
|
||||
behavior: "smooth",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private _toggleYamlMode = () => {
|
||||
|
||||
@@ -75,11 +75,14 @@ export default class HaScriptFields extends LitElement {
|
||||
)!;
|
||||
row.updateComplete.then(() => {
|
||||
row.openSidebar();
|
||||
row.focus();
|
||||
|
||||
if (this.narrow) {
|
||||
row.scrollIntoView();
|
||||
row.scrollIntoView({
|
||||
block: "start",
|
||||
behavior: "smooth",
|
||||
});
|
||||
}
|
||||
row.focus();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -207,6 +207,7 @@ export class HaManualScriptEditor extends LitElement {
|
||||
hidden: !this._sidebarConfig,
|
||||
overlay: !this.isWide,
|
||||
})}
|
||||
.narrow=${this.narrow}
|
||||
.isWide=${this.isWide}
|
||||
.hass=${this.hass}
|
||||
.config=${this._sidebarConfig}
|
||||
@@ -511,18 +512,14 @@ export class HaManualScriptEditor extends LitElement {
|
||||
}
|
||||
|
||||
@media all and (max-width: 870px) {
|
||||
.sidebar.overlay {
|
||||
max-height: 70vh;
|
||||
max-height: 70dvh;
|
||||
height: auto;
|
||||
width: 100%;
|
||||
box-shadow: 0px -8px 16px rgba(0, 0, 0, 0.2);
|
||||
.split-view {
|
||||
gap: 0;
|
||||
margin-right: -8px;
|
||||
}
|
||||
}
|
||||
|
||||
@media all and (max-width: 870px) {
|
||||
.sidebar.overlay.hidden {
|
||||
.sidebar {
|
||||
height: 0;
|
||||
width: 0;
|
||||
flex: 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,10 @@ export const waMainStyles = css`
|
||||
--wa-focus-ring-offset: 2px;
|
||||
--wa-focus-ring: var(--wa-focus-ring-style) var(--wa-focus-ring-width)
|
||||
var(--wa-focus-ring-color);
|
||||
|
||||
--wa-space-l: 24px;
|
||||
--wa-shadow-l: 0 8px 8px -4px rgba(0, 0, 0, 0.2);
|
||||
--wa-form-control-padding-block: 0.75em;
|
||||
}
|
||||
`;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user