Add select option tile feature (#17971)

This commit is contained in:
Paul Bottein 2023-09-20 12:43:21 +02:00 committed by GitHub
parent 3349031cbd
commit 4b5c7021ff
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 216 additions and 23 deletions

View File

@ -1,10 +1,12 @@
import { Ripple } from "@material/mwc-ripple"; import { Ripple } from "@material/mwc-ripple";
import { RippleHandlers } from "@material/mwc-ripple/ripple-handlers"; import { RippleHandlers } from "@material/mwc-ripple/ripple-handlers";
import { SelectBase } from "@material/mwc-select/mwc-select-base"; import { SelectBase } from "@material/mwc-select/mwc-select-base";
import { mdiMenuDown } from "@mdi/js";
import { css, html, nothing } from "lit"; import { css, html, nothing } from "lit";
import { import {
customElement, customElement,
eventOptions, eventOptions,
property,
query, query,
queryAsync, queryAsync,
state, state,
@ -24,6 +26,12 @@ export class HaControlSelectMenu extends SelectBase {
@query(".select-anchor") protected anchorElement!: HTMLDivElement | null; @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<Ripple | null>; @queryAsync("mwc-ripple") private _ripple!: Promise<Ripple | null>;
@state() private _shouldRenderRipple = false; @state() private _shouldRenderRipple = false;
@ -36,7 +44,9 @@ export class HaControlSelectMenu extends SelectBase {
"select-no-value": !this.selectedText, "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` return html`
<div class="select ${classMap(classes)}"> <div class="select ${classMap(classes)}">
@ -57,6 +67,7 @@ export class HaControlSelectMenu extends SelectBase {
aria-invalid=${!this.isUiValid} aria-invalid=${!this.isUiValid}
aria-haspopup="listbox" aria-haspopup="listbox"
aria-labelledby=${ifDefined(labelledby)} aria-labelledby=${ifDefined(labelledby)}
aria-label=${ifDefined(labelAttribute)}
aria-required=${this.required} aria-required=${this.required}
@click=${this.onClick} @click=${this.onClick}
@focus=${this.onFocus} @focus=${this.onFocus}
@ -72,11 +83,14 @@ export class HaControlSelectMenu extends SelectBase {
> >
${this.renderIcon()} ${this.renderIcon()}
<div class="content"> <div class="content">
<p id="label" class="label">${this.label}</p> ${this.hideLabel
? nothing
: html`<p id="label" class="label">${this.label}</p>`}
${this.selectedText ${this.selectedText
? html`<p class="value">${this.selectedText}</p>` ? html`<p class="value">${this.selectedText}</p>`
: nothing} : nothing}
</div> </div>
${this.renderArrow()}
${this._shouldRenderRipple && !this.disabled ${this._shouldRenderRipple && !this.disabled
? html` <mwc-ripple></mwc-ripple> ` ? html` <mwc-ripple></mwc-ripple> `
: nothing} : nothing}
@ -86,13 +100,29 @@ export class HaControlSelectMenu extends SelectBase {
`; `;
} }
private renderArrow() {
if (!this.showArrow) return nothing;
return html`
<div class="icon">
<ha-svg-icon .path=${mdiMenuDown}></ha-svg-icon>
</div>
`;
}
private renderIcon() { private renderIcon() {
const index = this.mdcFoundation?.getSelectedIndex(); const index = this.mdcFoundation?.getSelectedIndex();
const items = this.menuElement?.items ?? []; const items = this.menuElement?.items ?? [];
const item = index != null ? items[index] : undefined; const item = index != null ? items[index] : undefined;
const icon = const defaultIcon = this.querySelector("[slot='icon']");
item?.querySelector("[slot='graphic']") ?? const icon = (item?.querySelector("[slot='graphic']") ?? null) as
(null as HaSvgIcon | HaIcon | null); | HaSvgIcon
| HaIcon
| null;
if (!defaultIcon && !icon) {
return null;
}
return html` return html`
<div class="icon"> <div class="icon">
@ -171,14 +201,18 @@ export class HaControlSelectMenu extends SelectBase {
--control-select-menu-background-color: var(--disabled-color); --control-select-menu-background-color: var(--disabled-color);
--control-select-menu-background-opacity: 0.2; --control-select-menu-background-opacity: 0.2;
--control-select-menu-border-radius: 14px; --control-select-menu-border-radius: 14px;
--control-select-menu-height: 48px;
--control-select-menu-padding: 6px 10px;
--mdc-icon-size: 20px; --mdc-icon-size: 20px;
font-size: 14px;
line-height: 1.4;
width: auto; width: auto;
color: var(--primary-text-color); color: var(--primary-text-color);
-webkit-tap-highlight-color: transparent; -webkit-tap-highlight-color: transparent;
} }
.select-anchor { .select-anchor {
height: 48px; height: var(--control-select-menu-height);
padding: 6px 10px; padding: var(--control-select-menu-padding);
overflow: hidden; overflow: hidden;
position: relative; position: relative;
cursor: pointer; cursor: pointer;
@ -193,15 +227,12 @@ export class HaControlSelectMenu extends SelectBase {
--mdc-ripple-color: var(--control-select-menu-background-color); --mdc-ripple-color: var(--control-select-menu-background-color);
/* For safari border-radius overflow */ /* For safari border-radius overflow */
z-index: 0; z-index: 0;
font-size: inherit;
transition: color 180ms ease-in-out; transition: color 180ms ease-in-out;
gap: 10px; gap: 10px;
width: 100%; width: 100%;
user-select: none; user-select: none;
font-size: 14px;
font-style: normal; font-style: normal;
font-weight: 400; font-weight: 400;
line-height: 20px;
letter-spacing: 0.25px; letter-spacing: 0.25px;
} }
.content { .content {
@ -223,8 +254,7 @@ export class HaControlSelectMenu extends SelectBase {
} }
.label { .label {
font-size: 12px; font-size: 0.85em;
line-height: 16px;
letter-spacing: 0.4px; letter-spacing: 0.4px;
} }

View File

@ -410,7 +410,6 @@ export class HuiTileCard extends LitElement implements LovelaceCard {
ha-card { ha-card {
--mdc-ripple-color: var(--tile-color); --mdc-ripple-color: var(--tile-color);
height: 100%; height: 100%;
z-index: 0;
overflow: hidden; overflow: hidden;
transition: transition:
box-shadow 180ms ease-in-out, box-shadow 180ms ease-in-out,

View File

@ -1,6 +1,5 @@
import "../tile-features/hui-alarm-modes-tile-feature"; import "../tile-features/hui-alarm-modes-tile-feature";
import "../tile-features/hui-climate-hvac-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-open-close-tile-feature";
import "../tile-features/hui-cover-position-tile-feature"; import "../tile-features/hui-cover-position-tile-feature";
import "../tile-features/hui-cover-tilt-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-lawn-mower-commands-tile-feature";
import "../tile-features/hui-light-brightness-tile-feature"; import "../tile-features/hui-light-brightness-tile-feature";
import "../tile-features/hui-light-color-temp-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-vacuum-commands-tile-feature";
import "../tile-features/hui-water-heater-operation-modes-tile-feature"; import "../tile-features/hui-water-heater-operation-modes-tile-feature";
import { LovelaceTileFeatureConfig } from "../tile-features/types"; import { LovelaceTileFeatureConfig } from "../tile-features/types";
@ -28,6 +29,7 @@ const TYPES: Set<LovelaceTileFeatureConfig["type"]> = new Set([
"lawn-mower-commands", "lawn-mower-commands",
"light-brightness", "light-brightness",
"light-color-temp", "light-color-temp",
"select-options",
"target-temperature", "target-temperature",
"vacuum-commands", "vacuum-commands",
"water-heater-operation-modes", "water-heater-operation-modes",

View File

@ -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 { supportsLawnMowerCommandTileFeature } from "../../tile-features/hui-lawn-mower-commands-tile-feature";
import { supportsLightBrightnessTileFeature } from "../../tile-features/hui-light-brightness-tile-feature"; import { supportsLightBrightnessTileFeature } from "../../tile-features/hui-light-brightness-tile-feature";
import { supportsLightColorTempTileFeature } from "../../tile-features/hui-light-color-temp-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 { supportsTargetTemperatureTileFeature } from "../../tile-features/hui-target-temperature-tile-feature";
import { supportsVacuumCommandTileFeature } from "../../tile-features/hui-vacuum-commands-tile-feature"; import { supportsVacuumCommandTileFeature } from "../../tile-features/hui-vacuum-commands-tile-feature";
import { supportsWaterHeaterOperationModesTileFeature } from "../../tile-features/hui-water-heater-operation-modes-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[] = [ const FEATURE_TYPES: FeatureType[] = [
"alarm-modes", "alarm-modes",
"climate-hvac-modes", "climate-hvac-modes",
"target-temperature",
"cover-open-close", "cover-open-close",
"cover-position", "cover-position",
"cover-tilt-position", "cover-tilt-position",
@ -55,6 +55,8 @@ const FEATURE_TYPES: FeatureType[] = [
"lawn-mower-commands", "lawn-mower-commands",
"light-brightness", "light-brightness",
"light-color-temp", "light-color-temp",
"select-options",
"target-temperature",
"vacuum-commands", "vacuum-commands",
"water-heater-operation-modes", "water-heater-operation-modes",
]; ];
@ -83,6 +85,7 @@ const SUPPORTS_FEATURE_TYPES: Record<FeatureType, SupportsFeature | undefined> =
"vacuum-commands": supportsVacuumCommandTileFeature, "vacuum-commands": supportsVacuumCommandTileFeature,
"water-heater-operation-modes": "water-heater-operation-modes":
supportsWaterHeaterOperationModesTileFeature, supportsWaterHeaterOperationModesTileFeature,
"select-options": supportsSelectOptionTileFeature,
}; };
const CUSTOM_FEATURE_ENTRIES: Record< const CUSTOM_FEATURE_ENTRIES: Record<

View File

@ -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`
<div class="container">
<ha-control-select-menu
show-arrow
hide-label
.label=${"Option"}
.value=${stateObj.state}
.disabled=${this.stateObj.state === UNAVAILABLE}
fixedMenuPosition
naturalMenuWidth
@selected=${this._valueChanged}
@closed=${stopPropagation}
>
${stateObj.attributes.options!.map(
(option) => html`
<ha-list-item .value=${option}>
${this.hass!.formatEntityState(stateObj, option)}
</ha-list-item>
`
)}
</ha-control-select-menu>
</div>
`;
}
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;
}
}

View File

@ -40,6 +40,10 @@ export interface ClimateHvacModesTileFeatureConfig {
hvac_modes?: HvacMode[]; hvac_modes?: HvacMode[];
} }
export interface SelectOptionsTileFeatureConfig {
type: "select-options";
}
export interface TargetTemperatureTileFeatureConfig { export interface TargetTemperatureTileFeatureConfig {
type: "target-temperature"; type: "target-temperature";
} }
@ -86,7 +90,8 @@ export type LovelaceTileFeatureConfig =
| LightColorTempTileFeatureConfig | LightColorTempTileFeatureConfig
| VacuumCommandsTileFeatureConfig | VacuumCommandsTileFeatureConfig
| TargetTemperatureTileFeatureConfig | TargetTemperatureTileFeatureConfig
| WaterHeaterOperationModesTileFeatureConfig; | WaterHeaterOperationModesTileFeatureConfig
| SelectOptionsTileFeatureConfig;
export type LovelaceTileFeatureContext = { export type LovelaceTileFeatureContext = {
entity_id?: string; entity_id?: string;

View File

@ -17,15 +17,15 @@ export default <T extends Constructor<HassBaseEl>>(superClass: T) => {
} }
const oldHass = changedProps.get("hass") as HomeAssistant | undefined; const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
if (this.hass) { if (
if ( this.hass &&
this.hass.localize !== oldHass?.localize || (!oldHass ||
this.hass.localize !== oldHass.localize ||
this.hass.locale !== oldHass.locale || this.hass.locale !== oldHass.locale ||
this.hass.config !== oldHass.config || this.hass.config !== oldHass.config ||
this.hass.entities !== oldHass.entities this.hass.entities !== oldHass.entities)
) { ) {
this._updateStateDisplay(); this._updateStateDisplay();
}
} }
} }