Skip to content

Commit 8b83042

Browse files
authored
fix(slider): improve snapping + step logic (#8169)
**Related Issue:** #6229 ## Summary Updates slider to consider `min` when determining the closest step.
1 parent db109e0 commit 8b83042

File tree

11 files changed

+209
-110
lines changed

11 files changed

+209
-110
lines changed

packages/calcite-components/src/components/action-bar/action-bar.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -378,8 +378,8 @@ export class ActionBar
378378
};
379379

380380
handleTooltipSlotChange = (event: Event): void => {
381-
const tooltips = slotChangeGetAssignedElements(event).filter((el) =>
382-
el?.matches("calcite-tooltip"),
381+
const tooltips = slotChangeGetAssignedElements(event).filter(
382+
(el) => el?.matches("calcite-tooltip"),
383383
) as HTMLCalciteTooltipElement[];
384384

385385
this.expandTooltip = tooltips[0];

packages/calcite-components/src/components/action-pad/action-pad.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -244,16 +244,16 @@ export class ActionPad
244244
}
245245

246246
handleDefaultSlotChange = (event: Event): void => {
247-
const groups = slotChangeGetAssignedElements(event).filter((el) =>
248-
el?.matches("calcite-action-group"),
247+
const groups = slotChangeGetAssignedElements(event).filter(
248+
(el) => el?.matches("calcite-action-group"),
249249
) as HTMLCalciteActionGroupElement[];
250250

251251
this.setGroupLayout(groups);
252252
};
253253

254254
handleTooltipSlotChange = (event: Event): void => {
255-
const tooltips = slotChangeGetAssignedElements(event).filter((el) =>
256-
el?.matches("calcite-tooltip"),
255+
const tooltips = slotChangeGetAssignedElements(event).filter(
256+
(el) => el?.matches("calcite-tooltip"),
257257
) as HTMLCalciteTooltipElement[];
258258

259259
this.expandTooltip = tooltips[0];

packages/calcite-components/src/components/list-item/utils.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,8 @@ export function getListItemChildren(slotEl: HTMLSlotElement): HTMLCalciteListIte
1919
.map((group) => Array.from(group.querySelectorAll(listItemSelector)))
2020
.reduce((previousValue, currentValue) => [...previousValue, ...currentValue], []);
2121

22-
const listItemChildren = assignedElements.filter((el) =>
23-
el?.matches(listItemSelector),
22+
const listItemChildren = assignedElements.filter(
23+
(el) => el?.matches(listItemSelector),
2424
) as HTMLCalciteListItemElement[];
2525

2626
const listItemListChildren = (assignedElements.filter((el) => el?.matches(listSelector)) as HTMLCalciteListElement[])

packages/calcite-components/src/components/panel/panel.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -303,8 +303,8 @@ export class Panel
303303
};
304304

305305
handleActionBarSlotChange = (event: Event): void => {
306-
const actionBars = slotChangeGetAssignedElements(event).filter((el) =>
307-
el?.matches("calcite-action-bar"),
306+
const actionBars = slotChangeGetAssignedElements(event).filter(
307+
(el) => el?.matches("calcite-action-bar"),
308308
) as HTMLCalciteActionBarElement[];
309309

310310
actionBars.forEach((actionBar) => (actionBar.layout = "horizontal"));

packages/calcite-components/src/components/select/select.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -284,8 +284,8 @@ export class Select
284284

285285
this.clearInternalSelect();
286286

287-
optionsAndGroups.forEach((optionOrGroup) =>
288-
this.selectEl?.append(this.toNativeElement(optionOrGroup)),
287+
optionsAndGroups.forEach(
288+
(optionOrGroup) => this.selectEl?.append(this.toNativeElement(optionOrGroup)),
289289
);
290290
};
291291

packages/calcite-components/src/components/shell-panel/shell-panel.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -663,8 +663,8 @@ export class ShellPanel implements ConditionalSlotComponent, LocalizedComponent,
663663
};
664664

665665
handleActionBarSlotChange = (event: Event): void => {
666-
const actionBars = slotChangeGetAssignedElements(event).filter((el) =>
667-
el?.matches("calcite-action-bar"),
666+
const actionBars = slotChangeGetAssignedElements(event).filter(
667+
(el) => el?.matches("calcite-action-bar"),
668668
) as HTMLCalciteActionBarElement[];
669669

670670
this.actionBars = actionBars;

packages/calcite-components/src/components/slider/slider.e2e.ts

Lines changed: 165 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
import { E2EElement, E2EPage, newE2EPage } from "@stencil/core/testing";
1+
import { E2EElement, E2EPage, EventSpy, newE2EPage } from "@stencil/core/testing";
22
import { html } from "../../../support/formatting";
33
import { defaults, disabled, formAssociated, hidden, labelable, renders } from "../../tests/commonTests";
4-
import { getElementXY } from "../../tests/utils";
4+
import { getElementRect, getElementXY, isElementFocused } from "../../tests/utils";
55
import { CSS } from "./resources";
66

77
describe("calcite-slider", () => {
@@ -184,13 +184,13 @@ describe("calcite-slider", () => {
184184
`);
185185
const slider = await page.find("calcite-slider");
186186
expect(await slider.getProperty("value")).toBe(0);
187+
const trackRect = await getElementRect(page, "calcite-slider", ".track");
187188

188-
const [trackX, trackY] = await getElementXY(page, "calcite-slider", ".track");
189-
await page.mouse.move(trackX, trackY);
189+
await page.mouse.move(trackRect.x, trackRect.y);
190190
await page.mouse.down();
191-
await page.mouse.move(trackX + 4, trackY);
192-
await page.waitForChanges();
191+
await page.mouse.move(trackRect.x + 5, trackRect.y);
193192
await page.mouse.up();
193+
await page.waitForChanges();
194194

195195
expect(await slider.getProperty("value")).toBe(4.48);
196196
});
@@ -300,8 +300,9 @@ describe("calcite-slider", () => {
300300
await page.mouse.up();
301301
await page.waitForChanges();
302302

303-
let isThumbFocused = await page.$eval("calcite-slider", (slider) =>
304-
slider.shadowRoot.activeElement?.classList.contains("thumb--value"),
303+
let isThumbFocused = await page.$eval(
304+
"calcite-slider",
305+
(slider) => slider.shadowRoot.activeElement?.classList.contains("thumb--value"),
305306
);
306307

307308
expect(isThumbFocused).toBe(true);
@@ -312,8 +313,9 @@ describe("calcite-slider", () => {
312313
await page.mouse.up();
313314
await page.waitForChanges();
314315

315-
isThumbFocused = await page.$eval("calcite-slider", (slider) =>
316-
slider.shadowRoot.activeElement?.classList.contains("thumb--value"),
316+
isThumbFocused = await page.$eval(
317+
"calcite-slider",
318+
(slider) => slider.shadowRoot.activeElement?.classList.contains("thumb--value"),
317319
);
318320

319321
expect(isThumbFocused).toBe(true);
@@ -324,8 +326,9 @@ describe("calcite-slider", () => {
324326
await page.mouse.up();
325327
await page.waitForChanges();
326328

327-
isThumbFocused = await page.$eval("calcite-slider", (slider) =>
328-
slider.shadowRoot.activeElement?.classList.contains("thumb--value"),
329+
isThumbFocused = await page.$eval(
330+
"calcite-slider",
331+
(slider) => slider.shadowRoot.activeElement?.classList.contains("thumb--value"),
329332
);
330333

331334
expect(isThumbFocused).toBe(true);
@@ -356,8 +359,9 @@ describe("calcite-slider", () => {
356359
await page.mouse.up();
357360
await page.waitForChanges();
358361

359-
const isMinThumbFocused = await page.$eval("calcite-slider", (slider) =>
360-
slider.shadowRoot.activeElement?.classList.contains("thumb--minValue"),
362+
const isMinThumbFocused = await page.$eval(
363+
"calcite-slider",
364+
(slider) => slider.shadowRoot.activeElement?.classList.contains("thumb--minValue"),
361365
);
362366

363367
expect(await slider.getProperty("minValue")).toBe(0);
@@ -378,8 +382,9 @@ describe("calcite-slider", () => {
378382
await page.mouse.up();
379383
await page.waitForChanges();
380384

381-
const isMaxThumbFocused = await page.$eval("calcite-slider", (slider) =>
382-
slider.shadowRoot.activeElement?.classList.contains("thumb--value"),
385+
const isMaxThumbFocused = await page.$eval(
386+
"calcite-slider",
387+
(slider) => slider.shadowRoot.activeElement?.classList.contains("thumb--value"),
383388
);
384389

385390
expect(await slider.getProperty("minValue")).toBe(0);
@@ -400,8 +405,9 @@ describe("calcite-slider", () => {
400405
await page.mouse.up();
401406
await page.waitForChanges();
402407

403-
const isMaxThumbFocused = await page.$eval("calcite-slider", (slider) =>
404-
slider.shadowRoot.activeElement?.classList.contains("thumb--value"),
408+
const isMaxThumbFocused = await page.$eval(
409+
"calcite-slider",
410+
(slider) => slider.shadowRoot.activeElement?.classList.contains("thumb--value"),
405411
);
406412

407413
expect(await slider.getProperty("minValue")).toBe(0);
@@ -650,68 +656,100 @@ describe("calcite-slider", () => {
650656
});
651657
});
652658

653-
describe("when range thumbs overlap at min edge", () => {
654-
const commonSliderAttrs = `<calcite-slider
659+
describe("when range thumbs overlap", () => {
660+
let page: E2EPage;
661+
let changeEvent: EventSpy;
662+
let inputEvent: EventSpy;
663+
let element: E2EElement;
664+
let trackRect: DOMRect;
665+
666+
const commonSliderAttrs = `
655667
min="5"
656668
max="100"
657-
min-value="5"
658-
max-value="5"
659669
step="10"
660670
ticks="10"
661671
label-handles
662672
label-ticks
663673
snap
664674
style="width:${sliderWidthFor1To1PixelValueTrack}"`;
665675

666-
it("click/tap should grab the max value thumb", async () => {
667-
const page = await newE2EPage({
668-
html: `<calcite-slider ${commonSliderAttrs}></calcite-slider>`,
676+
async function assertValuesUnchanged(minMaxValue: number): Promise<void> {
677+
expect(await element.getProperty("minValue")).toBe(minMaxValue);
678+
expect(await element.getProperty("maxValue")).toBe(minMaxValue);
679+
expect(changeEvent).toHaveReceivedEventTimes(0);
680+
expect(inputEvent).toHaveReceivedEventTimes(0);
681+
}
682+
683+
async function setUpTest(sliderAttrs: string): Promise<void> {
684+
page = await newE2EPage();
685+
await page.setContent(html`<calcite-slider ${sliderAttrs}></calcite-slider>`);
686+
687+
element = await page.find("calcite-slider");
688+
trackRect = await getElementRect(page, "calcite-slider", `.${CSS.track}`);
689+
changeEvent = await element.spyOnEvent("calciteSliderChange");
690+
inputEvent = await element.spyOnEvent("calciteSliderInput");
691+
}
692+
693+
describe("at min edge", () => {
694+
const expectedValue = 5;
695+
696+
it("click/tap should grab the max value thumb", async () => {
697+
await setUpTest(`${commonSliderAttrs} min-value="${expectedValue}" max-value="${expectedValue}"`);
698+
699+
await assertValuesUnchanged(5);
700+
701+
await page.mouse.click(trackRect.x, trackRect.y);
702+
await page.waitForChanges();
703+
704+
const isMaxThumbFocused = await isElementFocused(page, `.${CSS.thumbValue}`, { shadowed: true });
705+
706+
expect(isMaxThumbFocused).toBe(true);
707+
await assertValuesUnchanged(5);
669708
});
670-
await page.waitForChanges();
671-
const element = await page.find("calcite-slider");
672-
const changeEvent = await element.spyOnEvent("calciteSliderChange");
673-
const inputEvent = await element.spyOnEvent("calciteSliderInput");
674-
expect(await element.getProperty("minValue")).toBe(5);
675-
expect(await element.getProperty("maxValue")).toBe(5);
676709

677-
const [trackX, trackY] = await getElementXY(page, "calcite-slider", ".track");
678-
await page.mouse.click(trackX, trackY);
679-
await page.waitForChanges();
710+
it("mirrored: click/tap should grab the max value thumb", async () => {
711+
await setUpTest(`${commonSliderAttrs} min-value="${expectedValue}" max-value="${expectedValue}" mirrored`);
680712

681-
const isMaxThumbFocused = await page.$eval("calcite-slider", (slider) =>
682-
slider.shadowRoot.activeElement?.classList.contains("thumb--value"),
683-
);
713+
await assertValuesUnchanged(5);
684714

685-
expect(isMaxThumbFocused).toBe(true);
686-
expect(await element.getProperty("minValue")).toBe(5);
687-
expect(await element.getProperty("maxValue")).toBe(5);
688-
expect(changeEvent).toHaveReceivedEventTimes(0);
689-
expect(inputEvent).toHaveReceivedEventTimes(0);
715+
await page.mouse.click(trackRect.x + trackRect.width, trackRect.y);
716+
await page.waitForChanges();
717+
718+
const isMaxThumbFocused = await isElementFocused(page, `.${CSS.thumbValue}`, { shadowed: true });
719+
720+
expect(isMaxThumbFocused).toBe(true);
721+
await assertValuesUnchanged(5);
722+
});
690723
});
691724

692-
it("mirrored: click/tap should grab the max value thumb", async () => {
693-
const page = await newE2EPage({
694-
html: `<calcite-slider ${commonSliderAttrs} mirrored></calcite-slider>`,
725+
describe("at max edge", () => {
726+
const expectedValue = 100;
727+
728+
it("click/tap should grab the min value thumb", async () => {
729+
await setUpTest(`${commonSliderAttrs} min-value="${expectedValue}" max-value="${expectedValue}"`);
730+
731+
await page.mouse.click(trackRect.x + trackRect.width, trackRect.y);
732+
await page.waitForChanges();
733+
734+
const isMinThumbFocused = await isElementFocused(page, `.${CSS.thumbMinValue}`, { shadowed: true });
735+
736+
expect(isMinThumbFocused).toBe(true);
737+
await assertValuesUnchanged(expectedValue);
695738
});
696-
const element = await page.find("calcite-slider");
697-
const changeEvent = await element.spyOnEvent("calciteSliderChange");
698-
const inputEvent = await element.spyOnEvent("calciteSliderInput");
699-
expect(await element.getProperty("minValue")).toBe(5);
700-
expect(await element.getProperty("maxValue")).toBe(5);
701739

702-
const [trackX, trackY] = await getElementXY(page, "calcite-slider", ".track");
703-
await page.mouse.click(trackX + 100, trackY);
704-
await page.waitForChanges();
740+
it("mirrored: click/tap should grab the max value thumb", async () => {
741+
await setUpTest(`${commonSliderAttrs} min-value="${expectedValue}" max-value="${expectedValue}" mirrored`);
705742

706-
const isMaxThumbFocused = await page.$eval("calcite-slider", (slider) =>
707-
slider.shadowRoot.activeElement?.classList.contains("thumb--value"),
708-
);
743+
await assertValuesUnchanged(expectedValue);
709744

710-
expect(isMaxThumbFocused).toBe(true);
711-
expect(await element.getProperty("minValue")).toBe(5);
712-
expect(await element.getProperty("maxValue")).toBe(5);
713-
expect(changeEvent).toHaveReceivedEventTimes(0);
714-
expect(inputEvent).toHaveReceivedEventTimes(0);
745+
await page.mouse.click(trackRect.x, trackRect.y);
746+
await page.waitForChanges();
747+
748+
const isMinThumbFocused = await isElementFocused(page, `.${CSS.thumbMinValue}`, { shadowed: true });
749+
750+
expect(isMinThumbFocused).toBe(true);
751+
await assertValuesUnchanged(expectedValue);
752+
});
715753
});
716754
});
717755

@@ -866,4 +904,70 @@ describe("calcite-slider", () => {
866904
}
867905
});
868906
});
907+
908+
describe("snap + step interaction", () => {
909+
let page: E2EPage;
910+
911+
beforeEach(async () => {
912+
page = await newE2EPage();
913+
});
914+
915+
async function dragThumbToMax(): Promise<void> {
916+
const trackRect = await getElementRect(page, "calcite-slider", ".track");
917+
const thumbRect = await getElementRect(page, "calcite-slider", ".thumb--value");
918+
const thumbWidth = thumbRect.width;
919+
const trackWidth = trackRect.width;
920+
const dragDistance = trackWidth - thumbWidth;
921+
922+
await page.mouse.move(trackRect.x, trackRect.y);
923+
await page.mouse.down();
924+
await page.mouse.move(trackRect.x + dragDistance, trackRect.y);
925+
await page.mouse.up();
926+
await page.waitForChanges();
927+
}
928+
929+
it("honors snap value with step", async () => {
930+
await page.setContent(html`<calcite-slider max="10" min="1" snap step="2" ticks="2"></calcite-slider>`);
931+
const slider = await page.find("calcite-slider");
932+
933+
expect(await slider.getProperty("value")).toBe(1);
934+
935+
await dragThumbToMax();
936+
expect(await slider.getProperty("value")).toBe(9);
937+
});
938+
939+
it("honors snap value with step (fractional)", async () => {
940+
await page.setContent(html`<calcite-slider max="10" min="1.5" snap step="1" ticks="1"></calcite-slider>`);
941+
const slider = await page.find("calcite-slider");
942+
943+
expect(await slider.getProperty("value")).toBe(1.5);
944+
945+
await dragThumbToMax();
946+
expect(await slider.getProperty("value")).toBe(9.5);
947+
});
948+
949+
it("snaps to max limit beyond upper bound", async () => {
950+
await page.setContent(
951+
html`<calcite-slider max="10.5" min="0" snap step="1" ticks="1" value="10.5"></calcite-slider>`,
952+
);
953+
const slider = await page.find("calcite-slider");
954+
955+
expect(await slider.getProperty("value")).toBe(10);
956+
957+
await dragThumbToMax();
958+
expect(await slider.getProperty("value")).toBe(10);
959+
});
960+
961+
it("snaps to max limit at upper bound", async () => {
962+
await page.setContent(
963+
html`<calcite-slider max="10.4" min="0" snap step="1" ticks="1" value="10.4"></calcite-slider>`,
964+
);
965+
const slider = await page.find("calcite-slider");
966+
967+
expect(await slider.getProperty("value")).toBe(10);
968+
969+
await dragThumbToMax();
970+
expect(await slider.getProperty("value")).toBe(10);
971+
});
972+
});
869973
});

0 commit comments

Comments
 (0)