fix: preserve chart width on mobile

This commit is contained in:
2026-08-21 16:46:42 +08:00
parent 7b8ee07b91
commit e3ead133f4
2 changed files with 120 additions and 107 deletions

View File

@@ -71,131 +71,140 @@
} }
</script> </script>
<div class="relative min-h-[290px] w-full overflow-hidden" dir="ltr"> <!-- svelte-ignore a11y_no_noninteractive_tabindex (the horizontally scrollable chart must be keyboard reachable) -->
{#if data.length === 0} <div
<div class="text-muted-foreground grid h-[260px] place-items-center text-sm"> class="w-full overflow-x-auto overscroll-x-contain"
داده‌ای برای این بازه ثبت نشده است. dir="ltr"
</div> role="region"
{:else} aria-label="نمودار روند پرداخت‌های موفق، قابل پیمایش افقی"
<svg tabindex="0"
viewBox={`0 0 ${width} ${height}`} >
class="h-auto min-h-[280px] w-full overflow-visible" <div class="relative min-h-[290px] min-w-[760px] md:min-w-0">
role="img" {#if data.length === 0}
aria-label="روند پرداخت‌های موفق" <div class="text-muted-foreground grid h-[260px] place-items-center text-sm">
> داده‌ای برای این بازه ثبت نشده است.
<defs> </div>
<linearGradient id="payment-area" x1="0" x2="0" y1="0" y2="1"> {:else}
<stop offset="0" stop-color="var(--primary)" stop-opacity=".22" /> <svg
<stop offset="1" stop-color="var(--primary)" stop-opacity="0" /> viewBox={`0 0 ${width} ${height}`}
</linearGradient> class="h-auto min-h-[280px] w-full overflow-visible"
</defs> role="img"
{#each [0, 1, 2, 3, 4] as row} aria-label="روند پرداخت‌های موفق"
{@const y = pad.top + (row * (height - pad.top - pad.bottom)) / 4} >
<defs>
<linearGradient id="payment-area" x1="0" x2="0" y1="0" y2="1">
<stop offset="0" stop-color="var(--primary)" stop-opacity=".22" />
<stop offset="1" stop-color="var(--primary)" stop-opacity="0" />
</linearGradient>
</defs>
{#each [0, 1, 2, 3, 4] as row}
{@const y = pad.top + (row * (height - pad.top - pad.bottom)) / 4}
<line
x1={pad.left}
x2={width - pad.right}
y1={y}
y2={y}
stroke="var(--border)"
stroke-dasharray="4 6"
/>
<text x={pad.left - 10} y={y + 4} fill="var(--muted-foreground)" font-size="10" text-anchor="end"
>{formatValue(Math.round(max * (1 - row / 4)))}</text
>
{/each}
<line x1={pad.left} x2={pad.left} y1={pad.top} y2={height - pad.bottom} stroke="var(--border)" />
<line <line
x1={pad.left} x1={pad.left}
x2={width - pad.right} x2={width - pad.right}
y1={y}
y2={y}
stroke="var(--border)"
stroke-dasharray="4 6"
/>
<text x={pad.left - 10} y={y + 4} fill="var(--muted-foreground)" font-size="10" text-anchor="end"
>{formatValue(Math.round(max * (1 - row / 4)))}</text
>
{/each}
<line x1={pad.left} x2={pad.left} y1={pad.top} y2={height - pad.bottom} stroke="var(--border)" />
<line
x1={pad.left}
x2={width - pad.right}
y1={height - pad.bottom}
y2={height - pad.bottom}
stroke="var(--border)"
/>
{#each xTickIndexes as tickIndex}
{@const tick = positions[tickIndex]}
<line
x1={tick.x}
x2={tick.x}
y1={height - pad.bottom} y1={height - pad.bottom}
y2={height - pad.bottom + 5} y2={height - pad.bottom}
stroke="var(--border)" stroke="var(--border)"
/> />
{#each xTickIndexes as tickIndex}
{@const tick = positions[tickIndex]}
<line
x1={tick.x}
x2={tick.x}
y1={height - pad.bottom}
y2={height - pad.bottom + 5}
stroke="var(--border)"
/>
<text
x={tick.x}
y={height - pad.bottom + 18}
fill="var(--muted-foreground)"
font-size="9"
text-anchor="middle"
direction="rtl">{axisLabel(tick)}</text
>
{/each}
<text <text
x={tick.x} x={(pad.left + width - pad.right) / 2}
y={height - pad.bottom + 18} y={height - 5}
fill="var(--muted-foreground)" fill="var(--muted-foreground)"
font-size="9" font-size="10"
text-anchor="middle" text-anchor="middle"
direction="rtl">{axisLabel(tick)}</text direction="rtl">{periodType === 'DAY' ? 'ساعت' : 'تاریخ'}</text
> >
{/each} <text
<text x={-((pad.top + height - pad.bottom) / 2)}
x={(pad.left + width - pad.right) / 2} y="13"
y={height - 5} fill="var(--muted-foreground)"
fill="var(--muted-foreground)" font-size="10"
font-size="10" text-anchor="middle"
text-anchor="middle" transform="rotate(-90)"
direction="rtl">{periodType === 'DAY' ? 'ساعت' : 'تاریخ'}</text direction="rtl">تعداد پرداخت موفق</text
> >
<text <path
x={-((pad.top + height - pad.bottom) / 2)} d={`${path} L ${positions.at(-1)?.x} ${height - pad.bottom} L ${positions[0]?.x} ${height - pad.bottom} Z`}
y="13" fill="url(#payment-area)"
fill="var(--muted-foreground)" />
font-size="10" <path
text-anchor="middle" d={path}
transform="rotate(-90)" fill="none"
direction="rtl">تعداد پرداخت موفق</text
>
<path
d={`${path} L ${positions.at(-1)?.x} ${height - pad.bottom} L ${positions[0]?.x} ${height - pad.bottom} Z`}
fill="url(#payment-area)"
/>
<path
d={path}
fill="none"
stroke="var(--primary)"
stroke-width="3"
stroke-linecap="round"
stroke-linejoin="round"
class="chart-line"
/>
{#each positions as point, index}
<circle
cx={point.x}
cy={point.y}
r={active === index ? 6 : 3.5}
fill="var(--card)"
stroke="var(--primary)" stroke="var(--primary)"
stroke-width="3" stroke-width="3"
tabindex="0" stroke-linecap="round"
role="button" stroke-linejoin="round"
aria-label={`${fullPointLabel(point)}، ${formatValue(point.bankPaidSessions)} پرداخت موفق`} class="chart-line"
onmouseenter={() => (active = index)}
onmouseleave={() => (active = null)}
onfocus={() => (active = index)}
onblur={() => (active = null)}
/> />
{/each} {#each positions as point, index}
</svg> <circle
{#if active !== null && positions[active]} cx={point.x}
<div cy={point.y}
class="pointer-events-none absolute z-10 w-44 transition-[left,top,transform] duration-150 ease-out motion-reduce:transition-none" r={active === index ? 6 : 3.5}
style={tooltipPosition(positions[active])} fill="var(--card)"
dir="rtl" stroke="var(--primary)"
> stroke-width="3"
tabindex="0"
role="button"
aria-label={`${fullPointLabel(point)}، ${formatValue(point.bankPaidSessions)} پرداخت موفق`}
onmouseenter={() => (active = index)}
onmouseleave={() => (active = null)}
onfocus={() => (active = index)}
onblur={() => (active = null)}
/>
{/each}
</svg>
{#if active !== null && positions[active]}
<div <div
class="rounded-md bg-zinc-950 p-3 text-right text-xs leading-6 text-white shadow-xl" class="pointer-events-none absolute z-10 w-44 transition-[left,top,transform] duration-150 ease-out motion-reduce:transition-none"
transition:scale={{ duration: 150, start: 0.96 }} style={tooltipPosition(positions[active])}
dir="rtl"
> >
<strong class="block">{fullPointLabel(positions[active])}</strong> <div
<span class="block text-white/75" class="rounded-md bg-zinc-950 p-3 text-right text-xs leading-6 text-white shadow-xl"
>{formatValue(positions[active].bankPaidSessions)} پرداخت موفق</span transition:scale={{ duration: 150, start: 0.96 }}
> >
<span class="block text-white/75">{formatMoney(positions[active].bankPaidAmountRial)}</span> <strong class="block">{fullPointLabel(positions[active])}</strong>
<span class="block text-white/75"
>{formatValue(positions[active].bankPaidSessions)} پرداخت موفق</span
>
<span class="block text-white/75">{formatMoney(positions[active].bankPaidAmountRial)}</span>
</div>
</div> </div>
</div> {/if}
{/if} {/if}
{/if} </div>
</div> </div>
<style> <style>

View File

@@ -28,5 +28,9 @@ describe('PaymentHistoryChart', () => {
expect(screen.getByText('۰۰:۰۰')).not.toBeNull(); expect(screen.getByText('۰۰:۰۰')).not.toBeNull();
expect(screen.getByText('۱۲:۰۰')).not.toBeNull(); expect(screen.getByText('۱۲:۰۰')).not.toBeNull();
expect(screen.getByText('ساعت')).not.toBeNull(); expect(screen.getByText('ساعت')).not.toBeNull();
const scrollRegion = screen.getByRole('region', { name: /قابل پیمایش افقی/ });
expect(scrollRegion.className).toContain('overflow-x-auto');
expect(scrollRegion.firstElementChild?.className).toContain('min-w-[760px]');
}); });
}); });