Skip to content

Commit 8f2a6f0

Browse files
authored
Config UI: reduce layout shift (#28413)
1 parent 99902ef commit 8f2a6f0

File tree

5 files changed

+57
-2
lines changed

5 files changed

+57
-2
lines changed

assets/js/components/Config/DeviceCard.vue

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,21 +26,25 @@
2626
@edit="$emit('edit')"
2727
/>
2828
</div>
29-
<div v-if="$slots.tags">
29+
<div v-if="$slots.tags" ref="tagsContainer" :style="tagsStyle">
3030
<hr class="my-3 divide" />
31-
<slot name="tags" />
31+
<div ref="tagsContent">
32+
<slot name="tags" />
33+
</div>
3234
</div>
3335
</div>
3436
</template>
3537

3638
<script>
3739
import DeviceCardEditIcon from "./DeviceCardEditIcon.vue";
40+
import settings from "../../settings";
3841
3942
export default {
4043
name: "DeviceCard",
4144
components: { DeviceCardEditIcon },
4245
props: {
4346
name: String,
47+
id: String,
4448
title: String,
4549
editable: Boolean,
4650
error: Boolean,
@@ -50,6 +54,49 @@ export default {
5054
badge: Boolean,
5155
},
5256
emits: ["edit"],
57+
data() {
58+
return {
59+
tagsMinHeight: null,
60+
resizeObserver: null,
61+
};
62+
},
63+
computed: {
64+
tagsStyle() {
65+
return this.tagsMinHeight ? { minHeight: `${this.tagsMinHeight}px` } : undefined;
66+
},
67+
},
68+
mounted() {
69+
if (!this.id) return;
70+
const cached = settings.cardHeights[this.id];
71+
if (cached > 0) {
72+
this.tagsMinHeight = cached;
73+
}
74+
// Cache tag heights to reduce layout shift. Hold cached min-height
75+
// until async data fills the space, then save and release.
76+
this.$nextTick(() => {
77+
const el = this.$refs.tagsContainer;
78+
const content = this.$refs.tagsContent;
79+
if (!el || !content) return;
80+
const initialHeight = content.offsetHeight;
81+
this.resizeObserver = new ResizeObserver(() => {
82+
if (content.offsetHeight <= initialHeight) return;
83+
const prev = el.style.minHeight;
84+
el.style.minHeight = "";
85+
const naturalHeight = Math.round(el.getBoundingClientRect().height);
86+
el.style.minHeight = prev;
87+
if (!this.tagsMinHeight || naturalHeight >= this.tagsMinHeight) {
88+
settings.cardHeights[this.id] = naturalHeight;
89+
this.tagsMinHeight = null;
90+
this.resizeObserver?.disconnect();
91+
this.resizeObserver = null;
92+
}
93+
});
94+
this.resizeObserver.observe(content);
95+
});
96+
},
97+
unmounted() {
98+
this.resizeObserver?.disconnect();
99+
},
53100
};
54101
</script>
55102

assets/js/components/Config/MeterCard.vue

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
<template>
22
<DeviceCard
3+
:id="`meter_${meterType}_${meter.name}`"
34
:title="cardTitle"
45
:name="meter.name"
56
:editable="!!meter.id"

assets/js/components/Config/TariffCard.vue

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
<template>
22
<DeviceCard
3+
:id="`tariff_${tariffType}_${tariff.name}`"
34
:title="cardTitle"
45
:name="tariff.name"
56
:editable="!!tariff.id"

assets/js/settings.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ const LAST_BATTERY_SMART_COST_LIMIT = "last_battery_smart_cost_limit";
2323
const LAST_TARGET_TIME = "last_target_time";
2424
const LAST_SOC_GOAL = "last_soc_goal";
2525
const LAST_ENERGY_GOAL = "last_energy_goal";
26+
const CONFIG_CARD_HEIGHTS = "config_card_heights";
2627

2728
function read(key: string) {
2829
return window.localStorage[key];
@@ -124,6 +125,7 @@ export interface Settings {
124125
lastTargetTime: string | null;
125126
lastSocGoal: number | undefined;
126127
lastEnergyGoal: number | undefined;
128+
cardHeights: Record<string, number>;
127129
}
128130

129131
const settings: Settings = reactive({
@@ -148,6 +150,7 @@ const settings: Settings = reactive({
148150
lastTargetTime: read(LAST_TARGET_TIME),
149151
lastSocGoal: readNumber(LAST_SOC_GOAL),
150152
lastEnergyGoal: readNumber(LAST_ENERGY_GOAL),
153+
cardHeights: readJSON(CONFIG_CARD_HEIGHTS),
151154
});
152155

153156
watch(() => settings.locale, save(SETTINGS_LOCALE));
@@ -171,6 +174,7 @@ watch(() => settings.lastBatterySmartCostLimit, saveNumber(LAST_BATTERY_SMART_CO
171174
watch(() => settings.lastTargetTime, save(LAST_TARGET_TIME));
172175
watch(() => settings.lastSocGoal, saveNumber(LAST_SOC_GOAL));
173176
watch(() => settings.lastEnergyGoal, saveNumber(LAST_ENERGY_GOAL));
177+
watch(() => settings.cardHeights, saveJSON(CONFIG_CARD_HEIGHTS), { deep: true });
174178

175179
export default settings;
176180

assets/js/views/Config.vue

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
<div class="p-0 config-list">
2727
<DeviceCard
2828
v-for="loadpoint in loadpoints"
29+
:id="`loadpoint_${loadpoint.name}`"
2930
:key="loadpoint.name"
3031
:title="loadpoint.title"
3132
:name="loadpoint.name"
@@ -57,6 +58,7 @@
5758
<div class="p-0 config-list">
5859
<DeviceCard
5960
v-for="vehicle in vehicles"
61+
:id="`vehicle_${vehicle.name}`"
6062
:key="vehicle.name"
6163
:title="vehicle.config?.title || vehicle.name"
6264
:name="vehicle.name"

0 commit comments

Comments
 (0)