Skip to content

Commit e689bab

Browse files
authored
fix(Slider): thumb positioning (#2015)
1 parent 8a6727f commit e689bab

6 files changed

Lines changed: 65 additions & 1 deletion

File tree

.changeset/fast-suns-write.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"bits-ui": patch
3+
---
4+
5+
fix(Slider): use ResizeObserver to recompute thumb positioning

eslint.config.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ export default tseslint.config(
6262
"packages/bits-ui/.svelte-kit/**/*",
6363
"tests/.svelte-kit/**/*",
6464
"bundle-analyzer/.temp-bundle-analysis/**/*",
65+
".tmp/",
6566
],
6667
},
6768
...oxlint.configs["flat/recommended"],

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@
4848
"pnpm": ">=10.12.1",
4949
"node": ">=20"
5050
},
51-
"packageManager": "pnpm@10.17.1",
51+
"packageManager": "pnpm@10.33.0",
5252
"private": true,
5353
"pnpm": {
5454
"onlyBuiltDependencies": [

packages/bits-ui/src/lib/bits/slider/slider.svelte.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import { createBitsAttrs, boolToStr, boolToEmptyStrOrUndef } from "$lib/internal
2929
import { kbd } from "$lib/internal/kbd.js";
3030
import { isElementOrSVGElement } from "$lib/internal/is.js";
3131
import { isValidIndex } from "$lib/internal/arrays.js";
32+
import { SvelteResizeObserver } from "$lib/internal/svelte-resize-observer.svelte.js";
3233
import type {
3334
BitsKeyboardEvent,
3435
OnChangeFn,
@@ -64,6 +65,7 @@ abstract class SliderBaseRootState {
6465
readonly opts: SliderBaseRootStateOpts;
6566
readonly attachment: RefAttachment;
6667
isActive = $state(false);
68+
#layoutVersion = $state(0);
6769
readonly direction: "rl" | "lr" | "tb" | "bt" = $derived.by(() => {
6870
if (this.opts.orientation.current === "horizontal") {
6971
return this.opts.dir.current === "rtl" ? "rl" : "lr";
@@ -82,8 +84,13 @@ abstract class SliderBaseRootState {
8284
this.opts = opts;
8385
this.attachment = attachRef(opts.ref);
8486
this.domContext = new DOMContext(this.opts.ref);
87+
new SvelteResizeObserver(() => this.opts.ref.current, this.#handleLayoutChange);
8588
}
8689

90+
#handleLayoutChange = (): void => {
91+
this.#layoutVersion += 1;
92+
};
93+
8794
isThumbActive(_index: number) {
8895
return this.isActive;
8996
}
@@ -100,6 +107,8 @@ abstract class SliderBaseRootState {
100107
};
101108

102109
getThumbScale = (): [number, number] => {
110+
void this.#layoutVersion;
111+
103112
// If trackPadding is explicitly set, use it directly instead of calculating from thumb size
104113
const trackPadding = this.opts.trackPadding?.current;
105114
if (trackPadding !== undefined && trackPadding > 0) {
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<script lang="ts">
2+
import { Accordion, Slider } from "bits-ui";
3+
4+
const items = ["1", "2", "3"] as const;
5+
</script>
6+
7+
<Accordion.Root type="multiple" value={["1"]}>
8+
{#each items as item (item)}
9+
<Accordion.Item value={item}>
10+
<Accordion.Header>
11+
<Accordion.Trigger data-testid="{item}-trigger">Slider {item}</Accordion.Trigger>
12+
</Accordion.Header>
13+
<Accordion.Content data-testid="{item}-content">
14+
<div style="padding: 8px 0;">
15+
<Slider.Root type="single" value={0} style="display: flex; width: 100px;">
16+
{#snippet children({ thumbItems })}
17+
<span
18+
style="position: relative; height: 8px; width: 100%; flex-grow: 1; overflow: hidden;"
19+
>
20+
<Slider.Range data-testid="range-{item}" />
21+
</span>
22+
{#each thumbItems as { index } (index)}
23+
<Slider.Thumb
24+
{index}
25+
data-testid="thumb-{item}"
26+
style="display: block; width: 20px; height: 20px;"
27+
/>
28+
{/each}
29+
{/snippet}
30+
</Slider.Root>
31+
</div>
32+
</Accordion.Content>
33+
</Accordion.Item>
34+
{/each}
35+
</Accordion.Root>

tests/src/tests/slider/slider.browser.test.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import SliderRangeTest, { type SliderMultiRangeTestProps } from "./slider-range-
77
import SliderWithLabelsTest, {
88
type SliderWithLabelsTestProps,
99
} from "./slider-test-with-labels.svelte";
10+
import SliderInAccordionTest from "./slider-in-accordion-test.svelte";
1011
import { page, userEvent, type Locator } from "@vitest/browser/context";
1112

1213
const kbd = getTestKbd();
@@ -137,6 +138,19 @@ it("should not allow the value to change when the `disabled` prop is set to true
137138
expectPercentage({ percentage: 30, thumb, range });
138139
});
139140

141+
it("should recalculate thumb position when a hidden slider becomes visible", async () => {
142+
render(SliderInAccordionTest);
143+
144+
const secondThumb = page.getByTestId("thumb-2");
145+
await expect.element(secondThumb).toBeInTheDocument();
146+
147+
await userEvent.click(page.getByTestId("2-trigger"));
148+
149+
await vi.waitFor(() => {
150+
expect(isCloseEnough(10, (secondThumb.element() as HTMLElement).style.left)).toBeTruthy();
151+
});
152+
});
153+
140154
describe("range", () => {
141155
it("should have a thumb positioned at 20% of the container and one at 80%", async () => {
142156
setup({}, "range");

0 commit comments

Comments
 (0)