mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-24 09:46:36 +00:00
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:
parent
370ec9cd98
commit
5ce31f3177
@ -59,6 +59,8 @@ const A11Y_KEY_CODES = new Set([
|
||||
"End",
|
||||
]);
|
||||
|
||||
export type ControlCircularSliderMode = "start" | "end" | "full";
|
||||
|
||||
@customElement("ha-control-circular-slider")
|
||||
export class HaControlCircularSlider extends LitElement {
|
||||
@property({ type: Boolean, reflect: true })
|
||||
@ -67,8 +69,11 @@ export class HaControlCircularSlider extends LitElement {
|
||||
@property({ type: Boolean })
|
||||
public dual?: boolean;
|
||||
|
||||
@property({ type: Boolean, reflect: true })
|
||||
public inverted?: boolean;
|
||||
@property({ type: String })
|
||||
public mode?: ControlCircularSliderMode;
|
||||
|
||||
@property({ type: Boolean })
|
||||
public inactive?: boolean;
|
||||
|
||||
@property({ type: String })
|
||||
public label?: string;
|
||||
@ -407,12 +412,10 @@ export class HaControlCircularSlider extends LitElement {
|
||||
protected renderArc(
|
||||
id: string,
|
||||
value: number | undefined,
|
||||
inverted: boolean | undefined
|
||||
mode: ControlCircularSliderMode
|
||||
) {
|
||||
if (this.disabled) return nothing;
|
||||
|
||||
const limit = inverted ? this.max : this.min;
|
||||
|
||||
const path = svgArc({
|
||||
x: 0,
|
||||
y: 0,
|
||||
@ -421,82 +424,100 @@ export class HaControlCircularSlider extends LitElement {
|
||||
r: RADIUS,
|
||||
});
|
||||
|
||||
const limit = mode === "end" ? this.max : this.min;
|
||||
|
||||
const current = this.current ?? 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
|
||||
? inverted
|
||||
const activeArc = showActive
|
||||
? mode === "end"
|
||||
? this._strokeDashArc(target, current)
|
||||
: this._strokeDashArc(current, target)
|
||||
: this._strokeCircleDashArc(target);
|
||||
|
||||
const arcDashArray = inverted
|
||||
? this._strokeDashArc(target, limit)
|
||||
: this._strokeDashArc(limit, target);
|
||||
const coloredArc =
|
||||
mode === "full"
|
||||
? this._strokeDashArc(this.min, this.max)
|
||||
: mode === "end"
|
||||
? this._strokeDashArc(target, limit)
|
||||
: this._strokeDashArc(limit, target);
|
||||
|
||||
const targetCircleDashArray = this._strokeCircleDashArc(target);
|
||||
const targetCircle = this._strokeCircleDashArc(target);
|
||||
|
||||
const currentCircleDashArray =
|
||||
const currentCircle =
|
||||
this.current != null &&
|
||||
showActive &&
|
||||
current <= this.max &&
|
||||
current >= this.min
|
||||
this.current <= this.max &&
|
||||
this.current >= this.min &&
|
||||
(showActive || this.mode === "full")
|
||||
? this._strokeCircleDashArc(this.current)
|
||||
: undefined;
|
||||
|
||||
return svg`
|
||||
<path
|
||||
class="arc arc-clear"
|
||||
d=${path}
|
||||
stroke-dasharray=${arcDashArray[0]}
|
||||
stroke-dashoffset=${arcDashArray[1]}
|
||||
/>
|
||||
<path
|
||||
class="arc arc-background ${classMap({ [id]: true })}"
|
||||
d=${path}
|
||||
stroke-dasharray=${arcDashArray[0]}
|
||||
stroke-dashoffset=${arcDashArray[1]}
|
||||
/>
|
||||
<path
|
||||
.id=${id}
|
||||
d=${path}
|
||||
class="arc arc-active ${classMap({ [id]: true })}"
|
||||
stroke-dasharray=${activeArcDashArray[0]}
|
||||
stroke-dashoffset=${activeArcDashArray[1]}
|
||||
role="slider"
|
||||
tabindex="0"
|
||||
aria-valuemin=${this.min}
|
||||
aria-valuemax=${this.max}
|
||||
aria-valuenow=${
|
||||
this._localValue != null
|
||||
? this._steppedValue(this._localValue)
|
||||
: undefined
|
||||
<g class=${classMap({ inactive: Boolean(this.inactive) })}>
|
||||
<path
|
||||
class="arc arc-clear"
|
||||
d=${path}
|
||||
stroke-dasharray=${coloredArc[0]}
|
||||
stroke-dashoffset=${coloredArc[1]}
|
||||
/>
|
||||
<path
|
||||
class="arc arc-colored ${classMap({ [id]: true })}"
|
||||
d=${path}
|
||||
stroke-dasharray=${coloredArc[0]}
|
||||
stroke-dashoffset=${coloredArc[1]}
|
||||
/>
|
||||
<path
|
||||
.id=${id}
|
||||
d=${path}
|
||||
class="arc arc-active ${classMap({ [id]: true })}"
|
||||
stroke-dasharray=${activeArc[0]}
|
||||
stroke-dashoffset=${activeArc[1]}
|
||||
role="slider"
|
||||
tabindex="0"
|
||||
aria-valuemin=${this.min}
|
||||
aria-valuemax=${this.max}
|
||||
aria-valuenow=${
|
||||
this._localValue != null
|
||||
? this._steppedValue(this._localValue)
|
||||
: undefined
|
||||
}
|
||||
aria-disabled=${this.disabled}
|
||||
aria-label=${ifDefined(this.lowLabel ?? this.label)}
|
||||
@keydown=${this._handleKeyDown}
|
||||
@keyup=${this._handleKeyUp}
|
||||
/>
|
||||
${
|
||||
currentCircle
|
||||
? svg`
|
||||
<path
|
||||
class="current arc-current"
|
||||
d=${path}
|
||||
stroke-dasharray=${currentCircle[0]}
|
||||
stroke-dashoffset=${currentCircle[1]}
|
||||
/>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
aria-disabled=${this.disabled}
|
||||
aria-label=${ifDefined(this.lowLabel ?? this.label)}
|
||||
@keydown=${this._handleKeyDown}
|
||||
@keyup=${this._handleKeyUp}
|
||||
/>
|
||||
${
|
||||
currentCircleDashArray
|
||||
? svg`
|
||||
<path
|
||||
class="current arc-current"
|
||||
d=${path}
|
||||
stroke-dasharray=${currentCircleDashArray[0]}
|
||||
stroke-dashoffset=${currentCircleDashArray[1]}
|
||||
/>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
<path
|
||||
class="target"
|
||||
d=${path}
|
||||
stroke-dasharray=${targetCircleDashArray[0]}
|
||||
stroke-dashoffset=${targetCircleDashArray[1]}
|
||||
/>
|
||||
<path
|
||||
class="target-border ${classMap({ [id]: true })}"
|
||||
d=${path}
|
||||
stroke-dasharray=${targetCircle[0]}
|
||||
stroke-dashoffset=${targetCircle[1]}
|
||||
/>
|
||||
<path
|
||||
class="target"
|
||||
d=${path}
|
||||
stroke-dasharray=${targetCircle[0]}
|
||||
stroke-dashoffset=${targetCircle[1]}
|
||||
/>
|
||||
</g>
|
||||
`;
|
||||
}
|
||||
|
||||
@ -551,11 +572,11 @@ export class HaControlCircularSlider extends LitElement {
|
||||
? this.renderArc(
|
||||
this.dual ? "low" : "value",
|
||||
lowValue,
|
||||
this.inverted
|
||||
(!this.dual && this.mode) || "start"
|
||||
)
|
||||
: nothing}
|
||||
${this.dual && highValue != null
|
||||
? this.renderArc("high", highValue, true)
|
||||
? this.renderArc("high", highValue, "end")
|
||||
: nothing}
|
||||
</g>
|
||||
</g>
|
||||
@ -634,6 +655,19 @@ export class HaControlCircularSlider extends LitElement {
|
||||
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 {
|
||||
fill: none;
|
||||
stroke-linecap: round;
|
||||
@ -655,7 +689,7 @@ export class HaControlCircularSlider extends LitElement {
|
||||
.arc-clear {
|
||||
stroke: var(--clear-background-color);
|
||||
}
|
||||
.arc-background {
|
||||
.arc-colored {
|
||||
opacity: 0.5;
|
||||
}
|
||||
.arc-active {
|
||||
@ -667,6 +701,7 @@ export class HaControlCircularSlider extends LitElement {
|
||||
|
||||
.pressed .arc,
|
||||
.pressed .target,
|
||||
.pressed .target-border,
|
||||
.pressed .current {
|
||||
transition:
|
||||
stroke-width 300ms ease-in-out,
|
||||
@ -674,6 +709,11 @@ export class HaControlCircularSlider extends LitElement {
|
||||
opacity 180ms ease-in-out;
|
||||
}
|
||||
|
||||
.inactive .arc,
|
||||
.inactive .arc-current {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.value {
|
||||
stroke: var(--control-circular-slider-color);
|
||||
}
|
||||
|
@ -163,6 +163,7 @@ export class HaMoreInfoClimateHumidity extends LitElement {
|
||||
})}
|
||||
>
|
||||
<ha-control-circular-slider
|
||||
.inactive=${!active}
|
||||
.value=${this._targetHumidity}
|
||||
.min=${this._min}
|
||||
.max=${this._max}
|
||||
|
@ -18,12 +18,14 @@ import { clamp } from "../../../../common/number/clamp";
|
||||
import { formatNumber } from "../../../../common/number/format_number";
|
||||
import { debounce } from "../../../../common/util/debounce";
|
||||
import "../../../../components/ha-control-circular-slider";
|
||||
import type { ControlCircularSliderMode } from "../../../../components/ha-control-circular-slider";
|
||||
import "../../../../components/ha-outlined-icon-button";
|
||||
import "../../../../components/ha-svg-icon";
|
||||
import {
|
||||
CLIMATE_HVAC_ACTION_TO_MODE,
|
||||
ClimateEntity,
|
||||
ClimateEntityFeature,
|
||||
HvacMode,
|
||||
} from "../../../../data/climate";
|
||||
import { UNAVAILABLE } from "../../../../data/entity";
|
||||
import { HomeAssistant } from "../../../../types";
|
||||
@ -31,6 +33,16 @@ import { moreInfoControlCircularSliderStyle } from "../ha-more-info-control-circ
|
||||
|
||||
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")
|
||||
export class HaMoreInfoClimateTemperature extends LitElement {
|
||||
@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 (
|
||||
supportsTargetTemperature &&
|
||||
this._targetTemperature.value != null &&
|
||||
this.stateObj.state !== UNAVAILABLE
|
||||
) {
|
||||
const hasOnlyCoolMode =
|
||||
hvacModes.length === 2 &&
|
||||
hvacModes.includes("cool") &&
|
||||
hvacModes.includes("off");
|
||||
return html`
|
||||
<div
|
||||
class="container"
|
||||
@ -287,7 +297,10 @@ export class HaMoreInfoClimateTemperature extends LitElement {
|
||||
})}
|
||||
>
|
||||
<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}
|
||||
.min=${this._min}
|
||||
.max=${this._max}
|
||||
@ -324,6 +337,7 @@ export class HaMoreInfoClimateTemperature extends LitElement {
|
||||
})}
|
||||
>
|
||||
<ha-control-circular-slider
|
||||
.inactive=${!active}
|
||||
dual
|
||||
.low=${this._targetTemperature.low}
|
||||
.high=${this._targetTemperature.high}
|
||||
|
@ -184,7 +184,8 @@ export class HaMoreInfoHumidifierHumidity extends LitElement {
|
||||
})}
|
||||
>
|
||||
<ha-control-circular-slider
|
||||
.inverted=${inverted}
|
||||
.inactive=${!active}
|
||||
.mode=${inverted ? "end" : "start"}
|
||||
.value=${targetHumidity}
|
||||
.min=${this._min}
|
||||
.max=${this._max}
|
||||
|
@ -9,6 +9,7 @@ import {
|
||||
} from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import { stateActive } from "../../../../common/entity/state_active";
|
||||
import { stateColorCss } from "../../../../common/entity/state_color";
|
||||
import { supportsFeature } from "../../../../common/entity/supports-feature";
|
||||
import { clamp } from "../../../../common/number/clamp";
|
||||
@ -163,6 +164,7 @@ export class HaMoreInfoWaterHeaterTemperature extends LitElement {
|
||||
);
|
||||
|
||||
const stateColor = stateColorCss(this.stateObj);
|
||||
const active = stateActive(this.stateObj);
|
||||
|
||||
if (
|
||||
supportsTargetTemperature &&
|
||||
@ -177,6 +179,7 @@ export class HaMoreInfoWaterHeaterTemperature extends LitElement {
|
||||
})}
|
||||
>
|
||||
<ha-control-circular-slider
|
||||
.inactive=${!active}
|
||||
.value=${this._targetTemperature}
|
||||
.min=${this._min}
|
||||
.max=${this._max}
|
||||
|
Loading…
x
Reference in New Issue
Block a user