fix: prevent amount range filter crash
This commit is contained in:
@@ -2,7 +2,6 @@
|
||||
import SlidersHorizontalIcon from '@lucide/svelte/icons/sliders-horizontal';
|
||||
|
||||
import * as Popover from '$lib/components/ui/popover';
|
||||
import { Slider } from '$lib/components/ui/slider';
|
||||
import { formatValue } from '$lib/format';
|
||||
|
||||
const LIMIT = 10_000_000_000;
|
||||
@@ -19,22 +18,16 @@
|
||||
} = $props();
|
||||
|
||||
let open = $state(false);
|
||||
let localMin = $state(0);
|
||||
let localMax = $state(LIMIT);
|
||||
let localMin = $state(normalizedRange()[0]);
|
||||
let localMax = $state(normalizedRange()[1]);
|
||||
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(() => {
|
||||
localMin = Math.max(0, Math.min(minAmount ?? 0, LIMIT));
|
||||
localMax = Math.max(localMin, Math.min(maxAmount ?? LIMIT, LIMIT));
|
||||
sliderValue = [localMin, localMax];
|
||||
});
|
||||
|
||||
function updateRange(value: number[]) {
|
||||
const [nextMin = 0, nextMax = LIMIT] = [...value].sort((left, right) => left - right);
|
||||
localMin = nextMin;
|
||||
localMax = nextMax;
|
||||
sliderValue = [nextMin, nextMax];
|
||||
function normalizedRange(): [number, number] {
|
||||
const nextMin = Math.max(0, Math.min(minAmount ?? 0, LIMIT));
|
||||
const nextMax = Math.max(nextMin, Math.min(maxAmount ?? LIMIT, LIMIT));
|
||||
return [nextMin, nextMax];
|
||||
}
|
||||
|
||||
function apply() {
|
||||
@@ -43,7 +36,12 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<Popover.Root bind:open>
|
||||
<Popover.Root
|
||||
bind:open
|
||||
onOpenChange={(nextOpen) => {
|
||||
if (nextOpen) [localMin, localMax] = normalizedRange();
|
||||
}}
|
||||
>
|
||||
<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"
|
||||
aria-label="فیلتر بازه مبلغ"
|
||||
@@ -64,16 +62,39 @@
|
||||
<output class="bg-muted rounded-md px-2 py-1">تا {formatValue(localMax)} ریال</output>
|
||||
</div>
|
||||
|
||||
<Slider
|
||||
value={sliderValue}
|
||||
onValueChange={updateRange}
|
||||
min={0}
|
||||
max={LIMIT}
|
||||
step={STEP}
|
||||
dir="rtl"
|
||||
class="my-2 h-8"
|
||||
aria-label="بازه مبلغ تراکنش"
|
||||
/>
|
||||
<div class="relative my-2 h-8" dir="rtl">
|
||||
<span class="bg-muted absolute top-1/2 right-0 left-0 h-1 -translate-y-1/2 rounded-full">
|
||||
<span
|
||||
class="bg-primary absolute h-full rounded-full"
|
||||
style:right={`${minPosition}%`}
|
||||
style:left={`${100 - maxPosition}%`}
|
||||
></span>
|
||||
</span>
|
||||
<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">
|
||||
<button
|
||||
@@ -94,3 +115,57 @@
|
||||
</div>
|
||||
</Popover.Content>
|
||||
</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>
|
||||
|
||||
25
src/lib/components/AmountRangeFilter.test.ts
Normal file
25
src/lib/components/AmountRangeFilter.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user