fix: prevent amount range filter crash
This commit is contained in:
@@ -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>
|
||||||
|
|||||||
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