fix: prevent amount range filter crash

This commit is contained in:
2026-08-21 02:19:51 +03:30
parent 0fb044cd59
commit 9f575ec151
2 changed files with 126 additions and 26 deletions

View File

@@ -2,7 +2,6 @@
import SlidersHorizontalIcon from '@lucide/svelte/icons/sliders-horizontal'; import SlidersHorizontalIcon from '@lucide/svelte/icons/sliders-horizontal';
import * as Popover from '$lib/components/ui/popover'; import * as Popover from '$lib/components/ui/popover';
import { Slider } from '$lib/components/ui/slider';
import { formatValue } from '$lib/format'; import { formatValue } from '$lib/format';
const LIMIT = 10_000_000_000; const LIMIT = 10_000_000_000;
@@ -19,22 +18,16 @@
} = $props(); } = $props();
let open = $state(false); let open = $state(false);
let localMin = $state(0); let localMin = $state(normalizedRange()[0]);
let localMax = $state(LIMIT); let localMax = $state(normalizedRange()[1]);
const hasFilter = $derived(minAmount !== null || maxAmount !== null); const hasFilter = $derived(minAmount !== null || maxAmount !== null);
let sliderValue = $state([0, LIMIT]); const minPosition = $derived((localMin / LIMIT) * 100);
const maxPosition = $derived((localMax / LIMIT) * 100);
$effect(() => { function normalizedRange(): [number, number] {
localMin = Math.max(0, Math.min(minAmount ?? 0, LIMIT)); const nextMin = Math.max(0, Math.min(minAmount ?? 0, LIMIT));
localMax = Math.max(localMin, Math.min(maxAmount ?? LIMIT, LIMIT)); const nextMax = Math.max(nextMin, Math.min(maxAmount ?? LIMIT, LIMIT));
sliderValue = [localMin, localMax]; return [nextMin, nextMax];
});
function updateRange(value: number[]) {
const [nextMin = 0, nextMax = LIMIT] = [...value].sort((left, right) => left - right);
localMin = nextMin;
localMax = nextMax;
sliderValue = [nextMin, nextMax];
} }
function apply() { function apply() {
@@ -43,7 +36,12 @@
} }
</script> </script>
<Popover.Root bind:open> <Popover.Root
bind:open
onOpenChange={(nextOpen) => {
if (nextOpen) [localMin, localMax] = normalizedRange();
}}
>
<Popover.Trigger <Popover.Trigger
class="border-input bg-background hover:bg-muted focus-visible:border-ring focus-visible:ring-ring/30 inline-flex min-h-11 cursor-pointer items-center gap-1.5 rounded-md border px-3 text-xs font-medium transition-colors focus-visible:ring-1 focus-visible:outline-none sm:min-h-9" class="border-input bg-background hover:bg-muted focus-visible:border-ring focus-visible:ring-ring/30 inline-flex min-h-11 cursor-pointer items-center gap-1.5 rounded-md border px-3 text-xs font-medium transition-colors focus-visible:ring-1 focus-visible:outline-none sm:min-h-9"
aria-label="فیلتر بازه مبلغ" aria-label="فیلتر بازه مبلغ"
@@ -64,16 +62,39 @@
<output class="bg-muted rounded-md px-2 py-1">تا {formatValue(localMax)} ریال</output> <output class="bg-muted rounded-md px-2 py-1">تا {formatValue(localMax)} ریال</output>
</div> </div>
<Slider <div class="relative my-2 h-8" dir="rtl">
value={sliderValue} <span class="bg-muted absolute top-1/2 right-0 left-0 h-1 -translate-y-1/2 rounded-full">
onValueChange={updateRange} <span
min={0} class="bg-primary absolute h-full rounded-full"
max={LIMIT} style:right={`${minPosition}%`}
step={STEP} style:left={`${100 - maxPosition}%`}
dir="rtl" ></span>
class="my-2 h-8" </span>
aria-label="بازه مبلغ تراکنش" <input
/> type="range"
min={0}
max={LIMIT}
step={STEP}
bind:value={localMin}
oninput={() => {
if (localMin > localMax) localMin = localMax;
}}
class="amount-range-input absolute inset-0 h-8 w-full"
aria-label="کمینه مبلغ تراکنش"
/>
<input
type="range"
min={0}
max={LIMIT}
step={STEP}
bind:value={localMax}
oninput={() => {
if (localMax < localMin) localMax = localMin;
}}
class="amount-range-input absolute inset-0 h-8 w-full"
aria-label="بیشینه مبلغ تراکنش"
/>
</div>
<div class="flex gap-2"> <div class="flex gap-2">
<button <button
@@ -94,3 +115,57 @@
</div> </div>
</Popover.Content> </Popover.Content>
</Popover.Root> </Popover.Root>
<style>
.amount-range-input {
appearance: none;
background: transparent;
pointer-events: none;
}
.amount-range-input::-webkit-slider-runnable-track {
height: 0.25rem;
background: transparent;
}
.amount-range-input::-webkit-slider-thumb {
width: 1rem;
height: 1rem;
margin-top: -0.375rem;
appearance: none;
border: 2px solid var(--background);
border-radius: 9999px;
background: var(--primary);
box-shadow: 0 1px 2px rgb(0 0 0 / 0.12);
cursor: grab;
pointer-events: auto;
}
.amount-range-input::-moz-range-track {
height: 0.25rem;
background: transparent;
}
.amount-range-input::-moz-range-thumb {
width: 1rem;
height: 1rem;
border: 2px solid var(--background);
border-radius: 9999px;
background: var(--primary);
box-shadow: 0 1px 2px rgb(0 0 0 / 0.12);
cursor: grab;
pointer-events: auto;
}
.amount-range-input:focus-visible {
outline: none;
}
.amount-range-input:focus-visible::-webkit-slider-thumb {
box-shadow: 0 0 0 3px color-mix(in oklab, var(--ring) 30%, transparent);
}
.amount-range-input:focus-visible::-moz-range-thumb {
box-shadow: 0 0 0 3px color-mix(in oklab, var(--ring) 30%, transparent);
}
</style>

View File

@@ -0,0 +1,25 @@
import { fireEvent, render, screen } from '@testing-library/svelte';
import { describe, expect, it, vi } from 'vitest';
import AmountRangeFilter from './AmountRangeFilter.svelte';
describe('AmountRangeFilter', () => {
it('moves both amount bounds without resetting or crashing', async () => {
const onchange = vi.fn();
render(AmountRangeFilter, { minAmount: null, maxAmount: null, onchange });
await fireEvent.click(screen.getByRole('button', { name: 'فیلتر بازه مبلغ' }));
const thumbs = await screen.findAllByRole('slider');
expect(thumbs).toHaveLength(2);
await fireEvent.input(thumbs[0], { target: { value: '100000' } });
await fireEvent.input(thumbs[1], { target: { value: '9999900000' } });
expect(screen.getByText('از ۱۰۰٬۰۰۰ ریال')).toBeTruthy();
expect(screen.getByText('تا ۹٬۹۹۹٬۹۰۰٬۰۰۰ ریال')).toBeTruthy();
await fireEvent.click(screen.getByRole('button', { name: 'اعمال بازه' }));
expect(onchange).toHaveBeenCalledWith(100_000, 9_999_900_000);
});
});