Skip to content

Commit

Permalink
Add select option tile feature (#17971)
Browse files Browse the repository at this point in the history
  • Loading branch information
piitaya authored Sep 20, 2023
1 parent 3349031 commit 4b5c702
Show file tree
Hide file tree
Showing 7 changed files with 216 additions and 23 deletions.
54 changes: 42 additions & 12 deletions src/components/ha-control-select-menu.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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<Ripple | null>;

@state() private _shouldRenderRipple = false;
Expand All @@ -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`
<div class="select ${classMap(classes)}">
Expand All @@ -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}
Expand All @@ -72,11 +83,14 @@ export class HaControlSelectMenu extends SelectBase {
>
${this.renderIcon()}
<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
? html`<p class="value">${this.selectedText}</p>`
: nothing}
</div>
${this.renderArrow()}
${this._shouldRenderRipple && !this.disabled
? html` <mwc-ripple></mwc-ripple> `
: nothing}
Expand All @@ -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() {
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`
<div class="icon">
Expand Down Expand Up @@ -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;
Expand All @@ -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 {
Expand All @@ -223,8 +254,7 @@ export class HaControlSelectMenu extends SelectBase {
}
.label {
font-size: 12px;
line-height: 16px;
font-size: 0.85em;
letter-spacing: 0.4px;
}
Expand Down
1 change: 0 additions & 1 deletion src/panels/lovelace/cards/hui-tile-card.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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";
Expand All @@ -28,6 +29,7 @@ const TYPES: Set<LovelaceTileFeatureConfig["type"]> = new Set([
"lawn-mower-commands",
"light-brightness",
"light-color-temp",
"select-options",
"target-temperature",
"vacuum-commands",
"water-heater-operation-modes",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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",
Expand All @@ -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",
];
Expand Down Expand Up @@ -83,6 +85,7 @@ const SUPPORTS_FEATURE_TYPES: Record<FeatureType, SupportsFeature | undefined> =
"vacuum-commands": supportsVacuumCommandTileFeature,
"water-heater-operation-modes":
supportsWaterHeaterOperationModesTileFeature,
"select-options": supportsSelectOptionTileFeature,
};

const CUSTOM_FEATURE_ENTRIES: Record<
Expand Down
154 changes: 154 additions & 0 deletions src/panels/lovelace/tile-features/hui-select-options-tile-feature.ts
Original file line number Diff line number Diff line change
@@ -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;
}
}
7 changes: 6 additions & 1 deletion src/panels/lovelace/tile-features/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@ export interface ClimateHvacModesTileFeatureConfig {
hvac_modes?: HvacMode[];
}

export interface SelectOptionsTileFeatureConfig {
type: "select-options";
}

export interface TargetTemperatureTileFeatureConfig {
type: "target-temperature";
}
Expand Down Expand Up @@ -86,7 +90,8 @@ export type LovelaceTileFeatureConfig =
| LightColorTempTileFeatureConfig
| VacuumCommandsTileFeatureConfig
| TargetTemperatureTileFeatureConfig
| WaterHeaterOperationModesTileFeatureConfig;
| WaterHeaterOperationModesTileFeatureConfig
| SelectOptionsTileFeatureConfig;

export type LovelaceTileFeatureContext = {
entity_id?: string;
Expand Down
Loading

0 comments on commit 4b5c702

Please sign in to comment.