UI Charts: swipe on charts to switch time range (#32614)

This commit is contained in:
Michael Geers 2026-08-07 18:47:25 +02:00 • committed by GitHub
parent 4314a64896
commit 950ca489e5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 50 additions and 10 deletions

View file

@ -5,6 +5,8 @@
:label="windowLabel"
:prev-disabled="prevDisabled"
:next-disabled="nextDisabled"
:highlight-prev="highlightPrev"
:highlight-next="highlightNext"
@prev="changeOffset(-1)"
@next="changeOffset(1)"
/>
@ -44,6 +46,8 @@ import { batteryColor } from "@/colors";
import type { Legend } from "../Sessions/types";
import type { BatterySeries } from "./types";
import Card from "../Helper/Card.vue";
import { attachSwipeHandler } from "@/utils/swipe";
import { hapticFeedback } from "@/utils/haptic";
import SelectGroup from "../Helper/SelectGroup.vue";
import LegendList from "../Sessions/LegendList.vue";
import WindowNav from "./WindowNav.vue";
@ -71,6 +75,10 @@ export default defineComponent({
dayOffset: 0,
focusedBattery: null as number | null,
selectedUnit: (settings.batteryUnit || "soc") as "soc" | "energy",
highlightPrev: false,
highlightNext: false,
highlightTimer: null as ReturnType<typeof setTimeout> | null,
detachSwipe: null as (() => void) | null,
};
},
computed: {
@ -145,7 +153,32 @@ export default defineComponent({
}
},
},
mounted() {
this.detachSwipe = attachSwipeHandler(this.$el as HTMLElement, {
onSwipeLeft: () => this.swipe(1),
onSwipeRight: () => this.swipe(-1),
});
},
unmounted() {
this.detachSwipe?.();
if (this.highlightTimer) clearTimeout(this.highlightTimer);
},
methods: {
swipe(dir: number) {
if (dir < 0 ? this.prevDisabled : this.nextDisabled) return;
this.changeOffset(dir);
this.flashHighlight(dir < 0 ? "prev" : "next");
hapticFeedback("medium");
},
flashHighlight(dir: "prev" | "next") {
if (this.highlightTimer) clearTimeout(this.highlightTimer);
this.highlightPrev = dir === "prev";
this.highlightNext = dir === "next";
this.highlightTimer = setTimeout(() => {
this.highlightPrev = false;
this.highlightNext = false;
}, 300);
},
updateUnit(value: string | number | boolean | null) {
this.selectedUnit = value === "energy" ? "energy" : "soc";
settings.batteryUnit = this.selectedUnit;

View file

@ -3,6 +3,7 @@
<DateNavigatorButton
prev
:disabled="prevDisabled"
:highlight="highlightPrev"
:on-click="() => $emit('prev')"
data-testid="battery-chart-prev"
/>
@ -12,6 +13,7 @@
<DateNavigatorButton
next
:disabled="nextDisabled"
:highlight="highlightNext"
:on-click="() => $emit('next')"
data-testid="battery-chart-next"
/>
@ -29,6 +31,8 @@ export default defineComponent({
label: { type: String, default: "" },
prevDisabled: Boolean,
nextDisabled: Boolean,
highlightPrev: Boolean,
highlightNext: Boolean,
},
emits: ["prev", "next"],
});

View file

@ -146,6 +146,7 @@ import DateNavigatorButton from "./DateNavigatorButton.vue";
import formatter from "@/mixins/formatter";
import type { SelectOption } from "@/types/evcc";
import { attachSwipeHandler } from "@/utils/swipe";
import { hapticFeedback } from "@/utils/haptic";
function daysInMonth(year: number, month: number) {
return new Date(year, month, 0).getDate();
@ -279,7 +280,6 @@ export default defineComponent({
this.detachSwipe = attachSwipeHandler(document.body, {
onSwipeLeft: () => this.swipeNext(),
onSwipeRight: () => this.swipePrev(),
ignoreSelector: "canvas, [_echarts_instance_]",
});
},
unmounted() {
@ -393,6 +393,7 @@ export default defineComponent({
else if (this.showYear && this.hasPrevYear) this.emitPrevYear();
else return;
this.flashHighlight("prev");
hapticFeedback("medium");
},
swipeNext() {
if (this.showDay && this.hasNextDay) this.emitNextDay();
@ -400,6 +401,7 @@ export default defineComponent({
else if (this.showYear && this.hasNextYear) this.emitNextYear();
else return;
this.flashHighlight("next");
hapticFeedback("medium");
},
},
});

View file

@ -1,3 +1,4 @@
export function hapticFeedback(): void {
navigator.vibrate?.(5);
// the native app shims navigator.vibrate; duration maps to impact strength (<=50 light, <=100 medium)
export function hapticFeedback(style: "light" | "medium" = "light"): void {
navigator.vibrate?.(style === "medium" ? 60 : 5);
}

View file

@ -1,7 +1,6 @@
interface SwipeOptions {
onSwipeLeft?: () => void;
onSwipeRight?: () => void;
ignoreSelector?: string;
minDistance?: number;
maxDuration?: number;
}
@ -119,8 +118,7 @@ export function attachSwipeHandler(el: HTMLElement, options: SwipeOptions): () =
ignored = true;
return;
}
const target = e.target as Element | null;
ignored = !!(options.ignoreSelector && target?.closest(options.ignoreSelector));
ignored = false;
startX = e.touches[0].clientX;
startY = e.touches[0].clientY;
startTime = Date.now();
@ -139,11 +137,13 @@ export function attachSwipeHandler(el: HTMLElement, options: SwipeOptions): () =
else options.onSwipeRight?.();
};
el.addEventListener("touchstart", onTouchStart, { passive: true });
el.addEventListener("touchend", onTouchEnd, { passive: true });
// capture phase, so the touch tooltip gate's stopPropagation on chart elements
// cannot starve the swipe detection
el.addEventListener("touchstart", onTouchStart, { capture: true, passive: true });
el.addEventListener("touchend", onTouchEnd, { capture: true, passive: true });
return () => {
el.removeEventListener("touchstart", onTouchStart);
el.removeEventListener("touchend", onTouchEnd);
el.removeEventListener("touchstart", onTouchStart, { capture: true });
el.removeEventListener("touchend", onTouchEnd, { capture: true });
};
}