diff --git a/package-lock.json b/package-lock.json index 4613b523c20..bbfadb3a015 100644 --- a/package-lock.json +++ b/package-lock.json @@ -36,7 +36,7 @@ "@types/react-dom": "^16.0.9", "@types/semver": "7.5.0", "@types/shell-quote": "1.7.1", - "@types/sortablejs": "1.15.1", + "@types/sortablejs": "1.15.2", "@types/yargs": "17.0.24", "@typescript-eslint/eslint-plugin": "5.60.1", "@typescript-eslint/parser": "5.48.2", @@ -9628,9 +9628,9 @@ "dev": true }, "node_modules/@types/sortablejs": { - "version": "1.15.1", - "resolved": "https://registry.npmjs.org/@types/sortablejs/-/sortablejs-1.15.1.tgz", - "integrity": "sha512-g/JwBNToh6oCTAwNS8UGVmjO7NLDKsejVhvE4x1eWiPTC3uCuNsa/TD4ssvX3du+MLiM+SHPNDuijp8y76JzLQ==", + "version": "1.15.2", + "resolved": "https://registry.npmjs.org/@types/sortablejs/-/sortablejs-1.15.2.tgz", + "integrity": "sha512-mOIv/EnPMzAZAVbuh9uGjOZ1BBdimP9Y6IPGntsvQJtko5yapSDKB7GwB3AOlF5N3bkpk4sBwQRpS3aEkiUbaA==", "dev": true }, "node_modules/@types/source-list-map": { @@ -40443,7 +40443,7 @@ }, "packages/calcite-components": { "name": "@esri/calcite-components", - "version": "1.7.1-next.2", + "version": "1.8.0-next.0", "license": "SEE LICENSE.md", "dependencies": { "@floating-ui/dom": "1.5.1", @@ -40470,10 +40470,10 @@ }, "packages/calcite-components-react": { "name": "@esri/calcite-components-react", - "version": "1.7.1-next.2", + "version": "1.8.0-next.0", "license": "SEE LICENSE.md", "dependencies": { - "@esri/calcite-components": "^1.7.1-next.2" + "@esri/calcite-components": "^1.8.0-next.0" }, "peerDependencies": { "react": ">=16.7", @@ -42704,7 +42704,7 @@ "@esri/calcite-components-react": { "version": "file:packages/calcite-components-react", "requires": { - "@esri/calcite-components": "^1.7.1-next.2" + "@esri/calcite-components": "^1.8.0-next.0" } }, "@esri/calcite-design-tokens": { @@ -47704,9 +47704,9 @@ "dev": true }, "@types/sortablejs": { - "version": "1.15.1", - "resolved": "https://registry.npmjs.org/@types/sortablejs/-/sortablejs-1.15.1.tgz", - "integrity": "sha512-g/JwBNToh6oCTAwNS8UGVmjO7NLDKsejVhvE4x1eWiPTC3uCuNsa/TD4ssvX3du+MLiM+SHPNDuijp8y76JzLQ==", + "version": "1.15.2", + "resolved": "https://registry.npmjs.org/@types/sortablejs/-/sortablejs-1.15.2.tgz", + "integrity": "sha512-mOIv/EnPMzAZAVbuh9uGjOZ1BBdimP9Y6IPGntsvQJtko5yapSDKB7GwB3AOlF5N3bkpk4sBwQRpS3aEkiUbaA==", "dev": true }, "@types/source-list-map": { diff --git a/package.json b/package.json index 2fba95369d7..a6920ee512b 100644 --- a/package.json +++ b/package.json @@ -49,7 +49,7 @@ "@types/react-dom": "^16.0.9", "@types/semver": "7.5.0", "@types/shell-quote": "1.7.1", - "@types/sortablejs": "1.15.1", + "@types/sortablejs": "1.15.2", "@types/yargs": "17.0.24", "@typescript-eslint/eslint-plugin": "5.60.1", "@typescript-eslint/parser": "5.48.2", diff --git a/packages/calcite-components-react/CHANGELOG.md b/packages/calcite-components-react/CHANGELOG.md index 7d4f81125f4..99ef797c505 100644 --- a/packages/calcite-components-react/CHANGELOG.md +++ b/packages/calcite-components-react/CHANGELOG.md @@ -3,6 +3,10 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [1.8.0-next.0](https://github.com/Esri/calcite-design-system/compare/@esri/calcite-components-react@1.7.1-next.2...@esri/calcite-components-react@1.8.0-next.0) (2023-09-05) + +**Note:** Version bump only for package @esri/calcite-components-react + ## [1.7.1-next.2](https://github.com/Esri/calcite-design-system/compare/@esri/calcite-components-react@1.7.1-next.1...@esri/calcite-components-react@1.7.1-next.2) (2023-09-05) **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 098ba01ce16..1408efdf98e 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.7.1-next.2", + "version": "1.8.0-next.0", "homepage": "https://developers.arcgis.com/calcite-design-system/", "description": "A set of React components that wrap calcite components", "license": "SEE LICENSE.md", @@ -20,7 +20,7 @@ "dist/" ], "dependencies": { - "@esri/calcite-components": "^1.7.1-next.2" + "@esri/calcite-components": "^1.8.0-next.0" }, "peerDependencies": { "react": ">=16.7", diff --git a/packages/calcite-components/CHANGELOG.md b/packages/calcite-components/CHANGELOG.md index aecd5fee8d7..69c299ab4d5 100644 --- a/packages/calcite-components/CHANGELOG.md +++ b/packages/calcite-components/CHANGELOG.md @@ -3,6 +3,16 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [1.8.0-next.0](https://github.com/Esri/calcite-design-system/compare/@esri/calcite-components@1.7.1-next.2...@esri/calcite-components@1.8.0-next.0) (2023-09-05) + +### Features + +- **flow:** split up custom flow item interfaces ([#7666](https://github.com/Esri/calcite-design-system/issues/7666)) ([6c22e7c](https://github.com/Esri/calcite-design-system/commit/6c22e7c60525385550ecd76a19abfe58f729f5bf)), closes [#7608](https://github.com/Esri/calcite-design-system/issues/7608) + +### Bug Fixes + +- **time-picker:** focus corresponding input when nudge buttons are clicked ([#7650](https://github.com/Esri/calcite-design-system/issues/7650)) ([9c7d846](https://github.com/Esri/calcite-design-system/commit/9c7d846fc376cc50726dc6662b99afe466021a54)), closes [#7533](https://github.com/Esri/calcite-design-system/issues/7533) + ## [1.7.1-next.2](https://github.com/Esri/calcite-design-system/compare/@esri/calcite-components@1.7.1-next.1...@esri/calcite-components@1.7.1-next.2) (2023-09-05) ### Bug Fixes diff --git a/packages/calcite-components/conventions/README.md b/packages/calcite-components/conventions/README.md index bff430b437f..3a92d3937ff 100644 --- a/packages/calcite-components/conventions/README.md +++ b/packages/calcite-components/conventions/README.md @@ -419,15 +419,51 @@ For such cases, the following pattern will enable developers to create custom ch #### Parent component - Must provide a `customItemSelectors` property to allow querying for custom elements in addition to their expected children. -- An interface for `HTMLElement` must be created in the parent's `interfaces.d.ts` file, where the necessary child APIs must be extracted. - **`parent/interfaces.d.ts`** - ```ts - type ChildComponentLike = Pick & HTMLElement; - ``` - **`parent/parent.tsx`** - ```tsx - @Prop() selectedItem: HTMLChildComponentElement | ChildComponentLike; - ``` +- An interface for the class (used by custom item classes) and element (used by parent component APIs) must be created in the parent's `interfaces.d.ts` file, where the necessary child APIs must be extracted. + +**Example** + +**`parent/interfaces.d.ts`** + +```ts +type ChildComponentLike = Pick; +type ChildComponentLikeElement = ChilcComponentLike & HTMLElement; +``` + +**`parent/parent.tsx`** + +```tsx + @Prop() selectedItem: HTMLChildComponentElement | ChildComponentLikeElement; +``` + +**`custom-item/custom-item.tsx`** + +```tsx +export class CustomItem implements ChildComponentLike { + private childComponentEl: HTMLChildComponentLikeElement; + + @Prop() required: boolean; + @Prop() props: string; + @Prop() from: number; + + @Method() async parent(): Promise { + await this.childComponentEl.parent(); + } + + render(): VNode { + return ( + + (this.childComponentEl = el)} + /> + + ); + } +} +``` #### Custom child component diff --git a/packages/calcite-components/package.json b/packages/calcite-components/package.json index c9b6c927d8c..ee7afc3d580 100644 --- a/packages/calcite-components/package.json +++ b/packages/calcite-components/package.json @@ -1,6 +1,6 @@ { "name": "@esri/calcite-components", - "version": "1.7.1-next.2", + "version": "1.8.0-next.0", "homepage": "https://developers.arcgis.com/calcite-design-system/", "description": "Web Components for Esri's Calcite Design System.", "main": "dist/index.cjs.js", diff --git a/packages/calcite-components/src/components/flow/interfaces.ts b/packages/calcite-components/src/components/flow/interfaces.ts index be33fbfcbb8..a4380c878ff 100644 --- a/packages/calcite-components/src/components/flow/interfaces.ts +++ b/packages/calcite-components/src/components/flow/interfaces.ts @@ -1,7 +1,5 @@ export type FlowDirection = "advancing" | "retreating"; -export type FlowItemLikeElement = Pick< - HTMLCalciteFlowItemElement, - "beforeBack" | "menuOpen" | "setFocus" | "showBackButton" -> & - HTMLElement; +export type FlowItemLike = Pick; + +export type FlowItemLikeElement = FlowItemLike & HTMLElement; diff --git a/packages/calcite-components/src/components/input-date-picker/input-date-picker.tsx b/packages/calcite-components/src/components/input-date-picker/input-date-picker.tsx index a008dd3139d..544ad940262 100644 --- a/packages/calcite-components/src/components/input-date-picker/input-date-picker.tsx +++ b/packages/calcite-components/src/components/input-date-picker/input-date-picker.tsx @@ -58,6 +58,7 @@ import { } from "../../utils/loadable"; import { connectLocalized, + defaultNumberingSystem, disconnectLocalized, LocalizedComponent, NumberingSystem, @@ -1002,10 +1003,17 @@ export class InputDatePicker ) : null; + const formattingOptions = { + // we explicitly set numberingSystem to prevent the browser-inferred value + // see https://github.com/Esri/calcite-design-system/issues/3079#issuecomment-1168964195 for more info + numberingSystem: defaultNumberingSystem, + }; + const localizedDate = - date && this.formatNumerals(date.toLocaleDateString(this.effectiveLocale)); + date && this.formatNumerals(date.toLocaleDateString(this.effectiveLocale, formattingOptions)); const localizedEndDate = - endDate && this.formatNumerals(endDate.toLocaleDateString(this.effectiveLocale)); + endDate && + this.formatNumerals(endDate.toLocaleDateString(this.effectiveLocale, formattingOptions)); this.setInputValue(localizedDate ?? "", "start"); this.setInputValue((this.range && localizedEndDate) ?? "", "end"); diff --git a/packages/calcite-components/src/components/list-item/list-item.e2e.ts b/packages/calcite-components/src/components/list-item/list-item.e2e.ts index 759485f4070..6ffe4f73cd8 100755 --- a/packages/calcite-components/src/components/list-item/list-item.e2e.ts +++ b/packages/calcite-components/src/components/list-item/list-item.e2e.ts @@ -1,6 +1,7 @@ import { newE2EPage } from "@stencil/core/testing"; import { defaults, disabled, focusable, hidden, renders, slots } from "../../tests/commonTests"; import { CSS, SLOTS } from "./resources"; +import { html } from "../../../support/formatting"; describe("calcite-list-item", () => { describe("renders", () => { @@ -133,6 +134,42 @@ describe("calcite-list-item", () => { expect(eventSpy).toHaveReceivedEventTimes(1); }); + it("does not emit calciteListItemSelect on Enter within action slots", async () => { + const page = await newE2EPage(); + await page.setContent(html` + `); + + await page.waitForChanges(); + + const eventSpy = await page.spyOnEvent("calciteListItemSelect"); + + const actionsStart = await page.find(`calcite-list-item >>> .${CSS.actionsStart}`); + await actionsStart.focus(); + await page.keyboard.press("Enter"); + + expect(eventSpy).toHaveReceivedEventTimes(0); + + const actionsEnd = await page.find(`calcite-list-item >>> .${CSS.actionsEnd}`); + await actionsEnd.focus(); + await page.keyboard.press("Enter"); + + expect(eventSpy).toHaveReceivedEventTimes(0); + }); + it("emits calciteListItemSelect on click", async () => { const page = await newE2EPage({ html: ``, diff --git a/packages/calcite-components/src/components/list-item/list-item.scss b/packages/calcite-components/src/components/list-item/list-item.scss index a65b32f5d4d..5ab1314e2a4 100755 --- a/packages/calcite-components/src/components/list-item/list-item.scss +++ b/packages/calcite-components/src/components/list-item/list-item.scss @@ -55,7 +55,7 @@ td { tr:focus, td:focus { - @apply focus-inset z-sticky; + @apply focus-inset; } .content, diff --git a/packages/calcite-components/src/components/list-item/list-item.tsx b/packages/calcite-components/src/components/list-item/list-item.tsx index 5672455bf07..ac08f309b4d 100644 --- a/packages/calcite-components/src/components/list-item/list-item.tsx +++ b/packages/calcite-components/src/components/list-item/list-item.tsx @@ -703,7 +703,11 @@ export class ListItem const cells = [actionsStartEl, contentEl, actionsEndEl].filter(Boolean); const currentIndex = cells.findIndex((cell) => composedPath.includes(cell)); - if (key === "Enter") { + if ( + key === "Enter" && + !composedPath.includes(actionsStartEl) && + !composedPath.includes(actionsEndEl) + ) { event.preventDefault(); this.toggleSelected(); } else if (key === "ArrowRight") { diff --git a/packages/calcite-components/src/components/list/list.scss b/packages/calcite-components/src/components/list/list.scss index 3d450907e49..f33d96030f6 100755 --- a/packages/calcite-components/src/components/list/list.scss +++ b/packages/calcite-components/src/components/list/list.scss @@ -13,9 +13,7 @@ flex w-full flex-col - bg-transparent - relative - z-default; + bg-transparent; * { @apply box-border; } diff --git a/packages/calcite-components/src/components/modal/modal.e2e.ts b/packages/calcite-components/src/components/modal/modal.e2e.ts index 68100fcbecb..830d4699ee9 100644 --- a/packages/calcite-components/src/components/modal/modal.e2e.ts +++ b/packages/calcite-components/src/components/modal/modal.e2e.ts @@ -23,7 +23,7 @@ describe("calcite-modal properties", () => { const modal = await page.find("calcite-modal"); modal.setProperty("closeButtonDisabled", true); await page.waitForChanges(); - const closeButton = await page.find("calcite-modal >>> .close"); + const closeButton = await page.find(`calcite-modal >>> .${CSS.close}`); expect(closeButton).toBe(null); }); @@ -298,6 +298,55 @@ describe("opening and closing behavior", () => { ]); }); + it("emits when closing on click", async () => { + const page = await newE2EPage(); + await page.setContent(html``); + const modal = await page.find("calcite-modal"); + + const beforeOpenSpy = await modal.spyOnEvent("calciteModalBeforeOpen"); + const openSpy = await modal.spyOnEvent("calciteModalOpen"); + const beforeCloseSpy = await modal.spyOnEvent("calciteModalBeforeClose"); + const closeSpy = await modal.spyOnEvent("calciteModalClose"); + + expect(beforeOpenSpy).toHaveReceivedEventTimes(0); + expect(openSpy).toHaveReceivedEventTimes(0); + expect(beforeCloseSpy).toHaveReceivedEventTimes(0); + expect(closeSpy).toHaveReceivedEventTimes(0); + + expect(await modal.isVisible()).toBe(false); + + const modalBeforeOpen = page.waitForEvent("calciteModalBeforeOpen"); + const modalOpen = page.waitForEvent("calciteModalOpen"); + modal.setProperty("open", true); + await page.waitForChanges(); + + await modalBeforeOpen; + await modalOpen; + + expect(beforeOpenSpy).toHaveReceivedEventTimes(1); + expect(openSpy).toHaveReceivedEventTimes(1); + expect(beforeCloseSpy).toHaveReceivedEventTimes(0); + expect(closeSpy).toHaveReceivedEventTimes(0); + + expect(await modal.isVisible()).toBe(true); + + const modalBeforeClose = page.waitForEvent("calciteModalBeforeClose"); + const modalClose = page.waitForEvent("calciteModalClose"); + const closeButton = await page.find(`calcite-modal >>> .${CSS.close}`); + await closeButton.click(); + await page.waitForChanges(); + + await modalBeforeClose; + await modalClose; + + expect(beforeOpenSpy).toHaveReceivedEventTimes(1); + expect(openSpy).toHaveReceivedEventTimes(1); + expect(beforeCloseSpy).toHaveReceivedEventTimes(1); + expect(closeSpy).toHaveReceivedEventTimes(1); + + expect(await modal.isVisible()).toBe(false); + }); + it("emits when set to open on initial render", async () => { const page = await newProgrammaticE2EPage(); @@ -474,7 +523,7 @@ describe("calcite-modal accessibility checks", () => { const createModalHTML = (contentHTML?: string, attrs?: string) => `${contentHTML}`; - const closeButtonTargetSelector = ".close"; + const closeButtonTargetSelector = `.${CSS.close}`; const focusableContentTargetClass = "test"; const focusableContentHTML = html`

Title

@@ -543,7 +592,7 @@ describe("calcite-modal accessibility checks", () => { modal.setProperty("open", true); await page.waitForChanges(); expect(await modal.isVisible()).toBe(true); - const closeButton = await page.find("calcite-modal >>> .close"); + const closeButton = await page.find(`calcite-modal >>> .${CSS.close}`); await closeButton.click(); await page.waitForChanges(); expect(await modal.isVisible()).toBe(false); diff --git a/packages/calcite-components/src/components/modal/modal.tsx b/packages/calcite-components/src/components/modal/modal.tsx index db9dcffd38e..c661d852551 100644 --- a/packages/calcite-components/src/components/modal/modal.tsx +++ b/packages/calcite-components/src/components/modal/modal.tsx @@ -172,7 +172,6 @@ export class Modal setUpLoadableComponent(this); // when modal initially renders, if active was set we need to open as watcher doesn't fire if (this.open) { - onToggleOpenCloseComponent(this); requestAnimationFrame(() => this.openModal()); } } @@ -290,7 +289,7 @@ export class Modal aria-label={this.messages.close} class={CSS.close} key="button" - onClick={this.closeModal} + onClick={this.handleCloseClick} title={this.messages.close} // eslint-disable-next-line react/jsx-sort-props -- ref should be last so node attrs/props are in sync (see https://github.com/Esri/calcite-design-system/pull/6530) ref={(el) => (this.closeButtonEl = el)} @@ -405,7 +404,7 @@ export class Modal @Listen("keydown", { target: "window" }) handleEscape(event: KeyboardEvent): void { if (this.open && !this.escapeDisabled && event.key === "Escape" && !event.defaultPrevented) { - this.closeModal(); + this.open = false; event.preventDefault(); } } @@ -502,18 +501,25 @@ export class Modal } @Watch("open") - async toggleModal(value: boolean): Promise { + toggleModal(value: boolean): void { if (this.ignoreOpenChange) { return; } + if (value) { + this.openModal(); + } else { + this.closeModal(); + } + } + + @Watch("opened") + handleOpenedChange(value: boolean): void { onToggleOpenCloseComponent(this); if (value) { this.transitionEl?.classList.add(CSS.openingIdle); - this.openModal(); } else { this.transitionEl?.classList.add(CSS.closingIdle); - this.closeModal(); } } @@ -522,15 +528,12 @@ export class Modal this.el.removeEventListener("calciteModalOpen", this.openEnd); }; - /** Open the modal */ - private openModal() { - if (this.ignoreOpenChange) { - return; - } + private handleCloseClick = () => { + this.open = false; + }; - this.ignoreOpenChange = true; + private openModal() { this.el.addEventListener("calciteModalOpen", this.openEnd); - this.open = true; this.opened = true; const titleEl = getSlotted(this.el, SLOTS.header); const contentEl = getSlotted(this.el, SLOTS.content); @@ -543,7 +546,6 @@ export class Modal // use an inline style instead of a utility class to avoid global class declarations. document.documentElement.style.setProperty("overflow", "hidden"); } - this.ignoreOpenChange = false; } private handleOutsideClose = (): void => { @@ -551,15 +553,10 @@ export class Modal return; } - this.closeModal(); + this.open = false; }; - /** Close the modal, first running the `beforeClose` method */ closeModal = async (): Promise => { - if (this.ignoreOpenChange) { - return; - } - if (this.beforeClose) { try { await this.beforeClose(this.el); @@ -574,11 +571,8 @@ export class Modal } } - this.ignoreOpenChange = true; - this.open = false; this.opened = false; this.removeOverflowHiddenClass(); - this.ignoreOpenChange = false; }; private removeOverflowHiddenClass(): void { diff --git a/packages/calcite-components/src/components/sheet/sheet.e2e.ts b/packages/calcite-components/src/components/sheet/sheet.e2e.ts index 2dde28d9fc4..3040292c767 100644 --- a/packages/calcite-components/src/components/sheet/sheet.e2e.ts +++ b/packages/calcite-components/src/components/sheet/sheet.e2e.ts @@ -409,6 +409,55 @@ describe("calcite-sheet properties", () => { ]); }); + it("emits when closing on click", async () => { + const page = await newE2EPage(); + await page.setContent(html``); + const sheet = await page.find("calcite-sheet"); + + const beforeOpenSpy = await sheet.spyOnEvent("calciteSheetBeforeOpen"); + const openSpy = await sheet.spyOnEvent("calciteSheetOpen"); + const beforeCloseSpy = await sheet.spyOnEvent("calciteSheetBeforeClose"); + const closeSpy = await sheet.spyOnEvent("calciteSheetClose"); + + expect(beforeOpenSpy).toHaveReceivedEventTimes(0); + expect(openSpy).toHaveReceivedEventTimes(0); + expect(beforeCloseSpy).toHaveReceivedEventTimes(0); + expect(closeSpy).toHaveReceivedEventTimes(0); + + expect(await sheet.isVisible()).toBe(false); + + const sheetBeforeOpen = page.waitForEvent("calciteSheetBeforeOpen"); + const sheetOpen = page.waitForEvent("calciteSheetOpen"); + sheet.setProperty("open", true); + await page.waitForChanges(); + + await sheetBeforeOpen; + await sheetOpen; + + expect(beforeOpenSpy).toHaveReceivedEventTimes(1); + expect(openSpy).toHaveReceivedEventTimes(1); + expect(beforeCloseSpy).toHaveReceivedEventTimes(0); + expect(closeSpy).toHaveReceivedEventTimes(0); + + expect(await sheet.isVisible()).toBe(true); + + const sheetBeforeClose = page.waitForEvent("calciteSheetBeforeClose"); + const sheetClose = page.waitForEvent("calciteSheetClose"); + const scrim = await page.find(`calcite-sheet >>> .${CSS.scrim}`); + await scrim.click(); + await page.waitForChanges(); + + await sheetBeforeClose; + await sheetClose; + + expect(beforeOpenSpy).toHaveReceivedEventTimes(1); + expect(openSpy).toHaveReceivedEventTimes(1); + expect(beforeCloseSpy).toHaveReceivedEventTimes(1); + expect(closeSpy).toHaveReceivedEventTimes(1); + + expect(await sheet.isVisible()).toBe(false); + }); + it("emits when set to open on initial render", async () => { const page = await newProgrammaticE2EPage(); diff --git a/packages/calcite-components/src/components/sheet/sheet.tsx b/packages/calcite-components/src/components/sheet/sheet.tsx index b4f755ce8c7..23c261a0a79 100644 --- a/packages/calcite-components/src/components/sheet/sheet.tsx +++ b/packages/calcite-components/src/components/sheet/sheet.tsx @@ -89,12 +89,11 @@ export class Sheet implements OpenCloseComponent, FocusTrapComponent, LoadableCo @Prop({ mutable: true, reflect: true }) open = false; @Watch("open") - async toggleSheet(value: boolean): Promise { + toggleSheet(value: boolean): void { if (this.ignoreOpenChange) { return; } - onToggleOpenCloseComponent(this); if (value) { this.openSheet(); } else { @@ -102,6 +101,11 @@ export class Sheet implements OpenCloseComponent, FocusTrapComponent, LoadableCo } } + @Watch("opened") + handleOpenedChange(): void { + onToggleOpenCloseComponent(this); + } + /** * We use an internal property to handle styles for when a modal is actually opened, not just when the open attribute is applied. This is a property because we need to apply styles to the host element and to keep the styles present while beforeClose is . * @@ -138,7 +142,6 @@ export class Sheet implements OpenCloseComponent, FocusTrapComponent, LoadableCo setUpLoadableComponent(this); // when sheet initially renders, if active was set we need to open as watcher doesn't fire if (this.open) { - onToggleOpenCloseComponent(this); requestAnimationFrame(() => this.openSheet()); } } @@ -224,7 +227,7 @@ export class Sheet implements OpenCloseComponent, FocusTrapComponent, LoadableCo @Listen("keydown", { target: "window" }) handleEscape(event: KeyboardEvent): void { if (this.open && !this.escapeDisabled && event.key === "Escape" && !event.defaultPrevented) { - this.closeSheet(); + this.open = false; event.preventDefault(); } } @@ -306,20 +309,13 @@ export class Sheet implements OpenCloseComponent, FocusTrapComponent, LoadableCo }; private openSheet(): void { - if (this.ignoreOpenChange) { - return; - } - this.el.addEventListener("calciteSheetOpen", this.openEnd); - this.open = true; this.opened = true; if (!this.slottedInShell) { this.initialOverflowCSS = document.documentElement.style.overflow; // use an inline style instead of a utility class to avoid global class declarations. document.documentElement.style.setProperty("overflow", "hidden"); } - - this.ignoreOpenChange = false; } private handleOutsideClose = (): void => { @@ -327,14 +323,10 @@ export class Sheet implements OpenCloseComponent, FocusTrapComponent, LoadableCo return; } - this.closeSheet(); + this.open = false; }; private closeSheet = async (): Promise => { - if (this.ignoreOpenChange) { - return; - } - if (this.beforeClose) { try { await this.beforeClose(this.el); @@ -349,11 +341,8 @@ export class Sheet implements OpenCloseComponent, FocusTrapComponent, LoadableCo } } - this.ignoreOpenChange = true; - this.open = false; this.opened = false; this.removeOverflowHiddenClass(); - this.ignoreOpenChange = false; }; private removeOverflowHiddenClass(): void { diff --git a/packages/calcite-components/src/components/time-picker/resources.ts b/packages/calcite-components/src/components/time-picker/resources.ts index 32834f0f5eb..1e9987a6dfa 100644 --- a/packages/calcite-components/src/components/time-picker/resources.ts +++ b/packages/calcite-components/src/components/time-picker/resources.ts @@ -19,6 +19,7 @@ export const CSS = { fractionalSecond: "fractionalSecond", hour: "hour", input: "input", + inputFocus: "inputFocus", meridiem: "meridiem", minute: "minute", second: "second", diff --git a/packages/calcite-components/src/components/time-picker/time-picker.e2e.ts b/packages/calcite-components/src/components/time-picker/time-picker.e2e.ts index 8b531d5ef46..88d14bd71d4 100644 --- a/packages/calcite-components/src/components/time-picker/time-picker.e2e.ts +++ b/packages/calcite-components/src/components/time-picker/time-picker.e2e.ts @@ -2,7 +2,7 @@ import { newE2EPage } from "@stencil/core/testing"; import { accessible, defaults, focusable, hidden, renders, t9n } from "../../tests/commonTests"; import { formatTimePart } from "../../utils/time"; import { CSS } from "./resources"; -import { getElementXY } from "../../tests/utils"; +import { getElementXY, getFocusedElementProp } from "../../tests/utils"; const letterKeys = [ "a", @@ -59,6 +59,106 @@ describe("calcite-time-picker", () => { ]); }); + describe("focusing", () => { + it("should focus input when corresponding nudge up button is clicked", async () => { + const page = await newE2EPage(); + await page.setContent(``); + + const minuteElAriaLabel = (await page.find(`calcite-time-picker >>> .${CSS.minute}`)).getAttribute("aria-label"); + const minuteUpEl = await page.find(`calcite-time-picker >>> .${CSS.buttonMinuteUp}`); + + await minuteUpEl.click(); + await page.waitForChanges(); + + expect(await getFocusedElementProp(page, "ariaLabel", { shadow: true })).toEqual(minuteElAriaLabel); + + const secondElAriaLabel = (await page.find(`calcite-time-picker >>> .${CSS.second}`)).getAttribute("aria-label"); + const secondUpEl = await page.find(`calcite-time-picker >>> .${CSS.buttonSecondUp}`); + + await secondUpEl.click(); + await page.waitForChanges(); + + expect(await getFocusedElementProp(page, "ariaLabel", { shadow: true })).toEqual(secondElAriaLabel); + + const fractionalSecondElAriaLabel = ( + await page.find(`calcite-time-picker >>> .${CSS.fractionalSecond}`) + ).getAttribute("aria-label"); + const fractionalSecondUpEl = await page.find(`calcite-time-picker >>> .${CSS.buttonFractionalSecondUp}`); + + await fractionalSecondUpEl.click(); + await page.waitForChanges(); + + expect(await getFocusedElementProp(page, "ariaLabel", { shadow: true })).toEqual(fractionalSecondElAriaLabel); + + const meridiemElAriaLabel = (await page.find(`calcite-time-picker >>> .${CSS.meridiem}`)).getAttribute( + "aria-label" + ); + const meridiemUpEl = await page.find(`calcite-time-picker >>> .${CSS.buttonMeridiemUp}`); + + await meridiemUpEl.click(); + await page.waitForChanges(); + + expect(await getFocusedElementProp(page, "ariaLabel", { shadow: true })).toEqual(meridiemElAriaLabel); + + const hourElAriaLabel = (await page.find(`calcite-time-picker >>> .${CSS.hour}`)).getAttribute("aria-label"); + const hourUpEl = await page.find(`calcite-time-picker >>> .${CSS.buttonHourUp}`); + + await hourUpEl.click(); + await page.waitForChanges(); + + expect(await getFocusedElementProp(page, "ariaLabel", { shadow: true })).toEqual(hourElAriaLabel); + }); + + it("should focus input when corresponding nudge down button is clicked", async () => { + const page = await newE2EPage(); + await page.setContent(``); + + const minuteElAriaLabel = (await page.find(`calcite-time-picker >>> .${CSS.minute}`)).getAttribute("aria-label"); + const minuteDownEl = await page.find(`calcite-time-picker >>> .${CSS.buttonMinuteDown}`); + + await minuteDownEl.click(); + await page.waitForChanges(); + + expect(await getFocusedElementProp(page, "ariaLabel", { shadow: true })).toEqual(minuteElAriaLabel); + + const secondElAriaLabel = (await page.find(`calcite-time-picker >>> .${CSS.second}`)).getAttribute("aria-label"); + const secondDownEl = await page.find(`calcite-time-picker >>> .${CSS.buttonSecondDown}`); + + await secondDownEl.click(); + await page.waitForChanges(); + + expect(await getFocusedElementProp(page, "ariaLabel", { shadow: true })).toEqual(secondElAriaLabel); + + const fractionalSecondElAriaLabel = ( + await page.find(`calcite-time-picker >>> .${CSS.fractionalSecond}`) + ).getAttribute("aria-label"); + const fractionalSecondDownEl = await page.find(`calcite-time-picker >>> .${CSS.buttonFractionalSecondDown}`); + + await fractionalSecondDownEl.click(); + await page.waitForChanges(); + + expect(await getFocusedElementProp(page, "ariaLabel", { shadow: true })).toEqual(fractionalSecondElAriaLabel); + + const meridiemElAriaLabel = (await page.find(`calcite-time-picker >>> .${CSS.meridiem}`)).getAttribute( + "aria-label" + ); + const meridiemDownEl = await page.find(`calcite-time-picker >>> .${CSS.buttonMeridiemDown}`); + + await meridiemDownEl.click(); + await page.waitForChanges(); + + expect(await getFocusedElementProp(page, "ariaLabel", { shadow: true })).toEqual(meridiemElAriaLabel); + + const hourElAriaLabel = (await page.find(`calcite-time-picker >>> .${CSS.hour}`)).getAttribute("aria-label"); + const hourDownEl = await page.find(`calcite-time-picker >>> .${CSS.buttonHourDown}`); + + await hourDownEl.click(); + await page.waitForChanges(); + + expect(await getFocusedElementProp(page, "ariaLabel", { shadow: true })).toEqual(hourElAriaLabel); + }); + }); + describe("should focus the first focusable element when setFocus is called (ltr)", () => { focusable(`calcite-time-picker`, { shadowFocusTargetSelector: `.${CSS.input}.${CSS.hour}`, diff --git a/packages/calcite-components/src/components/time-picker/time-picker.scss b/packages/calcite-components/src/components/time-picker/time-picker.scss index 141ab342389..ca3cccf73e5 100644 --- a/packages/calcite-components/src/components/time-picker/time-picker.scss +++ b/packages/calcite-components/src/components/time-picker/time-picker.scss @@ -69,9 +69,12 @@ &:focus, &:hover:focus { @apply outline-none; + outline-offset: 0; + } + &.inputFocus, + &:hover.inputFocus { box-shadow: inset 0 0 0 2px var(--calcite-ui-brand); z-index: theme("zIndex.header"); - outline-offset: 0; } } diff --git a/packages/calcite-components/src/components/time-picker/time-picker.tsx b/packages/calcite-components/src/components/time-picker/time-picker.tsx index e245d5197af..2dd13d15d6f 100644 --- a/packages/calcite-components/src/components/time-picker/time-picker.tsx +++ b/packages/calcite-components/src/components/time-picker/time-picker.tsx @@ -129,8 +129,6 @@ export class TimePicker @Element() el: HTMLCalciteTimePickerElement; - private activeEl: HTMLSpanElement; - private fractionalSecondEl: HTMLSpanElement; private hourEl: HTMLSpanElement; @@ -141,6 +139,8 @@ export class TimePicker private minuteEl: HTMLSpanElement; + private pointerActivated = false; + private secondEl: HTMLSpanElement; // -------------------------------------------------------------------------- @@ -149,6 +149,8 @@ export class TimePicker // // -------------------------------------------------------------------------- + @State() activeEl: HTMLSpanElement; + @State() effectiveLocale = ""; @Watch("effectiveLocale") @@ -220,7 +222,9 @@ export class TimePicker //-------------------------------------------------------------------------- @Listen("blur") - hostBlurHandler(): void { + blurHandler(): void { + this.activeEl = undefined; + this.pointerActivated = false; this.calciteInternalTimePickerBlur.emit(); } @@ -231,6 +235,7 @@ export class TimePicker @Listen("keydown") keyDownHandler(event: KeyboardEvent): void { + this.pointerActivated = false; const { defaultPrevented, key } = event; if (defaultPrevented) { @@ -309,6 +314,11 @@ export class TimePicker } } + @Listen("pointerdown") + pointerDownHandler(): void { + this.pointerActivated = true; + } + //-------------------------------------------------------------------------- // // Public Methods @@ -337,10 +347,6 @@ export class TimePicker this[`${target || "hour"}El`]?.focus(); } - private decrementFractionalSecond = (): void => { - this.nudgeFractionalSecond("down"); - }; - private decrementHour = (): void => { const newHour = !this.hour ? 0 : this.hour === "00" ? 23 : parseInt(this.hour) - 1; this.setValuePart("hour", newHour); @@ -371,6 +377,9 @@ export class TimePicker }; private focusHandler = (event: FocusEvent): void => { + if (this.pointerActivated) { + return; + } this.activeEl = event.currentTarget as HTMLSpanElement; }; @@ -414,6 +423,24 @@ export class TimePicker } }; + private fractionalSecondDownClickHandler = (): void => { + this.activeEl = this.fractionalSecondEl; + this.fractionalSecondEl.focus(); + this.nudgeFractionalSecond("down"); + }; + + private fractionalSecondUpClickHandler = (): void => { + this.activeEl = this.fractionalSecondEl; + this.fractionalSecondEl.focus(); + this.nudgeFractionalSecond("up"); + }; + + private hourDownClickHandler = (): void => { + this.activeEl = this.hourEl; + this.hourEl.focus(); + this.decrementHour(); + }; + private hourKeyDownHandler = (event: KeyboardEvent): void => { const { key } = event; if (numberKeys.includes(key)) { @@ -462,8 +489,10 @@ export class TimePicker } }; - private incrementFractionalSecond = (): void => { - this.nudgeFractionalSecond("up"); + private hourUpClickHandler = (): void => { + this.activeEl = this.hourEl; + this.hourEl.focus(); + this.incrementHour(); }; private incrementMeridiem = (): void => { @@ -497,6 +526,16 @@ export class TimePicker this.incrementMinuteOrSecond("second"); }; + private inputClickHandler = (event: MouseEvent): void => { + this.activeEl = event.target as HTMLSpanElement; + }; + + private meridiemUpClickHandler = (): void => { + this.activeEl = this.meridiemEl; + this.meridiemEl.focus(); + this.incrementMeridiem(); + }; + private meridiemKeyDownHandler = (event: KeyboardEvent): void => { switch (event.key) { case "a": @@ -523,6 +562,24 @@ export class TimePicker } }; + private meridiemDownClickHandler = (): void => { + this.activeEl = this.meridiemEl; + this.meridiemEl.focus(); + this.decrementMeridiem(); + }; + + private minuteDownClickHandler = (): void => { + this.activeEl = this.minuteEl; + this.minuteEl.focus(); + this.decrementMinute(); + }; + + private minuteUpClickHandler = (): void => { + this.activeEl = this.minuteEl; + this.minuteEl.focus(); + this.incrementMinute(); + }; + private minuteKeyDownHandler = (event: KeyboardEvent): void => { const { key } = event; if (numberKeys.includes(key)) { @@ -644,6 +701,18 @@ export class TimePicker } }; + private secondDownClickHandler = (): void => { + this.activeEl = this.secondEl; + this.secondEl.focus(); + this.decrementSecond(); + }; + + private secondUpClickHandler = (): void => { + this.activeEl = this.secondEl; + this.secondEl.focus(); + this.incrementSecond(); + }; + private setHourEl = (el: HTMLSpanElement) => (this.hourEl = el); private setMeridiemEl = (el: HTMLSpanElement) => (this.meridiemEl = el); @@ -890,7 +959,7 @@ export class TimePicker [CSS.buttonHourUp]: true, [CSS.buttonTopLeft]: true, }} - onClick={this.incrementHour} + onClick={this.hourUpClickHandler} role="button" > @@ -904,7 +973,9 @@ export class TimePicker class={{ [CSS.input]: true, [CSS.hour]: true, + [CSS.inputFocus]: this.activeEl && this.activeEl === this.hourEl, }} + onClick={this.inputClickHandler} onFocus={this.focusHandler} onKeyDown={this.hourKeyDownHandler} role="spinbutton" @@ -921,7 +992,7 @@ export class TimePicker [CSS.buttonHourDown]: true, [CSS.buttonBottomLeft]: true, }} - onClick={this.decrementHour} + onClick={this.hourDownClickHandler} role="button" > @@ -935,7 +1006,7 @@ export class TimePicker [CSS.button]: true, [CSS.buttonMinuteUp]: true, }} - onClick={this.incrementMinute} + onClick={this.minuteUpClickHandler} role="button" > @@ -949,7 +1020,9 @@ export class TimePicker class={{ [CSS.input]: true, [CSS.minute]: true, + [CSS.inputFocus]: this.activeEl && this.activeEl === this.minuteEl, }} + onClick={this.inputClickHandler} onFocus={this.focusHandler} onKeyDown={this.minuteKeyDownHandler} role="spinbutton" @@ -965,7 +1038,7 @@ export class TimePicker [CSS.button]: true, [CSS.buttonMinuteDown]: true, }} - onClick={this.decrementMinute} + onClick={this.minuteDownClickHandler} role="button" > @@ -980,7 +1053,7 @@ export class TimePicker [CSS.button]: true, [CSS.buttonSecondUp]: true, }} - onClick={this.incrementSecond} + onClick={this.secondUpClickHandler} role="button" > @@ -994,7 +1067,9 @@ export class TimePicker class={{ [CSS.input]: true, [CSS.second]: true, + [CSS.inputFocus]: this.activeEl && this.activeEl === this.secondEl, }} + onClick={this.inputClickHandler} onFocus={this.focusHandler} onKeyDown={this.secondKeyDownHandler} role="spinbutton" @@ -1010,7 +1085,7 @@ export class TimePicker [CSS.button]: true, [CSS.buttonSecondDown]: true, }} - onClick={this.decrementSecond} + onClick={this.secondDownClickHandler} role="button" > @@ -1028,7 +1103,7 @@ export class TimePicker [CSS.button]: true, [CSS.buttonFractionalSecondUp]: true, }} - onClick={this.incrementFractionalSecond} + onClick={this.fractionalSecondUpClickHandler} role="button" > @@ -1042,7 +1117,9 @@ export class TimePicker class={{ [CSS.input]: true, [CSS.fractionalSecond]: true, + [CSS.inputFocus]: this.activeEl && this.activeEl === this.fractionalSecondEl, }} + onClick={this.inputClickHandler} onFocus={this.focusHandler} onKeyDown={this.fractionalSecondKeyDownHandler} role="spinbutton" @@ -1058,7 +1135,7 @@ export class TimePicker [CSS.button]: true, [CSS.buttonFractionalSecondDown]: true, }} - onClick={this.decrementFractionalSecond} + onClick={this.fractionalSecondDownClickHandler} role="button" > @@ -1083,7 +1160,7 @@ export class TimePicker [CSS.buttonMeridiemUp]: true, [CSS.buttonTopRight]: true, }} - onClick={this.incrementMeridiem} + onClick={this.meridiemUpClickHandler} role="button" > @@ -1097,7 +1174,9 @@ export class TimePicker class={{ [CSS.input]: true, [CSS.meridiem]: true, + [CSS.inputFocus]: this.activeEl && this.activeEl === this.meridiemEl, }} + onClick={this.inputClickHandler} onFocus={this.focusHandler} onKeyDown={this.meridiemKeyDownHandler} role="spinbutton" @@ -1114,7 +1193,7 @@ export class TimePicker [CSS.buttonMeridiemDown]: true, [CSS.buttonBottomRight]: true, }} - onClick={this.decrementMeridiem} + onClick={this.meridiemDownClickHandler} role="button" > diff --git a/packages/calcite-components/src/demos/modal.html b/packages/calcite-components/src/demos/modal.html index 48f48a382d9..16fa4782b92 100644 --- a/packages/calcite-components/src/demos/modal.html +++ b/packages/calcite-components/src/demos/modal.html @@ -1092,6 +1092,22 @@

Test custom sizes

Custom width and height preview + + +
+ +

Test rejected beforeClose

+
test
+ Cancel +
+ + beforeClose rejected + +
@@ -1100,6 +1116,11 @@

Test custom sizes

const heightInput = document.querySelector("#css-modal-height-adjuster"); const widthInput = document.querySelector("#css-modal-width-adjuster"); const optionsSegmentedControl = document.querySelector("#css-modal-options-adjuster"); + const beforeCloseRejected = document.getElementById("js-modal-before-close"); + + beforeCloseRejected.beforeClose = () => { + return new Promise((_resolve, reject) => setTimeout(reject, 300)); + }; heightInput.addEventListener("calciteInputInput", (event) => { customSizeModal.style.setProperty("--calcite-modal-height", event.target.value); diff --git a/packages/calcite-components/src/utils/locale.ts b/packages/calcite-components/src/utils/locale.ts index e500c83708f..3e624caff05 100644 --- a/packages/calcite-components/src/utils/locale.ts +++ b/packages/calcite-components/src/utils/locale.ts @@ -136,6 +136,8 @@ const isNumberingSystemSupported = (numberingSystem: string): numberingSystem is const browserNumberingSystem = new Intl.NumberFormat().resolvedOptions().numberingSystem; +// for consistent browser behavior, we normalize numberingSystem to prevent the browser-inferred value +// see https://github.com/Esri/calcite-design-system/issues/3079#issuecomment-1168964195 for more info export const defaultNumberingSystem = browserNumberingSystem === "arab" || !isNumberingSystemSupported(browserNumberingSystem) ? "latn" diff --git a/packages/calcite-components/src/utils/openCloseComponent.ts b/packages/calcite-components/src/utils/openCloseComponent.ts index cc10277457e..67d10f3a139 100644 --- a/packages/calcite-components/src/utils/openCloseComponent.ts +++ b/packages/calcite-components/src/utils/openCloseComponent.ts @@ -14,6 +14,11 @@ export interface OpenCloseComponent { */ open?: boolean; + /** + * When true, the component is open. + */ + opened?: boolean; + /** * Specifies the name of transitionProp. */ @@ -55,22 +60,26 @@ const componentToTransitionListeners = new WeakMap< [HTMLDivElement, typeof transitionStart, typeof transitionEnd] >(); -function transitionStart(event: TransitionEvent): void { +function transitionStart(this: OpenCloseComponent, event: TransitionEvent): void { if (event.propertyName === this.openTransitionProp && event.target === this.transitionEl) { - this.open ? this.onBeforeOpen() : this.onBeforeClose(); + isOpen(this) ? this.onBeforeOpen() : this.onBeforeClose(); } } -function transitionEnd(event: TransitionEvent): void { +function transitionEnd(this: OpenCloseComponent, event: TransitionEvent): void { if (event.propertyName === this.openTransitionProp && event.target === this.transitionEl) { - this.open ? this.onOpen() : this.onClose(); + isOpen(this) ? this.onOpen() : this.onClose(); } } +function isOpen(component: OpenCloseComponent): boolean { + return "opened" in component ? component.opened : component.open; +} + function emitImmediately(component: OpenCloseComponent, nonOpenCloseComponent = false): void { - (nonOpenCloseComponent ? component[component.transitionProp] : component.open) + (nonOpenCloseComponent ? component[component.transitionProp] : isOpen(component)) ? component.onBeforeOpen() : component.onBeforeClose(); - (nonOpenCloseComponent ? component[component.transitionProp] : component.open) + (nonOpenCloseComponent ? component[component.transitionProp] : isOpen(component)) ? component.onOpen() : component.onClose(); } @@ -131,7 +140,7 @@ export function onToggleOpenCloseComponent(component: OpenCloseComponent, nonOpe if (event.propertyName === component.openTransitionProp && event.target === component.transitionEl) { clearTimeout(fallbackTimeoutId); component.transitionEl.removeEventListener("transitionstart", onStart); - (nonOpenCloseComponent ? component[component.transitionProp] : component.open) + (nonOpenCloseComponent ? component[component.transitionProp] : isOpen(component)) ? component.onBeforeOpen() : component.onBeforeClose(); } @@ -139,7 +148,7 @@ export function onToggleOpenCloseComponent(component: OpenCloseComponent, nonOpe function onEndOrCancel(event: TransitionEvent): void { if (event.propertyName === component.openTransitionProp && event.target === component.transitionEl) { - (nonOpenCloseComponent ? component[component.transitionProp] : component.open) + (nonOpenCloseComponent ? component[component.transitionProp] : isOpen(component)) ? component.onOpen() : component.onClose();