Skip to content

Commit da2cf3e

Browse files
fix: handle CLS issues in chart modal (#2032)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
1 parent b4b9cc8 commit da2cf3e

File tree

1 file changed

+186
-111
lines changed

1 file changed

+186
-111
lines changed

app/components/Package/TrendsChart.vue

Lines changed: 186 additions & 111 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<script setup lang="ts">
22
import type { Theme as VueDataUiTheme, VueUiXyConfig, VueUiXyDatasetItem } from 'vue-data-ui'
33
import { VueUiXy } from 'vue-data-ui/vue-ui-xy'
4-
import { useDebounceFn, useElementSize } from '@vueuse/core'
4+
import { useDebounceFn, useElementSize, useTimeoutFn } from '@vueuse/core'
55
import { useCssVariables } from '~/composables/useColors'
66
import { OKLCH_NEUTRAL_FALLBACK, transparentizeOklch, lightenOklch } from '~/utils/colors'
77
import { getFrameworkColor, isListedFramework } from '~/utils/frameworks'
@@ -1350,12 +1350,46 @@ function drawSvgPrintLegend(svg: Record<string, any>) {
13501350
return seriesNames.join('\n')
13511351
}
13521352
1353+
const showCorrectionControls = shallowRef(false)
1354+
const isResizing = shallowRef(false)
1355+
1356+
const chartHeight = computed(() => {
1357+
if (isMobile.value) {
1358+
return 950
1359+
}
1360+
return showCorrectionControls.value && props.inModal ? 494 : 600
1361+
})
1362+
1363+
const { start } = useTimeoutFn(
1364+
() => {
1365+
isResizing.value = false
1366+
},
1367+
200,
1368+
{ immediate: false },
1369+
)
1370+
1371+
function pauseChartTransitions() {
1372+
isResizing.value = true
1373+
start()
1374+
}
1375+
1376+
watch(
1377+
chartHeight,
1378+
(newH, oldH) => {
1379+
if (newH !== oldH) {
1380+
// Avoids triggering chart line transitions when the chart is resized
1381+
pauseChartTransitions()
1382+
}
1383+
},
1384+
{ immediate: true },
1385+
)
1386+
13531387
// VueUiXy chart component configuration
13541388
const chartConfig = computed<VueUiXyConfig>(() => {
13551389
return {
13561390
theme: isDarkMode.value ? 'dark' : ('' as VueDataUiTheme),
13571391
chart: {
1358-
height: isMobile.value ? 950 : 600,
1392+
height: chartHeight.value,
13591393
backgroundColor: colors.value.bg,
13601394
padding: { bottom: displayedGranularity.value === 'yearly' ? 84 : 64, right: 128 }, // padding right is set to leave space of last datapoint label(s)
13611395
userOptions: {
@@ -1559,7 +1593,6 @@ const chartConfig = computed<VueUiXyConfig>(() => {
15591593
})
15601594
15611595
const isDownloadsMetric = computed(() => selectedMetric.value === 'downloads')
1562-
const showCorrectionControls = shallowRef(false)
15631596
15641597
const packageAnomalies = computed(() => getAnomaliesForPackages(effectivePackageNames.value))
15651598
const hasAnomalies = computed(() => packageAnomalies.value.length > 0)
@@ -1648,6 +1681,8 @@ watch(selectedMetric, value => {
16481681

16491682
<button
16501683
v-if="showResetButton"
1684+
:aria-expanded="showCorrectionControls"
1685+
aria-controls="trends-correction-controls"
16511686
type="button"
16521687
aria-label="Reset date range"
16531688
class="self-end flex items-center justify-center px-2.5 py-2.25 border border-transparent rounded-md text-fg-subtle hover:text-fg transition-colors hover:border-border focus-visible:outline-accent/70 sm:mb-0"
@@ -1671,115 +1706,141 @@ watch(selectedMetric, value => {
16711706
/>
16721707
{{ $t('package.trends.data_correction') }}
16731708
</button>
1674-
<div v-if="showCorrectionControls" class="grid grid-cols-2 sm:flex items-end gap-3">
1675-
<label class="flex flex-col gap-1 flex-1">
1676-
<span class="text-2xs font-mono text-fg-subtle tracking-wide uppercase">
1677-
{{ $t('package.trends.average_window') }}
1678-
<span class="text-fg-muted">({{ settings.chartFilter.averageWindow }})</span>
1679-
</span>
1680-
<input
1681-
v-model.number="settings.chartFilter.averageWindow"
1682-
type="range"
1683-
min="0"
1684-
max="20"
1685-
step="1"
1686-
class="accent-[var(--accent-color,var(--fg-subtle))]"
1687-
/>
1688-
</label>
1689-
<label class="flex flex-col gap-1 flex-1">
1690-
<span class="text-2xs font-mono text-fg-subtle tracking-wide uppercase">
1691-
{{ $t('package.trends.smoothing') }}
1692-
<span class="text-fg-muted">({{ settings.chartFilter.smoothingTau }})</span>
1693-
</span>
1694-
<input
1695-
v-model.number="settings.chartFilter.smoothingTau"
1696-
type="range"
1697-
min="0"
1698-
max="20"
1699-
step="1"
1700-
class="accent-[var(--accent-color,var(--fg-subtle))]"
1701-
/>
1702-
</label>
1703-
<label class="flex flex-col gap-1 flex-1">
1704-
<span class="text-2xs font-mono text-fg-subtle tracking-wide uppercase">
1705-
{{ $t('package.trends.prediction') }}
1706-
<span class="text-fg-muted">({{ settings.chartFilter.predictionPoints }})</span>
1707-
</span>
1708-
<input
1709-
v-model.number="settings.chartFilter.predictionPoints"
1710-
type="range"
1711-
min="0"
1712-
max="30"
1713-
step="1"
1714-
class="accent-[var(--accent-color,var(--fg-subtle))]"
1715-
/>
1716-
</label>
1717-
<div class="flex flex-col gap-1 shrink-0">
1718-
<span
1719-
class="text-2xs font-mono text-fg-subtle tracking-wide uppercase flex items-center justify-between"
1720-
>
1721-
{{ $t('package.trends.known_anomalies') }}
1722-
<TooltipApp interactive :to="inModal ? '#chart-modal' : undefined">
1723-
<button
1724-
type="button"
1725-
class="i-lucide:info w-3.5 h-3.5 text-fg-muted cursor-help"
1726-
:aria-label="$t('package.trends.known_anomalies')"
1709+
<div
1710+
class="overflow-hidden transition-[opacity] duration-200 ease-out"
1711+
id="trends-correction-controls"
1712+
:aria-hidden="!showCorrectionControls"
1713+
:inert="!showCorrectionControls"
1714+
:class="
1715+
showCorrectionControls
1716+
? 'max-h-[220px] opacity-100'
1717+
: 'max-h-0 opacity-0 pointer-events-none'
1718+
"
1719+
>
1720+
<div class="pt-1 min-h-[160px] sm:min-h-[76px]">
1721+
<div class="grid grid-cols-2 sm:flex items-end gap-3">
1722+
<label class="flex flex-col gap-1 flex-1">
1723+
<span class="text-2xs font-mono text-fg-subtle tracking-wide uppercase">
1724+
{{ $t('package.trends.average_window') }}
1725+
<span class="text-fg-muted">({{ settings.chartFilter.averageWindow }})</span>
1726+
</span>
1727+
<input
1728+
v-model.number="settings.chartFilter.averageWindow"
1729+
:disabled="!showCorrectionControls"
1730+
type="range"
1731+
min="0"
1732+
max="20"
1733+
step="1"
1734+
class="accent-[var(--accent-color,var(--fg-subtle))]"
17271735
/>
1728-
<template #content>
1729-
<div class="flex flex-col gap-3">
1730-
<p class="text-xs text-fg-muted">
1731-
{{ $t('package.trends.known_anomalies_description') }}
1732-
</p>
1733-
<div v-if="hasAnomalies">
1734-
<p class="text-xs text-fg-subtle font-medium">
1735-
{{ $t('package.trends.known_anomalies_ranges') }}
1736-
</p>
1737-
<ul class="text-xs text-fg-subtle list-disc list-inside">
1738-
<li v-for="a in packageAnomalies" :key="`${a.packageName}-${a.start}`">
1736+
</label>
1737+
<label class="flex flex-col gap-1 flex-1">
1738+
<span class="text-2xs font-mono text-fg-subtle tracking-wide uppercase">
1739+
{{ $t('package.trends.smoothing') }}
1740+
<span class="text-fg-muted">({{ settings.chartFilter.smoothingTau }})</span>
1741+
</span>
1742+
<input
1743+
v-model.number="settings.chartFilter.smoothingTau"
1744+
:disabled="!showCorrectionControls"
1745+
type="range"
1746+
min="0"
1747+
max="20"
1748+
step="1"
1749+
class="accent-[var(--accent-color,var(--fg-subtle))]"
1750+
/>
1751+
</label>
1752+
<label class="flex flex-col gap-1 flex-1">
1753+
<span class="text-2xs font-mono text-fg-subtle tracking-wide uppercase">
1754+
{{ $t('package.trends.prediction') }}
1755+
<span class="text-fg-muted">({{ settings.chartFilter.predictionPoints }})</span>
1756+
</span>
1757+
<input
1758+
v-model.number="settings.chartFilter.predictionPoints"
1759+
:disabled="!showCorrectionControls"
1760+
type="range"
1761+
min="0"
1762+
max="30"
1763+
step="1"
1764+
class="accent-[var(--accent-color,var(--fg-subtle))]"
1765+
/>
1766+
</label>
1767+
<div class="flex flex-col gap-1 shrink-0">
1768+
<span
1769+
class="text-2xs font-mono text-fg-subtle tracking-wide uppercase flex items-center justify-between"
1770+
>
1771+
{{ $t('package.trends.known_anomalies') }}
1772+
<TooltipApp
1773+
interactive
1774+
:to="inModal ? '#chart-modal' : undefined"
1775+
v-if="showCorrectionControls"
1776+
>
1777+
<button
1778+
type="button"
1779+
class="i-lucide:info w-3.5 h-3.5 text-fg-muted cursor-help"
1780+
:aria-label="$t('package.trends.known_anomalies')"
1781+
/>
1782+
<template #content>
1783+
<div class="flex flex-col gap-3">
1784+
<p class="text-xs text-fg-muted">
1785+
{{ $t('package.trends.known_anomalies_description') }}
1786+
</p>
1787+
<div v-if="hasAnomalies">
1788+
<p class="text-xs text-fg-subtle font-medium">
1789+
{{ $t('package.trends.known_anomalies_ranges') }}
1790+
</p>
1791+
<ul class="text-xs text-fg-subtle list-disc list-inside">
1792+
<li v-for="a in packageAnomalies" :key="`${a.packageName}-${a.start}`">
1793+
{{
1794+
isMultiPackageMode
1795+
? $t('package.trends.known_anomalies_range_named', {
1796+
packageName: a.packageName,
1797+
start: formatAnomalyDate(a.start),
1798+
end: formatAnomalyDate(a.end),
1799+
})
1800+
: $t('package.trends.known_anomalies_range', {
1801+
start: formatAnomalyDate(a.start),
1802+
end: formatAnomalyDate(a.end),
1803+
})
1804+
}}
1805+
</li>
1806+
</ul>
1807+
</div>
1808+
<p v-else class="text-xs text-fg-muted">
17391809
{{
1740-
isMultiPackageMode
1741-
? $t('package.trends.known_anomalies_range_named', {
1742-
packageName: a.packageName,
1743-
start: formatAnomalyDate(a.start),
1744-
end: formatAnomalyDate(a.end),
1745-
})
1746-
: $t('package.trends.known_anomalies_range', {
1747-
start: formatAnomalyDate(a.start),
1748-
end: formatAnomalyDate(a.end),
1749-
})
1810+
$t('package.trends.known_anomalies_none', effectivePackageNames.length)
17501811
}}
1751-
</li>
1752-
</ul>
1753-
</div>
1754-
<p v-else class="text-xs text-fg-muted">
1755-
{{ $t('package.trends.known_anomalies_none', effectivePackageNames.length) }}
1756-
</p>
1757-
<div class="flex justify-end">
1758-
<LinkBase
1759-
to="https://github.com/npmx-dev/npmx.dev/edit/main/app/utils/download-anomalies.data.ts"
1760-
class="text-xs text-accent"
1761-
>
1762-
{{ $t('package.trends.known_anomalies_contribute') }}
1763-
</LinkBase>
1764-
</div>
1765-
</div>
1766-
</template>
1767-
</TooltipApp>
1768-
</span>
1769-
<label
1770-
class="flex items-center gap-1.5 text-2xs font-mono text-fg-subtle cursor-pointer h-4"
1771-
:class="{ 'opacity-50': !hasAnomalies }"
1772-
>
1773-
<input
1774-
:checked="settings.chartFilter.anomaliesFixed"
1775-
@change="
1776-
settings.chartFilter.anomaliesFixed = ($event.target as HTMLInputElement).checked
1777-
"
1778-
type="checkbox"
1779-
class="accent-[var(--accent-color,var(--fg-subtle))]"
1780-
/>
1781-
{{ $t('package.trends.apply_correction') }}
1782-
</label>
1812+
</p>
1813+
<div class="flex justify-end">
1814+
<LinkBase
1815+
to="https://github.com/npmx-dev/npmx.dev/edit/main/app/utils/download-anomalies.data.ts"
1816+
class="text-xs text-accent"
1817+
>
1818+
{{ $t('package.trends.known_anomalies_contribute') }}
1819+
</LinkBase>
1820+
</div>
1821+
</div>
1822+
</template>
1823+
</TooltipApp>
1824+
</span>
1825+
<label
1826+
class="flex items-center gap-1.5 text-2xs font-mono text-fg-subtle cursor-pointer h-4"
1827+
:class="{ 'opacity-50': !hasAnomalies }"
1828+
>
1829+
<input
1830+
:checked="settings.chartFilter.anomaliesFixed"
1831+
:disabled="!showCorrectionControls"
1832+
@change="
1833+
settings.chartFilter.anomaliesFixed = (
1834+
$event.target as HTMLInputElement
1835+
).checked
1836+
"
1837+
type="checkbox"
1838+
class="accent-[var(--accent-color,var(--fg-subtle))]"
1839+
/>
1840+
{{ $t('package.trends.apply_correction') }}
1841+
</label>
1842+
</div>
1843+
</div>
17831844
</div>
17841845
</div>
17851846
</div>
@@ -1798,14 +1859,23 @@ watch(selectedMetric, value => {
17981859
<div
17991860
role="region"
18001861
aria-labelledby="trends-chart-title"
1801-
:class="isMobile === false && width > 0 ? 'min-h-[567px]' : 'min-h-[260px]'"
1862+
:class="
1863+
isMobile === false && width > 0
1864+
? showCorrectionControls
1865+
? 'h-[491px]'
1866+
: 'h-[567px]'
1867+
: 'min-h-[260px]'
1868+
"
18021869
>
18031870
<ClientOnly v-if="chartData.dataset">
18041871
<div :data-pending="pending" :data-minimap-visible="maxDatapoints > 6">
18051872
<VueUiXy
18061873
:dataset="normalisedDataset"
18071874
:config="chartConfig"
1808-
class="[direction:ltr]"
1875+
:class="{
1876+
'[direction:ltr]': true,
1877+
'no-transition': isResizing,
1878+
}"
18091879
@zoomStart="setIsZoom"
18101880
@zoomEnd="setIsZoom"
18111881
@zoomReset="isZoomed = false"
@@ -2102,6 +2172,11 @@ watch(selectedMetric, value => {
21022172
top: calc(100% - 2rem) !important;
21032173
}
21042174
2175+
.no-transition line,
2176+
.no-transition circle {
2177+
transition: none !important;
2178+
}
2179+
21052180
input::-webkit-date-and-time-value {
21062181
margin-inline: 4px;
21072182
}

0 commit comments

Comments
 (0)