Adapt circular slider style for climate, water_heater and humidifier (#17677)

* Add more rendering mode for circular slider

* Improve transitions
This commit is contained in:
Paul Bottein 2023-08-23 14:35:54 +02:00 committed by GitHub
parent 370ec9cd98
commit 5ce31f3177
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 134 additions and 75 deletions

View File

@ -59,6 +59,8 @@ const A11Y_KEY_CODES = new Set([
"End", "End",
]); ]);
export type ControlCircularSliderMode = "start" | "end" | "full";
@customElement("ha-control-circular-slider") @customElement("ha-control-circular-slider")
export class HaControlCircularSlider extends LitElement { export class HaControlCircularSlider extends LitElement {
@property({ type: Boolean, reflect: true }) @property({ type: Boolean, reflect: true })
@ -67,8 +69,11 @@ export class HaControlCircularSlider extends LitElement {
@property({ type: Boolean }) @property({ type: Boolean })
public dual?: boolean; public dual?: boolean;
@property({ type: Boolean, reflect: true }) @property({ type: String })
public inverted?: boolean; public mode?: ControlCircularSliderMode;
@property({ type: Boolean })
public inactive?: boolean;
@property({ type: String }) @property({ type: String })
public label?: string; public label?: string;
@ -407,12 +412,10 @@ export class HaControlCircularSlider extends LitElement {
protected renderArc( protected renderArc(
id: string, id: string,
value: number | undefined, value: number | undefined,
inverted: boolean | undefined mode: ControlCircularSliderMode
) { ) {
if (this.disabled) return nothing; if (this.disabled) return nothing;
const limit = inverted ? this.max : this.min;
const path = svgArc({ const path = svgArc({
x: 0, x: 0,
y: 0, y: 0,
@ -421,50 +424,61 @@ export class HaControlCircularSlider extends LitElement {
r: RADIUS, r: RADIUS,
}); });
const limit = mode === "end" ? this.max : this.min;
const current = this.current ?? limit; const current = this.current ?? limit;
const target = value ?? limit; const target = value ?? limit;
const showActive = inverted ? target <= current : current <= target; const showActive =
mode === "end"
? target <= current
: mode === "start"
? current <= target
: false;
const activeArcDashArray = showActive const activeArc = showActive
? inverted ? mode === "end"
? this._strokeDashArc(target, current) ? this._strokeDashArc(target, current)
: this._strokeDashArc(current, target) : this._strokeDashArc(current, target)
: this._strokeCircleDashArc(target); : this._strokeCircleDashArc(target);
const arcDashArray = inverted const coloredArc =
mode === "full"
? this._strokeDashArc(this.min, this.max)
: mode === "end"
? this._strokeDashArc(target, limit) ? this._strokeDashArc(target, limit)
: this._strokeDashArc(limit, target); : this._strokeDashArc(limit, target);
const targetCircleDashArray = this._strokeCircleDashArc(target); const targetCircle = this._strokeCircleDashArc(target);
const currentCircleDashArray = const currentCircle =
this.current != null && this.current != null &&
showActive && this.current <= this.max &&
current <= this.max && this.current >= this.min &&
current >= this.min (showActive || this.mode === "full")
? this._strokeCircleDashArc(this.current) ? this._strokeCircleDashArc(this.current)
: undefined; : undefined;
return svg` return svg`
<g class=${classMap({ inactive: Boolean(this.inactive) })}>
<path <path
class="arc arc-clear" class="arc arc-clear"
d=${path} d=${path}
stroke-dasharray=${arcDashArray[0]} stroke-dasharray=${coloredArc[0]}
stroke-dashoffset=${arcDashArray[1]} stroke-dashoffset=${coloredArc[1]}
/> />
<path <path
class="arc arc-background ${classMap({ [id]: true })}" class="arc arc-colored ${classMap({ [id]: true })}"
d=${path} d=${path}
stroke-dasharray=${arcDashArray[0]} stroke-dasharray=${coloredArc[0]}
stroke-dashoffset=${arcDashArray[1]} stroke-dashoffset=${coloredArc[1]}
/> />
<path <path
.id=${id} .id=${id}
d=${path} d=${path}
class="arc arc-active ${classMap({ [id]: true })}" class="arc arc-active ${classMap({ [id]: true })}"
stroke-dasharray=${activeArcDashArray[0]} stroke-dasharray=${activeArc[0]}
stroke-dashoffset=${activeArcDashArray[1]} stroke-dashoffset=${activeArc[1]}
role="slider" role="slider"
tabindex="0" tabindex="0"
aria-valuemin=${this.min} aria-valuemin=${this.min}
@ -480,23 +494,30 @@ export class HaControlCircularSlider extends LitElement {
@keyup=${this._handleKeyUp} @keyup=${this._handleKeyUp}
/> />
${ ${
currentCircleDashArray currentCircle
? svg` ? svg`
<path <path
class="current arc-current" class="current arc-current"
d=${path} d=${path}
stroke-dasharray=${currentCircleDashArray[0]} stroke-dasharray=${currentCircle[0]}
stroke-dashoffset=${currentCircleDashArray[1]} stroke-dashoffset=${currentCircle[1]}
/> />
` `
: nothing : nothing
} }
<path
class="target-border ${classMap({ [id]: true })}"
d=${path}
stroke-dasharray=${targetCircle[0]}
stroke-dashoffset=${targetCircle[1]}
/>
<path <path
class="target" class="target"
d=${path} d=${path}
stroke-dasharray=${targetCircleDashArray[0]} stroke-dasharray=${targetCircle[0]}
stroke-dashoffset=${targetCircleDashArray[1]} stroke-dashoffset=${targetCircle[1]}
/> />
</g>
`; `;
} }
@ -551,11 +572,11 @@ export class HaControlCircularSlider extends LitElement {
? this.renderArc( ? this.renderArc(
this.dual ? "low" : "value", this.dual ? "low" : "value",
lowValue, lowValue,
this.inverted (!this.dual && this.mode) || "start"
) )
: nothing} : nothing}
${this.dual && highValue != null ${this.dual && highValue != null
? this.renderArc("high", highValue, true) ? this.renderArc("high", highValue, "end")
: nothing} : nothing}
</g> </g>
</g> </g>
@ -634,6 +655,19 @@ export class HaControlCircularSlider extends LitElement {
opacity 180ms ease-in-out; opacity 180ms ease-in-out;
} }
.target-border {
fill: none;
stroke-linecap: round;
stroke-width: 24px;
stroke: white;
transition:
stroke-width 300ms ease-in-out,
stroke-dasharray 300ms ease-in-out,
stroke-dashoffset 300ms ease-in-out,
stroke 180ms ease-in-out,
opacity 180ms ease-in-out;
}
.current { .current {
fill: none; fill: none;
stroke-linecap: round; stroke-linecap: round;
@ -655,7 +689,7 @@ export class HaControlCircularSlider extends LitElement {
.arc-clear { .arc-clear {
stroke: var(--clear-background-color); stroke: var(--clear-background-color);
} }
.arc-background { .arc-colored {
opacity: 0.5; opacity: 0.5;
} }
.arc-active { .arc-active {
@ -667,6 +701,7 @@ export class HaControlCircularSlider extends LitElement {
.pressed .arc, .pressed .arc,
.pressed .target, .pressed .target,
.pressed .target-border,
.pressed .current { .pressed .current {
transition: transition:
stroke-width 300ms ease-in-out, stroke-width 300ms ease-in-out,
@ -674,6 +709,11 @@ export class HaControlCircularSlider extends LitElement {
opacity 180ms ease-in-out; opacity 180ms ease-in-out;
} }
.inactive .arc,
.inactive .arc-current {
opacity: 0;
}
.value { .value {
stroke: var(--control-circular-slider-color); stroke: var(--control-circular-slider-color);
} }

View File

@ -163,6 +163,7 @@ export class HaMoreInfoClimateHumidity extends LitElement {
})} })}
> >
<ha-control-circular-slider <ha-control-circular-slider
.inactive=${!active}
.value=${this._targetHumidity} .value=${this._targetHumidity}
.min=${this._min} .min=${this._min}
.max=${this._max} .max=${this._max}

View File

@ -18,12 +18,14 @@ import { clamp } from "../../../../common/number/clamp";
import { formatNumber } from "../../../../common/number/format_number"; import { formatNumber } from "../../../../common/number/format_number";
import { debounce } from "../../../../common/util/debounce"; import { debounce } from "../../../../common/util/debounce";
import "../../../../components/ha-control-circular-slider"; import "../../../../components/ha-control-circular-slider";
import type { ControlCircularSliderMode } from "../../../../components/ha-control-circular-slider";
import "../../../../components/ha-outlined-icon-button"; import "../../../../components/ha-outlined-icon-button";
import "../../../../components/ha-svg-icon"; import "../../../../components/ha-svg-icon";
import { import {
CLIMATE_HVAC_ACTION_TO_MODE, CLIMATE_HVAC_ACTION_TO_MODE,
ClimateEntity, ClimateEntity,
ClimateEntityFeature, ClimateEntityFeature,
HvacMode,
} from "../../../../data/climate"; } from "../../../../data/climate";
import { UNAVAILABLE } from "../../../../data/entity"; import { UNAVAILABLE } from "../../../../data/entity";
import { HomeAssistant } from "../../../../types"; import { HomeAssistant } from "../../../../types";
@ -31,6 +33,16 @@ import { moreInfoControlCircularSliderStyle } from "../ha-more-info-control-circ
type Target = "value" | "low" | "high"; type Target = "value" | "low" | "high";
const SLIDER_MODES: Record<HvacMode, ControlCircularSliderMode> = {
auto: "full",
cool: "end",
dry: "full",
fan_only: "full",
heat: "start",
heat_cool: "full",
off: "full",
};
@customElement("ha-more-info-climate-temperature") @customElement("ha-more-info-climate-temperature")
export class HaMoreInfoClimateTemperature extends LitElement { export class HaMoreInfoClimateTemperature extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@ -267,17 +279,15 @@ export class HaMoreInfoClimateTemperature extends LitElement {
); );
} }
const hvacModes = this.stateObj.attributes.hvac_modes; const activeModes = this.stateObj.attributes.hvac_modes.filter(
(m) => m !== "off"
);
if ( if (
supportsTargetTemperature && supportsTargetTemperature &&
this._targetTemperature.value != null && this._targetTemperature.value != null &&
this.stateObj.state !== UNAVAILABLE this.stateObj.state !== UNAVAILABLE
) { ) {
const hasOnlyCoolMode =
hvacModes.length === 2 &&
hvacModes.includes("cool") &&
hvacModes.includes("off");
return html` return html`
<div <div
class="container" class="container"
@ -287,7 +297,10 @@ export class HaMoreInfoClimateTemperature extends LitElement {
})} })}
> >
<ha-control-circular-slider <ha-control-circular-slider
.inverted=${mode === "cool" || hasOnlyCoolMode} .inactive=${!active}
.mode=${mode === "off" && activeModes.length === 1
? SLIDER_MODES[activeModes[0]]
: SLIDER_MODES[mode]}
.value=${this._targetTemperature.value} .value=${this._targetTemperature.value}
.min=${this._min} .min=${this._min}
.max=${this._max} .max=${this._max}
@ -324,6 +337,7 @@ export class HaMoreInfoClimateTemperature extends LitElement {
})} })}
> >
<ha-control-circular-slider <ha-control-circular-slider
.inactive=${!active}
dual dual
.low=${this._targetTemperature.low} .low=${this._targetTemperature.low}
.high=${this._targetTemperature.high} .high=${this._targetTemperature.high}

