Skip to content

Commit 32d1d0a

Browse files
fix: stabilise modal chart render after transition (#1034)
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
1 parent 348b967 commit 32d1d0a

File tree

2 files changed

+66
-7
lines changed

2 files changed

+66
-7
lines changed

app/components/Modal.client.vue

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@ const props = defineProps<{
55
66
const dialogRef = useTemplateRef('dialogRef')
77
8+
const emit = defineEmits<{
9+
(e: 'transitioned'): void
10+
}>()
11+
812
const modalTitleId = computed(() => {
913
const id = getCurrentInstance()?.attrs.id
1014
return id ? `${id}-title` : undefined
@@ -14,6 +18,20 @@ function handleModalClose() {
1418
dialogRef.value?.close()
1519
}
1620
21+
/**
22+
* Emits `transitioned` once the dialog has finished its open opacity transition.
23+
* This is used by consumers that need to run layout-sensitive logic (for example
24+
* dispatching a resize) only after the modal is fully displayed.
25+
*/
26+
function onDialogTransitionEnd(event: TransitionEvent) {
27+
const el = dialogRef.value
28+
if (!el) return
29+
if (!el.open) return
30+
if (event.target !== el) return
31+
if (event.propertyName !== 'opacity') return
32+
emit('transitioned')
33+
}
34+
1735
defineExpose({
1836
showModal: () => dialogRef.value?.showModal(),
1937
close: () => dialogRef.value?.close(),
@@ -28,6 +46,7 @@ defineExpose({
2846
class="w-full bg-bg border border-border rounded-lg shadow-xl max-h-[90vh] overflow-y-auto overscroll-contain m-0 m-auto p-6 text-fg focus-visible:outline focus-visible:outline-accent/70"
2947
:aria-labelledby="modalTitleId"
3048
v-bind="$attrs"
49+
@transitionend="onDialogTransitionEnd"
3150
>
3251
<!-- Modal top header section -->
3352
<div class="flex items-center justify-between mb-6">

app/components/Package/WeeklyDownloadStats.vue

Lines changed: 47 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,16 +9,27 @@ const props = defineProps<{
99
}>()
1010
1111
const chartModal = useModal('chart-modal')
12-
12+
const hasChartModalTransitioned = shallowRef(false)
1313
const isChartModalOpen = shallowRef(false)
14+
1415
async function openChartModal() {
1516
isChartModalOpen.value = true
17+
hasChartModalTransitioned.value = false
1618
// ensure the component renders before opening the dialog
1719
await nextTick()
1820
await nextTick()
1921
chartModal.open()
2022
}
2123
24+
function handleModalClose() {
25+
isChartModalOpen.value = false
26+
hasChartModalTransitioned.value = false
27+
}
28+
29+
function handleModalTransitioned() {
30+
hasChartModalTransitioned.value = true
31+
}
32+
2233
const { fetchPackageDownloadEvolution } = useCharts()
2334
2435
const { accentColors, selectedAccentColor } = useAccentColor()
@@ -249,16 +260,45 @@ const config = computed(() => {
249260
</CollapsibleSection>
250261
</div>
251262

252-
<PackageChartModal v-if="isChartModalOpen" @close="isChartModalOpen = false">
253-
<PackageDownloadAnalytics
254-
:weeklyDownloads="weeklyDownloads"
255-
:inModal="true"
256-
:packageName="props.packageName"
257-
:createdIso="createdIso"
263+
<PackageChartModal @close="handleModalClose" @transitioned="handleModalTransitioned">
264+
<!-- The Chart is mounted after the dialog has transitioned -->
265+
<!-- This avoids flaky behavior that hides the chart's minimap half of the time -->
266+
<Transition name="opacity" mode="out-in">
267+
<PackageDownloadAnalytics
268+
v-if="hasChartModalTransitioned"
269+
:weeklyDownloads="weeklyDownloads"
270+
:inModal="true"
271+
:packageName="props.packageName"
272+
:createdIso="createdIso"
273+
/>
274+
</Transition>
275+
276+
<!-- This placeholder bears the same dimensions as the PackageDownloadAnalytics component -->
277+
<!-- Avoids CLS when the dialog has transitioned -->
278+
<div
279+
v-if="!hasChartModalTransitioned"
280+
class="w-full aspect-[390/634.5] sm:aspect-[718/622.797]"
258281
/>
259282
</PackageChartModal>
260283
</template>
261284

285+
<style scoped>
286+
.opacity-enter-active,
287+
.opacity-leave-active {
288+
transition: opacity 200ms ease;
289+
}
290+
291+
.opacity-enter-from,
292+
.opacity-leave-to {
293+
opacity: 0;
294+
}
295+
296+
.opacity-enter-to,
297+
.opacity-leave-from {
298+
opacity: 1;
299+
}
300+
</style>
301+
262302
<style>
263303
/** Overrides */
264304
.vue-ui-sparkline-title span {

0 commit comments

Comments
 (0)