mirror of
https://github.com/home-assistant/frontend.git
synced 2025-11-09 10:59:50 +00:00
Add home assistant bottom sheet (#26948)
Co-authored-by: Wendelin <12148533+wendevlin@users.noreply.github.com>
This commit is contained in:
@@ -1,262 +1,62 @@
|
|||||||
import { css, html, LitElement } from "lit";
|
import { css, html, LitElement, type PropertyValues } from "lit";
|
||||||
import { customElement, query, state } from "lit/decorators";
|
import "@home-assistant/webawesome/dist/components/drawer/drawer";
|
||||||
import { styleMap } from "lit/directives/style-map";
|
import { customElement, property, state } from "lit/decorators";
|
||||||
import { fireEvent } from "../common/dom/fire_event";
|
|
||||||
|
|
||||||
const ANIMATION_DURATION_MS = 300;
|
export const BOTTOM_SHEET_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")
|
@customElement("ha-bottom-sheet")
|
||||||
export class HaBottomSheet extends LitElement {
|
export class HaBottomSheet extends LitElement {
|
||||||
@query("dialog") private _dialog!: HTMLDialogElement;
|
@property({ type: Boolean }) public open = false;
|
||||||
|
|
||||||
private _dragging = false;
|
@state() private _drawerOpen = false;
|
||||||
|
|
||||||
private _dragStartY = 0;
|
private _handleAfterHide() {
|
||||||
|
this.open = false;
|
||||||
|
const ev = new Event("closed", {
|
||||||
|
bubbles: true,
|
||||||
|
composed: true,
|
||||||
|
});
|
||||||
|
this.dispatchEvent(ev);
|
||||||
|
}
|
||||||
|
|
||||||
private _initialSize = 0;
|
protected updated(changedProperties: PropertyValues): void {
|
||||||
|
super.updated(changedProperties);
|
||||||
@state() private _dialogMaxViewpointHeight = 70;
|
if (changedProperties.has("open")) {
|
||||||
|
this._drawerOpen = this.open;
|
||||||
@state() private _dialogMinViewpointHeight = 55;
|
}
|
||||||
|
}
|
||||||
@state() private _dialogViewportHeight?: number;
|
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return html`<dialog
|
return html`
|
||||||
open
|
<wa-drawer
|
||||||
@transitionend=${this._handleTransitionEnd}
|
placement="bottom"
|
||||||
style=${styleMap({
|
.open=${this._drawerOpen}
|
||||||
height: this._dialogViewportHeight
|
@wa-after-hide=${this._handleAfterHide}
|
||||||
? `${this._dialogViewportHeight}vh`
|
without-header
|
||||||
: "auto",
|
>
|
||||||
maxHeight: `${this._dialogMaxViewpointHeight}vh`,
|
<slot></slot>
|
||||||
minHeight: `${this._dialogMinViewpointHeight}vh`,
|
</wa-drawer>
|
||||||
})}
|
`;
|
||||||
>
|
|
||||||
<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;
|
|
||||||
this._dialogMinViewpointHeight = 20;
|
|
||||||
} 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`
|
static styles = css`
|
||||||
.handle-wrapper {
|
wa-drawer {
|
||||||
position: absolute;
|
--wa-color-surface-raised: var(
|
||||||
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;
|
|
||||||
padding-bottom: 76px;
|
|
||||||
}
|
|
||||||
.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,
|
--ha-dialog-surface-background,
|
||||||
var(--mdc-theme-surface, #fff)
|
var(--mdc-theme-surface, #fff)
|
||||||
);
|
);
|
||||||
display: flex;
|
--spacing: 0;
|
||||||
flex-direction: column;
|
--size: auto;
|
||||||
top: 0;
|
--show-duration: ${BOTTOM_SHEET_ANIMATION_DURATION_MS}ms;
|
||||||
inset-inline-start: 0;
|
--hide-duration: ${BOTTOM_SHEET_ANIMATION_DURATION_MS}ms;
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
wa-drawer::part(dialog) {
|
||||||
dialog.show {
|
border-top-left-radius: var(--ha-border-radius-lg);
|
||||||
transform: translateY(0);
|
border-top-right-radius: var(--ha-border-radius-lg);
|
||||||
|
max-height: 90vh;
|
||||||
|
}
|
||||||
|
wa-drawer::part(body) {
|
||||||
|
padding-bottom: var(--safe-area-inset-bottom);
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@@ -265,8 +65,4 @@ declare global {
|
|||||||
interface HTMLElementTagNameMap {
|
interface HTMLElementTagNameMap {
|
||||||
"ha-bottom-sheet": HaBottomSheet;
|
"ha-bottom-sheet": HaBottomSheet;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface HASSDomEvents {
|
|
||||||
"bottom-sheet-closed": undefined;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
271
src/components/ha-resizable-bottom-sheet.ts
Normal file
271
src/components/ha-resizable-bottom-sheet.ts
Normal file
@@ -0,0 +1,271 @@
|
|||||||
|
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";
|
||||||
|
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.
|
||||||
|
*
|
||||||
|
* @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-resizable-bottom-sheet")
|
||||||
|
export class HaResizableBottomSheet extends LitElement {
|
||||||
|
@query("dialog") private _dialog!: HTMLDialogElement;
|
||||||
|
|
||||||
|
private _dragging = false;
|
||||||
|
|
||||||
|
private _dragStartY = 0;
|
||||||
|
|
||||||
|
private _initialSize = 0;
|
||||||
|
|
||||||
|
@state() private _dialogMaxViewpointHeight = 70;
|
||||||
|
|
||||||
|
@state() private _dialogMinViewpointHeight = 55;
|
||||||
|
|
||||||
|
@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`,
|
||||||
|
minHeight: `${this._dialogMinViewpointHeight}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;
|
||||||
|
this._dialogMinViewpointHeight = 20;
|
||||||
|
} 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;
|
||||||
|
padding-bottom: 76px;
|
||||||
|
}
|
||||||
|
.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 ${BOTTOM_SHEET_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-resizable-bottom-sheet": HaResizableBottomSheet;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface HASSDomEvents {
|
||||||
|
"bottom-sheet-closed": undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import { css, html, LitElement, nothing } from "lit";
|
import { css, html, LitElement, nothing } from "lit";
|
||||||
import { customElement, property, state } from "lit/decorators";
|
import { customElement, property, state } from "lit/decorators";
|
||||||
import { fireEvent } from "../../common/dom/fire_event";
|
import { fireEvent } from "../../common/dom/fire_event";
|
||||||
|
import "../../components/ha-bottom-sheet";
|
||||||
import { createCloseHeading } from "../../components/ha-dialog";
|
import { createCloseHeading } from "../../components/ha-dialog";
|
||||||
import "../../components/ha-icon";
|
import "../../components/ha-icon";
|
||||||
import "../../components/ha-md-list";
|
import "../../components/ha-md-list";
|
||||||
@@ -40,6 +41,54 @@ export class ListItemsDialog
|
|||||||
return nothing;
|
return nothing;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const content = html`
|
||||||
|
<div class="container">
|
||||||
|
<ha-md-list>
|
||||||
|
${this._params.items.map(
|
||||||
|
(item) => html`
|
||||||
|
<ha-md-list-item
|
||||||
|
type="button"
|
||||||
|
@click=${this._itemClicked}
|
||||||
|
.item=${item}
|
||||||
|
>
|
||||||
|
${item.iconPath
|
||||||
|
? html`
|
||||||
|
<ha-svg-icon
|
||||||
|
.path=${item.iconPath}
|
||||||
|
slot="start"
|
||||||
|
class="item-icon"
|
||||||
|
></ha-svg-icon>
|
||||||
|
`
|
||||||
|
: item.icon
|
||||||
|
? html`
|
||||||
|
<ha-icon
|
||||||
|
icon=${item.icon}
|
||||||
|
slot="start"
|
||||||
|
class="item-icon"
|
||||||
|
></ha-icon>
|
||||||
|
`
|
||||||
|
: nothing}
|
||||||
|
<span class="headline">${item.label}</span>
|
||||||
|
${item.description
|
||||||
|
? html`
|
||||||
|
<span class="supporting-text">${item.description}</span>
|
||||||
|
`
|
||||||
|
: nothing}
|
||||||
|
</ha-md-list-item>
|
||||||
|
`
|
||||||
|
)}
|
||||||
|
</ha-md-list>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
if (this._params.mode === "bottom-sheet") {
|
||||||
|
return html`
|
||||||
|
<ha-bottom-sheet placement="bottom" open @closed=${this._dialogClosed}>
|
||||||
|
${content}
|
||||||
|
</ha-bottom-sheet>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<ha-dialog
|
<ha-dialog
|
||||||
open
|
open
|
||||||
@@ -47,43 +96,7 @@ export class ListItemsDialog
|
|||||||
@closed=${this._dialogClosed}
|
@closed=${this._dialogClosed}
|
||||||
hideActions
|
hideActions
|
||||||
>
|
>
|
||||||
<div class="container">
|
${content}
|
||||||
<ha-md-list>
|
|
||||||
${this._params.items.map(
|
|
||||||
(item) => html`
|
|
||||||
<ha-md-list-item
|
|
||||||
type="button"
|
|
||||||
@click=${this._itemClicked}
|
|
||||||
.item=${item}
|
|
||||||
>
|
|
||||||
${item.iconPath
|
|
||||||
? html`
|
|
||||||
<ha-svg-icon
|
|
||||||
.path=${item.iconPath}
|
|
||||||
slot="start"
|
|
||||||
class="item-icon"
|
|
||||||
></ha-svg-icon>
|
|
||||||
`
|
|
||||||
: item.icon
|
|
||||||
? html`
|
|
||||||
<ha-icon
|
|
||||||
icon=${item.icon}
|
|
||||||
slot="start"
|
|
||||||
class="item-icon"
|
|
||||||
></ha-icon>
|
|
||||||
`
|
|
||||||
: nothing}
|
|
||||||
<span class="headline">${item.label}</span>
|
|
||||||
${item.description
|
|
||||||
? html`
|
|
||||||
<span class="supporting-text">${item.description}</span>
|
|
||||||
`
|
|
||||||
: nothing}
|
|
||||||
</ha-md-list-item>
|
|
||||||
`
|
|
||||||
)}
|
|
||||||
</ha-md-list>
|
|
||||||
</div>
|
|
||||||
</ha-dialog>
|
</ha-dialog>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ interface ListItem {
|
|||||||
export interface ListItemsDialogParams {
|
export interface ListItemsDialogParams {
|
||||||
title?: string;
|
title?: string;
|
||||||
items: ListItem[];
|
items: ListItem[];
|
||||||
|
mode?: "dialog" | "bottom-sheet";
|
||||||
}
|
}
|
||||||
|
|
||||||
export const showListItemsDialog = (
|
export const showListItemsDialog = (
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { css, html, LitElement, nothing } from "lit";
|
import { css, html, LitElement, nothing } from "lit";
|
||||||
import { customElement, property, query, state } from "lit/decorators";
|
import { customElement, property, query, state } from "lit/decorators";
|
||||||
import "../../../components/ha-bottom-sheet";
|
import "../../../components/ha-resizable-bottom-sheet";
|
||||||
import type { HaBottomSheet } from "../../../components/ha-bottom-sheet";
|
import type { HaResizableBottomSheet } from "../../../components/ha-resizable-bottom-sheet";
|
||||||
import {
|
import {
|
||||||
isCondition,
|
isCondition,
|
||||||
isScriptField,
|
isScriptField,
|
||||||
@@ -37,7 +37,8 @@ export default class HaAutomationSidebar extends LitElement {
|
|||||||
|
|
||||||
@state() private _yamlMode = false;
|
@state() private _yamlMode = false;
|
||||||
|
|
||||||
@query("ha-bottom-sheet") private _bottomSheetElement?: HaBottomSheet;
|
@query("ha-resizable-bottom-sheet")
|
||||||
|
private _bottomSheetElement?: HaResizableBottomSheet;
|
||||||
|
|
||||||
private _renderContent() {
|
private _renderContent() {
|
||||||
// get config type
|
// get config type
|
||||||
@@ -147,9 +148,9 @@ export default class HaAutomationSidebar extends LitElement {
|
|||||||
|
|
||||||
if (this.narrow) {
|
if (this.narrow) {
|
||||||
return html`
|
return html`
|
||||||
<ha-bottom-sheet @bottom-sheet-closed=${this._closeSidebar}>
|
<ha-resizable-bottom-sheet @bottom-sheet-closed=${this._closeSidebar}>
|
||||||
${this._renderContent()}
|
${this._renderContent()}
|
||||||
</ha-bottom-sheet>
|
</ha-resizable-bottom-sheet>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -230,10 +230,10 @@ class HUIRoot extends LitElement {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: mdiSofa,
|
icon: mdiSofa,
|
||||||
key: "ui.panel.lovelace.menu.add_area",
|
key: "ui.panel.lovelace.menu.create_area",
|
||||||
visible: true,
|
visible: true,
|
||||||
action: this._addArea,
|
action: this._createArea,
|
||||||
overflowAction: this._handleAddArea,
|
overflowAction: this._handleCreateArea,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: mdiAccount,
|
icon: mdiAccount,
|
||||||
@@ -366,6 +366,7 @@ class HUIRoot extends LitElement {
|
|||||||
}
|
}
|
||||||
showListItemsDialog(this, {
|
showListItemsDialog(this, {
|
||||||
title: title,
|
title: title,
|
||||||
|
mode: this.narrow ? "bottom-sheet" : "dialog",
|
||||||
items: i.subItems!.map((si) => ({
|
items: i.subItems!.map((si) => ({
|
||||||
iconPath: si.icon,
|
iconPath: si.icon,
|
||||||
label: this.hass!.localize(si.key),
|
label: this.hass!.localize(si.key),
|
||||||
@@ -837,14 +838,14 @@ class HUIRoot extends LitElement {
|
|||||||
showNewAutomationDialog(this, { mode: "automation" });
|
showNewAutomationDialog(this, { mode: "automation" });
|
||||||
};
|
};
|
||||||
|
|
||||||
private _handleAddArea(ev: CustomEvent<RequestSelectedDetail>): void {
|
private _handleCreateArea(ev: CustomEvent<RequestSelectedDetail>): void {
|
||||||
if (!shouldHandleRequestSelectedEvent(ev)) {
|
if (!shouldHandleRequestSelectedEvent(ev)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this._addArea();
|
this._createArea();
|
||||||
}
|
}
|
||||||
|
|
||||||
private _addArea = async () => {
|
private _createArea = async () => {
|
||||||
await this.hass.loadFragmentTranslation("config");
|
await this.hass.loadFragmentTranslation("config");
|
||||||
showAreaRegistryDetailDialog(this, {
|
showAreaRegistryDetailDialog(this, {
|
||||||
createEntry: async (values) => {
|
createEntry: async (values) => {
|
||||||
@@ -854,13 +855,15 @@ class HUIRoot extends LitElement {
|
|||||||
}
|
}
|
||||||
showToast(this, {
|
showToast(this, {
|
||||||
message: this.hass.localize(
|
message: this.hass.localize(
|
||||||
"ui.panel.lovelace.menu.add_area_success"
|
"ui.panel.lovelace.menu.create_area_success"
|
||||||
),
|
),
|
||||||
action: {
|
action: {
|
||||||
action: () => {
|
action: () => {
|
||||||
navigate(`/config/areas/area/${area.area_id}`);
|
navigate(`/config/areas/area/${area.area_id}`);
|
||||||
},
|
},
|
||||||
text: this.hass.localize("ui.panel.lovelace.menu.add_area_action"),
|
text: this.hass.localize(
|
||||||
|
"ui.panel.lovelace.menu.create_area_action"
|
||||||
|
),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -7059,9 +7059,9 @@
|
|||||||
"add": "Add to Home Assistant",
|
"add": "Add to Home Assistant",
|
||||||
"add_device": "Add device",
|
"add_device": "Add device",
|
||||||
"create_automation": "Create automation",
|
"create_automation": "Create automation",
|
||||||
"add_area": "Add area",
|
"create_area": "Create area",
|
||||||
"add_area_success": "Area added",
|
"create_area_success": "Area created",
|
||||||
"add_area_action": "View area",
|
"create_area_action": "View area",
|
||||||
"add_person_success": "Person added",
|
"add_person_success": "Person added",
|
||||||
"add_person_action": "View persons",
|
"add_person_action": "View persons",
|
||||||
"add_person": "Add person"
|
"add_person": "Add person"
|
||||||
|
|||||||
Reference in New Issue
Block a user