diff --git a/package-lock.json b/package-lock.json index afb00c2b990..f545c6a42fa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -41314,7 +41314,7 @@ }, "packages/calcite-components": { "name": "@esri/calcite-components", - "version": "1.5.0-next.34", + "version": "1.5.0-next.36", "license": "SEE LICENSE.md", "dependencies": { "@floating-ui/dom": "1.5.1", @@ -41341,10 +41341,10 @@ }, "packages/calcite-components-react": { "name": "@esri/calcite-components-react", - "version": "1.5.0-next.34", + "version": "1.5.0-next.36", "license": "SEE LICENSE.md", "dependencies": { - "@esri/calcite-components": "^1.5.0-next.34" + "@esri/calcite-components": "^1.5.0-next.36" }, "peerDependencies": { "react": ">=16.7", @@ -43211,7 +43211,7 @@ "@esri/calcite-components-react": { "version": "file:packages/calcite-components-react", "requires": { - "@esri/calcite-components": "^1.5.0-next.34" + "@esri/calcite-components": "^1.5.0-next.36" } }, "@esri/calcite-design-tokens": { diff --git a/packages/calcite-components-react/CHANGELOG.md b/packages/calcite-components-react/CHANGELOG.md index f4335e430b2..51c4a6913e0 100644 --- a/packages/calcite-components-react/CHANGELOG.md +++ b/packages/calcite-components-react/CHANGELOG.md @@ -3,6 +3,14 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [1.5.0-next.36](https://github.com/Esri/calcite-design-system/compare/@esri/calcite-components-react@1.5.0-next.35...@esri/calcite-components-react@1.5.0-next.36) (2023-08-03) + +**Note:** Version bump only for package @esri/calcite-components-react + +## [1.5.0-next.35](https://github.com/Esri/calcite-design-system/compare/@esri/calcite-components-react@1.5.0-next.34...@esri/calcite-components-react@1.5.0-next.35) (2023-08-03) + +**Note:** Version bump only for package @esri/calcite-components-react + ## [1.5.0-next.34](https://github.com/Esri/calcite-design-system/compare/@esri/calcite-components-react@1.5.0-next.33...@esri/calcite-components-react@1.5.0-next.34) (2023-08-03) **Note:** Version bump only for package @esri/calcite-components-react diff --git a/packages/calcite-components-react/package.json b/packages/calcite-components-react/package.json index e6ecc0a7e30..8244b2b115f 100644 --- a/packages/calcite-components-react/package.json +++ b/packages/calcite-components-react/package.json @@ -1,7 +1,7 @@ { "name": "@esri/calcite-components-react", "sideEffects": false, - "version": "1.5.0-next.34", + "version": "1.5.0-next.36", "description": "A set of React components that wrap calcite components", "license": "SEE LICENSE.md", "scripts": { @@ -17,7 +17,7 @@ "dist/" ], "dependencies": { - "@esri/calcite-components": "^1.5.0-next.34" + "@esri/calcite-components": "^1.5.0-next.36" }, "peerDependencies": { "react": ">=16.7", diff --git a/packages/calcite-components/CHANGELOG.md b/packages/calcite-components/CHANGELOG.md index 07e543aa043..7150318f033 100644 --- a/packages/calcite-components/CHANGELOG.md +++ b/packages/calcite-components/CHANGELOG.md @@ -3,6 +3,18 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [1.5.0-next.36](https://github.com/Esri/calcite-design-system/compare/@esri/calcite-components@1.5.0-next.35...@esri/calcite-components@1.5.0-next.36) (2023-08-03) + +### Features + +- **block:** improve block's content layout to allow scrolling ([#7367](https://github.com/Esri/calcite-design-system/issues/7367)) ([ecbf17b](https://github.com/Esri/calcite-design-system/commit/ecbf17b3dac6cd79d21f44811d0b5e8f52ab7237)), closes [#5686](https://github.com/Esri/calcite-design-system/issues/5686) [/github.com/Esri/calcite-design-system/issues/5686#issuecomment-1310423881](https://github.com/Esri//github.com/Esri/calcite-design-system/issues/5686/issues/issuecomment-1310423881) + +## [1.5.0-next.35](https://github.com/Esri/calcite-design-system/compare/@esri/calcite-components@1.5.0-next.34...@esri/calcite-components@1.5.0-next.35) (2023-08-03) + +### Bug Fixes + +- **color-picker:** draw slider thumbs within bounds ([#7398](https://github.com/Esri/calcite-design-system/issues/7398)) ([2f37854](https://github.com/Esri/calcite-design-system/commit/2f378548dda9e91719b726a77ab6893e562a20ce)), closes [#7005](https://github.com/Esri/calcite-design-system/issues/7005) + ## [1.5.0-next.34](https://github.com/Esri/calcite-design-system/compare/@esri/calcite-components@1.5.0-next.33...@esri/calcite-components@1.5.0-next.34) (2023-08-03) ### Bug Fixes diff --git a/packages/calcite-components/package.json b/packages/calcite-components/package.json index e3c18bea465..d08fc067d7b 100644 --- a/packages/calcite-components/package.json +++ b/packages/calcite-components/package.json @@ -1,6 +1,6 @@ { "name": "@esri/calcite-components", - "version": "1.5.0-next.34", + "version": "1.5.0-next.36", "description": "Web Components for Esri's Calcite Design System.", "main": "dist/index.cjs.js", "module": "dist/index.js", diff --git a/packages/calcite-components/src/components/block/block.scss b/packages/calcite-components/src/components/block/block.scss index c2bf852b622..b91924a8df9 100644 --- a/packages/calcite-components/src/components/block/block.scss +++ b/packages/calcite-components/src/components/block/block.scss @@ -139,7 +139,7 @@ calcite-handle { } .content { - @apply animate-in flex-1 relative; + @apply animate-in flex-1 relative min-h-0; padding-block: var(--calcite-block-padding, theme("spacing.2")); padding-inline: var(--calcite-block-padding, theme("spacing[2.5]")); } diff --git a/packages/calcite-components/src/components/block/block.stories.ts b/packages/calcite-components/src/components/block/block.stories.ts index 5f100f674a1..9b79cc4607c 100644 --- a/packages/calcite-components/src/components/block/block.stories.ts +++ b/packages/calcite-components/src/components/block/block.stories.ts @@ -243,3 +243,38 @@ export const loadingWithStatusIcon_TestOnly = (): string => `; + +export const scrollingContainerSetup_TestOnly = (): string => html` + + + + + + `; + +scrollingContainerSetup_TestOnly.parameters = { chromatic: { delay: 500 } }; diff --git a/packages/calcite-components/src/components/color-picker/color-picker.e2e.ts b/packages/calcite-components/src/components/color-picker/color-picker.e2e.ts index 0e10d672b09..9bf305b8003 100644 --- a/packages/calcite-components/src/components/color-picker/color-picker.e2e.ts +++ b/packages/calcite-components/src/components/color-picker/color-picker.e2e.ts @@ -519,12 +519,12 @@ describe("calcite-color-picker", () => { const expectedColorSamples = [ "#ff0000", "#ffd900", - "#48ff00", - "#00ff91", + "#4cff00", + "#00ff8c", "#0095ff", - "#4800ff", - "#ff00dd", - "#ff0004", + "#4400ff", + "#ff00e1", + "#ff0008", ]; for (let i = 0; i < expectedColorSamples.length; i++) { @@ -678,7 +678,7 @@ describe("calcite-color-picker", () => { [hueScopeX, hueScopeY] = await getElementXY(page, "calcite-color-picker", `.${CSS.hueScope}`); [hueScopeCenterX, hueScopeCenterY] = getScopeCenter(hueScopeX, hueScopeY); - expect(hueScopeCenterX).toBe(hueSliderX); + expect(hueScopeCenterX).toBe(hueSliderX + DIMENSIONS.m.thumb.radius); await page.mouse.move(hueScopeCenterX, hueScopeCenterY); await page.mouse.down(); @@ -689,7 +689,7 @@ describe("calcite-color-picker", () => { [hueScopeX] = await getElementXY(page, "calcite-color-picker", `.${CSS.hueScope}`); [hueScopeCenterX] = getScopeCenter(hueScopeX, hueScopeY); - expect(hueScopeCenterX).toBe(hueSliderX + DIMENSIONS.m.slider.width - 1); + expect(hueScopeCenterX).toBe(hueSliderX + DIMENSIONS.m.slider.width - DIMENSIONS.m.thumb.radius); }); describe("unsupported value handling", () => { @@ -2197,23 +2197,23 @@ describe("calcite-color-picker", () => { const getScopeLeftOffset = async () => parseFloat((await scope.getComputedStyle()).left); - expect(await getScopeLeftOffset()).toBe(-0.5); + expect(await getScopeLeftOffset()).toBeCloseTo(DIMENSIONS.m.thumb.radius - 0.5, 0); await nudgeAQuarterOfSlider(); - expect(await getScopeLeftOffset()).toBe(67.5); + expect(await getScopeLeftOffset()).toBeCloseTo(67.5, 0); await nudgeAQuarterOfSlider(); - expect(await getScopeLeftOffset()).toBe(135.5); + expect(await getScopeLeftOffset()).toBeCloseTo(135.5, 0); await nudgeAQuarterOfSlider(); // hue wraps around, so we nudge it back to assert position at the edge await scope.press("ArrowLeft"); - expect(await getScopeLeftOffset()).toBeLessThanOrEqual(203.5); - expect(await getScopeLeftOffset()).toBeGreaterThan(202.5); + expect(await getScopeLeftOffset()).toBeLessThanOrEqual(193.5); + expect(await getScopeLeftOffset()).toBeGreaterThan(189.5); // nudge it to wrap around await scope.press("ArrowRight"); - expect(await getScopeLeftOffset()).toBe(-0.5); + expect(await getScopeLeftOffset()).toBeCloseTo(DIMENSIONS.m.thumb.radius - 0.5, 0); }); it("allows editing hue slider via keyboard", async () => { @@ -2245,84 +2245,89 @@ describe("calcite-color-picker", () => { expect(await hueSliderScope.getComputedStyle()).toMatchObject({ top: "9.5px", - left: "-0.5px", + left: `${DIMENSIONS.m.thumb.radius - 0.5}px`, }); }); - }); - describe("mouse", () => { - const scopeSizeOffset = 0.8; - it("should update value when color field scope is moved", async () => { - const page = await newE2EPage(); - await page.setContent(``); - const colorPicker = await page.find("calcite-color-picker"); - const [colorFieldScopeX, colorFieldScopeY] = await getElementXY( - page, - "calcite-color-picker", - `.${CSS.colorFieldScope}` - ); - const value = await colorPicker.getProperty("value"); + describe("mouse", () => { + const scopeSizeOffset = 0.8; + it("should update value when color field scope is moved", async () => { + const page = await newE2EPage(); + await page.setContent(``); + const colorPicker = await page.find("calcite-color-picker"); + + const [colorFieldScopeX, colorFieldScopeY] = await getElementXY( + page, + "calcite-color-picker", + `.${CSS.colorFieldScope}` + ); + const value = await colorPicker.getProperty("value"); - await page.mouse.move(colorFieldScopeX, colorFieldScopeY + scopeSizeOffset); - await page.mouse.down(); - await page.mouse.up(); - await page.waitForChanges(); - expect(await colorPicker.getProperty("value")).not.toBe(value); - }); + await page.mouse.move(colorFieldScopeX, colorFieldScopeY + scopeSizeOffset); + await page.mouse.down(); + await page.mouse.up(); + await page.waitForChanges(); + expect(await colorPicker.getProperty("value")).not.toBe(value); + }); - it("should update value when hue scope is moved", async () => { - const page = await newE2EPage(); - await page.setContent(``); - const colorPicker = await page.find("calcite-color-picker"); + it("should update value when hue scope is moved", async () => { + const page = await newE2EPage(); + await page.setContent(``); + const colorPicker = await page.find("calcite-color-picker"); - const [hueScopeX, hueScopeY] = await getElementXY(page, "calcite-color-picker", `.${CSS.hueScope}`); - const value = await colorPicker.getProperty("value"); + const [hueScopeX, hueScopeY] = await getElementXY(page, "calcite-color-picker", `.${CSS.hueScope}`); + const value = await colorPicker.getProperty("value"); - await page.mouse.move(hueScopeX + scopeSizeOffset, hueScopeY); - await page.mouse.down(); - await page.mouse.up(); - await page.waitForChanges(); - expect(await colorPicker.getProperty("value")).not.toBe(value); - }); + await page.mouse.move(hueScopeX + scopeSizeOffset, hueScopeY); + await page.mouse.down(); + await page.mouse.up(); + await page.waitForChanges(); + expect(await colorPicker.getProperty("value")).not.toBe(value); + }); - it("should update value when opacity scope is moved", async () => { - const page = await newE2EPage(); - await page.setContent(``); - const [opacityScopeX, opacityScopeY] = await getElementXY(page, "calcite-color-picker", `.${CSS.opacityScope}`); - const colorPicker = await page.find("calcite-color-picker"); - const value = await colorPicker.getProperty("value"); - - await page.mouse.move(opacityScopeX - 2, opacityScopeY); - await page.mouse.down(); - await page.mouse.up(); - await page.waitForChanges(); - expect(await colorPicker.getProperty("value")).not.toBe(value); + it("should update value when opacity scope is moved", async () => { + const page = await newE2EPage(); + await page.setContent(``); + const [opacityScopeX, opacityScopeY] = await getElementXY( + page, + "calcite-color-picker", + `.${CSS.opacityScope}` + ); + const colorPicker = await page.find("calcite-color-picker"); + const value = await colorPicker.getProperty("value"); + + await page.mouse.move(opacityScopeX - 2, opacityScopeY); + await page.mouse.down(); + await page.mouse.up(); + await page.waitForChanges(); + expect(await colorPicker.getProperty("value")).not.toBe(value); + }); }); - }); - describe("alpha channel", () => { - it("allows editing alpha value via keyboard", async () => { - const page = await newE2EPage(); - await page.setContent(``); + describe("alpha channel", () => { + it("allows editing alpha value via keyboard", async () => { + const page = await newE2EPage(); + await page.setContent(``); - const picker = await page.find("calcite-color-picker"); - const scope = await page.find(`calcite-color-picker >>> .${CSS.opacityScope}`); + const picker = await page.find("calcite-color-picker"); + const scope = await page.find(`calcite-color-picker >>> .${CSS.opacityScope}`); - await scope.press("ArrowDown"); - expect(await picker.getProperty("value")).toBe("#fffffffc"); - await scope.press("ArrowDown"); - expect(await picker.getProperty("value")).toBe("#fffffffa"); - await scope.press("ArrowDown"); - expect(await picker.getProperty("value")).toBe("#fffffff7"); + await scope.press("ArrowDown"); + expect(await picker.getProperty("value")).toBe("#fffffffc"); + await scope.press("ArrowDown"); + expect(await picker.getProperty("value")).toBe("#fffffffa"); + await scope.press("ArrowDown"); + expect(await picker.getProperty("value")).toBe("#fffffff7"); - await scope.press("ArrowUp"); - expect(await picker.getProperty("value")).toBe("#fffffffa"); + await scope.press("ArrowUp"); + expect(await picker.getProperty("value")).toBe("#fffffffa"); - await scope.press("ArrowRight"); - expect(await picker.getProperty("value")).toBe("#fffffffc"); + await scope.press("ArrowRight"); + expect(await picker.getProperty("value")).toBe("#fffffffc"); - await scope.press("ArrowLeft"); - expect(await picker.getProperty("value")).toBe("#fffffffa"); + await scope.press("ArrowLeft"); + expect(await picker.getProperty("value")).toBe("#fffffffa"); + }); }); }); }); diff --git a/packages/calcite-components/src/components/color-picker/color-picker.tsx b/packages/calcite-components/src/components/color-picker/color-picker.tsx index 1bcbbce0b24..f39f75607a9 100644 --- a/packages/calcite-components/src/components/color-picker/color-picker.tsx +++ b/packages/calcite-components/src/components/color-picker/color-picker.tsx @@ -23,6 +23,7 @@ import { DEFAULT_STORAGE_KEY_PREFIX, DIMENSIONS, HSV_LIMITS, + HUE_LIMIT_CONSTRAINED, OPACITY_LIMITS, RGB_LIMITS, SCOPE_SIZE, @@ -63,7 +64,7 @@ import { LocalizedComponent, NumberingSystem, } from "../../utils/locale"; -import { clamp } from "../../utils/math"; +import { clamp, closeToRangeEdge, remap } from "../../utils/math"; import { connectMessages, disconnectMessages, @@ -619,7 +620,7 @@ export class ColorPicker } else if (clientX < bounds.x) { samplingX = 0; } else { - samplingX = bounds.width - 1; + samplingX = bounds.width; } if (clientY < bounds.y + bounds.height && clientY > bounds.y) { @@ -1095,7 +1096,7 @@ export class ColorPicker slider: { width }, }, } = this; - const hue = (360 / width) * x; + const hue = (HUE_LIMIT_CONSTRAINED / width) * x; this.internalColorSet(this.baseColorFieldColor.hue(hue), false); } @@ -1385,7 +1386,6 @@ export class ColorPicker const startAngle = 0; const endAngle = 2 * Math.PI; const outlineWidth = 1; - radius = radius - outlineWidth; context.beginPath(); context.arc(x, y, radius, startAngle, endAngle); @@ -1412,19 +1412,20 @@ export class ColorPicker const { dimensions: { - slider: { height, width }, + slider: { width }, thumb: { radius }, }, } = this; - const x = hsvColor.hue() / (360 / width); - const y = radius - height / 2 + height / 2; + const x = hsvColor.hue() / (HUE_LIMIT_CONSTRAINED / width); + const y = radius; + const sliderBoundX = this.getSliderBoundX(x, width, radius); requestAnimationFrame(() => { - this.hueScopeLeft = x; + this.hueScopeLeft = sliderBoundX; }); - this.drawThumb(this.hueSliderRenderingContext, radius, x, y, hsvColor); + this.drawThumb(this.hueSliderRenderingContext, radius, sliderBoundX, y, hsvColor); } private drawHueSlider(): void { @@ -1441,7 +1442,15 @@ export class ColorPicker const gradient = context.createLinearGradient(0, 0, width, 0); - const hueSliderColorStopKeywords = ["red", "yellow", "lime", "cyan", "blue", "magenta", "red"]; + const hueSliderColorStopKeywords = [ + "red", + "yellow", + "lime", + "cyan", + "blue", + "magenta", + "#ff0004" /* 1 unit less than #ff0 to avoid duplicate values within range */, + ]; const offset = 1 / (hueSliderColorStopKeywords.length - 1); let currentOffset = 0; @@ -1565,12 +1574,23 @@ export class ColorPicker const x = alphaToOpacity(hsvColor.alpha()) / (OPACITY_LIMITS.max / width); const y = radius; + const sliderBoundX = this.getSliderBoundX(x, width, radius); requestAnimationFrame(() => { - this.opacityScopeLeft = x; + this.opacityScopeLeft = sliderBoundX; }); - this.drawThumb(this.opacitySliderRenderingContext, radius, x, y, hsvColor); + this.drawThumb(this.opacitySliderRenderingContext, radius, sliderBoundX, y, hsvColor); + } + + private getSliderBoundX(x: number, width: number, radius: number): number { + const closeToEdge = closeToRangeEdge(x, width, radius); + + return closeToEdge === 0 + ? x + : closeToEdge === -1 + ? remap(x, 0, width, radius, radius * 2) + : remap(x, 0, width, width - radius * 2, width - radius); } private storeOpacityScope = (node: HTMLDivElement): void => { diff --git a/packages/calcite-components/src/components/color-picker/resources.ts b/packages/calcite-components/src/components/color-picker/resources.ts index 51a4e7357be..7048ecbc433 100644 --- a/packages/calcite-components/src/components/color-picker/resources.ts +++ b/packages/calcite-components/src/components/color-picker/resources.ts @@ -48,6 +48,9 @@ export const HSV_LIMITS = { v: 100, }; +// 0 and 360 represent the same value, so we limit the hue to 359 +export const HUE_LIMIT_CONSTRAINED = HSV_LIMITS.h - 1; + export const OPACITY_LIMITS = { min: 0, max: 100, diff --git a/packages/calcite-components/src/components/combobox-item-group/combobox-item-group.scss b/packages/calcite-components/src/components/combobox-item-group/combobox-item-group.scss index 65ec78890cc..dd6e2de9e8d 100644 --- a/packages/calcite-components/src/components/combobox-item-group/combobox-item-group.scss +++ b/packages/calcite-components/src/components/combobox-item-group/combobox-item-group.scss @@ -49,3 +49,7 @@ padding-inline: var(--calcite-combobox-item-spacing-unit-s); margin-inline-start: var(--calcite-combobox-item-indent-value); } + +::slotted(calcite-combobox-item-group:not([after-empty-group])) { + padding-block-start: var(--calcite-combobox-item-spacing-unit-l); +} diff --git a/packages/calcite-components/src/components/combobox-item-group/combobox-item-group.tsx b/packages/calcite-components/src/components/combobox-item-group/combobox-item-group.tsx index 6578f1fcb5e..d2d0dcca904 100644 --- a/packages/calcite-components/src/components/combobox-item-group/combobox-item-group.tsx +++ b/packages/calcite-components/src/components/combobox-item-group/combobox-item-group.tsx @@ -21,6 +21,13 @@ export class ComboboxItemGroup { // // -------------------------------------------------------------------------- + /** + * When `true`, signifies that the group comes after another group without any children (items or sub-groups), otherwise indicates that the group comes after another group that has children. Used for styling. + * + * @internal + */ + @Prop({ reflect: true }) afterEmptyGroup = false; + /** Specifies the parent and grandparent `calcite-combobox-item`s, which are set on `calcite-combobox`. */ @Prop({ mutable: true }) ancestors: ComboboxChildElement[]; diff --git a/packages/calcite-components/src/components/combobox/combobox.scss b/packages/calcite-components/src/components/combobox/combobox.scss index 0ef0c134e64..6d3bfbb7736 100644 --- a/packages/calcite-components/src/components/combobox/combobox.scss +++ b/packages/calcite-components/src/components/combobox/combobox.scss @@ -209,3 +209,7 @@ @include hidden-form-input(); @include base-component(); + +::slotted(calcite-combobox-item-group:not(:first-child)) { + padding-block-start: var(--calcite-combobox-item-spacing-unit-l); +} diff --git a/packages/calcite-components/src/components/combobox/combobox.stories.ts b/packages/calcite-components/src/components/combobox/combobox.stories.ts index 5c6b6a948e2..2daa9ef6b06 100644 --- a/packages/calcite-components/src/components/combobox/combobox.stories.ts +++ b/packages/calcite-components/src/components/combobox/combobox.stories.ts @@ -449,17 +449,31 @@ export const withSelectorIndicatorAndIcons_TestOnly = (): string => html` `; -export const nestedGroups_TestOnly = - (): string => html` - - - - - - +export const nestedGroups_TestOnly = (): string => html` + + + + + + + + + + + + + + + + + + + + -`; + +`; export const clearDisabled_TestOnly = (): string => html` diff --git a/packages/calcite-components/src/components/combobox/combobox.tsx b/packages/calcite-components/src/components/combobox/combobox.tsx index d80a63b2e0f..6fe2676744f 100644 --- a/packages/calcite-components/src/components/combobox/combobox.tsx +++ b/packages/calcite-components/src/components/combobox/combobox.tsx @@ -967,6 +967,18 @@ export class Combobox if (!this.allowCustomValues) { this.setMaxScrollerHeight(); } + + this.groupItems.forEach((groupItem, index, items) => { + if (index === 0) { + groupItem.afterEmptyGroup = false; + } + + const nextGroupItem = items[index + 1]; + + if (nextGroupItem) { + nextGroupItem.afterEmptyGroup = groupItem.children.length === 0; + } + }); }; getData(): ItemData[] { diff --git a/packages/calcite-components/src/components/input-number/input-number.e2e.ts b/packages/calcite-components/src/components/input-number/input-number.e2e.ts index e09f82888e1..a0706e9d015 100644 --- a/packages/calcite-components/src/components/input-number/input-number.e2e.ts +++ b/packages/calcite-components/src/components/input-number/input-number.e2e.ts @@ -713,9 +713,9 @@ describe("calcite-input-number", () => { await page.setContent(html``); const element = await page.find("calcite-input-number"); await element.click(); - await page.waitForChanges; + await page.waitForChanges(); await element.callMethod("blur"); - await page.waitForChanges; + await page.waitForChanges(); element.setProperty("value", "2"); await page.waitForChanges(); expect(await element.getProperty("value")).toBe("2"); diff --git a/packages/calcite-components/src/components/input/input.e2e.ts b/packages/calcite-components/src/components/input/input.e2e.ts index 09742e6fa36..da335abcc4e 100644 --- a/packages/calcite-components/src/components/input/input.e2e.ts +++ b/packages/calcite-components/src/components/input/input.e2e.ts @@ -678,9 +678,9 @@ describe("calcite-input", () => { await page.setContent(html``); const element = await page.find("calcite-input"); await element.click(); - await page.waitForChanges; + await page.waitForChanges(); await element.callMethod("blur"); - await page.waitForChanges; + await page.waitForChanges(); element.setProperty("value", "2"); await page.waitForChanges(); expect(await element.getProperty("value")).toBe("2"); diff --git a/packages/calcite-components/src/components/modal/modal.e2e.ts b/packages/calcite-components/src/components/modal/modal.e2e.ts index 66131631a17..30a4a205732 100644 --- a/packages/calcite-components/src/components/modal/modal.e2e.ts +++ b/packages/calcite-components/src/components/modal/modal.e2e.ts @@ -1,8 +1,8 @@ import { newE2EPage } from "@stencil/core/testing"; import { focusable, renders, slots, hidden, t9n } from "../../tests/commonTests"; import { html } from "../../../support/formatting"; -import { CSS, SLOTS, DURATIONS } from "./resources"; -import { isElementFocused, newProgrammaticE2EPage, skipAnimations } from "../../tests/utils"; +import { CSS, SLOTS } from "./resources"; +import { GlobalTestProps, isElementFocused, newProgrammaticE2EPage, skipAnimations } from "../../tests/utils"; describe("calcite-modal properties", () => { describe("renders", () => { @@ -132,29 +132,24 @@ describe("calcite-modal properties", () => { }); describe("opening and closing behavior", () => { - function getTransitionTransform( - modalSelector: string, - modalContainerSelector: string, - type: "none" | "matrix" - ): boolean { - const modalContainer = document - .querySelector(modalSelector) - .shadowRoot.querySelector(modalContainerSelector); - return getComputedStyle(modalContainer).transform.startsWith(type); - } - - const getTransitionDuration = (): { duration: string } => { - const modal = document.querySelector("calcite-modal"); - const { transitionDuration } = window.getComputedStyle(modal); - return { - duration: transitionDuration, - }; - }; - - it.skip("opens and closes", async () => { + it("opens and closes", async () => { const page = await newE2EPage(); - await page.setContent(html``); + await page.setContent(html``); const modal = await page.find("calcite-modal"); + + type ModalEventOrderWindow = GlobalTestProps<{ events: string[] }>; + + await page.$eval("calcite-modal", (modal: HTMLCalciteModalElement) => { + const receivedEvents: string[] = []; + (window as ModalEventOrderWindow).events = receivedEvents; + + ["calciteModalBeforeOpen", "calciteModalOpen", "calciteModalBeforeClose", "calciteModalClose"].forEach( + (eventType) => { + modal.addEventListener(eventType, (event) => receivedEvents.push(event.type)); + } + ); + }); + const beforeOpenSpy = await modal.spyOnEvent("calciteModalBeforeOpen"); const openSpy = await modal.spyOnEvent("calciteModalOpen"); const beforeCloseSpy = await modal.spyOnEvent("calciteModalBeforeClose"); @@ -164,54 +159,45 @@ describe("opening and closing behavior", () => { expect(openSpy).toHaveReceivedEventTimes(0); expect(beforeCloseSpy).toHaveReceivedEventTimes(0); expect(closeSpy).toHaveReceivedEventTimes(0); - await page.waitForFunction(getTransitionTransform, {}, "calcite-modal", `.${CSS.modal}`, "none"); + expect(await modal.isVisible()).toBe(false); + + const modalBeforeOpen = page.waitForEvent("calciteModalBeforeOpen"); + const modalOpen = page.waitForEvent("calciteModalOpen"); await modal.setProperty("open", true); - let waitForEvent = page.waitForEvent("calciteModalBeforeOpen"); await page.waitForChanges(); - await waitForEvent; - - expect(beforeOpenSpy).toHaveReceivedEventTimes(1); - expect(openSpy).toHaveReceivedEventTimes(0); - expect(beforeCloseSpy).toHaveReceivedEventTimes(0); - expect(closeSpy).toHaveReceivedEventTimes(0); - await page.waitForFunction(getTransitionTransform, {}, "calcite-modal", `.${CSS.modal}`, "matrix"); - waitForEvent = page.waitForEvent("calciteModalOpen"); - await waitForEvent; + await modalBeforeOpen; + await modalOpen; expect(beforeOpenSpy).toHaveReceivedEventTimes(1); expect(openSpy).toHaveReceivedEventTimes(1); expect(beforeCloseSpy).toHaveReceivedEventTimes(0); expect(closeSpy).toHaveReceivedEventTimes(0); - expect(await modal.getProperty("open")).toBe(true); - await page.waitForFunction(getTransitionTransform, {}, "calcite-modal", `.${CSS.modal}`, "matrix"); - await page.waitForFunction(getTransitionTransform, {}, "calcite-modal", `.${CSS.modal}`, "none"); + expect(await modal.isVisible()).toBe(true); + + const modalBeforeClose = page.waitForEvent("calciteModalBeforeClose"); + const modalClose = page.waitForEvent("calciteModalClose"); await modal.setProperty("open", false); - waitForEvent = page.waitForEvent("calciteModalBeforeClose"); await page.waitForChanges(); - await waitForEvent; - const opacityTransition = await page.evaluate(getTransitionDuration); - expect(opacityTransition.duration).toEqual(`${DURATIONS.test}s`); + await modalBeforeClose; + await modalClose; expect(beforeOpenSpy).toHaveReceivedEventTimes(1); expect(openSpy).toHaveReceivedEventTimes(1); expect(beforeCloseSpy).toHaveReceivedEventTimes(1); - expect(closeSpy).toHaveReceivedEventTimes(0); - await page.waitForFunction(getTransitionTransform, {}, "calcite-modal", `.${CSS.modal}`, "matrix"); + expect(closeSpy).toHaveReceivedEventTimes(1); - waitForEvent = page.waitForEvent("calciteModalClose"); - await waitForEvent; + expect(await modal.isVisible()).toBe(false); - expect(beforeOpenSpy).toHaveReceivedEventTimes(1); - expect(openSpy).toHaveReceivedEventTimes(1); - expect(beforeCloseSpy).toHaveReceivedEventTimes(1); - expect(closeSpy).toHaveReceivedEventTimes(1); - await page.waitForFunction(getTransitionTransform, {}, "calcite-modal", `.${CSS.modal}`, "matrix"); - await page.waitForFunction(getTransitionTransform, {}, "calcite-modal", `.${CSS.modal}`, "none"); - expect(await modal.getProperty("open")).toBe(false); + expect(await page.evaluate(() => (window as ModalEventOrderWindow).events)).toEqual([ + "calciteModalBeforeOpen", + "calciteModalOpen", + "calciteModalBeforeClose", + "calciteModalClose", + ]); }); it("emits when set to open on initial render", async () => { @@ -220,18 +206,16 @@ describe("opening and closing behavior", () => { const beforeOpenSpy = await page.spyOnEvent("calciteModalBeforeOpen"); const openSpy = await page.spyOnEvent("calciteModalOpen"); - await page.evaluate((transitionDuration: string): void => { + const waitForBeforeOpenEvent = page.waitForEvent("calciteModalBeforeOpen"); + const waitForOpenEvent = page.waitForEvent("calciteModalOpen"); + + await page.evaluate((): void => { const modal = document.createElement("calcite-modal"); modal.open = true; - modal.style.transition = `opacity ${transitionDuration}s`; document.body.append(modal); - }, `${DURATIONS.test}`); - - await page.waitForTimeout(DURATIONS.test); - - const waitForBeforeOpenEvent = page.waitForEvent("calciteModalBeforeOpen"); - const waitForOpenEvent = page.waitForEvent("calciteModalOpen"); + }); + await page.waitForChanges(); await waitForBeforeOpenEvent; await waitForOpenEvent; @@ -246,20 +230,16 @@ describe("opening and closing behavior", () => { const beforeOpenSpy = await page.spyOnEvent("calciteModalBeforeOpen"); const openSpy = await page.spyOnEvent("calciteModalOpen"); + const waitForOpenEvent = page.waitForEvent("calciteModalOpen"); + const waitForBeforeOpenEvent = page.waitForEvent("calciteModalBeforeOpen"); + await page.evaluate((): void => { const modal = document.createElement("calcite-modal"); modal.open = true; document.body.append(modal); }); - const opacityTransition = await page.evaluate(getTransitionDuration); - expect(opacityTransition.duration).toEqual("0s"); - - await page.waitForChanges; - - const waitForOpenEvent = page.waitForEvent("calciteModalOpen"); - const waitForBeforeOpenEvent = page.waitForEvent("calciteModalBeforeOpen"); - + await page.waitForChanges(); await waitForBeforeOpenEvent; await waitForOpenEvent; @@ -267,7 +247,7 @@ describe("opening and closing behavior", () => { expect(openSpy).toHaveReceivedEventTimes(1); }); - it.skip("emits when duration is set to 0", async () => { + it("emits when duration is set to 0", async () => { const page = await newProgrammaticE2EPage(); await skipAnimations(page); @@ -283,9 +263,6 @@ describe("opening and closing behavior", () => { document.body.append(modal); }); - const opacityTransition = await page.evaluate(getTransitionDuration); - expect(opacityTransition.duration).toEqual("0s"); - await page.waitForChanges(); await beforeOpenSpy; await openSpy; @@ -320,11 +297,11 @@ describe("calcite-modal accessibility checks", () => { ` ); - await skipAnimations(page); - await page.waitForChanges(); const modal = await page.find("calcite-modal"); + const opened = page.waitForEvent("calciteModalOpen"); modal.setProperty("open", true); await page.waitForChanges(); + await opened; expect(await isElementFocused(page, `.${CSS.close}`, { shadowed: true })).toBe(true); await page.keyboard.press("Tab"); diff --git a/packages/calcite-components/src/components/tooltip/tooltip.e2e.ts b/packages/calcite-components/src/components/tooltip/tooltip.e2e.ts index 3a37b1b079d..1f61788888a 100644 --- a/packages/calcite-components/src/components/tooltip/tooltip.e2e.ts +++ b/packages/calcite-components/src/components/tooltip/tooltip.e2e.ts @@ -783,6 +783,73 @@ describe("calcite-tooltip", () => { expect(beforeCloseEvent).toHaveReceivedEventTimes(1); expect(closeEvent).toHaveReceivedEventTimes(1); } + + it("when open, it emits close events if no longer rendered", async () => { + const page = await newE2EPage(); + await page.setContent(html` + + + + referenceElement + content + + + some other content + `); + + const beforeCloseEvent = await page.spyOnEvent("calciteTooltipBeforeClose"); + const closeEvent = await page.spyOnEvent("calciteTooltipClose"); + const beforeOpenEvent = await page.spyOnEvent("calciteTooltipBeforeOpen"); + const openEvent = await page.spyOnEvent("calciteTooltipOpen"); + + const container = await page.find(".container"); + const tooltip = await page.find(`calcite-tooltip`); + + expect(await tooltip.isVisible()).toBe(false); + + await container.hover(); + await page.waitForChanges(); + + const ref = await page.find("#ref"); + await ref.hover(); + + await page.waitForTimeout(TOOLTIP_OPEN_DELAY_MS); + await page.waitForChanges(); + + expect(await tooltip.isVisible()).toBe(true); + + expect(beforeOpenEvent).toHaveReceivedEventTimes(1); + expect(openEvent).toHaveReceivedEventTimes(1); + expect(beforeCloseEvent).toHaveReceivedEventTimes(0); + expect(closeEvent).toHaveReceivedEventTimes(0); + + const hoverOutsideContainer = await page.find(".hoverOutsideContainer"); + await hoverOutsideContainer.hover(); + + await page.waitForTimeout(TOOLTIP_CLOSE_DELAY_MS); + await page.waitForChanges(); + + expect(await tooltip.isVisible()).not.toBe(true); + + expect(beforeOpenEvent).toHaveReceivedEventTimes(1); + expect(openEvent).toHaveReceivedEventTimes(1); + expect(beforeCloseEvent).toHaveReceivedEventTimes(1); + expect(closeEvent).toHaveReceivedEventTimes(1); + }); }); it.skip("should open hovered tooltip while pointer is moving", async () => { @@ -905,6 +972,7 @@ describe("calcite-tooltip", () => { describe("within shadowRoot", () => { async function defineTestComponents(page: E2EPage): Promise { await page.setContent(""); + await page.evaluate((): void => { const customComponents: { name: string; html: string }[] = [ { diff --git a/packages/calcite-components/src/components/tooltip/tooltip.tsx b/packages/calcite-components/src/components/tooltip/tooltip.tsx index 04257cab6e3..94ee94a73c0 100644 --- a/packages/calcite-components/src/components/tooltip/tooltip.tsx +++ b/packages/calcite-components/src/components/tooltip/tooltip.tsx @@ -168,6 +168,12 @@ export class Tooltip implements FloatingUIComponent, OpenCloseComponent { } } + async componentWillLoad(): Promise { + if (this.open) { + onToggleOpenCloseComponent(this); + } + } + componentDidLoad(): void { if (this.referenceElement && !this.effectiveReferenceElement) { this.setUpReferenceElement(); diff --git a/packages/calcite-components/src/utils/math.spec.ts b/packages/calcite-components/src/utils/math.spec.ts index be7eee4b27a..2bbdd9feb28 100644 --- a/packages/calcite-components/src/utils/math.spec.ts +++ b/packages/calcite-components/src/utils/math.spec.ts @@ -1,4 +1,4 @@ -import { clamp, decimalPlaces } from "./math"; +import { clamp, closeToRangeEdge, decimalPlaces, remap } from "./math"; describe("clamp", () => { it("clamps numbers within min/max", () => { @@ -14,3 +14,27 @@ describe("decimalPlaces", () => { expect(decimalPlaces(123.123)).toBe(3); }); }); + +describe("remap", () => { + it("remaps numbers", () => { + expect(remap(5, 0, 10, 0, 100)).toBe(50); + expect(remap(0, -100, 100, 0, 100)).toBe(50); + expect(remap(0.5, 0, 1, 0, 100)).toBe(50); + }); +}); + +describe("closeToRangeEdge", () => { + it("returns -1 if close to lower edge", () => { + expect(closeToRangeEdge(0, 100, 10)).toBe(-1); + expect(closeToRangeEdge(9, 100, 10)).toBe(-1); + }); + + it("returns 1 if close to upper edge", () => { + expect(closeToRangeEdge(100, 100, 10)).toBe(1); + expect(closeToRangeEdge(91, 100, 10)).toBe(1); + }); + + it("returns 0 if not close to edge", () => { + expect(closeToRangeEdge(50, 100, 10)).toBe(0); + }); +}); diff --git a/packages/calcite-components/src/utils/math.ts b/packages/calcite-components/src/utils/math.ts index 3bc251556bc..8698bb5da96 100644 --- a/packages/calcite-components/src/utils/math.ts +++ b/packages/calcite-components/src/utils/math.ts @@ -15,3 +15,19 @@ export const decimalPlaces = (value: number): number => { (match[2] ? +match[2] : 0) ); }; + +export function remap(value: number, fromMin: number, fromMax: number, toMin: number, toMax: number): number { + return ((value - fromMin) * (toMax - toMin)) / (fromMax - fromMin) + toMin; +} + +/** + * Helper to determine if a value is close to the edge of a range within a threshold. + * + * @param value + * @param range + * @param threshold + * @returns -1 if close to lower edge, 1 if close to upper edge, 0 otherwise. + */ +export function closeToRangeEdge(value: number, range: number, threshold: number): number { + return value < threshold ? -1 : value > range - threshold ? 1 : 0; +} diff --git a/packages/calcite-components/src/utils/openCloseComponent.ts b/packages/calcite-components/src/utils/openCloseComponent.ts index c76a657b287..fef61e73a00 100644 --- a/packages/calcite-components/src/utils/openCloseComponent.ts +++ b/packages/calcite-components/src/utils/openCloseComponent.ts @@ -66,12 +66,34 @@ function transitionEnd(event: TransitionEvent): void { } } +function emitImmediately(component: OpenCloseComponent, nonOpenCloseComponent = false): void { + (nonOpenCloseComponent ? component[component.transitionProp] : component.open) + ? component.onBeforeOpen() + : component.onBeforeClose(); + (nonOpenCloseComponent ? component[component.transitionProp] : component.open) + ? component.onOpen() + : component.onClose(); +} + /** * Helper to determine globally set transition duration on the given openTransitionProp, which is imported and set in the @Watch("open"). * Used to emit (before)open/close events both for when the opacity transition is present and when there is none (transition-duration is set to 0). * - * @param component - * @param nonOpenCloseComponent + * @example + * import { onToggleOpenCloseComponent, OpenCloseComponent } from "../../utils/openCloseComponent"; + * + * async componentWillLoad() { + * // When component initially renders, if `open` was set we need to trigger on load as watcher doesn't fire. + * if (this.open) { + * onToggleOpenCloseComponent(this); + * } + * @Watch("open") + * async toggleModal(value: boolean): Promise { + * onToggleOpenCloseComponent(this); + * } + * + * @param component - OpenCloseComponent uses `open` prop to emit (before)open/close. + * @param nonOpenCloseComponent - OpenCloseComponent uses `expanded` prop to emit (before)open/close. */ export function onToggleOpenCloseComponent(component: OpenCloseComponent, nonOpenCloseComponent = false): void { readTask((): void => { @@ -81,42 +103,54 @@ export function onToggleOpenCloseComponent(component: OpenCloseComponent, nonOpe (item) => item === component.openTransitionProp ); const transitionDuration = allTransitionPropsArray[openTransitionPropIndex + 1]; + if (transitionDuration === "0s") { - (nonOpenCloseComponent ? component[component.transitionProp] : component.open) - ? component.onBeforeOpen() - : component.onBeforeClose(); - (nonOpenCloseComponent ? component[component.transitionProp] : component.open) - ? component.onOpen() - : component.onClose(); - } else { - component.transitionEl.addEventListener( - "transitionstart", - () => { - (nonOpenCloseComponent ? component[component.transitionProp] : component.open) - ? component.onBeforeOpen() - : component.onBeforeClose(); - }, - { once: true } - ); - component.transitionEl.addEventListener( - "transitionend", - () => { - (nonOpenCloseComponent ? component[component.transitionProp] : component.open) - ? component.onOpen() - : component.onClose(); - }, - { once: true } - ); + emitImmediately(component, nonOpenCloseComponent); + return; + } + + const fallbackTimeoutId = setTimeout((): void => { + component.transitionEl.removeEventListener("transitionstart", onStart); + component.transitionEl.removeEventListener("transitionend", onEndOrCancel); + component.transitionEl.removeEventListener("transitioncancel", onEndOrCancel); + emitImmediately(component, nonOpenCloseComponent); + }, parseFloat(transitionDuration) * 1000); + + component.transitionEl.addEventListener("transitionstart", onStart); + component.transitionEl.addEventListener("transitionend", onEndOrCancel); + component.transitionEl.addEventListener("transitioncancel", onEndOrCancel); + + function onStart(event: TransitionEvent): void { + if (event.propertyName === component.openTransitionProp && event.target === component.transitionEl) { + clearTimeout(fallbackTimeoutId); + component.transitionEl.removeEventListener("transitionstart", onStart); + (nonOpenCloseComponent ? component[component.transitionProp] : component.open) + ? component.onBeforeOpen() + : component.onBeforeClose(); + } + } + + function onEndOrCancel(event: TransitionEvent): void { + if (event.propertyName === component.openTransitionProp && event.target === component.transitionEl) { + (nonOpenCloseComponent ? component[component.transitionProp] : component.open) + ? component.onOpen() + : component.onClose(); + + component.transitionEl.removeEventListener("transitionend", onEndOrCancel); + component.transitionEl.removeEventListener("transitioncancel", onEndOrCancel); + } } } }); } + /** * Helper to keep track of transition listeners on setTransitionEl and connectedCallback on OpenCloseComponent components. * * For component which do not have open prop, use `onToggleOpenCloseComponent` implementation. * * @param component + * @deprecated Call `onToggleOpenClose` in `componentWillLoad` and `open` property watchers instead. */ export function connectOpenCloseComponent(component: OpenCloseComponent): void { disconnectOpenCloseComponent(component); @@ -138,6 +172,7 @@ export function connectOpenCloseComponent(component: OpenCloseComponent): void { * Helper to tear down transition listeners on disconnectedCallback on OpenCloseComponent components. * * @param component + * @deprecated Call `onToggleOpenClose` in `componentWillLoad` and `open` property watchers instead. */ export function disconnectOpenCloseComponent(component: OpenCloseComponent): void { if (!componentToTransitionListeners.has(component)) {