diff --git a/src/components/ha-control-select-menu.ts b/src/components/ha-control-select-menu.ts index 1984f50ffd24..04422d890e57 100644 --- a/src/components/ha-control-select-menu.ts +++ b/src/components/ha-control-select-menu.ts @@ -1,10 +1,12 @@ import { Ripple } from "@material/mwc-ripple"; import { RippleHandlers } from "@material/mwc-ripple/ripple-handlers"; import { SelectBase } from "@material/mwc-select/mwc-select-base"; +import { mdiMenuDown } from "@mdi/js"; import { css, html, nothing } from "lit"; import { customElement, eventOptions, + property, query, queryAsync, state, @@ -24,6 +26,12 @@ export class HaControlSelectMenu extends SelectBase { @query(".select-anchor") protected anchorElement!: HTMLDivElement | null; + @property({ type: Boolean, attribute: "show-arrow" }) + public showArrow?: boolean; + + @property({ type: Boolean, attribute: "hide-label" }) + public hideLabel?: boolean; + @queryAsync("mwc-ripple") private _ripple!: Promise; @state() private _shouldRenderRipple = false; @@ -36,7 +44,9 @@ export class HaControlSelectMenu extends SelectBase { "select-no-value": !this.selectedText, }; - const labelledby = this.label ? "label" : undefined; + const labelledby = this.label && !this.hideLabel ? "label" : undefined; + const labelAttribute = + this.label && this.hideLabel ? this.label : undefined; return html`
@@ -57,6 +67,7 @@ export class HaControlSelectMenu extends SelectBase { aria-invalid=${!this.isUiValid} aria-haspopup="listbox" aria-labelledby=${ifDefined(labelledby)} + aria-label=${ifDefined(labelAttribute)} aria-required=${this.required} @click=${this.onClick} @focus=${this.onFocus} @@ -72,11 +83,14 @@ export class HaControlSelectMenu extends SelectBase { > ${this.renderIcon()}
-

${this.label}

+ ${this.hideLabel + ? nothing + : html`

${this.label}

`} ${this.selectedText ? html`

${this.selectedText}

` : nothing}
+ ${this.renderArrow()} ${this._shouldRenderRipple && !this.disabled ? html` ` : nothing} @@ -86,13 +100,29 @@ export class HaControlSelectMenu extends SelectBase { `; } + private renderArrow() { + if (!this.showArrow) return nothing; + + return html` +
+ +
+ `; + } + private renderIcon() { const index = this.mdcFoundation?.getSelectedIndex(); const items = this.menuElement?.items ?? []; const item = index != null ? items[index] : undefined; - const icon = - item?.querySelector("[slot='graphic']") ?? - (null as HaSvgIcon | HaIcon | null); + const defaultIcon = this.querySelector("[slot='icon']"); + const icon = (item?.querySelector("[slot='graphic']") ?? null) as + | HaSvgIcon + | HaIcon + | null; + + if (!defaultIcon && !icon) { + return null; + } return html`
@@ -171,14 +201,18 @@ export class HaControlSelectMenu extends SelectBase { --control-select-menu-background-color: var(--disabled-color); --control-select-menu-background-opacity: 0.2; --control-select-menu-border-radius: 14px; + --control-select-menu-height: 48px; + --control-select-menu-padding: 6px 10px; --mdc-icon-size: 20px; + font-size: 14px; + line-height: 1.4; width: auto; color: var(--primary-text-color); -webkit-tap-highlight-color: transparent; } .select-anchor { - height: 48px; - padding: 6px 10px; + height: var(--control-select-menu-height); + padding: var(--control-select-menu-padding); overflow: hidden; position: relative; cursor: pointer; @@ -193,15 +227,12 @@ export class HaControlSelectMenu extends SelectBase { --mdc-ripple-color: var(--control-select-menu-background-color); /* For safari border-radius overflow */ z-index: 0; - font-size: inherit; transition: color 180ms ease-in-out; gap: 10px; width: 100%; user-select: none; - font-size: 14px; font-style: normal; font-weight: 400; - line-height: 20px; letter-spacing: 0.25px; } .content { @@ -223,8 +254,7 @@ export class HaControlSelectMenu extends SelectBase { } .label { - font-size: 12px; - line-height: 16px; + font-size: 0.85em; letter-spacing: 0.4px; } diff --git a/src/panels/lovelace/cards/hui-tile-card.ts b/src/panels/lovelace/cards/hui-tile-card.ts index 915bed56d44a..89e61417203f 100644 --- a/src/panels/lovelace/cards/hui-tile-card.ts +++ b/src/panels/lovelace/cards/hui-tile-card.ts @@ -410,7 +410,6 @@ export class HuiTileCard extends LitElement implements LovelaceCard { ha-card { --mdc-ripple-color: var(--tile-color); height: 100%; - z-index: 0; overflow: hidden; transition: box-shadow 180ms ease-in-out, diff --git a/src/panels/lovelace/create-element/create-tile-feature-element.ts b/src/panels/lovelace/create-element/create-tile-feature-element.ts index 41f6e2b699bc..e8af650baad8 100644 --- a/src/panels/lovelace/create-element/create-tile-feature-element.ts +++ b/src/panels/lovelace/create-element/create-tile-feature-element.ts @@ -1,6 +1,5 @@ import "../tile-features/hui-alarm-modes-tile-feature"; import "../tile-features/hui-climate-hvac-modes-tile-feature"; -import "../tile-features/hui-target-temperature-tile-feature"; import "../tile-features/hui-cover-open-close-tile-feature"; import "../tile-features/hui-cover-position-tile-feature"; import "../tile-features/hui-cover-tilt-position-tile-feature"; @@ -9,6 +8,8 @@ import "../tile-features/hui-fan-speed-tile-feature"; import "../tile-features/hui-lawn-mower-commands-tile-feature"; import "../tile-features/hui-light-brightness-tile-feature"; import "../tile-features/hui-light-color-temp-tile-feature"; +import "../tile-features/hui-select-options-tile-feature"; +import "../tile-features/hui-target-temperature-tile-feature"; import "../tile-features/hui-vacuum-commands-tile-feature"; import "../tile-features/hui-water-heater-operation-modes-tile-feature"; import { LovelaceTileFeatureConfig } from "../tile-features/types"; @@ -28,6 +29,7 @@ const TYPES: Set = new Set([ "lawn-mower-commands", "light-brightness", "light-color-temp", + "select-options", "target-temperature", "vacuum-commands", "water-heater-operation-modes", diff --git a/src/panels/lovelace/editor/config-elements/hui-tile-card-features-editor.ts b/src/panels/lovelace/editor/config-elements/hui-tile-card-features-editor.ts index 773eb60f13eb..ea8d25b91a3b 100644 --- a/src/panels/lovelace/editor/config-elements/hui-tile-card-features-editor.ts +++ b/src/panels/lovelace/editor/config-elements/hui-tile-card-features-editor.ts @@ -35,6 +35,7 @@ import { supportsFanSpeedTileFeature } from "../../tile-features/hui-fan-speed-t import { supportsLawnMowerCommandTileFeature } from "../../tile-features/hui-lawn-mower-commands-tile-feature"; import { supportsLightBrightnessTileFeature } from "../../tile-features/hui-light-brightness-tile-feature"; import { supportsLightColorTempTileFeature } from "../../tile-features/hui-light-color-temp-tile-feature"; +import { supportsSelectOptionTileFeature } from "../../tile-features/hui-select-options-tile-feature"; import { supportsTargetTemperatureTileFeature } from "../../tile-features/hui-target-temperature-tile-feature"; import { supportsVacuumCommandTileFeature } from "../../tile-features/hui-vacuum-commands-tile-feature"; import { supportsWaterHeaterOperationModesTileFeature } from "../../tile-features/hui-water-heater-operation-modes-tile-feature"; @@ -46,7 +47,6 @@ type SupportsFeature = (stateObj: HassEntity) => boolean; const FEATURE_TYPES: FeatureType[] = [ "alarm-modes", "climate-hvac-modes", - "target-temperature", "cover-open-close", "cover-position", "cover-tilt-position", @@ -55,6 +55,8 @@ const FEATURE_TYPES: FeatureType[] = [ "lawn-mower-commands", "light-brightness", "light-color-temp", + "select-options", + "target-temperature", "vacuum-commands", "water-heater-operation-modes", ]; @@ -83,6 +85,7 @@ const SUPPORTS_FEATURE_TYPES: Record = "vacuum-commands": supportsVacuumCommandTileFeature, "water-heater-operation-modes": supportsWaterHeaterOperationModesTileFeature, + "select-options": supportsSelectOptionTileFeature, }; const CUSTOM_FEATURE_ENTRIES: Record< diff --git a/src/panels/lovelace/tile-features/hui-select-options-tile-feature.ts b/src/panels/lovelace/tile-features/hui-select-options-tile-feature.ts new file mode 100644 index 000000000000..13c2fb956722 --- /dev/null +++ b/src/panels/lovelace/tile-features/hui-select-options-tile-feature.ts @@ -0,0 +1,154 @@ +import { HassEntity } from "home-assistant-js-websocket"; +import { css, html, LitElement, nothing, PropertyValues } from "lit"; +import { customElement, property, query, state } from "lit/decorators"; +import { stopPropagation } from "../../../common/dom/stop_propagation"; +import { computeDomain } from "../../../common/entity/compute_domain"; +import "../../../components/ha-control-select-menu"; +import type { HaControlSelectMenu } from "../../../components/ha-control-select-menu"; +import { UNAVAILABLE } from "../../../data/entity"; +import { InputSelectEntity } from "../../../data/input_select"; +import { SelectEntity } from "../../../data/select"; +import { HomeAssistant } from "../../../types"; +import { LovelaceTileFeature } from "../types"; +import { SelectOptionsTileFeatureConfig } from "./types"; + +export const supportsSelectOptionTileFeature = (stateObj: HassEntity) => { + const domain = computeDomain(stateObj.entity_id); + return domain === "select" || domain === "input_select"; +}; + +@customElement("hui-select-options-tile-feature") +class HuiSelectOptionsTileFeature + extends LitElement + implements LovelaceTileFeature +{ + @property({ attribute: false }) public hass?: HomeAssistant; + + @property({ attribute: false }) public stateObj?: + | SelectEntity + | InputSelectEntity; + + @state() private _config?: SelectOptionsTileFeatureConfig; + + @state() _currentOption?: string; + + @query("ha-control-select-menu", true) + private _haSelect!: HaControlSelectMenu; + + static getStubConfig(): SelectOptionsTileFeatureConfig { + return { + type: "select-options", + }; + } + + public setConfig(config: SelectOptionsTileFeatureConfig): void { + if (!config) { + throw new Error("Invalid configuration"); + } + this._config = config; + } + + protected willUpdate(changedProp: PropertyValues): void { + super.willUpdate(changedProp); + if (changedProp.has("stateObj") && this.stateObj) { + this._currentOption = this.stateObj.state; + } + } + + protected updated(changedProps: PropertyValues) { + super.updated(changedProps); + if (changedProps.has("hass")) { + const oldHass = changedProps.get("hass") as HomeAssistant | undefined; + if ( + this.hass && + this.hass.formatEntityAttributeValue !== + oldHass?.formatEntityAttributeValue + ) { + this._haSelect.layoutOptions(); + } + } + } + + private async _valueChanged(ev: CustomEvent) { + const option = (ev.target as any).value as string; + + if (option === this.stateObj!.state) return; + + const oldOption = this.stateObj!.state; + this._currentOption = option; + + try { + await this._setOption(option); + } catch (err) { + this._currentOption = oldOption; + } + } + + private async _setOption(option: string) { + const domain = computeDomain(this.stateObj!.entity_id); + await this.hass!.callService(domain, "select_option", { + entity_id: this.stateObj!.entity_id, + option: option, + }); + } + + protected render() { + if ( + !this._config || + !this.hass || + !this.stateObj || + !supportsSelectOptionTileFeature(this.stateObj) + ) { + return nothing; + } + + const stateObj = this.stateObj; + + return html` +
+ + ${stateObj.attributes.options!.map( + (option) => html` + + ${this.hass!.formatEntityState(stateObj, option)} + + ` + )} + +
+ `; + } + + static get styles() { + return css` + ha-control-select-menu { + box-sizing: border-box; + --control-select-menu-height: 40px; + --control-select-menu-border-radius: 10px; + line-height: 1.2; + display: block; + width: 100%; + } + .container { + padding: 0 12px 12px 12px; + width: auto; + } + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + "hui-select-options-tile-feature": HuiSelectOptionsTileFeature; + } +} diff --git a/src/panels/lovelace/tile-features/types.ts b/src/panels/lovelace/tile-features/types.ts index 6401c28ec5a8..809cffe1d910 100644 --- a/src/panels/lovelace/tile-features/types.ts +++ b/src/panels/lovelace/tile-features/types.ts @@ -40,6 +40,10 @@ export interface ClimateHvacModesTileFeatureConfig { hvac_modes?: HvacMode[]; } +export interface SelectOptionsTileFeatureConfig { + type: "select-options"; +} + export interface TargetTemperatureTileFeatureConfig { type: "target-temperature"; } @@ -86,7 +90,8 @@ export type LovelaceTileFeatureConfig = | LightColorTempTileFeatureConfig | VacuumCommandsTileFeatureConfig | TargetTemperatureTileFeatureConfig - | WaterHeaterOperationModesTileFeatureConfig; + | WaterHeaterOperationModesTileFeatureConfig + | SelectOptionsTileFeatureConfig; export type LovelaceTileFeatureContext = { entity_id?: string; diff --git a/src/state/state-display-mixin.ts b/src/state/state-display-mixin.ts index 5f160c41e072..4ba3243b5120 100644 --- a/src/state/state-display-mixin.ts +++ b/src/state/state-display-mixin.ts @@ -17,15 +17,15 @@ export default >(superClass: T) => { } const oldHass = changedProps.get("hass") as HomeAssistant | undefined; - if (this.hass) { - if ( - this.hass.localize !== oldHass?.localize || + if ( + this.hass && + (!oldHass || + this.hass.localize !== oldHass.localize || this.hass.locale !== oldHass.locale || this.hass.config !== oldHass.config || - this.hass.entities !== oldHass.entities - ) { - this._updateStateDisplay(); - } + this.hass.entities !== oldHass.entities) + ) { + this._updateStateDisplay(); } }