Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions app/components/Modal.client.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ const props = defineProps<{

const dialogRef = useTemplateRef('dialogRef')

const emit = defineEmits<{
(e: 'transitioned'): void
}>()

const modalTitleId = computed(() => {
const id = getCurrentInstance()?.attrs.id
return id ? `${id}-title` : undefined
Expand All @@ -14,6 +18,20 @@ function handleModalClose() {
dialogRef.value?.close()
}

/**
* Emits `transitioned` once the dialog has finished its open opacity transition.
* This is used by consumers that need to run layout-sensitive logic (for example
* dispatching a resize) only after the modal is fully displayed.
*/
function onDialogTransitionEnd(event: TransitionEvent) {
const el = dialogRef.value
if (!el) return
if (!el.open) return
if (event.target !== el) return
if (event.propertyName !== 'opacity') return
emit('transitioned')
}

defineExpose({
showModal: () => dialogRef.value?.showModal(),
close: () => dialogRef.value?.close(),
Expand All @@ -28,6 +46,7 @@ defineExpose({
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"
:aria-labelledby="modalTitleId"
v-bind="$attrs"
@transitionend="onDialogTransitionEnd"
>
<!-- Modal top header section -->
<div class="flex items-center justify-between mb-6">
Expand Down
54 changes: 47 additions & 7 deletions app/components/Package/WeeklyDownloadStats.vue
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,27 @@ const props = defineProps<{
}>()

const chartModal = useModal('chart-modal')

const hasChartModalTransitioned = shallowRef(false)
const isChartModalOpen = shallowRef(false)

async function openChartModal() {
isChartModalOpen.value = true
hasChartModalTransitioned.value = false
// ensure the component renders before opening the dialog
await nextTick()
await nextTick()
chartModal.open()
}

function handleModalClose() {
isChartModalOpen.value = false
hasChartModalTransitioned.value = false
}

function handleModalTransitioned() {
hasChartModalTransitioned.value = true
}

const { fetchPackageDownloadEvolution } = useCharts()

const { accentColors, selectedAccentColor } = useAccentColor()
Expand Down Expand Up @@ -249,16 +260,45 @@ const config = computed(() => {
</CollapsibleSection>
</div>

<PackageChartModal v-if="isChartModalOpen" @close="isChartModalOpen = false">
<PackageDownloadAnalytics
:weeklyDownloads="weeklyDownloads"
:inModal="true"
:packageName="props.packageName"
:createdIso="createdIso"
<PackageChartModal @close="handleModalClose" @transitioned="handleModalTransitioned">
<!-- The Chart is mounted after the dialog has transitioned -->
<!-- This avoids flaky behavior that hides the chart's minimap half of the time -->
<Transition name="opacity" mode="out-in">
<PackageDownloadAnalytics
v-if="hasChartModalTransitioned"
:weeklyDownloads="weeklyDownloads"
:inModal="true"
:packageName="props.packageName"
:createdIso="createdIso"
/>
</Transition>

<!-- This placeholder bears the same dimensions as the PackageDownloadAnalytics component -->
<!-- Avoids CLS when the dialog has transitioned -->
<div
v-if="!hasChartModalTransitioned"
class="w-full aspect-[390/634.5] sm:aspect-[718/622.797]"
/>
</PackageChartModal>
</template>

<style scoped>
.opacity-enter-active,
.opacity-leave-active {
transition: opacity 200ms ease;
}

.opacity-enter-from,
.opacity-leave-to {
opacity: 0;
}

.opacity-enter-to,
.opacity-leave-from {
opacity: 1;
}
</style>

<style>
/** Overrides */
.vue-ui-sparkline-title span {
Expand Down
Loading