mirror of
https://github.com/home-assistant/frontend.git
synced 2026-09-15 21:40:58 +00:00
Compare commits
1
Commits
dev
...
kiosk-advanced
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b52f72ad47 |
@@ -10,6 +10,7 @@ import { applyThemesOnElement } from "../../src/common/dom/apply_themes_on_eleme
|
||||
import { dynamicElement } from "../../src/common/dom/dynamic-element-directive";
|
||||
import type { HASSDomEvent } from "../../src/common/dom/fire_event";
|
||||
import { setDirectionStyles } from "../../src/common/util/compute_rtl";
|
||||
import { NO_KIOSK_ELEMENTS_HIDDEN } from "../../src/data/kiosk_mode";
|
||||
import "../../src/components/ha-button";
|
||||
import "../../src/components/ha-drawer";
|
||||
import { HaExpansionPanel } from "../../src/components/ha-expansion-panel";
|
||||
@@ -633,6 +634,7 @@ class HaGallery extends LitElement {
|
||||
floors: {},
|
||||
hassUrl: (path) => path,
|
||||
kioskMode: false,
|
||||
kioskElementsHidden: NO_KIOSK_ELEMENTS_HIDDEN,
|
||||
language: "en",
|
||||
loadBackendTranslation: async () => galleryLocalize,
|
||||
loadFragmentTranslation: async () => undefined,
|
||||
|
||||
@@ -93,7 +93,8 @@ class HaMenuButton extends LitElement {
|
||||
}
|
||||
|
||||
const showButton =
|
||||
this._ui?.kioskMode === false &&
|
||||
!!this._ui &&
|
||||
!this._ui.kioskElementsHidden.has("sidebar_button") &&
|
||||
(this._narrow || this._ui.dockedSidebar === "always_hidden");
|
||||
|
||||
this._show = showButton || this._alwaysVisible;
|
||||
|
||||
@@ -115,6 +115,7 @@ const updateUi = (
|
||||
value.panelUrl !== hass.panelUrl ||
|
||||
value.dockedSidebar !== hass.dockedSidebar ||
|
||||
value.kioskMode !== hass.kioskMode ||
|
||||
value.kioskElementsHidden !== hass.kioskElementsHidden ||
|
||||
value.enableShortcuts !== hass.enableShortcuts ||
|
||||
value.vibrate !== hass.vibrate ||
|
||||
value.suspendWhenHidden !== hass.suspendWhenHidden
|
||||
@@ -126,6 +127,7 @@ const updateUi = (
|
||||
panelUrl: hass.panelUrl,
|
||||
dockedSidebar: hass.dockedSidebar,
|
||||
kioskMode: hass.kioskMode,
|
||||
kioskElementsHidden: hass.kioskElementsHidden,
|
||||
enableShortcuts: hass.enableShortcuts,
|
||||
vibrate: hass.vibrate,
|
||||
suspendWhenHidden: hass.suspendWhenHidden,
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
/**
|
||||
* The parts of the UI a kiosk client can hide.
|
||||
*
|
||||
* A client names them over the external bus (`kiosk_mode/set`) as either
|
||||
* `excluded_elements` (hide exactly these) or `included_elements` (hide
|
||||
* everything except these), so the same set can be described from either side.
|
||||
*/
|
||||
export const KIOSK_ELEMENTS = [
|
||||
/** The sidebar itself: never docked, only reachable as an overlay. */
|
||||
"sidebar",
|
||||
/** The hamburger button that opens the sidebar, in every panel's toolbar. */
|
||||
"sidebar_button",
|
||||
/** The dashboard view tabs. The current view's title is shown instead. */
|
||||
"dashboard_tabs",
|
||||
/** The "+" menu that adds a device, automation, area or person. */
|
||||
"dashboard_add_button",
|
||||
/** The search button that opens the quick bar. */
|
||||
"dashboard_search_button",
|
||||
/** The Assist button. */
|
||||
"dashboard_assist_button",
|
||||
/** The pencil that switches the dashboard into edit mode. */
|
||||
"dashboard_edit_button",
|
||||
/** The header the app panel draws above an ingress add-on iframe. */
|
||||
"app_panel_header",
|
||||
] as const;
|
||||
|
||||
export type KioskElement = (typeof KIOSK_ELEMENTS)[number];
|
||||
|
||||
const KIOSK_ELEMENTS_SET: ReadonlySet<string> = new Set(KIOSK_ELEMENTS);
|
||||
|
||||
/**
|
||||
* Nothing hidden. Shared instance so context consumers that compare by
|
||||
* reference don't see a change every time kiosk mode is switched off.
|
||||
*/
|
||||
export const NO_KIOSK_ELEMENTS_HIDDEN: ReadonlySet<KioskElement> = new Set();
|
||||
|
||||
/**
|
||||
* What a client gets when it enables kiosk mode without naming any elements.
|
||||
* This is the set kiosk mode hid back when it was a single boolean, so a client
|
||||
* that only sends `enable` keeps the behavior it has today.
|
||||
*/
|
||||
export const DEFAULT_KIOSK_ELEMENTS_HIDDEN: ReadonlySet<KioskElement> = new Set(
|
||||
[
|
||||
"sidebar",
|
||||
"sidebar_button",
|
||||
"dashboard_add_button",
|
||||
"dashboard_search_button",
|
||||
"dashboard_edit_button",
|
||||
"app_panel_header",
|
||||
]
|
||||
);
|
||||
|
||||
export interface KioskModeParams {
|
||||
enable: boolean;
|
||||
/** Hide exactly these. Mutually exclusive with `includedElements`. */
|
||||
excludedElements?: readonly string[];
|
||||
/** Hide everything except these. Mutually exclusive with `excludedElements`. */
|
||||
includedElements?: readonly string[];
|
||||
}
|
||||
|
||||
const isKioskElement = (value: string): value is KioskElement =>
|
||||
KIOSK_ELEMENTS_SET.has(value);
|
||||
|
||||
/**
|
||||
* Resolve a kiosk request into the elements to hide.
|
||||
*
|
||||
* Element names this frontend doesn't know are ignored rather than rejected, so
|
||||
* a newer client naming an element an older frontend has never heard of still
|
||||
* gets the rest of its request honored.
|
||||
*/
|
||||
export const resolveKioskElementsHidden = ({
|
||||
enable,
|
||||
excludedElements,
|
||||
includedElements,
|
||||
}: KioskModeParams): ReadonlySet<KioskElement> => {
|
||||
if (!enable) {
|
||||
return NO_KIOSK_ELEMENTS_HIDDEN;
|
||||
}
|
||||
if (excludedElements) {
|
||||
return new Set(excludedElements.filter(isKioskElement));
|
||||
}
|
||||
if (includedElements) {
|
||||
const shown = new Set(includedElements);
|
||||
return new Set(KIOSK_ELEMENTS.filter((element) => !shown.has(element)));
|
||||
}
|
||||
return DEFAULT_KIOSK_ELEMENTS_HIDDEN;
|
||||
};
|
||||
@@ -98,7 +98,27 @@ export const handleExternalMessage = (
|
||||
} else if (msg.command === "bar_code/aborted") {
|
||||
barCodeListeners.forEach((listener) => listener(msg));
|
||||
} else if (msg.command === "kiosk_mode/set") {
|
||||
fireEvent(window, "hass-kiosk-mode", { enable: msg.payload.enable });
|
||||
const { enable, excluded_elements, included_elements } = msg.payload;
|
||||
// The two lists are two ways of describing the same set, so a message
|
||||
// carrying both has no single meaning we could act on.
|
||||
if (excluded_elements && included_elements) {
|
||||
bus.fireMessage({
|
||||
id: msg.id,
|
||||
type: "result",
|
||||
success: false,
|
||||
error: {
|
||||
code: "invalid_format",
|
||||
message:
|
||||
"Pass either excluded_elements or included_elements, not both",
|
||||
},
|
||||
});
|
||||
return true;
|
||||
}
|
||||
fireEvent(window, "hass-kiosk-mode", {
|
||||
enable,
|
||||
excludedElements: excluded_elements,
|
||||
includedElements: included_elements,
|
||||
});
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { NavigateOptions } from "../common/navigate";
|
||||
import type { AutomationConfig } from "../data/automation";
|
||||
import type { KioskElement } from "../data/kiosk_mode";
|
||||
|
||||
const CALLBACK_EXTERNAL_BUS = "externalBus";
|
||||
|
||||
@@ -327,6 +328,16 @@ export interface EMIncomingMessageKioskModeSet {
|
||||
command: "kiosk_mode/set";
|
||||
payload: {
|
||||
enable: boolean;
|
||||
/**
|
||||
* Hide exactly these parts of the UI. Mutually exclusive with
|
||||
* `included_elements`; sending both fails the command.
|
||||
*/
|
||||
excluded_elements?: KioskElement[];
|
||||
/**
|
||||
* Hide every part of the UI except these. Mutually exclusive with
|
||||
* `excluded_elements`; sending both fails the command.
|
||||
*/
|
||||
included_elements?: KioskElement[];
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
} from "../data/context";
|
||||
import { updateHassGroups } from "../data/context/updateContext";
|
||||
import type { IconCategory } from "../data/icons";
|
||||
import { NO_KIOSK_ELEMENTS_HIDDEN } from "../data/kiosk_mode";
|
||||
import type { EntityRegistryDisplayEntry } from "../data/entity/entity_registry";
|
||||
import {
|
||||
DateFormat,
|
||||
@@ -464,6 +465,7 @@ export const provideHass = (
|
||||
vibrate: true,
|
||||
debugConnection: false,
|
||||
kioskMode: false,
|
||||
kioskElementsHidden: NO_KIOSK_ELEMENTS_HIDDEN,
|
||||
suspendWhenHidden: false,
|
||||
// @ts-ignore
|
||||
async callService(domain, service, data, target, _notify, returnResponse) {
|
||||
|
||||
@@ -51,7 +51,9 @@ export class HomeAssistantMain extends LitElement {
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const sidebarNarrow =
|
||||
this._sidebarNarrow || this._externalSidebar || this.hass.kioskMode;
|
||||
this._sidebarNarrow ||
|
||||
this._externalSidebar ||
|
||||
this.hass.kioskElementsHidden.has("sidebar");
|
||||
|
||||
const isPanelReady =
|
||||
this.hass.panels && this.hass.userData && this.hass.systemData;
|
||||
@@ -105,7 +107,7 @@ export class HomeAssistantMain extends LitElement {
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (this._sidebarNarrow || this.hass.kioskMode) {
|
||||
if (this._sidebarNarrow || this.hass.kioskElementsHidden.has("sidebar")) {
|
||||
this._drawerOpen = ev.detail?.open ?? !this._drawerOpen;
|
||||
} else {
|
||||
fireEvent(this, "hass-dock-sidebar", {
|
||||
@@ -144,7 +146,9 @@ export class HomeAssistantMain extends LitElement {
|
||||
|
||||
this.toggleAttribute(
|
||||
"modal",
|
||||
this._sidebarNarrow || this._externalSidebar || this.hass.kioskMode
|
||||
this._sidebarNarrow ||
|
||||
this._externalSidebar ||
|
||||
this.hass.kioskElementsHidden.has("sidebar")
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -49,8 +49,6 @@ class HaPanelApp extends LitElement {
|
||||
|
||||
@state() private _loadingMessage?: string;
|
||||
|
||||
@state() private _kioskMode = false;
|
||||
|
||||
@state() private _iframeLoaded = false;
|
||||
|
||||
// Set when the addon signals (via subscribe-properties) that it handles the
|
||||
@@ -83,11 +81,6 @@ class HaPanelApp extends LitElement {
|
||||
) {
|
||||
this._sendPropertiesToIframe();
|
||||
}
|
||||
|
||||
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
|
||||
if (oldHass && oldHass.kioskMode !== this.hass.kioskMode) {
|
||||
this._kioskMode = this.hass.kioskMode;
|
||||
}
|
||||
}
|
||||
|
||||
public connectedCallback() {
|
||||
@@ -121,10 +114,12 @@ class HaPanelApp extends LitElement {
|
||||
></hass-loading-screen>`;
|
||||
}
|
||||
|
||||
const hideHeader = this.hass.kioskElementsHidden.has("app_panel_header");
|
||||
|
||||
// Make sure this all is 1 template so hiding toolbar doesn't reload iframe
|
||||
return html`
|
||||
${
|
||||
!this._kioskMode &&
|
||||
!hideHeader &&
|
||||
(this.narrow || this.hass.dockedSidebar === "always_hidden")
|
||||
? html`
|
||||
<div class="header">
|
||||
@@ -141,7 +136,7 @@ class HaPanelApp extends LitElement {
|
||||
<iframe
|
||||
class=${classMap({
|
||||
loaded: this._iframeLoaded,
|
||||
"kiosk-mode": this._kioskMode,
|
||||
"kiosk-mode": hideHeader,
|
||||
"handle-safe-area": this._handleSafeArea,
|
||||
})}
|
||||
title=${this._addon.name}
|
||||
|
||||
@@ -257,7 +257,9 @@ class HUIRoot extends LitElement {
|
||||
icon: mdiPlus,
|
||||
key: "ui.panel.lovelace.menu.add",
|
||||
visible:
|
||||
!this._editMode && this.hass.user?.is_admin && !this.hass.kioskMode,
|
||||
!this._editMode &&
|
||||
this.hass.user?.is_admin &&
|
||||
!this.hass.kioskElementsHidden.has("dashboard_add_button"),
|
||||
overflow: this.narrow,
|
||||
subItems: [
|
||||
{
|
||||
@@ -300,7 +302,9 @@ class HUIRoot extends LitElement {
|
||||
? "(⌘ + K)"
|
||||
: "(Ctrl + K)"
|
||||
: undefined,
|
||||
visible: !this._editMode && !this.hass.kioskMode,
|
||||
visible:
|
||||
!this._editMode &&
|
||||
!this.hass.kioskElementsHidden.has("dashboard_search_button"),
|
||||
overflow: this.narrow,
|
||||
},
|
||||
{
|
||||
@@ -311,7 +315,9 @@ class HUIRoot extends LitElement {
|
||||
suffix:
|
||||
this.hass.enableShortcuts && !isMobileClient ? "(A)" : undefined,
|
||||
visible:
|
||||
!this._editMode && this._conversation(this.hass.config.components),
|
||||
!this._editMode &&
|
||||
this._conversation(this.hass.config.components) &&
|
||||
!this.hass.kioskElementsHidden.has("dashboard_assist_button"),
|
||||
overflow: this.narrow,
|
||||
},
|
||||
{
|
||||
@@ -347,7 +353,7 @@ class HUIRoot extends LitElement {
|
||||
!this._editMode &&
|
||||
this.hass!.user?.is_admin &&
|
||||
!this.hass!.config.recovery_mode &&
|
||||
!this.hass.kioskMode &&
|
||||
!this.hass.kioskElementsHidden.has("dashboard_edit_button") &&
|
||||
!this.noEdit,
|
||||
overflow: true,
|
||||
overflow_can_promote: true,
|
||||
@@ -549,6 +555,7 @@ class HUIRoot extends LitElement {
|
||||
|
||||
const isSubview = curViewConfig?.subview;
|
||||
const hasTabViews = views.filter((view) => !view.subview).length > 1;
|
||||
const hideTabs = this.hass.kioskElementsHidden.has("dashboard_tabs");
|
||||
|
||||
return html`
|
||||
<div
|
||||
@@ -605,13 +612,19 @@ class HUIRoot extends LitElement {
|
||||
${curViewConfig.title}
|
||||
</div>
|
||||
`
|
||||
: hasTabViews
|
||||
? tabs
|
||||
: html`
|
||||
: hideTabs
|
||||
? html`
|
||||
<div class="main-title">
|
||||
${views[0]?.title ?? dashboardTitle}
|
||||
${curViewConfig?.title ?? dashboardTitle}
|
||||
</div>
|
||||
`
|
||||
: hasTabViews
|
||||
? tabs
|
||||
: html`
|
||||
<div class="main-title">
|
||||
${views[0]?.title ?? dashboardTitle}
|
||||
</div>
|
||||
`
|
||||
}
|
||||
<div class="action-items">
|
||||
${this._renderActionItems()}
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
subscribeFrontendUserData,
|
||||
} from "../data/frontend";
|
||||
import { forwardHaptic } from "../data/haptics";
|
||||
import { NO_KIOSK_ELEMENTS_HIDDEN } from "../data/kiosk_mode";
|
||||
import { serviceCallWillDisconnect } from "../data/service";
|
||||
import {
|
||||
DateFormat,
|
||||
@@ -85,6 +86,7 @@ export const connectionMixin = <T extends Constructor<HassBaseEl>>(
|
||||
localize: () => "",
|
||||
translationMetadata,
|
||||
kioskMode: false,
|
||||
kioskElementsHidden: NO_KIOSK_ELEMENTS_HIDDEN,
|
||||
dockedSidebar: "docked",
|
||||
vibrate: true,
|
||||
debugConnection: __DEV__,
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import type { PropertyValues } from "lit";
|
||||
import type { HASSDomEvent } from "../common/dom/fire_event";
|
||||
import type { KioskModeParams } from "../data/kiosk_mode";
|
||||
import { resolveKioskElementsHidden } from "../data/kiosk_mode";
|
||||
import type { Constructor, HomeAssistant } from "../types";
|
||||
import { storeState } from "../util/ha-pref-storage";
|
||||
import type { HassBaseEl } from "./hass-base-mixin";
|
||||
@@ -12,7 +14,7 @@ declare global {
|
||||
// for fire event
|
||||
interface HASSDomEvents {
|
||||
"hass-dock-sidebar": DockSidebarParams;
|
||||
"hass-kiosk-mode": { enable: boolean };
|
||||
"hass-kiosk-mode": KioskModeParams;
|
||||
}
|
||||
// for add event listener
|
||||
interface HTMLElementEventMap {
|
||||
@@ -32,7 +34,10 @@ export default <T extends Constructor<HassBaseEl>>(superClass: T) =>
|
||||
storeState(this.hass!);
|
||||
});
|
||||
window.addEventListener("hass-kiosk-mode", (ev) => {
|
||||
this._updateHass({ kioskMode: ev.detail.enable });
|
||||
this._updateHass({
|
||||
kioskMode: ev.detail.enable,
|
||||
kioskElementsHidden: resolveKioskElementsHidden(ev.detail),
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -18,6 +18,7 @@ import type { AreaRegistryEntry } from "./data/area/area_registry";
|
||||
import type { DeviceRegistryEntry } from "./data/device/device_registry";
|
||||
import type { EntityRegistryDisplayEntry } from "./data/entity/entity_registry";
|
||||
import type { FloorRegistryEntry } from "./data/floor_registry";
|
||||
import type { KioskElement } from "./data/kiosk_mode";
|
||||
import type {
|
||||
CoreFrontendSystemData,
|
||||
CoreFrontendUserData,
|
||||
@@ -305,6 +306,8 @@ export interface HomeAssistantUI {
|
||||
panelUrl: string;
|
||||
dockedSidebar: "docked" | "always_hidden" | "auto";
|
||||
kioskMode: boolean;
|
||||
/** Parts of the UI a kiosk client asked to hide. Empty when kiosk mode is off. */
|
||||
kioskElementsHidden: ReadonlySet<KioskElement>;
|
||||
enableShortcuts: boolean;
|
||||
vibrate: boolean;
|
||||
suspendWhenHidden: boolean;
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import type { KioskElement } from "../../src/data/kiosk_mode";
|
||||
import {
|
||||
DEFAULT_KIOSK_ELEMENTS_HIDDEN,
|
||||
KIOSK_ELEMENTS,
|
||||
NO_KIOSK_ELEMENTS_HIDDEN,
|
||||
resolveKioskElementsHidden,
|
||||
} from "../../src/data/kiosk_mode";
|
||||
|
||||
const hidden = (params: Parameters<typeof resolveKioskElementsHidden>[0]) =>
|
||||
[...resolveKioskElementsHidden(params)].sort();
|
||||
|
||||
describe("resolveKioskElementsHidden", () => {
|
||||
it("hides nothing when kiosk mode is off, whatever the client listed", () => {
|
||||
expect(
|
||||
resolveKioskElementsHidden({
|
||||
enable: false,
|
||||
excludedElements: ["sidebar"],
|
||||
})
|
||||
).toBe(NO_KIOSK_ELEMENTS_HIDDEN);
|
||||
});
|
||||
|
||||
it("falls back to the default set when no elements are named", () => {
|
||||
expect(resolveKioskElementsHidden({ enable: true })).toBe(
|
||||
DEFAULT_KIOSK_ELEMENTS_HIDDEN
|
||||
);
|
||||
});
|
||||
|
||||
it("hides exactly the excluded elements", () => {
|
||||
expect(
|
||||
hidden({
|
||||
enable: true,
|
||||
excludedElements: ["sidebar_button", "dashboard_edit_button"],
|
||||
})
|
||||
).toEqual(["dashboard_edit_button", "sidebar_button"]);
|
||||
});
|
||||
|
||||
it("hides everything but the included elements", () => {
|
||||
const shown: KioskElement[] = ["sidebar", "dashboard_tabs"];
|
||||
expect(hidden({ enable: true, includedElements: shown })).toEqual(
|
||||
KIOSK_ELEMENTS.filter((element) => !shown.includes(element)).sort()
|
||||
);
|
||||
});
|
||||
|
||||
it("hides everything when the included list is empty", () => {
|
||||
expect(hidden({ enable: true, includedElements: [] })).toEqual(
|
||||
[...KIOSK_ELEMENTS].sort()
|
||||
);
|
||||
});
|
||||
|
||||
// A newer client may name elements this frontend has never heard of; the rest
|
||||
// of its request still has to be honored.
|
||||
it("ignores unknown element names in either list", () => {
|
||||
expect(
|
||||
hidden({ enable: true, excludedElements: ["sidebar", "from_the_future"] })
|
||||
).toEqual(["sidebar"]);
|
||||
expect(
|
||||
hidden({
|
||||
enable: true,
|
||||
includedElements: [...KIOSK_ELEMENTS, "from_the_future"],
|
||||
})
|
||||
).toEqual([]);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user