Circular slider improvements (#17008)

This commit is contained in:
Paul Bottein 2023-06-26 11:49:11 +02:00 committed by GitHub
parent f77f7b3c36
commit 1dfd859a2d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 158 additions and 61 deletions

View File

@ -10,23 +10,23 @@ export class DemoHaCircularSlider extends LitElement {
private current = 22; private current = 22;
@state() @state()
private value = 19; private low = 19;
@state() @state()
private high = 25; private high = 25;
@state() @state()
private changingValue?: number; private changingLow?: number;
@state() @state()
private changingHigh?: number; private changingHigh?: number;
private _valueChanged(ev) { private _lowChanged(ev) {
this.value = ev.detail.value; this.low = ev.detail.value;
} }
private _valueChanging(ev) { private _lowChanging(ev) {
this.changingValue = ev.detail.value; this.changingLow = ev.detail.value;
} }
private _highChanged(ev) { private _highChanged(ev) {
@ -63,19 +63,40 @@ export class DemoHaCircularSlider extends LitElement {
<div class="card-content"> <div class="card-content">
<p class="title"><b>Single</b></p> <p class="title"><b>Single</b></p>
<ha-control-circular-slider <ha-control-circular-slider
@value-changed=${this._valueChanged} @value-changed=${this._lowChanged}
@value-changing=${this._valueChanging} @value-changing=${this._lowChanging}
.value=${this.value} .value=${this.low}
.current=${this.current} .current=${this.current}
step="1" step="1"
min="10" min="10"
max="30" max="30"
></ha-control-circular-slider> ></ha-control-circular-slider>
<div> <div>
Value: ${this.value} °C Low: ${this.low} °C
<br /> <br />
Changing: Changing:
${this.changingValue != null ? `${this.changingValue} °C` : "-"} ${this.changingLow != null ? `${this.changingLow} °C` : "-"}
</div>
</div>
</ha-card>
<ha-card>
<div class="card-content">
<p class="title"><b>Inverted</b></p>
<ha-control-circular-slider
inverted
@value-changed=${this._highChanged}
@value-changing=${this._highChanging}
.value=${this.high}
.current=${this.current}
step="1"
min="10"
max="30"
></ha-control-circular-slider>
<div>
High: ${this.high} °C
<br />
Changing:
${this.changingHigh != null ? `${this.changingHigh} °C` : "-"}
</div> </div>
</div> </div>
</ha-card> </ha-card>
@ -84,11 +105,11 @@ export class DemoHaCircularSlider extends LitElement {
<p class="title"><b>Dual</b></p> <p class="title"><b>Dual</b></p>
<ha-control-circular-slider <ha-control-circular-slider
dual dual
@low-changed=${this._valueChanged} @low-changed=${this._lowChanged}
@low-changing=${this._valueChanging} @low-changing=${this._lowChanging}
@high-changed=${this._highChanged} @high-changed=${this._highChanged}
@high-changing=${this._highChanging} @high-changing=${this._highChanging}
.low=${this.value} .low=${this.low}
.high=${this.high} .high=${this.high}
.current=${this.current} .current=${this.current}
step="1" step="1"
@ -96,10 +117,10 @@ export class DemoHaCircularSlider extends LitElement {
max="30" max="30"
></ha-control-circular-slider> ></ha-control-circular-slider>
<div> <div>
Low value: ${this.value} °C Low value: ${this.low} °C
<br /> <br />
Low changing: Low changing:
${this.changingValue != null ? `${this.changingValue} °C` : "-"} ${this.changingLow != null ? `${this.changingLow} °C` : "-"}
<br /> <br />
High value: ${this.high} °C High value: ${this.high} °C
<br /> <br />
@ -132,6 +153,10 @@ export class DemoHaCircularSlider extends LitElement {
--control-circular-slider-background: #ff9800; --control-circular-slider-background: #ff9800;
--control-circular-slider-background-opacity: 0.3; --control-circular-slider-background-opacity: 0.3;
} }
ha-control-circular-slider[inverted] {
--control-circular-slider-color: #2196f3;
--control-circular-slider-background: #2196f3;
}
ha-control-circular-slider[dual] { ha-control-circular-slider[dual] {
--control-circular-slider-high-color: #2196f3; --control-circular-slider-high-color: #2196f3;
--control-circular-slider-low-color: #ff9800; --control-circular-slider-low-color: #ff9800;

View File

@ -68,6 +68,9 @@ export class HaControlCircularSlider extends LitElement {
@property({ type: Boolean }) @property({ type: Boolean })
public dual?: boolean; public dual?: boolean;
@property({ type: Boolean, reflect: true })
public inverted?: boolean;
@property({ type: String }) @property({ type: String })
public label?: string; public label?: string;
@ -80,15 +83,15 @@ export class HaControlCircularSlider extends LitElement {
@property({ type: Number }) @property({ type: Number })
public value?: number; public value?: number;
@property({ type: Number })
public current?: number;
@property({ type: Number }) @property({ type: Number })
public low?: number; public low?: number;
@property({ type: Number }) @property({ type: Number })
public high?: number; public high?: number;
@property({ type: Number })
public current?: number;
@property({ type: Number }) @property({ type: Number })
public step = 1; public step = 1;
@ -98,6 +101,15 @@ export class HaControlCircularSlider extends LitElement {
@property({ type: Number }) @property({ type: Number })
public max = 100; public max = 100;
@state()
public _localValue?: number = this.value;
@state()
public _localLow?: number = this.low;
@state()
public _localHigh?: number = this.high;
@state() @state()
public _activeSlider?: ActiveSlider; public _activeSlider?: ActiveSlider;
@ -120,17 +132,36 @@ export class HaControlCircularSlider extends LitElement {
private _boundedValue(value: number) { private _boundedValue(value: number) {
const min = const min =
this._activeSlider === "high" ? Math.min(this.low ?? this.max) : this.min; this._activeSlider === "high"
? Math.min(this._localLow ?? this.max)
: this.min;
const max = const max =
this._activeSlider === "low" ? Math.max(this.high ?? this.min) : this.max; this._activeSlider === "low"
? Math.max(this._localHigh ?? this.min)
: this.max;
return Math.min(Math.max(value, min), max); return Math.min(Math.max(value, min), max);
} }
protected firstUpdated(changedProperties: PropertyValues): void { protected firstUpdated(changedProps: PropertyValues): void {
super.firstUpdated(changedProperties); super.firstUpdated(changedProps);
this._setupListeners(); this._setupListeners();
} }
protected updated(changedProps: PropertyValues): void {
super.updated(changedProps);
if (!this._activeSlider) {
if (changedProps.has("value")) {
this._localValue = this.value;
}
if (changedProps.has("low")) {
this._localLow = this.low;
}
if (changedProps.has("high")) {
this._localHigh = this.high;
}
}
}
connectedCallback(): void { connectedCallback(): void {
super.connectedCallback(); super.connectedCallback();
this._setupListeners(); this._setupListeners();
@ -164,8 +195,8 @@ export class HaControlCircularSlider extends LitElement {
private _findActiveSlider(value: number): ActiveSlider { private _findActiveSlider(value: number): ActiveSlider {
if (!this.dual) return "value"; if (!this.dual) return "value";
const low = Math.max(this.low ?? this.min, this.min); const low = Math.max(this._localLow ?? this.min, this.min);
const high = Math.min(this.high ?? this.max, this.max); const high = Math.min(this._localHigh ?? this.max, this.max);
if (low >= value) { if (low >= value) {
return "low"; return "low";
} }
@ -178,13 +209,29 @@ export class HaControlCircularSlider extends LitElement {
} }
private _setActiveValue(value: number) { private _setActiveValue(value: number) {
if (!this._activeSlider) return; switch (this._activeSlider) {
this[this._activeSlider] = value; case "high":
this._localHigh = value;
break;
case "low":
this._localLow = value;
break;
case "value":
this._localValue = value;
break;
}
} }
private _getActiveValue(): number | undefined { private _getActiveValue(): number | undefined {
if (!this._activeSlider) return undefined; switch (this._activeSlider) {
return this[this._activeSlider]; case "high":
return this._localHigh;
case "low":
return this._localLow;
case "value":
return this._localValue;
}
return undefined;
} }
_setupListeners() { _setupListeners() {
@ -235,6 +282,7 @@ export class HaControlCircularSlider extends LitElement {
const raw = this._percentageToValue(percentage); const raw = this._percentageToValue(percentage);
const bounded = this._boundedValue(raw); const bounded = this._boundedValue(raw);
const stepped = this._steppedValue(bounded); const stepped = this._steppedValue(bounded);
this._setActiveValue(stepped);
if (this._activeSlider) { if (this._activeSlider) {
fireEvent(this, `${this._activeSlider}-changing`, { fireEvent(this, `${this._activeSlider}-changing`, {
value: undefined, value: undefined,
@ -340,23 +388,41 @@ export class HaControlCircularSlider extends LitElement {
} }
} }
private _strokeDashArc(
percentage: number,
inverted?: boolean
): [string, string] {
const maxRatio = MAX_ANGLE / 360;
const f = RADIUS * 2 * Math.PI;
if (inverted) {
const arcLength = (1 - percentage) * f * maxRatio;
const strokeDasharray = `${arcLength} ${f - arcLength}`;
const strokeDashOffset = `${arcLength + f * (1 - maxRatio)}`;
return [strokeDasharray, strokeDashOffset];
}
const arcLength = percentage * f * maxRatio;
const strokeDasharray = `${arcLength} ${f - arcLength}`;
const strokeDashOffset = "0";
return [strokeDasharray, strokeDashOffset];
}
protected render(): TemplateResult { protected render(): TemplateResult {
const trackPath = arc({ x: 0, y: 0, start: 0, end: MAX_ANGLE, r: RADIUS }); const trackPath = arc({ x: 0, y: 0, start: 0, end: MAX_ANGLE, r: RADIUS });
const maxRatio = MAX_ANGLE / 360; const lowValue = this.dual ? this._localLow : this._localValue;
const highValue = this._localHigh;
const f = RADIUS * 2 * Math.PI;
const lowValue = this.dual ? this.low : this.value;
const highValue = this.high;
const lowPercentage = this._valueToPercentage(lowValue ?? this.min); const lowPercentage = this._valueToPercentage(lowValue ?? this.min);
const highPercentage = this._valueToPercentage(highValue ?? this.max); const highPercentage = this._valueToPercentage(highValue ?? this.max);
const lowArcLength = lowPercentage * f * maxRatio; const [lowStrokeDasharray, lowStrokeDashOffset] = this._strokeDashArc(
const lowStrokeDasharray = `${lowArcLength} ${f - lowArcLength}`; lowPercentage,
this.inverted
);
const highArcLength = (1 - highPercentage) * f * maxRatio; const [highStrokeDasharray, highStrokeDashOffset] = this._strokeDashArc(
const highStrokeDasharray = `${highArcLength} ${f - highArcLength}`; highPercentage,
const highStrokeDashOffset = `${highArcLength + f * (1 - maxRatio)}`; true
);
const currentPercentage = this._valueToPercentage(this.current ?? 0); const currentPercentage = this._valueToPercentage(this.current ?? 0);
const currentAngle = currentPercentage * MAX_ANGLE; const currentAngle = currentPercentage * MAX_ANGLE;
@ -381,6 +447,8 @@ export class HaControlCircularSlider extends LitElement {
</g> </g>
<g id="display"> <g id="display">
<path class="background" d=${trackPath} /> <path class="background" d=${trackPath} />
${lowValue != null
? svg`
<circle <circle
.id=${this.dual ? "low" : "value"} .id=${this.dual ? "low" : "value"}
class="track" class="track"
@ -388,20 +456,22 @@ export class HaControlCircularSlider extends LitElement {
cy="0" cy="0"
r=${RADIUS} r=${RADIUS}
stroke-dasharray=${lowStrokeDasharray} stroke-dasharray=${lowStrokeDasharray}
stroke-dashoffset="0" stroke-dashoffset=${lowStrokeDashOffset}
role="slider" role="slider"
tabindex="0" tabindex="0"
aria-valuemin=${this.min} aria-valuemin=${this.min}
aria-valuemax=${this.max} aria-valuemax=${this.max}
aria-valuenow=${lowValue != null aria-valuenow=${
? this._steppedValue(lowValue) lowValue != null ? this._steppedValue(lowValue) : undefined
: undefined} }
aria-disabled=${this.disabled} aria-disabled=${this.disabled}
aria-label=${ifDefined(this.lowLabel ?? this.label)} aria-label=${ifDefined(this.lowLabel ?? this.label)}
@keydown=${this._handleKeyDown} @keydown=${this._handleKeyDown}
@keyup=${this._handleKeyUp} @keyup=${this._handleKeyUp}
/> />
${this.dual `
: nothing}
${this.dual && highValue != null
? svg` ? svg`
<circle <circle
id="high" id="high"
@ -496,6 +566,7 @@ export class HaControlCircularSlider extends LitElement {
fill: none; fill: none;
stroke: var(--control-circular-slider-background); stroke: var(--control-circular-slider-background);
opacity: var(--control-circular-slider-background-opacity); opacity: var(--control-circular-slider-background-opacity);
transition: stroke 180ms ease-in-out, opacity 180ms ease-in-out;
stroke-linecap: round; stroke-linecap: round;
stroke-width: 24px; stroke-width: 24px;
} }
@ -507,7 +578,8 @@ export class HaControlCircularSlider extends LitElement {
stroke-width: 24px; stroke-width: 24px;
transition: stroke-width 300ms ease-in-out, transition: stroke-width 300ms ease-in-out,
stroke-dasharray 300ms ease-in-out, stroke-dasharray 300ms ease-in-out,
stroke-dashoffset 300ms ease-in-out; stroke-dashoffset 300ms ease-in-out, stroke 180ms ease-in-out,
opacity 180ms ease-in-out;
} }
.track:focus-visible { .track:focus-visible {