From 52fdd43b0d9033a7d1e42d1cb23cc933092580d0 Mon Sep 17 00:00:00 2001 From: Ben Elan Date: Thu, 22 Jun 2023 12:18:17 -0700 Subject: [PATCH 1/5] build: ignore node_modules and build outputs when watching for changes during stencil tests (#7209) ## Summary Currently our `npm run test:watch` script watches files in `node_modules`, `dist`, and other build outputs, which doesn't make sense unless you're monkey patching stuff. I've been getting an error due to watching too many files since we transitioned to a monorepo: ```log [ ERROR ] runJest: Error: EMFILE: too many open files, watch '/app/packages/calcite-components' npm ERR! Lifecycle script `test` failed with error: npm ERR! Error: command failed npm ERR! in workspace: @esri/calcite-components@1.5.0-next.4 npm ERR! at location: /app/packages/calcite-components ``` Ignoring `node_modules` and the build outputs resolved the issue for me (ref: https://github.com/jestjs/jest/issues/8088#issuecomment-473101715). If anyone still experiences this error, installing watchman is another option (ref: https://github.com/jestjs/jest/issues/3436#issuecomment-299894570). [Jest uses watchman by default](https://jestjs.io/docs/configuration#watchman-boolean) if it is installed. --- packages/calcite-components/stencil.config.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/calcite-components/stencil.config.ts b/packages/calcite-components/stencil.config.ts index c9ae5734c90..0442b7d992c 100644 --- a/packages/calcite-components/stencil.config.ts +++ b/packages/calcite-components/stencil.config.ts @@ -127,6 +127,7 @@ export const create: () => Config = () => ({ }) ], testing: { + watchPathIgnorePatterns: ["/../../node_modules", "/dist", "/www", "/hydrate"], moduleNameMapper: { "^/assets/(.*)$": "/src/tests/iconPathDataStub.ts", "^lodash-es$": "lodash" From cbf399b48a3f6dbf083fd23642d9b9e666bfbe53 Mon Sep 17 00:00:00 2001 From: Calcite Admin Date: Thu, 22 Jun 2023 15:17:31 -0700 Subject: [PATCH 2/5] build: update browserslist db (#7192) This PR was automatically generated by the update-browserslist-db GitHub action Co-authored-by: jcfranco Co-authored-by: JC Franco --- package-lock.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/package-lock.json b/package-lock.json index d021bf3dfad..3f81e992f68 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15282,9 +15282,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001498", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001498.tgz", - "integrity": "sha512-LFInN2zAwx3ANrGCDZ5AKKJroHqNKyjXitdV5zRIVIaQlXKj3GmxUKagoKsjqUfckpAObPCEWnk5EeMlyMWcgw==", + "version": "1.0.30001504", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001504.tgz", + "integrity": "sha512-5uo7eoOp2mKbWyfMXnGO9rJWOGU8duvzEiYITW+wivukL7yHH4gX9yuRaobu6El4jPxo6jKZfG+N6fB621GD/Q==", "dev": true, "funding": [ { @@ -55345,9 +55345,9 @@ } }, "caniuse-lite": { - "version": "1.0.30001498", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001498.tgz", - "integrity": "sha512-LFInN2zAwx3ANrGCDZ5AKKJroHqNKyjXitdV5zRIVIaQlXKj3GmxUKagoKsjqUfckpAObPCEWnk5EeMlyMWcgw==", + "version": "1.0.30001504", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001504.tgz", + "integrity": "sha512-5uo7eoOp2mKbWyfMXnGO9rJWOGU8duvzEiYITW+wivukL7yHH4gX9yuRaobu6El4jPxo6jKZfG+N6fB621GD/Q==", "dev": true }, "capture-exit": { From 1b02dae4ef4e9594ece0a72bb8bc69fd2f7cf84a Mon Sep 17 00:00:00 2001 From: Matt Driscoll Date: Fri, 23 Jun 2023 13:32:41 -0700 Subject: [PATCH 3/5] fix(combobox, dropdown, input-date-picker, input-time-picker, popover, tooltip): Prevent repositioning from affecting other floating components (#7178) **Related Issue:** #7158 ## Summary This prevent repositioning from affecting other floating components caused by using a shared, debounced reposition function. Additionally, this will cancel any in-progress debounced calls if the component is disconnected. --------- Co-authored-by: JC Franco --- .../input-time-picker/input-time-picker.tsx | 2 +- .../src/utils/floating-ui.spec.ts | 33 +- .../src/utils/floating-ui.ts | 302 +++++++++--------- 3 files changed, 188 insertions(+), 149 deletions(-) diff --git a/packages/calcite-components/src/components/input-time-picker/input-time-picker.tsx b/packages/calcite-components/src/components/input-time-picker/input-time-picker.tsx index 20a1eea3017..1f4e8565ae3 100644 --- a/packages/calcite-components/src/components/input-time-picker/input-time-picker.tsx +++ b/packages/calcite-components/src/components/input-time-picker/input-time-picker.tsx @@ -581,7 +581,7 @@ export class InputTimePicker private getExtendedLocaleConfig( locale: string - ): Parameters[1] | undefined { + ): Parameters<(typeof dayjs)["updateLocale"]>[1] | undefined { if (locale === "ar") { return { meridiem: (hour) => (hour > 12 ? "م" : "ص"), diff --git a/packages/calcite-components/src/utils/floating-ui.spec.ts b/packages/calcite-components/src/utils/floating-ui.spec.ts index 3d3e2e08da1..1eb894da7bc 100644 --- a/packages/calcite-components/src/utils/floating-ui.spec.ts +++ b/packages/calcite-components/src/utils/floating-ui.spec.ts @@ -1,18 +1,20 @@ import { waitForAnimationFrame } from "../tests/utils"; -import { +import * as floatingUI from "./floating-ui"; +import { FloatingUIComponent } from "./floating-ui"; + +const { cleanupMap, connectFloatingUI, defaultOffsetDistance, disconnectFloatingUI, effectivePlacements, filterComputedPlacements, - FloatingUIComponent, getEffectivePlacement, placements, positionFloatingUI, reposition, repositionDebounceTimeout -} from "./floating-ui"; +} = floatingUI; import * as floatingUIDOM from "@floating-ui/dom"; @@ -56,8 +58,8 @@ describe("repositioning", () => { let referenceEl: HTMLButtonElement; let positionOptions: Parameters[1]; - beforeEach(() => { - fakeFloatingUiComponent = { + function createFakeFloatingUiComponent(): FloatingUIComponent { + return { open: false, reposition: async () => { /* noop */ @@ -65,6 +67,10 @@ describe("repositioning", () => { overlayPositioning: "absolute", placement: "auto" }; + } + + beforeEach(() => { + fakeFloatingUiComponent = createFakeFloatingUiComponent(); floatingEl = document.createElement("div"); referenceEl = document.createElement("button"); @@ -155,6 +161,23 @@ describe("repositioning", () => { expect(floatingEl.style.position).toBe("fixed"); }); }); + + it("debounces positioning per instance", async () => { + const positionSpy = jest.spyOn(floatingUI, "positionFloatingUI"); + fakeFloatingUiComponent.open = true; + + const anotherFakeFloatingUiComponent = createFakeFloatingUiComponent(); + anotherFakeFloatingUiComponent.open = true; + + floatingUI.reposition(fakeFloatingUiComponent, positionOptions, true); + expect(positionSpy).toHaveBeenCalledTimes(1); + + floatingUI.reposition(anotherFakeFloatingUiComponent, positionOptions, true); + expect(positionSpy).toHaveBeenCalledTimes(2); + + await new Promise((resolve) => setTimeout(resolve, repositionDebounceTimeout)); + expect(positionSpy).toHaveBeenCalledTimes(2); + }); }); it("should have correct value for defaultOffsetDistance", () => { diff --git a/packages/calcite-components/src/utils/floating-ui.ts b/packages/calcite-components/src/utils/floating-ui.ts index 9a66171a50e..c3ee1ee8cbf 100644 --- a/packages/calcite-components/src/utils/floating-ui.ts +++ b/packages/calcite-components/src/utils/floating-ui.ts @@ -15,7 +15,7 @@ import { VirtualElement } from "@floating-ui/dom"; import { Build } from "@stencil/core"; -import { debounce } from "lodash-es"; +import { debounce, DebouncedFunc } from "lodash-es"; import { config } from "./config"; import { getElementDir } from "./dom"; import { Layout } from "../components/interfaces"; @@ -23,7 +23,7 @@ import { getUserAgentData, getUserAgentString } from "./browser"; const floatingUIBrowserCheck = patchFloatingUiForNonChromiumBrowsers(); -export function isChrome109OrAbove(): boolean { +function isChrome109OrAbove(): boolean { const uaData = getUserAgentData(); if (uaData?.brands) { @@ -53,6 +53,138 @@ async function patchFloatingUiForNonChromiumBrowsers(): Promise { } } +/** + * Positions the floating element relative to the reference element. + * + * **Note:** exported for testing purposes only + * + * @param root0 + * @param root0.referenceEl + * @param root0.floatingEl + * @param root0.overlayPositioning + * @param root0.placement + * @param root0.flipDisabled + * @param root0.flipPlacements + * @param root0.offsetDistance + * @param root0.offsetSkidding + * @param root0.arrowEl + * @param root0.type + * @param component + * @param root0.referenceEl.referenceEl + * @param root0.referenceEl.floatingEl + * @param root0.referenceEl.overlayPositioning + * @param root0.referenceEl.placement + * @param root0.referenceEl.flipDisabled + * @param root0.referenceEl.flipPlacements + * @param root0.referenceEl.offsetDistance + * @param root0.referenceEl.offsetSkidding + * @param root0.referenceEl.arrowEl + * @param root0.referenceEl.type + * @param component.referenceEl + * @param component.floatingEl + * @param component.overlayPositioning + * @param component.placement + * @param component.flipDisabled + * @param component.flipPlacements + * @param component.offsetDistance + * @param component.offsetSkidding + * @param component.arrowEl + * @param component.type + */ +export const positionFloatingUI = + /* we export arrow function to allow us to spy on it during testing */ + async ( + component: FloatingUIComponent, + { + referenceEl, + floatingEl, + overlayPositioning = "absolute", + placement, + flipDisabled, + flipPlacements, + offsetDistance, + offsetSkidding, + arrowEl, + type + }: { + referenceEl: ReferenceElement; + floatingEl: HTMLElement; + overlayPositioning: Strategy; + placement: LogicalPlacement; + flipDisabled?: boolean; + flipPlacements?: EffectivePlacement[]; + offsetDistance?: number; + offsetSkidding?: number; + arrowEl?: SVGElement; + type: UIType; + } + ): Promise => { + if (!referenceEl || !floatingEl) { + return null; + } + + await floatingUIBrowserCheck; + + const { + x, + y, + placement: effectivePlacement, + strategy: position, + middlewareData + } = await computePosition(referenceEl, floatingEl, { + strategy: overlayPositioning, + placement: + placement === "auto" || placement === "auto-start" || placement === "auto-end" + ? undefined + : getEffectivePlacement(floatingEl, placement), + middleware: getMiddleware({ + placement, + flipDisabled, + flipPlacements, + offsetDistance, + offsetSkidding, + arrowEl, + type + }) + }); + + if (arrowEl && middlewareData.arrow) { + const { x, y } = middlewareData.arrow; + const side = effectivePlacement.split("-")[0] as Side; + const alignment = x != null ? "left" : "top"; + const transform = ARROW_CSS_TRANSFORM[side]; + const reset = { left: "", top: "", bottom: "", right: "" }; + + if ("floatingLayout" in component) { + component.floatingLayout = side === "left" || side === "right" ? "horizontal" : "vertical"; + } + + Object.assign(arrowEl.style, { + ...reset, + [alignment]: `${alignment == "left" ? x : y}px`, + [side]: "100%", + transform + }); + } + + const referenceHidden = middlewareData.hide?.referenceHidden; + const visibility = referenceHidden ? "hidden" : null; + const pointerEvents = visibility ? "none" : null; + + floatingEl.setAttribute(placementDataAttribute, effectivePlacement); + + const transform = `translate(${Math.round(x)}px,${Math.round(y)}px)`; + + Object.assign(floatingEl.style, { + visibility, + pointerEvents, + position, + top: "0", + left: "0", + transform + }); + }; + /** * Exported for testing purposes only */ @@ -321,151 +453,35 @@ export async function reposition( return; } - return delayed ? debouncedReposition(component, options) : positionFloatingUI(component, options); + const positionFunction = delayed ? getDebouncedReposition(component) : positionFloatingUI; + + return positionFunction(component, options); } -const debouncedReposition = debounce(positionFloatingUI, repositionDebounceTimeout, { - leading: true, - maxWait: repositionDebounceTimeout -}); +function getDebouncedReposition(component: FloatingUIComponent): DebouncedFunc { + let debounced = componentToDebouncedRepositionMap.get(component); -const ARROW_CSS_TRANSFORM = { - top: "", - left: "rotate(-90deg)", - bottom: "rotate(180deg)", - right: "rotate(90deg)" -}; - -/** - * Positions the floating element relative to the reference element. - * - * **Note:** exported for testing purposes only - * - * @param root0 - * @param root0.referenceEl - * @param root0.floatingEl - * @param root0.overlayPositioning - * @param root0.placement - * @param root0.flipDisabled - * @param root0.flipPlacements - * @param root0.offsetDistance - * @param root0.offsetSkidding - * @param root0.arrowEl - * @param root0.type - * @param component - * @param root0.referenceEl.referenceEl - * @param root0.referenceEl.floatingEl - * @param root0.referenceEl.overlayPositioning - * @param root0.referenceEl.placement - * @param root0.referenceEl.flipDisabled - * @param root0.referenceEl.flipPlacements - * @param root0.referenceEl.offsetDistance - * @param root0.referenceEl.offsetSkidding - * @param root0.referenceEl.arrowEl - * @param root0.referenceEl.type - * @param component.referenceEl - * @param component.floatingEl - * @param component.overlayPositioning - * @param component.placement - * @param component.flipDisabled - * @param component.flipPlacements - * @param component.offsetDistance - * @param component.offsetSkidding - * @param component.arrowEl - * @param component.type - */ -export async function positionFloatingUI( - component: FloatingUIComponent, - { - referenceEl, - floatingEl, - overlayPositioning = "absolute", - placement, - flipDisabled, - flipPlacements, - offsetDistance, - offsetSkidding, - arrowEl, - type - }: { - referenceEl: ReferenceElement; - floatingEl: HTMLElement; - overlayPositioning: Strategy; - placement: LogicalPlacement; - flipDisabled?: boolean; - flipPlacements?: EffectivePlacement[]; - offsetDistance?: number; - offsetSkidding?: number; - arrowEl?: SVGElement; - type: UIType; - } -): Promise { - if (!referenceEl || !floatingEl) { - return null; + if (debounced) { + return debounced; } - await floatingUIBrowserCheck; - - const { - x, - y, - placement: effectivePlacement, - strategy: position, - middlewareData - } = await computePosition(referenceEl, floatingEl, { - strategy: overlayPositioning, - placement: - placement === "auto" || placement === "auto-start" || placement === "auto-end" - ? undefined - : getEffectivePlacement(floatingEl, placement), - middleware: getMiddleware({ - placement, - flipDisabled, - flipPlacements, - offsetDistance, - offsetSkidding, - arrowEl, - type - }) + debounced = debounce(positionFloatingUI, repositionDebounceTimeout, { + leading: true, + maxWait: repositionDebounceTimeout }); - if (arrowEl && middlewareData.arrow) { - const { x, y } = middlewareData.arrow; - const side = effectivePlacement.split("-")[0] as Side; - const alignment = x != null ? "left" : "top"; - const transform = ARROW_CSS_TRANSFORM[side]; - const reset = { left: "", top: "", bottom: "", right: "" }; - - if ("floatingLayout" in component) { - component.floatingLayout = side === "left" || side === "right" ? "horizontal" : "vertical"; - } - - Object.assign(arrowEl.style, { - ...reset, - [alignment]: `${alignment == "left" ? x : y}px`, - [side]: "100%", - transform - }); - } - - const referenceHidden = middlewareData.hide?.referenceHidden; - const visibility = referenceHidden ? "hidden" : null; - const pointerEvents = visibility ? "none" : null; + componentToDebouncedRepositionMap.set(component, debounced); - floatingEl.setAttribute(placementDataAttribute, effectivePlacement); - - const transform = `translate(${Math.round(x)}px,${Math.round(y)}px)`; - - Object.assign(floatingEl.style, { - visibility, - pointerEvents, - position, - top: "0", - left: "0", - transform - }); + return debounced; } +const ARROW_CSS_TRANSFORM = { + top: "", + left: "rotate(-90deg)", + bottom: "rotate(180deg)", + right: "rotate(90deg)" +}; + /** * Exported for testing purposes only * @@ -473,6 +489,8 @@ export async function positionFloatingUI( */ export const cleanupMap = new WeakMap void>(); +const componentToDebouncedRepositionMap = new WeakMap>(); + /** * Helper to set up floating element interactions on connectedCallback. * @@ -532,13 +550,11 @@ export function disconnectFloatingUI( return; } - const cleanup = cleanupMap.get(component); - - if (cleanup) { - cleanup(); - } - + cleanupMap.get(component)?.(); cleanupMap.delete(component); + + componentToDebouncedRepositionMap.get(component)?.cancel(); + componentToDebouncedRepositionMap.delete(component); } const visiblePointerSize = 4; From 3ec84a636c09bb56793da831eaa5df9d3e9e94c5 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 23 Jun 2023 21:01:13 +0000 Subject: [PATCH 4/5] chore: release next --- package-lock.json | 8 ++++---- packages/calcite-components-react/CHANGELOG.md | 4 ++++ packages/calcite-components-react/package.json | 4 ++-- packages/calcite-components/CHANGELOG.md | 6 ++++++ packages/calcite-components/package.json | 2 +- 5 files changed, 17 insertions(+), 7 deletions(-) diff --git a/package-lock.json b/package-lock.json index 3f81e992f68..a45d5c422e4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -43876,7 +43876,7 @@ }, "packages/calcite-components": { "name": "@esri/calcite-components", - "version": "1.5.0-next.4", + "version": "1.5.0-next.5", "license": "SEE LICENSE.md", "dependencies": { "@floating-ui/dom": "1.4.1", @@ -43902,10 +43902,10 @@ }, "packages/calcite-components-react": { "name": "@esri/calcite-components-react", - "version": "1.5.0-next.4", + "version": "1.5.0-next.5", "license": "SEE LICENSE.md", "dependencies": { - "@esri/calcite-components": "^1.5.0-next.4" + "@esri/calcite-components": "^1.5.0-next.5" }, "peerDependencies": { "react": ">=16.7", @@ -45740,7 +45740,7 @@ "@esri/calcite-components-react": { "version": "file:packages/calcite-components-react", "requires": { - "@esri/calcite-components": "^1.5.0-next.4" + "@esri/calcite-components": "^1.5.0-next.5" } }, "@esri/calcite-design-tokens": { diff --git a/packages/calcite-components-react/CHANGELOG.md b/packages/calcite-components-react/CHANGELOG.md index b4ddd84a182..9f750d00395 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.5.0-next.5](https://github.com/Esri/calcite-components/compare/@esri/calcite-components-react@1.5.0-next.4...@esri/calcite-components-react@1.5.0-next.5) (2023-06-23) + +**Note:** Version bump only for package @esri/calcite-components-react + ## [1.5.0-next.4](https://github.com/Esri/calcite-components/compare/@esri/calcite-components-react@1.5.0-next.3...@esri/calcite-components-react@1.5.0-next.4) (2023-06-21) **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 17e0281590b..74b6e567b78 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.4", + "version": "1.5.0-next.5", "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.4" + "@esri/calcite-components": "^1.5.0-next.5" }, "peerDependencies": { "react": ">=16.7", diff --git a/packages/calcite-components/CHANGELOG.md b/packages/calcite-components/CHANGELOG.md index 62991258714..32e410a4509 100644 --- a/packages/calcite-components/CHANGELOG.md +++ b/packages/calcite-components/CHANGELOG.md @@ -3,6 +3,12 @@ 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.5](https://github.com/Esri/calcite-components/compare/@esri/calcite-components@1.5.0-next.4...@esri/calcite-components@1.5.0-next.5) (2023-06-23) + +### Bug Fixes + +- **combobox, dropdown, input-date-picker, input-time-picker, popover, tooltip:** Prevent repositioning from affecting other floating components ([#7178](https://github.com/Esri/calcite-components/issues/7178)) ([1b02dae](https://github.com/Esri/calcite-components/commit/1b02dae4ef4e9594ece0a72bb8bc69fd2f7cf84a)), closes [#7158](https://github.com/Esri/calcite-components/issues/7158) + ## [1.5.0-next.4](https://github.com/Esri/calcite-components/compare/@esri/calcite-components@1.5.0-next.3...@esri/calcite-components@1.5.0-next.4) (2023-06-21) ### Bug Fixes diff --git a/packages/calcite-components/package.json b/packages/calcite-components/package.json index a98a8a2688a..d943c59b66c 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.4", + "version": "1.5.0-next.5", "description": "Web Components for Esri's Calcite Design System.", "main": "dist/index.cjs.js", "module": "dist/index.js", From 11d93b2fdee5a03dd41db9056af2e5fcf83d10b1 Mon Sep 17 00:00:00 2001 From: Ben Elan Date: Sun, 25 Jun 2023 14:52:32 -0700 Subject: [PATCH 5/5] build(t9n): generate json file containing t9n values (#7214) **Related Issue:** #6143 ## Summary Generates `dist/extras/translations-json.json` which contains the t9n values per component in the following format: ```jsonc { "components": { "calcite-action": { "loading": { "en": "Close", "es": "Cerrar" // ... }, "disabled": { "en": "Close", "es": "Cerrar" // ... } }, "calcite-alert": { "loading": { "en": "Close", "es": "Cerrar" // ... }, "disabled": { "en": "Close", "es": "Cerrar" // ... } } } } ``` --- packages/calcite-components/package.json | 3 +- .../support/generateT9nDocsJSON.ts | 46 +++++++++++++++++++ 2 files changed, 48 insertions(+), 1 deletion(-) create mode 100755 packages/calcite-components/support/generateT9nDocsJSON.ts diff --git a/packages/calcite-components/package.json b/packages/calcite-components/package.json index d943c59b66c..fa9e6e03015 100644 --- a/packages/calcite-components/package.json +++ b/packages/calcite-components/package.json @@ -19,7 +19,7 @@ ], "scripts": { "build": "npm run util:prep-build-reqs && stencil build", - "postbuild": "npm run util:patch && git restore src/components/*/readme.md", + "postbuild": "npm run util:patch && npm run util:generate-t9n-docs-json && git restore src/components/*/readme.md", "build:watch": "npm run util:prep-build-reqs && stencil build --no-docs --watch", "build:watch-dev": "npm run util:prep-build-reqs && stencil build --no-docs --dev --watch", "build-storybook": "npm run util:build-docs && build-storybook --output-dir ./docs --quiet", @@ -43,6 +43,7 @@ "util:clean-tested-build": "npm ci && npm test && npm run build", "util:copy-assets": "npm run util:copy-icons", "util:copy-icons": "cpy \"../../node_modules/@esri/calcite-ui-icons/js/*.json\" \"./src/components/icon/assets/icon/\" --flat", + "util:generate-t9n-docs-json": "ts-node --esm support/generateT9nDocsJSON.ts", "util:generate-t9n-types": "ts-node --esm support/generateT9nTypes.ts", "util:hydration-styles": "ts-node --esm support/hydrationStyles.ts", "util:patch": "npm run util:patch-esm-resolution && npm run util:patch-tree-shaking", diff --git a/packages/calcite-components/support/generateT9nDocsJSON.ts b/packages/calcite-components/support/generateT9nDocsJSON.ts new file mode 100755 index 00000000000..2be3d3dcc30 --- /dev/null +++ b/packages/calcite-components/support/generateT9nDocsJSON.ts @@ -0,0 +1,46 @@ +// generates a JSON file containing the per component t9n translation values +(async () => { + const { dirname, resolve } = await import("path"); + const { fileURLToPath } = await import("url"); + const { + existsSync, + promises: { readFile, readdir, writeFile } + } = await import("fs"); + try { + const __dirname = dirname(fileURLToPath(import.meta.url)); + + const outfile = resolve(__dirname, "..", "dist", "extras", "translations-json.json"); + const assetsPaths = resolve(__dirname, "..", "dist", "calcite", "assets"); + const components = await readdir(assetsPaths); + + const data = {}; + const messagesFilenameRegex = /messages_(.*)\.json/; + + for (const component of components) { + const t9nPath = resolve(assetsPaths, component, "t9n"); + if (existsSync(t9nPath)) { + data[component] = {}; + const messagesFileMain = JSON.parse(await readFile(resolve(t9nPath, "messages.json"), { encoding: "utf-8" })); + Object.keys(messagesFileMain).forEach((key) => (data[component][key] = {})); + + const messagesFilenames = (await readdir(t9nPath, { withFileTypes: true })).map((dirent) => dirent.name); + for (const messagesFilename of messagesFilenames) { + const messagesFilenameMatch = messagesFilename.match(messagesFilenameRegex); + + if (messagesFilenameMatch && messagesFilenameMatch.length > 1) { + const lang = messagesFilenameMatch[1]; + const messagesFile = JSON.parse(await readFile(resolve(t9nPath, messagesFilename), { encoding: "utf-8" })); + + for (const [key, value] of Object.entries(messagesFile)) { + data[component][key][lang] = value; + } + } + } + } + } + await writeFile(outfile, JSON.stringify(data), "utf-8"); + } catch (err) { + console.error(err); + process.exit(1); + } +})();