Compare commits

...

3 Commits

Author SHA1 Message Date
Wendelin
e15ffee291 Fix control-switch rtl, add gallery rtl 2026-04-23 13:00:12 +02:00
Wendelin
1d92b0aebd Replace ha-switch with ha-control-switch in hui-entities-toggle component 2026-04-22 14:09:55 +02:00
Wendelin
6e991bc32c Use control switch for entity toggle 2026-04-21 17:11:12 +02:00
5 changed files with 251 additions and 99 deletions

View File

@@ -1,17 +1,22 @@
import "@material/mwc-drawer";
import "@material/mwc-top-app-bar-fixed";
import { mdiMenu } from "@mdi/js";
import { mdiMenu, mdiSwapHorizontal } from "@mdi/js";
import type { PropertyValues } from "lit";
import { LitElement, css, html } from "lit";
import { customElement, query, state } from "lit/decorators";
import { dynamicElement } from "../../src/common/dom/dynamic-element-directive";
import { setDirectionStyles } from "../../src/common/util/compute_rtl";
import "../../src/components/ha-button";
import { HaExpansionPanel } from "../../src/components/ha-expansion-panel";
import "../../src/components/ha-icon-button";
import "../../src/components/ha-svg-icon";
import "../../src/managers/notification-manager";
import { haStyle } from "../../src/resources/styles";
import { PAGES, SIDEBAR } from "../build/import-pages";
import "./components/page-description";
const RTL_STORAGE_KEY = "gallery-rtl";
const GITHUB_DEMO_URL =
"https://github.com/home-assistant/frontend/blob/dev/gallery/src/pages/";
@@ -29,6 +34,8 @@ class HaGallery extends LitElement {
document.location.hash.substring(1) ||
`${SIDEBAR[0].category}/${SIDEBAR[0].pages![0]}`;
@state() private _rtl = localStorage.getItem(RTL_STORAGE_KEY) === "true";
@query("notification-manager")
private _notifications!: HTMLElementTagNameMap["notification-manager"];
@@ -97,33 +104,43 @@ class HaGallery extends LitElement {
${dynamicElement(`demo-${this._page.replace("/", "-")}`)}
</div>
<div class="page-footer">
<div class="header">Help us to improve our documentation</div>
<div class="secondary">
Suggest an edit to this page, or provide/view feedback for this
page.
<div class="edit-docs">
<div class="header">Help us to improve our documentation</div>
<div class="secondary">
Suggest an edit to this page, or provide/view feedback for this
page.
</div>
<div>
${PAGES[this._page].description ||
Object.keys(PAGES[this._page].metadata).length > 0
? html`
<a
href=${`${GITHUB_DEMO_URL}${this._page}.markdown`}
target="_blank"
>
Edit text
</a>
`
: ""}
${PAGES[this._page].demo
? html`
<a
href=${`${GITHUB_DEMO_URL}${this._page}.ts`}
target="_blank"
>
Edit demo
</a>
`
: ""}
</div>
</div>
<div>
${PAGES[this._page].description ||
Object.keys(PAGES[this._page].metadata).length > 0
? html`
<a
href=${`${GITHUB_DEMO_URL}${this._page}.markdown`}
target="_blank"
>
Edit text
</a>
`
: ""}
${PAGES[this._page].demo
? html`
<a
href=${`${GITHUB_DEMO_URL}${this._page}.ts`}
target="_blank"
>
Edit demo
</a>
`
: ""}
<div class="rtl-toggle">
<ha-icon-button
@click=${this._toggleRtl}
.label=${this._rtl ? "Switch to LTR" : "Switch to RTL"}
>
<ha-svg-icon .path=${mdiSwapHorizontal}></ha-svg-icon>
</ha-icon-button>
</div>
</div>
</div>
@@ -138,6 +155,8 @@ class HaGallery extends LitElement {
firstUpdated(changedProps: PropertyValues) {
super.firstUpdated(changedProps);
this._applyDirection();
this.addEventListener("show-notification", (ev) =>
this._notifications.showDialog({ message: ev.detail.message })
);
@@ -164,6 +183,11 @@ class HaGallery extends LitElement {
updated(changedProps: PropertyValues) {
super.updated(changedProps);
if (changedProps.has("_rtl")) {
this._applyDirection();
}
if (!changedProps.has("_page")) {
return;
}
@@ -186,6 +210,15 @@ class HaGallery extends LitElement {
this._drawer.open = !this._drawer.open;
}
private _toggleRtl() {
this._rtl = !this._rtl;
localStorage.setItem(RTL_STORAGE_KEY, String(this._rtl));
}
private _applyDirection() {
setDirectionStyles(this._rtl ? "rtl" : "ltr", this);
}
static styles = [
haStyle,
css`
@@ -238,11 +271,16 @@ class HaGallery extends LitElement {
}
.page-footer {
display: flex;
border-radius: var(--ha-border-radius-lg);
background-color: var(--primary-background-color);
}
.edit-docs {
flex: 1;
text-align: center;
margin: 16px;
padding: 16px;
border-radius: var(--ha-border-radius-lg);
background-color: var(--primary-background-color);
}
.page-footer div {
@@ -266,6 +304,18 @@ class HaGallery extends LitElement {
margin: 0 8px;
text-decoration: none;
}
.rtl-toggle {
padding: var(--ha-space-4);
display: inline-flex;
align-items: flex-end;
margin-top: 12px !important;
}
.rtl-toggle ha-icon-button {
border: 1px solid var(--divider-color);
border-radius: var(--ha-border-radius-pill);
}
`,
];
}

View File

@@ -9,6 +9,7 @@ import { css, html, LitElement } from "lit";
import { customElement, state } from "lit/decorators";
import { ifDefined } from "lit/directives/if-defined";
import { repeat } from "lit/directives/repeat";
import { applyThemesOnElement } from "../../../../src/common/dom/apply_themes_on_element";
import "../../../../src/components/ha-card";
import "../../../../src/components/ha-control-switch";
@@ -50,59 +51,100 @@ export class DemoHaControlSwitch extends LitElement {
protected render(): TemplateResult {
return html`
${repeat(switches, (sw) => {
const { id, label, ...config } = sw;
return html`
<ha-card>
<div class="card-content">
<label id=${id}>${label}</label>
<pre>Config: ${JSON.stringify(config)}</pre>
<ha-control-switch
.checked=${this.checked}
class=${ifDefined(config.class)}
@change=${this.handleValueChanged}
.pathOn=${mdiLightbulb}
.pathOff=${mdiLightbulbOff}
.label=${label}
?disabled=${config.disabled}
?reversed=${config.reversed}
>
</ha-control-switch>
<div class="themes">
${["light", "dark"].map(
(mode) => html`
<div class=${mode}>
<ha-card header="ha-control-switch ${mode}">
${repeat(switches, (sw) => {
const { id, label, ...config } = sw;
return html`
<div class="card-content">
<label id="${mode}-${id}">${label}</label>
<pre>Config: ${JSON.stringify(config)}</pre>
<ha-control-switch
.checked=${this.checked}
class=${ifDefined(config.class)}
@change=${this.handleValueChanged}
.pathOn=${mdiLightbulb}
.pathOff=${mdiLightbulbOff}
.label=${label}
?disabled=${config.disabled}
?reversed=${config.reversed}
>
</ha-control-switch>
</div>
`;
})}
<div class="card-content">
<p class="title"><b>Vertical</b></p>
<div class="vertical-switches">
${repeat(switches, (sw) => {
const { label, ...config } = sw;
return html`
<ha-control-switch
.checked=${this.checked}
vertical
class=${ifDefined(config.class)}
@change=${this.handleValueChanged}
.label=${label}
.pathOn=${mdiGarageOpen}
.pathOff=${mdiGarage}
?disabled=${config.disabled}
?reversed=${config.reversed}
>
</ha-control-switch>
`;
})}
</div>
</div>
</ha-card>
</div>
</ha-card>
`;
})}
<ha-card>
<div class="card-content">
<p class="title"><b>Vertical</b></p>
<div class="vertical-switches">
${repeat(switches, (sw) => {
const { id, label, ...config } = sw;
return html`
<ha-control-switch
.checked=${this.checked}
vertical
class=${ifDefined(config.class)}
@change=${this.handleValueChanged}
.label=${label}
.pathOn=${mdiGarageOpen}
.pathOff=${mdiGarage}
?disabled=${config.disabled}
?reversed=${config.reversed}
>
</ha-control-switch>
`;
})}
</div>
</div>
</ha-card>
`
)}
</div>
`;
}
firstUpdated(changedProps) {
super.firstUpdated(changedProps);
applyThemesOnElement(
this.shadowRoot!.querySelector(".dark"),
{
default_theme: "default",
default_dark_theme: "default",
themes: {},
darkMode: true,
theme: "default",
},
undefined,
undefined,
true
);
}
static styles = css`
:host {
display: block;
}
.themes {
display: flex;
flex-direction: row;
justify-content: center;
flex-wrap: wrap;
gap: 16px;
padding: 16px;
}
.dark,
.light {
display: block;
background-color: var(--primary-background-color);
padding: 16px;
border-radius: var(--ha-border-radius-md);
}
ha-card {
max-width: 600px;
margin: 24px auto;
margin: 0 auto;
}
pre {
margin-top: 0;

View File

@@ -13,9 +13,9 @@ import {
} from "../../data/entity/entity";
import { forwardHaptic } from "../../data/haptics";
import type { HomeAssistant } from "../../types";
import "../ha-control-switch";
import "../ha-formfield";
import "../ha-icon-button";
import "../ha-switch";
const isOn = (stateObj?: HassEntity) =>
stateObj !== undefined &&
@@ -35,7 +35,7 @@ export class HaEntityToggle extends LitElement {
protected render(): TemplateResult {
if (!this.stateObj) {
return html` <ha-switch disabled></ha-switch> `;
return html`<ha-control-switch disabled></ha-control-switch> `;
}
if (
@@ -62,14 +62,14 @@ export class HaEntityToggle extends LitElement {
`;
}
const switchTemplate = html`<ha-switch
const switchTemplate = html`<ha-control-switch
aria-label=${`Toggle ${computeStateName(this.stateObj)} ${
this._isOn ? "off" : "on"
}`}
.checked=${this._isOn}
.disabled=${this.stateObj.state === UNAVAILABLE}
@change=${this._toggleChanged}
></ha-switch>`;
></ha-control-switch>`;
if (!this.label) {
return switchTemplate;
@@ -163,6 +163,10 @@ export class HaEntityToggle extends LitElement {
white-space: nowrap;
min-width: 38px;
}
ha-control-switch {
--control-switch-thickness: 20px;
--control-switch-off-color: var(--state-inactive-color);
}
ha-icon-button {
--ha-icon-button-size: 40px;
color: var(--ha-icon-button-inactive-color, var(--primary-text-color));
@@ -171,9 +175,6 @@ export class HaEntityToggle extends LitElement {
ha-icon-button.state-active {
color: var(--ha-icon-button-active-color, var(--primary-color));
}
ha-switch {
padding: 13px 5px;
}
`;
}

View File

@@ -11,6 +11,7 @@ import { css, html, LitElement } from "lit";
import { customElement, property, query } from "lit/decorators";
import { ifDefined } from "lit/directives/if-defined";
import { fireEvent } from "../common/dom/fire_event";
import { mainWindow } from "../common/dom/get_main_window";
import "./ha-svg-icon";
@customElement("ha-control-switch")
@@ -39,7 +40,7 @@ export class HaControlSwitch extends LitElement {
protected firstUpdated(changedProperties: PropertyValues): void {
super.firstUpdated(changedProperties);
this.setupListeners();
this.setupSwipeListeners();
}
private _toggle() {
@@ -50,7 +51,19 @@ export class HaControlSwitch extends LitElement {
connectedCallback(): void {
super.connectedCallback();
this.setupListeners();
this.setupSwipeListeners();
}
updated(changedProperties: PropertyValues<this>) {
super.updated(changedProperties);
if (
changedProperties.has("disabled") ||
changedProperties.has("vertical") ||
changedProperties.has("reversed")
) {
this.destroyListeners();
this.setupSwipeListeners();
}
}
disconnectedCallback(): void {
@@ -61,7 +74,11 @@ export class HaControlSwitch extends LitElement {
@query("#switch")
private switch!: HTMLDivElement;
setupListeners() {
setupSwipeListeners() {
if (this.disabled) {
return;
}
if (this.switch && !this._mc) {
this._mc = new Manager(this.switch, {
touchAction: this.touchAction ?? (this.vertical ? "pan-x" : "pan-y"),
@@ -90,13 +107,15 @@ export class HaControlSwitch extends LitElement {
} else {
this._mc.on("swiperight", () => {
if (this.disabled) return;
this.checked = !this.reversed;
const isRTL = mainWindow.document.dir === "rtl";
this.checked = (!this.reversed && !isRTL) || (this.reversed && isRTL);
fireEvent(this, "change");
});
this._mc.on("swipeleft", () => {
if (this.disabled) return;
this.checked = !!this.reversed;
const isRTL = mainWindow.document.dir === "rtl";
this.checked = (this.reversed && !isRTL) || (!this.reversed && isRTL);
fireEvent(this, "change");
});
}
@@ -116,11 +135,38 @@ export class HaControlSwitch extends LitElement {
}
private _keydown(ev: any) {
if (ev.key !== "Enter" && ev.key !== " ") {
const supportedKeys = ["Enter", " "];
if (this.vertical) {
supportedKeys.push("ArrowUp", "ArrowDown");
} else {
supportedKeys.push("ArrowLeft", "ArrowRight");
}
if (!supportedKeys.includes(ev.key)) {
return;
}
ev.preventDefault();
this._toggle();
const supportedToggleKeys = ["Enter", " "];
let allowTurnOn = !this.checked;
if (this.reversed) {
allowTurnOn = !allowTurnOn;
}
if (this.vertical) {
supportedToggleKeys.push(allowTurnOn ? "ArrowDown" : "ArrowUp");
} else {
if (mainWindow.document.dir === "rtl") {
allowTurnOn = !allowTurnOn;
}
supportedToggleKeys.push(allowTurnOn ? "ArrowRight" : "ArrowLeft");
}
if (supportedToggleKeys.includes(ev.key)) {
this._toggle();
}
}
protected render(): TemplateResult {
@@ -132,7 +178,7 @@ export class HaControlSwitch extends LitElement {
aria-checked=${this.checked ? "true" : "false"}
aria-label=${ifDefined(this.label)}
role="switch"
tabindex="0"
tabindex=${ifDefined(this.disabled ? undefined : "0")}
?checked=${this.checked}
?disabled=${this.disabled}
>
@@ -156,6 +202,7 @@ export class HaControlSwitch extends LitElement {
--control-switch-on-color: var(--primary-color);
--control-switch-off-color: var(--disabled-color);
--control-switch-background-opacity: 0.2;
--control-switch-hover-background-opacity: 0.4;
--control-switch-thickness: 40px;
--control-switch-border-radius: var(--ha-border-radius-lg);
--control-switch-padding: 4px;
@@ -167,10 +214,10 @@ export class HaControlSwitch extends LitElement {
transition: box-shadow 180ms ease-in-out;
-webkit-tap-highlight-color: transparent;
}
.switch:focus-visible {
.switch:not([disabled]):focus-visible {
box-shadow: 0 0 0 2px var(--control-switch-off-color);
}
.switch[checked]:focus-visible {
.switch[checked]:not([disabled]):focus-visible {
box-shadow: 0 0 0 2px var(--control-switch-on-color);
}
.switch {
@@ -199,6 +246,10 @@ export class HaControlSwitch extends LitElement {
transition: background-color 180ms ease-in-out;
opacity: var(--control-switch-background-opacity);
}
.switch:not([disabled]):focus-visible .background,
.switch:not([disabled]):hover .background {
opacity: var(--control-switch-hover-background-opacity);
}
.switch .button {
width: 50%;
height: 100%;
@@ -222,12 +273,19 @@ export class HaControlSwitch extends LitElement {
transform: translateX(100%);
background-color: var(--control-switch-on-color);
}
.switch[checked] .button:dir(rtl) {
transform: translateX(-100%);
background-color: var(--control-switch-on-color);
}
:host([reversed]) .switch {
flex-direction: row-reverse;
}
:host([reversed]) .switch[checked] .button {
transform: translateX(-100%);
}
:host([reversed]) .switch[checked] .button:dir(rtl) {
transform: translateX(100%);
}
:host([vertical]) {
width: var(--control-switch-thickness);
height: 100%;

View File

@@ -2,7 +2,7 @@ import type { PropertyValues } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { DOMAINS_TOGGLE } from "../../../common/const";
import "../../../components/ha-switch";
import "../../../components/ha-control-switch";
import type { HaSwitch } from "../../../components/ha-switch";
import { forwardHaptic } from "../../../data/haptics";
import type { HomeAssistant } from "../../../types";
@@ -33,7 +33,7 @@ class HuiEntitiesToggle extends LitElement {
}
return html`
<ha-switch
<ha-control-switch
aria-label=${this.hass!.localize(
"ui.panel.lovelace.card.entities.toggle"
)}
@@ -42,18 +42,19 @@ class HuiEntitiesToggle extends LitElement {
return stateObj && stateObj.state === "on";
})}
@change=${this._callService}
></ha-switch>
></ha-control-switch>
`;
}
static styles = css`
:host {
width: 38px;
display: block;
display: flex;
align-items: center;
}
ha-switch {
padding: 13px 5px;
margin: -4px -8px;
ha-control-switch {
--control-switch-thickness: 20px;
--control-switch-off-color: var(--state-inactive-color);
}
`;