diff --git a/packages/calcite-components/src/components/action-bar/action-bar.tsx b/packages/calcite-components/src/components/action-bar/action-bar.tsx index ae9959fe3ef..66bfe786c95 100755 --- a/packages/calcite-components/src/components/action-bar/action-bar.tsx +++ b/packages/calcite-components/src/components/action-bar/action-bar.tsx @@ -378,8 +378,8 @@ export class ActionBar }; handleTooltipSlotChange = (event: Event): void => { - const tooltips = slotChangeGetAssignedElements(event).filter((el) => - el?.matches("calcite-tooltip"), + const tooltips = slotChangeGetAssignedElements(event).filter( + (el) => el?.matches("calcite-tooltip"), ) as HTMLCalciteTooltipElement[]; this.expandTooltip = tooltips[0]; diff --git a/packages/calcite-components/src/components/action-pad/action-pad.tsx b/packages/calcite-components/src/components/action-pad/action-pad.tsx index 477e56b3b2e..fc621667259 100755 --- a/packages/calcite-components/src/components/action-pad/action-pad.tsx +++ b/packages/calcite-components/src/components/action-pad/action-pad.tsx @@ -244,16 +244,16 @@ export class ActionPad } handleDefaultSlotChange = (event: Event): void => { - const groups = slotChangeGetAssignedElements(event).filter((el) => - el?.matches("calcite-action-group"), + const groups = slotChangeGetAssignedElements(event).filter( + (el) => el?.matches("calcite-action-group"), ) as HTMLCalciteActionGroupElement[]; this.setGroupLayout(groups); }; handleTooltipSlotChange = (event: Event): void => { - const tooltips = slotChangeGetAssignedElements(event).filter((el) => - el?.matches("calcite-tooltip"), + const tooltips = slotChangeGetAssignedElements(event).filter( + (el) => el?.matches("calcite-tooltip"), ) as HTMLCalciteTooltipElement[]; this.expandTooltip = tooltips[0]; diff --git a/packages/calcite-components/src/components/list-item/utils.ts b/packages/calcite-components/src/components/list-item/utils.ts index 30e5f444ab4..84b351a799a 100644 --- a/packages/calcite-components/src/components/list-item/utils.ts +++ b/packages/calcite-components/src/components/list-item/utils.ts @@ -19,8 +19,8 @@ export function getListItemChildren(slotEl: HTMLSlotElement): HTMLCalciteListIte .map((group) => Array.from(group.querySelectorAll(listItemSelector))) .reduce((previousValue, currentValue) => [...previousValue, ...currentValue], []); - const listItemChildren = assignedElements.filter((el) => - el?.matches(listItemSelector), + const listItemChildren = assignedElements.filter( + (el) => el?.matches(listItemSelector), ) as HTMLCalciteListItemElement[]; const listItemListChildren = (assignedElements.filter((el) => el?.matches(listSelector)) as HTMLCalciteListElement[]) diff --git a/packages/calcite-components/src/components/panel/panel.tsx b/packages/calcite-components/src/components/panel/panel.tsx index 1fafafdaabc..4ae45d556bc 100644 --- a/packages/calcite-components/src/components/panel/panel.tsx +++ b/packages/calcite-components/src/components/panel/panel.tsx @@ -303,8 +303,8 @@ export class Panel }; handleActionBarSlotChange = (event: Event): void => { - const actionBars = slotChangeGetAssignedElements(event).filter((el) => - el?.matches("calcite-action-bar"), + const actionBars = slotChangeGetAssignedElements(event).filter( + (el) => el?.matches("calcite-action-bar"), ) as HTMLCalciteActionBarElement[]; actionBars.forEach((actionBar) => (actionBar.layout = "horizontal")); diff --git a/packages/calcite-components/src/components/select/select.tsx b/packages/calcite-components/src/components/select/select.tsx index 1f42eeac21d..8c7cc4a71f5 100644 --- a/packages/calcite-components/src/components/select/select.tsx +++ b/packages/calcite-components/src/components/select/select.tsx @@ -284,8 +284,8 @@ export class Select this.clearInternalSelect(); - optionsAndGroups.forEach((optionOrGroup) => - this.selectEl?.append(this.toNativeElement(optionOrGroup)), + optionsAndGroups.forEach( + (optionOrGroup) => this.selectEl?.append(this.toNativeElement(optionOrGroup)), ); }; diff --git a/packages/calcite-components/src/components/shell-panel/shell-panel.tsx b/packages/calcite-components/src/components/shell-panel/shell-panel.tsx index 2233e80fafc..e7028c8d209 100755 --- a/packages/calcite-components/src/components/shell-panel/shell-panel.tsx +++ b/packages/calcite-components/src/components/shell-panel/shell-panel.tsx @@ -663,8 +663,8 @@ export class ShellPanel implements ConditionalSlotComponent, LocalizedComponent, }; handleActionBarSlotChange = (event: Event): void => { - const actionBars = slotChangeGetAssignedElements(event).filter((el) => - el?.matches("calcite-action-bar"), + const actionBars = slotChangeGetAssignedElements(event).filter( + (el) => el?.matches("calcite-action-bar"), ) as HTMLCalciteActionBarElement[]; this.actionBars = actionBars; diff --git a/packages/calcite-components/src/components/slider/slider.e2e.ts b/packages/calcite-components/src/components/slider/slider.e2e.ts index ae551461e48..02a34013aff 100644 --- a/packages/calcite-components/src/components/slider/slider.e2e.ts +++ b/packages/calcite-components/src/components/slider/slider.e2e.ts @@ -1,7 +1,7 @@ -import { E2EElement, E2EPage, newE2EPage } from "@stencil/core/testing"; +import { E2EElement, E2EPage, EventSpy, newE2EPage } from "@stencil/core/testing"; import { html } from "../../../support/formatting"; import { defaults, disabled, formAssociated, hidden, labelable, renders } from "../../tests/commonTests"; -import { getElementXY } from "../../tests/utils"; +import { getElementRect, getElementXY, isElementFocused } from "../../tests/utils"; import { CSS } from "./resources"; describe("calcite-slider", () => { @@ -184,13 +184,13 @@ describe("calcite-slider", () => { `); const slider = await page.find("calcite-slider"); expect(await slider.getProperty("value")).toBe(0); + const trackRect = await getElementRect(page, "calcite-slider", ".track"); - const [trackX, trackY] = await getElementXY(page, "calcite-slider", ".track"); - await page.mouse.move(trackX, trackY); + await page.mouse.move(trackRect.x, trackRect.y); await page.mouse.down(); - await page.mouse.move(trackX + 4, trackY); - await page.waitForChanges(); + await page.mouse.move(trackRect.x + 5, trackRect.y); await page.mouse.up(); + await page.waitForChanges(); expect(await slider.getProperty("value")).toBe(4.48); }); @@ -300,8 +300,9 @@ describe("calcite-slider", () => { await page.mouse.up(); await page.waitForChanges(); - let isThumbFocused = await page.$eval("calcite-slider", (slider) => - slider.shadowRoot.activeElement?.classList.contains("thumb--value"), + let isThumbFocused = await page.$eval( + "calcite-slider", + (slider) => slider.shadowRoot.activeElement?.classList.contains("thumb--value"), ); expect(isThumbFocused).toBe(true); @@ -312,8 +313,9 @@ describe("calcite-slider", () => { await page.mouse.up(); await page.waitForChanges(); - isThumbFocused = await page.$eval("calcite-slider", (slider) => - slider.shadowRoot.activeElement?.classList.contains("thumb--value"), + isThumbFocused = await page.$eval( + "calcite-slider", + (slider) => slider.shadowRoot.activeElement?.classList.contains("thumb--value"), ); expect(isThumbFocused).toBe(true); @@ -324,8 +326,9 @@ describe("calcite-slider", () => { await page.mouse.up(); await page.waitForChanges(); - isThumbFocused = await page.$eval("calcite-slider", (slider) => - slider.shadowRoot.activeElement?.classList.contains("thumb--value"), + isThumbFocused = await page.$eval( + "calcite-slider", + (slider) => slider.shadowRoot.activeElement?.classList.contains("thumb--value"), ); expect(isThumbFocused).toBe(true); @@ -356,8 +359,9 @@ describe("calcite-slider", () => { await page.mouse.up(); await page.waitForChanges(); - const isMinThumbFocused = await page.$eval("calcite-slider", (slider) => - slider.shadowRoot.activeElement?.classList.contains("thumb--minValue"), + const isMinThumbFocused = await page.$eval( + "calcite-slider", + (slider) => slider.shadowRoot.activeElement?.classList.contains("thumb--minValue"), ); expect(await slider.getProperty("minValue")).toBe(0); @@ -378,8 +382,9 @@ describe("calcite-slider", () => { await page.mouse.up(); await page.waitForChanges(); - const isMaxThumbFocused = await page.$eval("calcite-slider", (slider) => - slider.shadowRoot.activeElement?.classList.contains("thumb--value"), + const isMaxThumbFocused = await page.$eval( + "calcite-slider", + (slider) => slider.shadowRoot.activeElement?.classList.contains("thumb--value"), ); expect(await slider.getProperty("minValue")).toBe(0); @@ -400,8 +405,9 @@ describe("calcite-slider", () => { await page.mouse.up(); await page.waitForChanges(); - const isMaxThumbFocused = await page.$eval("calcite-slider", (slider) => - slider.shadowRoot.activeElement?.classList.contains("thumb--value"), + const isMaxThumbFocused = await page.$eval( + "calcite-slider", + (slider) => slider.shadowRoot.activeElement?.classList.contains("thumb--value"), ); expect(await slider.getProperty("minValue")).toBe(0); @@ -650,12 +656,16 @@ describe("calcite-slider", () => { }); }); - describe("when range thumbs overlap at min edge", () => { - const commonSliderAttrs = ` { + let page: E2EPage; + let changeEvent: EventSpy; + let inputEvent: EventSpy; + let element: E2EElement; + let trackRect: DOMRect; + + const commonSliderAttrs = ` min="5" max="100" - min-value="5" - max-value="5" step="10" ticks="10" label-handles @@ -663,55 +673,83 @@ describe("calcite-slider", () => { snap style="width:${sliderWidthFor1To1PixelValueTrack}"`; - it("click/tap should grab the max value thumb", async () => { - const page = await newE2EPage({ - html: ``, + async function assertValuesUnchanged(minMaxValue: number): Promise { + expect(await element.getProperty("minValue")).toBe(minMaxValue); + expect(await element.getProperty("maxValue")).toBe(minMaxValue); + expect(changeEvent).toHaveReceivedEventTimes(0); + expect(inputEvent).toHaveReceivedEventTimes(0); + } + + async function setUpTest(sliderAttrs: string): Promise { + page = await newE2EPage(); + await page.setContent(html``); + + element = await page.find("calcite-slider"); + trackRect = await getElementRect(page, "calcite-slider", `.${CSS.track}`); + changeEvent = await element.spyOnEvent("calciteSliderChange"); + inputEvent = await element.spyOnEvent("calciteSliderInput"); + } + + describe("at min edge", () => { + const expectedValue = 5; + + it("click/tap should grab the max value thumb", async () => { + await setUpTest(`${commonSliderAttrs} min-value="${expectedValue}" max-value="${expectedValue}"`); + + await assertValuesUnchanged(5); + + await page.mouse.click(trackRect.x, trackRect.y); + await page.waitForChanges(); + + const isMaxThumbFocused = await isElementFocused(page, `.${CSS.thumbValue}`, { shadowed: true }); + + expect(isMaxThumbFocused).toBe(true); + await assertValuesUnchanged(5); }); - await page.waitForChanges(); - const element = await page.find("calcite-slider"); - const changeEvent = await element.spyOnEvent("calciteSliderChange"); - const inputEvent = await element.spyOnEvent("calciteSliderInput"); - expect(await element.getProperty("minValue")).toBe(5); - expect(await element.getProperty("maxValue")).toBe(5); - const [trackX, trackY] = await getElementXY(page, "calcite-slider", ".track"); - await page.mouse.click(trackX, trackY); - await page.waitForChanges(); + it("mirrored: click/tap should grab the max value thumb", async () => { + await setUpTest(`${commonSliderAttrs} min-value="${expectedValue}" max-value="${expectedValue}" mirrored`); - const isMaxThumbFocused = await page.$eval("calcite-slider", (slider) => - slider.shadowRoot.activeElement?.classList.contains("thumb--value"), - ); + await assertValuesUnchanged(5); - expect(isMaxThumbFocused).toBe(true); - expect(await element.getProperty("minValue")).toBe(5); - expect(await element.getProperty("maxValue")).toBe(5); - expect(changeEvent).toHaveReceivedEventTimes(0); - expect(inputEvent).toHaveReceivedEventTimes(0); + await page.mouse.click(trackRect.x + trackRect.width, trackRect.y); + await page.waitForChanges(); + + const isMaxThumbFocused = await isElementFocused(page, `.${CSS.thumbValue}`, { shadowed: true }); + + expect(isMaxThumbFocused).toBe(true); + await assertValuesUnchanged(5); + }); }); - it("mirrored: click/tap should grab the max value thumb", async () => { - const page = await newE2EPage({ - html: ``, + describe("at max edge", () => { + const expectedValue = 100; + + it("click/tap should grab the min value thumb", async () => { + await setUpTest(`${commonSliderAttrs} min-value="${expectedValue}" max-value="${expectedValue}"`); + + await page.mouse.click(trackRect.x + trackRect.width, trackRect.y); + await page.waitForChanges(); + + const isMinThumbFocused = await isElementFocused(page, `.${CSS.thumbMinValue}`, { shadowed: true }); + + expect(isMinThumbFocused).toBe(true); + await assertValuesUnchanged(expectedValue); }); - const element = await page.find("calcite-slider"); - const changeEvent = await element.spyOnEvent("calciteSliderChange"); - const inputEvent = await element.spyOnEvent("calciteSliderInput"); - expect(await element.getProperty("minValue")).toBe(5); - expect(await element.getProperty("maxValue")).toBe(5); - const [trackX, trackY] = await getElementXY(page, "calcite-slider", ".track"); - await page.mouse.click(trackX + 100, trackY); - await page.waitForChanges(); + it("mirrored: click/tap should grab the max value thumb", async () => { + await setUpTest(`${commonSliderAttrs} min-value="${expectedValue}" max-value="${expectedValue}" mirrored`); - const isMaxThumbFocused = await page.$eval("calcite-slider", (slider) => - slider.shadowRoot.activeElement?.classList.contains("thumb--value"), - ); + await assertValuesUnchanged(expectedValue); - expect(isMaxThumbFocused).toBe(true); - expect(await element.getProperty("minValue")).toBe(5); - expect(await element.getProperty("maxValue")).toBe(5); - expect(changeEvent).toHaveReceivedEventTimes(0); - expect(inputEvent).toHaveReceivedEventTimes(0); + await page.mouse.click(trackRect.x, trackRect.y); + await page.waitForChanges(); + + const isMinThumbFocused = await isElementFocused(page, `.${CSS.thumbMinValue}`, { shadowed: true }); + + expect(isMinThumbFocused).toBe(true); + await assertValuesUnchanged(expectedValue); + }); }); }); @@ -866,4 +904,70 @@ describe("calcite-slider", () => { } }); }); + + describe("snap + step interaction", () => { + let page: E2EPage; + + beforeEach(async () => { + page = await newE2EPage(); + }); + + async function dragThumbToMax(): Promise { + const trackRect = await getElementRect(page, "calcite-slider", ".track"); + const thumbRect = await getElementRect(page, "calcite-slider", ".thumb--value"); + const thumbWidth = thumbRect.width; + const trackWidth = trackRect.width; + const dragDistance = trackWidth - thumbWidth; + + await page.mouse.move(trackRect.x, trackRect.y); + await page.mouse.down(); + await page.mouse.move(trackRect.x + dragDistance, trackRect.y); + await page.mouse.up(); + await page.waitForChanges(); + } + + it("honors snap value with step", async () => { + await page.setContent(html``); + const slider = await page.find("calcite-slider"); + + expect(await slider.getProperty("value")).toBe(1); + + await dragThumbToMax(); + expect(await slider.getProperty("value")).toBe(9); + }); + + it("honors snap value with step (fractional)", async () => { + await page.setContent(html``); + const slider = await page.find("calcite-slider"); + + expect(await slider.getProperty("value")).toBe(1.5); + + await dragThumbToMax(); + expect(await slider.getProperty("value")).toBe(9.5); + }); + + it("snaps to max limit beyond upper bound", async () => { + await page.setContent( + html``, + ); + const slider = await page.find("calcite-slider"); + + expect(await slider.getProperty("value")).toBe(10); + + await dragThumbToMax(); + expect(await slider.getProperty("value")).toBe(10); + }); + + it("snaps to max limit at upper bound", async () => { + await page.setContent( + html``, + ); + const slider = await page.find("calcite-slider"); + + expect(await slider.getProperty("value")).toBe(10); + + await dragThumbToMax(); + expect(await slider.getProperty("value")).toBe(10); + }); + }); }); diff --git a/packages/calcite-components/src/components/slider/slider.tsx b/packages/calcite-components/src/components/slider/slider.tsx index 78612761207..cf395211325 100644 --- a/packages/calcite-components/src/components/slider/slider.tsx +++ b/packages/calcite-components/src/components/slider/slider.tsx @@ -282,7 +282,7 @@ export class Slider }); return ( - +
{ const mirror = this.shouldMirror(); const { activeProp, max, min, pageStep, step } = this; const value = this[activeProp]; @@ -508,15 +507,17 @@ export class Slider } else if (key === "End") { adjustment = max; } + if (isNaN(adjustment)) { return; } + event.preventDefault(); const fixedDecimalAdjustment = Number(adjustment.toFixed(decimalPlaces(step))); this.setValue({ [activeProp as SetValueProperty]: this.clamp(fixedDecimalAdjustment, activeProp), }); - } + }; @Listen("pointerdown") pointerDownHandler(event: PointerEvent): void { @@ -533,7 +534,7 @@ export class Slider prop = "minMaxValue"; } else { const closerToMax = Math.abs(this.maxValue - position) < Math.abs(this.minValue - position); - prop = closerToMax || position > this.maxValue ? "maxValue" : "minValue"; + prop = closerToMax || position >= this.maxValue ? "maxValue" : "minValue"; } } this.lastDragPropValue = this[prop]; @@ -751,19 +752,12 @@ export class Slider } private focusActiveHandle(valueX: number): void { - switch (this.dragProp) { - case "minValue": - this.minHandle.focus(); - break; - case "maxValue": - case "value": - this.maxHandle.focus(); - break; - case "minMaxValue": - this.getClosestHandle(valueX).focus(); - break; - default: - break; + if (this.dragProp === "minValue") { + this.minHandle.focus(); + } else if (this.dragProp === "maxValue" || this.dragProp === "value") { + this.maxHandle.focus(); + } else if (this.dragProp === "minMaxValue") { + this.getClosestHandle(valueX).focus(); } } @@ -903,6 +897,7 @@ export class Slider if (prop === "minValue") { value = Math.min(value, this.maxValue); } + return value; } @@ -918,26 +913,27 @@ export class Slider const percent = (x - left) / width; const mirror = this.shouldMirror(); const clampedValue = this.clamp(this.min + range * (mirror ? 1 - percent : percent)); - let value = Number(clampedValue.toFixed(decimalPlaces(this.step))); - if (this.snap && this.step) { - value = this.getClosestStep(value); - } - return value; + const value = Number(clampedValue.toFixed(decimalPlaces(this.step))); + + return !(this.snap && this.step) ? value : this.getClosestStep(value); } /** * Get closest allowed value along stepped values * - * @param num + * @param value * @internal */ - private getClosestStep(num: number): number { - num = Number(this.clamp(num).toFixed(decimalPlaces(this.step))); - if (this.step) { - const step = Math.round(num / this.step) * this.step; - num = Number(this.clamp(step).toFixed(decimalPlaces(this.step))); + private getClosestStep(value: number): number { + const { max, min, step } = this; + let snappedValue = Math.floor((value - min) / step) * step + min; + snappedValue = Math.min(Math.max(snappedValue, min), max); + + if (snappedValue > max) { + snappedValue -= step; } - return num; + + return snappedValue; } private getClosestHandle(valueX: number): HTMLDivElement { diff --git a/packages/calcite-components/src/components/table/table.tsx b/packages/calcite-components/src/components/table/table.tsx index 93b10785be0..93847016948 100644 --- a/packages/calcite-components/src/components/table/table.tsx +++ b/packages/calcite-components/src/components/table/table.tsx @@ -278,9 +278,8 @@ export class Table implements LocalizedComponent, LoadableComponent, T9nComponen break; } - const destinationCount = this.allRows?.find( - (row) => row.positionAll === rowPosition, - )?.cellCount; + const destinationCount = this.allRows?.find((row) => row.positionAll === rowPosition) + ?.cellCount; const adjustedPos = cellPosition > destinationCount ? destinationCount : cellPosition; diff --git a/packages/eslint-plugin-calcite-components/src/rules/ban-events.ts b/packages/eslint-plugin-calcite-components/src/rules/ban-events.ts index b80899ee7ee..23a5e3d5f92 100644 --- a/packages/eslint-plugin-calcite-components/src/rules/ban-events.ts +++ b/packages/eslint-plugin-calcite-components/src/rules/ban-events.ts @@ -60,7 +60,7 @@ const rule: Rule.RuleModule = { } }, "CallExpression:matches([callee.property.name=addEventListener], [callee.property.name=removeEventListener])": ( - node: CallExpression + node: CallExpression, ) => { if (stencil.isComponent()) { const eventName = (node.arguments[0] as Literal).value as string; diff --git a/packages/eslint-plugin-calcite-components/src/rules/ban-props-on-host.ts b/packages/eslint-plugin-calcite-components/src/rules/ban-props-on-host.ts index ec33cf6b66b..758e621b7d9 100644 --- a/packages/eslint-plugin-calcite-components/src/rules/ban-props-on-host.ts +++ b/packages/eslint-plugin-calcite-components/src/rules/ban-props-on-host.ts @@ -43,7 +43,7 @@ const rule: Rule.RuleModule = { context.report({ node: node, message: `Avoid setting unnecessary attributes/properties on : ${unauthorizedAttributes.join( - ", " + ", ", )}`, }); }