View File

@ -184,7 +184,8 @@ export class HaMoreInfoHumidifierHumidity extends LitElement {
})} })}
> >
<ha-control-circular-slider <ha-control-circular-slider
.inverted=${inverted} .inactive=${!active}
.mode=${inverted ? "end" : "start"}
.value=${targetHumidity} .value=${targetHumidity}
.min=${this._min} .min=${this._min}
.max=${this._max} .max=${this._max}

View File

@ -9,6 +9,7 @@ import {
} from "lit"; } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import { styleMap } from "lit/directives/style-map"; import { styleMap } from "lit/directives/style-map";
import { stateActive } from "../../../../common/entity/state_active";
import { stateColorCss } from "../../../../common/entity/state_color"; import { stateColorCss } from "../../../../common/entity/state_color";
import { supportsFeature } from "../../../../common/entity/supports-feature"; import { supportsFeature } from "../../../../common/entity/supports-feature";
import { clamp } from "../../../../common/number/clamp"; import { clamp } from "../../../../common/number/clamp";
@ -163,6 +164,7 @@ export class HaMoreInfoWaterHeaterTemperature extends LitElement {
); );
const stateColor = stateColorCss(this.stateObj); const stateColor = stateColorCss(this.stateObj);
const active = stateActive(this.stateObj);
if ( if (
supportsTargetTemperature && supportsTargetTemperature &&
@ -177,6 +179,7 @@ export class HaMoreInfoWaterHeaterTemperature extends LitElement {
})} })}
> >
<ha-control-circular-slider <ha-control-circular-slider
.inactive=${!active}
.value=${this._targetTemperature} .value=${this._targetTemperature}
.min=${this._min} .min=${this._min}
.max=${this._max} .max=${this._max}