diff --git a/.github/workflows/check-links.yml b/.github/workflows/check-links.yml index 7235e5a7a7..c242263eeb 100644 --- a/.github/workflows/check-links.yml +++ b/.github/workflows/check-links.yml @@ -13,4 +13,4 @@ jobs: - name: Link Checker id: lychee - uses: lycheeverse/lychee-action@v2.0.2 \ No newline at end of file + uses: lycheeverse/lychee-action@v2.0.2 diff --git a/.github/workflows/create-issue.yml b/.github/workflows/create-issue.yml index e83680b354..9a0556cb09 100644 --- a/.github/workflows/create-issue.yml +++ b/.github/workflows/create-issue.yml @@ -23,9 +23,6 @@ jobs: run: cd scripts && npm install - name: Run Autorespond Script - run: - node scripts/new_issue-message.js + run: node scripts/new_issue-message.js env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - diff --git a/.github/workflows/inactive-issues.yml b/.github/workflows/inactive-issues.yml index 8848f2c70a..2a7d1c8bb5 100644 --- a/.github/workflows/inactive-issues.yml +++ b/.github/workflows/inactive-issues.yml @@ -16,5 +16,5 @@ jobs: stale-issue-label: "stale" stale-issue-message: "This issue is stale because it has been open for 15 days with no activity. If there are no further updates or modifications within the next 15 days, it will be automatically closed." close-issue-message: "This issue has been closed as it has been inactive for 15 days since being marked as stale." - exempt-issue-labels: 'non-closable' - repo-token: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file + exempt-issue-labels: "non-closable" + repo-token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100644 index 0000000000..d0a778429a --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1 @@ +npx lint-staged \ No newline at end of file diff --git a/.lycheeignore b/.lycheeignore index 90a84c29b1..6e06c8b2cb 100644 --- a/.lycheeignore +++ b/.lycheeignore @@ -1,2 +1 @@ -http://localhost -%25PUBLIC_URL%25 \ No newline at end of file +http://localhost \ No newline at end of file diff --git a/apps/website/.eslintrc.json b/apps/website/.eslintrc.json deleted file mode 100644 index bffb357a71..0000000000 --- a/apps/website/.eslintrc.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "extends": "next/core-web-vitals" -} diff --git a/apps/website/eslint.config.js b/apps/website/eslint.config.js new file mode 100644 index 0000000000..5a35fc9e65 --- /dev/null +++ b/apps/website/eslint.config.js @@ -0,0 +1,9 @@ +import nextConfig from "@dxc-technology/eslint-config/next.js"; +import { fileURLToPath } from "url"; +import { dirname } from "path"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +/** @type {import("eslint").Config[]} */ +export default [{ ignores: ["out/**", ".next/**", "eslint.config.js"] }, ...nextConfig({ tsconfigRootDir: __dirname })]; diff --git a/apps/website/global-styles.css b/apps/website/global-styles.css index 85ab1d5744..d32ec47d6c 100644 --- a/apps/website/global-styles.css +++ b/apps/website/global-styles.css @@ -12,12 +12,13 @@ /* TODO: Remove global styles completely from the website */ body { margin: 0; - font-family: Open Sans, sans-serif; + font-family: + Open Sans, + sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } code { - font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", - monospace; + font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", monospace; } diff --git a/apps/website/next.config.js b/apps/website/next.config.ts similarity index 70% rename from apps/website/next.config.js rename to apps/website/next.config.ts index 7fb6d7f8f9..185f74340f 100644 --- a/apps/website/next.config.js +++ b/apps/website/next.config.ts @@ -1,12 +1,15 @@ -/** @type {import('next').NextConfig} */ -module.exports = { +import type { NextConfig } from "next"; +import type { Configuration } from "webpack"; + +const nextConfig: NextConfig = { images: { loader: "custom", }, output: "export", trailingSlash: true, - webpack: (config) => { - config.module.rules.push({ + webpack: (config: Configuration): Configuration => { + config.module = config.module || { rules: [] }; + config.module.rules?.push({ test: /\.md$/, use: "raw-loader", }); @@ -30,3 +33,5 @@ module.exports = { "@cloudscape-design/theming-runtime", ], }; + +export default nextConfig; diff --git a/apps/website/package.json b/apps/website/package.json index cde948d549..e550380230 100644 --- a/apps/website/package.json +++ b/apps/website/package.json @@ -5,7 +5,7 @@ "dev": "next dev", "build": "next build", "start": "next start", - "lint": "next lint" + "lint": "eslint . --max-warnings 0" }, "dependencies": { "@cloudscape-design/components": "^3.0.706", @@ -33,8 +33,8 @@ "@types/react": "^18", "@types/react-color": "^3.0.6", "@types/react-dom": "^18", - "eslint": "^8", - "eslint-config-next": "14.2.4", + "eslint": "^9.36.0", + "eslint-config-next": "15.5.4", "typescript": "^5.6.3" } } diff --git a/apps/website/pages/_document.tsx b/apps/website/pages/_document.tsx index dc97779195..4ccbe68f39 100644 --- a/apps/website/pages/_document.tsx +++ b/apps/website/pages/_document.tsx @@ -1,25 +1,27 @@ -import Document, { Head, Html, Main, NextScript } from "next/document"; +import Document, { DocumentContext, DocumentInitialProps, Head, Html, Main, NextScript } from "next/document"; import createEmotionServer from "@emotion/server/create-instance"; -import React from "react"; import createCache from "@emotion/cache"; +import { Children } from "react"; export default class MyDocument extends Document { - static async getInitialProps(ctx: any) { + static async getInitialProps(ctx: DocumentContext): Promise { const originalRenderPage = ctx.renderPage; const cache = createCache({ key: "css", prepend: true }); - const { extractCriticalToChunks } = createEmotionServer(cache); + const emotionServer = createEmotionServer(cache); ctx.renderPage = () => originalRenderPage({ - enhanceApp: (App: any) => + enhanceApp: (App) => + // eslint-disable-next-line @typescript-eslint/no-explicit-any function EnhanceApp(props: any) { return ; }, }); const initialProps = await Document.getInitialProps(ctx); - const emotionStyles = extractCriticalToChunks(initialProps.html); + const emotionStyles = emotionServer.extractCriticalToChunks(initialProps.html); + const emotionStyleTags = emotionStyles.styles.map((style) => ( diff --git a/packages/lib/.storybook/preview.tsx b/packages/lib/.storybook/preview.tsx index 361952531d..0b1158eaf7 100644 --- a/packages/lib/.storybook/preview.tsx +++ b/packages/lib/.storybook/preview.tsx @@ -1,8 +1,8 @@ -import type { Preview } from "@storybook/react"; -import { disabledRules } from "../test/accessibility/rules/common/disabledRules"; +import disabledRules from "../test/accessibility/rules/common/disabledRules"; import "../src/styles/variables.css"; +import { PreviewExtended } from "./types"; -const preview: Preview = { +const preview: PreviewExtended = { parameters: { controls: { matchers: { diff --git a/packages/lib/.storybook/test-runner.ts b/packages/lib/.storybook/test-runner.ts index f82ec29ed7..1bfb228e56 100644 --- a/packages/lib/.storybook/test-runner.ts +++ b/packages/lib/.storybook/test-runner.ts @@ -1,6 +1,6 @@ import { injectAxe, checkA11y, configureAxe } from "axe-playwright"; import { getStoryContext, type TestRunnerConfig } from "@storybook/test-runner"; -import { ViewportParameters, ViewportStyles } from "./types"; +import { PreviewExtended, ViewportStyles } from "./types"; const DEFAULT_VIEWPORT_SIZE = { width: 1280, height: 720 }; @@ -12,38 +12,37 @@ const a11yConfig: TestRunnerConfig = { // Get the entire context of a story, including parameters, args, argTypes, etc. const storyContext = await getStoryContext(page, context); // Apply viewport handle support - const viewPortParams: ViewportParameters = storyContext.parameters?.viewport; + const parameters = storyContext.parameters as Partial | undefined; + const viewPortParams = parameters?.viewport; const defaultViewport = viewPortParams?.defaultViewport; - const viewport = defaultViewport && viewPortParams?.viewports[defaultViewport]?.styles; + const viewport = defaultViewport ? viewPortParams.viewports?.[defaultViewport]?.styles : undefined; + const parsedViewportSizes: ViewportStyles = viewport - ? Object.entries(viewport).reduce( - (acc, [screen, size]) => ({ - ...acc, - [screen]: parseInt(size), - }), - {} as ViewportStyles - ) + ? Object.entries(viewport).reduce((acc, [screen, size]) => { + const safeSize = typeof size === "string" ? parseInt(size, 10) : undefined; + if (safeSize) acc[screen as keyof ViewportStyles] = safeSize; + return acc; + }, {} as ViewportStyles) : DEFAULT_VIEWPORT_SIZE; - if (parsedViewportSizes && Object.keys(parsedViewportSizes)?.length !== 0) { - page.setViewportSize(parsedViewportSizes); + if (parsedViewportSizes && Object.keys(parsedViewportSizes).length) { + await page.setViewportSize(parsedViewportSizes); } } catch (err) { console.error("Problem when loading the Story Context -> ", err); } }, + async postVisit(page, context) { try { // Get the entire context of a story, including parameters, args, argTypes, etc. const storyContext = await getStoryContext(page, context); - // Do not run a11y tests on disabled stories. - if (storyContext.parameters?.a11y?.disable) { - return; - } + const parameters = storyContext.parameters as Partial | undefined; + + if (parameters?.a11y?.disable) return; - // Apply story-level a11y rules await configureAxe(page, { - rules: storyContext?.parameters?.a11y?.config?.rules, + rules: parameters?.a11y?.config?.rules, }); } catch (err) { console.error("Problem when loading the Story Context -> ", err); @@ -58,4 +57,4 @@ const a11yConfig: TestRunnerConfig = { }, }; -module.exports = a11yConfig; +export default a11yConfig; diff --git a/packages/lib/.storybook/types.ts b/packages/lib/.storybook/types.ts index c0d8daca49..f4910bbcf3 100644 --- a/packages/lib/.storybook/types.ts +++ b/packages/lib/.storybook/types.ts @@ -1,17 +1,47 @@ +import { Preview } from "@storybook/react"; + +export interface ViewportStyles { + height: number; + width: number; +} type Styles = ViewportStyles | ((s: ViewportStyles | undefined) => ViewportStyles) | null; interface Viewport { name: string; styles: Styles; type: "desktop" | "mobile" | "tablet" | "other"; } -export interface ViewportStyles { - height: number; - width: number; -} interface ViewportMap { [key: string]: Viewport; } -export interface ViewportParameters { +interface ViewportParameters { viewports: ViewportMap; defaultViewport: string; } + +interface A11yRule { + id: string; + enabled: boolean; +} + +interface A11yParameters { + disable?: boolean; + config: { + rules: A11yRule[]; + }; + options?: Record; +} + +interface StorybookParameters { + controls: { + matchers: { + color: RegExp; + date: RegExp; + }; + }; + a11y: A11yParameters; + viewport?: ViewportParameters; +} + +export interface PreviewExtended extends Omit { + parameters: StorybookParameters; +} diff --git a/packages/lib/babel.config.js b/packages/lib/babel.config.js deleted file mode 100644 index 600350bcb8..0000000000 --- a/packages/lib/babel.config.js +++ /dev/null @@ -1,26 +0,0 @@ -module.exports = { - presets: [ - "@babel/preset-env", - [ - "@babel/preset-react", - { - runtime: "automatic", - }, - ], - "@babel/preset-typescript", - ], - plugins: [ - "@babel/plugin-proposal-optional-chaining", - "@babel/plugin-proposal-nullish-coalescing-operator", - "@babel/plugin-transform-runtime", - [ - "@emotion", - { - sourceMap: true, - autoLabel: "dev-only", - labelFormat: "[local]", - }, - ], - ], - ignore: ["**/*.stories.*", "**/*.d.ts"], -}; diff --git a/packages/lib/babel.config.json b/packages/lib/babel.config.json new file mode 100644 index 0000000000..658689a58c --- /dev/null +++ b/packages/lib/babel.config.json @@ -0,0 +1,26 @@ +{ + "presets": [ + "@babel/preset-env", + [ + "@babel/preset-react", + { + "runtime": "automatic" + } + ], + "@babel/preset-typescript" + ], + "plugins": [ + "@babel/plugin-proposal-optional-chaining", + "@babel/plugin-proposal-nullish-coalescing-operator", + "@babel/plugin-transform-runtime", + [ + "@emotion", + { + "sourceMap": true, + "autoLabel": "dev-only", + "labelFormat": "[local]" + } + ] + ], + "ignore": ["**/*.stories.jsx", "**/*.stories.tsx", "**/*.d.ts"] +} diff --git a/packages/lib/esbuild-plugin-babel.d.ts b/packages/lib/esbuild-plugin-babel.d.ts new file mode 100644 index 0000000000..8c1ef11730 --- /dev/null +++ b/packages/lib/esbuild-plugin-babel.d.ts @@ -0,0 +1,12 @@ +declare module "esbuild-plugin-babel" { + import type { Plugin } from "esbuild"; + + interface BabelPluginOptions { + configFile?: string; + filter?: RegExp; + } + + function babel(_options?: BabelPluginOptions): Plugin; + + export default babel; +} diff --git a/packages/lib/eslint.config.js b/packages/lib/eslint.config.js new file mode 100644 index 0000000000..e7686661f3 --- /dev/null +++ b/packages/lib/eslint.config.js @@ -0,0 +1,12 @@ +import libraryConfig from "@dxc-technology/eslint-config/library.js"; +import { fileURLToPath } from "url"; +import { dirname } from "path"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +/** @type {import("eslint").Config[]} */ +export default [ + { ignores: ["dist/**", "coverage/**", "eslint.config.js"] }, + ...libraryConfig({ tsconfigRootDir: __dirname }), +]; diff --git a/packages/lib/jest.config.accessibility.js b/packages/lib/jest.config.accessibility.js deleted file mode 100644 index 4989792ff8..0000000000 --- a/packages/lib/jest.config.accessibility.js +++ /dev/null @@ -1,9 +0,0 @@ -module.exports = { - moduleNameMapper: { - "\\.(css|less|scss|sass)$": "identity-obj-proxy", - "\\.(svg)$": "/test/mocks/svgMock.js", - "\\.(png)$": "/test/mocks/pngMock.js", - }, - testMatch: ["**/?(*.)+(accessibility.)(spec|test).[jt]s?(x)"], - setupFilesAfterEnv: ["/setupJestAxe.js"], -}; diff --git a/packages/lib/jest.config.accessibility.ts b/packages/lib/jest.config.accessibility.ts new file mode 100644 index 0000000000..a43d48f096 --- /dev/null +++ b/packages/lib/jest.config.accessibility.ts @@ -0,0 +1,16 @@ +import type { Config } from "jest"; + +const configAccessibility: Config = { + moduleNameMapper: { + "\\.(css|less|scss|sass)$": "identity-obj-proxy", + "\\.(svg)$": "/test/mocks/svgMock.ts", + "\\.(png)$": "/test/mocks/pngMock.ts", + }, + testMatch: ["**/?(*.)+(accessibility.)(spec|test).[jt]s?(x)"], + setupFilesAfterEnv: ["/setupJestAxe.ts"], + transform: { + "^.+\\.[tj]sx?$": "babel-jest", + }, +}; + +export default configAccessibility; diff --git a/packages/lib/jest.config.js b/packages/lib/jest.config.ts similarity index 68% rename from packages/lib/jest.config.js rename to packages/lib/jest.config.ts index 9b9356956f..c84a9d09a5 100644 --- a/packages/lib/jest.config.js +++ b/packages/lib/jest.config.ts @@ -1,4 +1,6 @@ -module.exports = { +import type { Config } from "jest"; + +const config: Config = { collectCoverage: true, coveragePathIgnorePatterns: [ "utils.ts", @@ -7,11 +9,13 @@ module.exports = { ], moduleNameMapper: { "\\.(css|less|scss|sass)$": "identity-obj-proxy", - "\\.(svg)$": "/test/mocks/svgMock.js", - "\\.(png)$": "/test/mocks/pngMock.js", + "\\.(svg)$": "/test/mocks/svgMock.ts", + "\\.(png)$": "/test/mocks/pngMock.ts", }, testMatch: ["**/?(*.)+(spec|test).[jt]s?(x)", "!**/?(*.)+(accessibility.)(spec|test).[jt]s?(x)"], transform: { "^.+\\.[tj]sx?$": "babel-jest", }, }; + +export default config; diff --git a/packages/lib/package.json b/packages/lib/package.json index 5db49e4340..027239dfc2 100644 --- a/packages/lib/package.json +++ b/packages/lib/package.json @@ -24,9 +24,9 @@ "storybook": "storybook dev -p 6006", "storybook:accessibility": "test-storybook", "storybook:accessibility:ci": "test-storybook --maxWorkers=2", - "test": "jest --env=jsdom --config=./jest.config.js", - "test:accessibility": "jest --env=jsdom --config=./jest.config.accessibility.js", - "test:watch": "jest --env=jsdom --config=./jest.config.js --watch" + "test": "jest --env=jsdom --config=./jest.config.ts", + "test:accessibility": "jest --env=jsdom --config=./jest.config.accessibility.ts", + "test:watch": "jest --env=jsdom --config=./jest.config.ts --watch" }, "peerDependencies": { "@emotion/react": "^11.14.0", @@ -52,7 +52,6 @@ "@babel/preset-env": "^7.16.8", "@babel/preset-react": "^7.16.7", "@babel/preset-typescript": "^7.16.7", - "@chromatic-com/storybook": "^1.5.0", "@dxc-technology/eslint-config": "*", "@dxc-technology/typescript-config": "*", "@emotion/babel-plugin": "^11.13.5", @@ -62,14 +61,17 @@ "@storybook/addon-essentials": "^8.1.10", "@storybook/addon-interactions": "^8.1.10", "@storybook/addon-links": "^8.1.10", - "@storybook/addon-viewport": "^8.2.9", - "@storybook/blocks": "^8.1.10", - "@storybook/react": "^8.1.10", - "@storybook/react-vite": "^8.1.10", - "@storybook/test": "^8.1.10", + "@storybook/addon-viewport": "^8.3.2", + "@storybook/blocks": "^8.1.11", + "@storybook/react": "^8.1.11", + "@storybook/react-vite": "^8.1.11", + "@storybook/test": "^8.1.11", "@storybook/test-runner": "^0.22.0", + "@testing-library/jest-dom": "^6.4.6", "@testing-library/react": "^16.0.1", "@testing-library/user-event": "^13.0.0", + "@turbo/gen": "^1.12.4", + "@types/color": "^3.0.6", "@types/eslint": "^8.56.5", "@types/jest": "^29.5.12", "@types/jest-axe": "^3.5.9", @@ -80,15 +82,22 @@ "axe-playwright": "^2.1.0", "chromatic": "^11.5.4", "esbuild-plugin-babel": "^0.2.3", - "eslint": "^8.57.0", + "eslint": "^9.36.0", + "eslint-config-prettier": "^10.1.8", + "eslint-plugin-import": "^2.29.0", + "eslint-plugin-jest": "^29.0.1", + "eslint-plugin-jsx-a11y": "^6.8.0", + "eslint-plugin-prettier": "^5.1.3", + "eslint-plugin-react": "^7.34.1", + "eslint-plugin-react-hooks": "^5.2.0", + "eslint-plugin-security": "^3.0.0", "eslint-plugin-storybook": "^0.8.0", "identity-obj-proxy": "^3.0.0", "jest": "^29.7.0", "jest-axe": "^10.0.0", "jest-environment-jsdom": "^29.7.0", "playwright": "^1.44.1", - "storybook": "^8.1.10", - "storybook-addon-pseudo-states": "^3.1.1", + "storybook": "^8.1.11", "tsup": "^8.1.0", "typescript": "^5.6.3" } diff --git a/packages/lib/setupJestAxe.js b/packages/lib/setupJestAxe.ts similarity index 100% rename from packages/lib/setupJestAxe.js rename to packages/lib/setupJestAxe.ts diff --git a/packages/lib/src/accordion/Accordion.accessibility.test.tsx b/packages/lib/src/accordion/Accordion.accessibility.test.tsx index f5850fca55..cfac5753dc 100644 --- a/packages/lib/src/accordion/Accordion.accessibility.test.tsx +++ b/packages/lib/src/accordion/Accordion.accessibility.test.tsx @@ -40,7 +40,7 @@ describe("Accordion component accessibility tests", () => { expect(results).toHaveNoViolations(); }); - it("Should not have basic accessibility issues", async () => { + it("Should not have basic accessibility issues with badge and status light", async () => { const { container } = render( { expect(results).toHaveNoViolations(); }); - it("Should not have basic accessibility issues for disabled mode", async () => { + it("Should not have basic accessibility issues for disabled mode with badge and status light", async () => { const { container } = render( margin && typeof margin === "object" && margin.bottom ? spaces[margin.bottom] : ""}; margin-left: ${({ margin }) => (margin && typeof margin === "object" && margin.left ? spaces[margin.left] : "")}; - cursor: "pointer"; + cursor: pointer; // first accordion > div:first-of-type:not(:only-of-type) { @@ -57,22 +57,39 @@ const AccordionContainer = styled.div<{ } `; +const AccordionItemWithProvider = ({ + child, + index, + contextValue, +}: { + child: React.ReactElement; + index: number; + contextValue: Omit; +}) => { + const memoizedContext = useMemo( + () => ({ index, ...contextValue }), + [index, contextValue.activeIndex, contextValue.handlerActiveChange, contextValue.independent] + ); + + return {child}; +}; + const DxcAccordion = (props: AccordionPropsType): JSX.Element => { - const { children, margin, onActiveChange } = props; + const { children, defaultIndexActive, independent, indexActive, margin, onActiveChange } = props; const [innerIndexActive, setInnerIndexActive] = useState( - props.independent - ? (props.defaultIndexActive ?? -1) - : Array.isArray(props.defaultIndexActive) - ? props.defaultIndexActive.filter((i) => i !== undefined) + independent + ? (defaultIndexActive ?? -1) + : Array.isArray(defaultIndexActive) + ? defaultIndexActive.filter((i) => i !== undefined) : [] ); const handlerActiveChange = useCallback( (index: number | number[]) => { - if (props.indexActive == null) { + if (indexActive == null) { setInnerIndexActive((prev) => { - if (props.independent) return typeof index === "number" ? (index === prev ? -1 : index) : prev; + if (independent) return typeof index === "number" ? (index === prev ? -1 : index) : prev; else { const prevArray = Array.isArray(prev) ? prev : []; return Array.isArray(index) @@ -85,24 +102,27 @@ const DxcAccordion = (props: AccordionPropsType): JSX.Element => { } onActiveChange?.(index as number & number[]); }, - [props.indexActive, props.independent, onActiveChange, innerIndexActive] + [indexActive, independent, onActiveChange, innerIndexActive] ); const contextValue = useMemo( () => ({ - activeIndex: props.indexActive ?? innerIndexActive, + activeIndex: indexActive ?? innerIndexActive, handlerActiveChange, - independent: props.independent, + independent, }), - [props.indexActive, innerIndexActive, handlerActiveChange, props.independent] + [indexActive, innerIndexActive, handlerActiveChange, independent] ); return ( {Children.map(children, (accordion, index) => ( - - {accordion} - + ))} ); diff --git a/packages/lib/src/accordion/AccordionItem.tsx b/packages/lib/src/accordion/AccordionItem.tsx index ae49259981..51323530c1 100644 --- a/packages/lib/src/accordion/AccordionItem.tsx +++ b/packages/lib/src/accordion/AccordionItem.tsx @@ -4,7 +4,6 @@ import { AccordionItemProps } from "./types"; import DxcIcon from "../icon/Icon"; import DxcFlex from "../flex/Flex"; import DxcContainer from "../container/Container"; -import React from "react"; import AccordionContext from "./AccordionContext"; const AccordionContainer = styled.div` @@ -154,11 +153,13 @@ const AccordionItem = ({ }: AccordionItemProps): JSX.Element => { const id = useId(); const { activeIndex, handlerActiveChange, index, independent } = useContext(AccordionContext) ?? {}; - const isItemExpanded = useMemo(() => { - return independent - ? activeIndex === index - : Array.isArray(activeIndex) && index !== undefined && activeIndex.includes(index); - }, [independent, activeIndex, index]); + const isItemExpanded = useMemo( + () => + independent + ? activeIndex === index + : Array.isArray(activeIndex) && index !== undefined && activeIndex.includes(index), + [independent, activeIndex, index] + ); const handleAccordionState = () => { if (index !== undefined) handlerActiveChange?.(index); @@ -203,12 +204,12 @@ const AccordionItem = ({ )} {badge && badge?.position === "after" && !assistiveText && ( - {disabled ? React.cloneElement(badge.element as ReactElement, { color: "grey" }) : badge.element} + {disabled ? cloneElement(badge.element as ReactElement, { color: "grey" }) : badge.element} )} {badge?.position !== "after" && statusLight && !assistiveText && ( - {disabled ? React.cloneElement(statusLight as ReactElement, { mode: "default" }) : statusLight} + {disabled ? cloneElement(statusLight as ReactElement, { mode: "default" }) : statusLight} )} diff --git a/packages/lib/src/action-icon/ActionIcon.stories.tsx b/packages/lib/src/action-icon/ActionIcon.stories.tsx index be36839843..cdb8bdb4df 100644 --- a/packages/lib/src/action-icon/ActionIcon.stories.tsx +++ b/packages/lib/src/action-icon/ActionIcon.stories.tsx @@ -1,10 +1,10 @@ +import { Meta, StoryObj } from "@storybook/react"; +import { userEvent, within } from "@storybook/test"; import Title from "../../.storybook/components/Title"; import ExampleContainer from "../../.storybook/components/ExampleContainer"; import DxcActionIcon from "./ActionIcon"; -import { userEvent, within } from "@storybook/test"; import DxcTooltip from "../tooltip/Tooltip"; import DxcInset from "../inset/Inset"; -import { Meta, StoryObj } from "@storybook/react"; export default { title: "Action Icon ", diff --git a/packages/lib/src/action-icon/ActionIcon.tsx b/packages/lib/src/action-icon/ActionIcon.tsx index db8bc8184b..b866ce52cb 100644 --- a/packages/lib/src/action-icon/ActionIcon.tsx +++ b/packages/lib/src/action-icon/ActionIcon.tsx @@ -35,8 +35,8 @@ const ActionIcon = styled.button` } `; -export default forwardRef( - ({ disabled = false, icon, onClick, tabIndex, title }, ref) => ( +const ForwardedActionIcon = forwardRef( + ({ disabled = false, title, icon, onClick, tabIndex }, ref) => ( ( ) ); + +ForwardedActionIcon.displayName = "ActionIcon"; + +export default ForwardedActionIcon; diff --git a/packages/lib/src/action-icon/types.ts b/packages/lib/src/action-icon/types.ts index 08ac678cbe..c527174941 100644 --- a/packages/lib/src/action-icon/types.ts +++ b/packages/lib/src/action-icon/types.ts @@ -1,5 +1,5 @@ -import { SVG } from "../common/utils"; import { MouseEvent } from "react"; +import { SVG } from "../common/utils"; type Props = { /** diff --git a/packages/lib/src/alert/Actions.tsx b/packages/lib/src/alert/Actions.tsx index ab2bf621b3..65ef62cfb8 100644 --- a/packages/lib/src/alert/Actions.tsx +++ b/packages/lib/src/alert/Actions.tsx @@ -35,4 +35,6 @@ const Actions = memo( ) ); +Actions.displayName = "Actions"; + export default Actions; diff --git a/packages/lib/src/alert/Alert.accessibility.test.tsx b/packages/lib/src/alert/Alert.accessibility.test.tsx index 21c8713932..c20264cdf2 100644 --- a/packages/lib/src/alert/Alert.accessibility.test.tsx +++ b/packages/lib/src/alert/Alert.accessibility.test.tsx @@ -2,11 +2,11 @@ import { render } from "@testing-library/react"; import { axe } from "../../test/accessibility/axe-helper"; import DxcAlert from "./Alert"; -(global as any).ResizeObserver = class ResizeObserver { - observe() {} - unobserve() {} - disconnect() {} -}; +global.ResizeObserver = jest.fn().mockImplementation(() => ({ + observe: jest.fn(), + unobserve: jest.fn(), + disconnect: jest.fn(), +})); const messages = [ { text: "Message 1", onClose: () => {} }, @@ -27,7 +27,9 @@ describe("Alert component accessibility tests", () => { expect(results).toHaveNoViolations(); }); it("Should not have basic accessibility issues for modal mode", async () => { - const { container } = render( {} }} />); + const { container } = render( + {} }} /> + ); const results = await axe(container); expect(results).toHaveNoViolations(); }); diff --git a/packages/lib/src/alert/Alert.stories.tsx b/packages/lib/src/alert/Alert.stories.tsx index 0e582fc398..73d018151c 100644 --- a/packages/lib/src/alert/Alert.stories.tsx +++ b/packages/lib/src/alert/Alert.stories.tsx @@ -1,8 +1,8 @@ +import { Meta, StoryObj } from "@storybook/react"; import DxcAlert from "./Alert"; import Title from "../../.storybook/components/Title"; import ExampleContainer from "../../.storybook/components/ExampleContainer"; import DxcLink from "../link/Link"; -import { Meta, StoryObj } from "@storybook/react"; export default { title: "Alert", diff --git a/packages/lib/src/alert/Alert.test.tsx b/packages/lib/src/alert/Alert.test.tsx index 44b883c38c..a62c5430d4 100644 --- a/packages/lib/src/alert/Alert.test.tsx +++ b/packages/lib/src/alert/Alert.test.tsx @@ -2,11 +2,11 @@ import "@testing-library/jest-dom"; import { render, fireEvent } from "@testing-library/react"; import DxcAlert from "./Alert"; -(global as any).ResizeObserver = class ResizeObserver { - observe() {} - unobserve() {} - disconnect() {} -}; +global.ResizeObserver = jest.fn().mockImplementation(() => ({ + observe: jest.fn(), + unobserve: jest.fn(), + disconnect: jest.fn(), +})); const messages = [ { text: "Message 1", onClose: () => {} }, @@ -48,11 +48,11 @@ describe("Alert component tests", () => { test("Inline alert calls correctly the function onClose of several messages", () => { const onClose1 = jest.fn(); const onClose2 = jest.fn(); - const messages = [ + const onCloseMessages = [ { text: "Message 1", onClose: onClose1 }, { text: "Message 2", onClose: onClose2 }, ]; - const { getByRole } = render(); + const { getByRole } = render(); const closeButton = getByRole("button", { name: "Close message" }); const nextButton = getByRole("button", { name: "Next message" }); fireEvent.click(closeButton); @@ -70,7 +70,7 @@ describe("Alert component tests", () => { }); test("Alert with several messages closes properly each one", () => { const { getByRole, getByText } = render(); - let closeButton = getByRole("button", { name: "Close message" }); + const closeButton = getByRole("button", { name: "Close message" }); const nextButton = getByRole("button", { name: "Next message" }); expect(getByText("1 of 4")).toBeTruthy(); expect(getByText("Message 1")).toBeTruthy(); diff --git a/packages/lib/src/alert/Alert.tsx b/packages/lib/src/alert/Alert.tsx index a9321f97a1..b2609c3728 100644 --- a/packages/lib/src/alert/Alert.tsx +++ b/packages/lib/src/alert/Alert.tsx @@ -92,7 +92,7 @@ const getIcon = (semantic: AlertPropsType["semantic"]) => { } }; -export default function DxcAlert({ +const DxcAlert = ({ closable = true, message = [], mode = "inline", @@ -100,7 +100,7 @@ export default function DxcAlert({ secondaryAction, semantic = "info", title = "", -}: AlertPropsType) { +}: AlertPropsType) => { const [messages, setMessages] = useState(Array.isArray(message) ? message : [message]); const [currentIndex, setCurrentIndex] = useState(0); @@ -122,7 +122,9 @@ export default function DxcAlert({ }, [messages, currentIndex, mode]); useEffect(() => { - if (currentIndex === messages.length) handlePrevOnClick(); + if (currentIndex === messages.length) { + handlePrevOnClick(); + } }, [currentIndex, messages, handlePrevOnClick]); return ( @@ -200,4 +202,6 @@ export default function DxcAlert({ ); -} +}; + +export default DxcAlert; diff --git a/packages/lib/src/alert/ModalAlertWrapper.tsx b/packages/lib/src/alert/ModalAlertWrapper.tsx index 3e2f5f4668..5456dd03a0 100644 --- a/packages/lib/src/alert/ModalAlertWrapper.tsx +++ b/packages/lib/src/alert/ModalAlertWrapper.tsx @@ -1,10 +1,10 @@ import { createPortal } from "react-dom"; +import { useEffect } from "react"; import { Global, css } from "@emotion/react"; import styled from "@emotion/styled"; import { responsiveSizes } from "../common/variables"; import FocusLock from "../utils/FocusLock"; import { ModalAlertWrapperProps } from "./types"; -import { useEffect } from "react"; const BodyStyle = () => ( void; children: ReactNode; }; + +export default Props; diff --git a/packages/lib/src/badge/Badge.stories.tsx b/packages/lib/src/badge/Badge.stories.tsx index d06073abd3..a27a984443 100644 --- a/packages/lib/src/badge/Badge.stories.tsx +++ b/packages/lib/src/badge/Badge.stories.tsx @@ -1,11 +1,11 @@ +import { Meta, StoryObj } from "@storybook/react"; +import { userEvent, within } from "@storybook/test"; import DxcBadge from "./Badge"; import Title from "../../.storybook/components/Title"; import ExampleContainer from "../../.storybook/components/ExampleContainer"; import DxcFlex from "../flex/Flex"; import DxcInset from "../inset/Inset"; -import { userEvent, within } from "@storybook/test"; import DxcTooltip from "../tooltip/Tooltip"; -import { Meta, StoryObj } from "@storybook/react"; export default { title: "Badge", diff --git a/packages/lib/src/bleed/Bleed.stories.tsx b/packages/lib/src/bleed/Bleed.stories.tsx index e9cfa60ab8..6c5cc28a37 100644 --- a/packages/lib/src/bleed/Bleed.stories.tsx +++ b/packages/lib/src/bleed/Bleed.stories.tsx @@ -1,9 +1,9 @@ +import { Meta, StoryObj } from "@storybook/react"; +import { ReactNode } from "react"; import Title from "../../.storybook/components/Title"; import DxcBleed from "./Bleed"; import DxcFlex from "../flex/Flex"; -import { Meta, StoryObj } from "@storybook/react"; import DxcContainer from "../container/Container"; -import { ReactNode } from "react"; export default { title: "Bleed", diff --git a/packages/lib/src/breadcrumbs/Breadcrumbs.accessibility.test.tsx b/packages/lib/src/breadcrumbs/Breadcrumbs.accessibility.test.tsx index e10125df95..09af499cb4 100644 --- a/packages/lib/src/breadcrumbs/Breadcrumbs.accessibility.test.tsx +++ b/packages/lib/src/breadcrumbs/Breadcrumbs.accessibility.test.tsx @@ -1,15 +1,13 @@ import { render } from "@testing-library/react"; import { axe, formatRules } from "../../test/accessibility/axe-helper"; import DxcBreadcrumbs from "./Breadcrumbs"; -import { disabledRules as rules } from "../../test/accessibility/rules/specific/breadcrumbs/disabledRules"; +import rules from "../../test/accessibility/rules/specific/breadcrumbs/disabledRules"; -(global as any).ResizeObserver = class ResizeObserver { - observe() {} - - unobserve() {} - - disconnect() {} -}; +global.ResizeObserver = jest.fn().mockImplementation(() => ({ + observe: jest.fn(), + unobserve: jest.fn(), + disconnect: jest.fn(), +})); const disabledRules = { rules: formatRules(rules), diff --git a/packages/lib/src/breadcrumbs/Breadcrumbs.stories.tsx b/packages/lib/src/breadcrumbs/Breadcrumbs.stories.tsx index c515ebbf83..cc5db466a0 100644 --- a/packages/lib/src/breadcrumbs/Breadcrumbs.stories.tsx +++ b/packages/lib/src/breadcrumbs/Breadcrumbs.stories.tsx @@ -1,11 +1,11 @@ +import { Meta, StoryObj } from "@storybook/react"; +import { userEvent, within } from "@storybook/test"; import Title from "../../.storybook/components/Title"; import ExampleContainer from "../../.storybook/components/ExampleContainer"; import DxcBreadcrumbs from "./Breadcrumbs"; import DxcContainer from "../container/Container"; -import { userEvent, within } from "@storybook/test"; -import { disabledRules } from "../../test/accessibility/rules/specific/breadcrumbs/disabledRules"; +import disabledRules from "../../test/accessibility/rules/specific/breadcrumbs/disabledRules"; import preview from "../../.storybook/preview"; -import { Meta, StoryObj } from "@storybook/react"; export default { title: "Breadcrumbs", @@ -15,7 +15,7 @@ export default { config: { rules: [ ...disabledRules.map((ruleId) => ({ id: ruleId, enabled: false })), - ...preview?.parameters?.a11y?.config?.rules, + ...(preview?.parameters?.a11y?.config?.rules || []), ], }, }, @@ -159,6 +159,6 @@ export const Chromatic: Story = { play: async ({ canvasElement }) => { const canvas = within(canvasElement); const dropdowns = canvas.getAllByRole("button"); - dropdowns[2] != null && (await userEvent.click(dropdowns[2])); + if (dropdowns[2] != null) await userEvent.click(dropdowns[2]); }, }; diff --git a/packages/lib/src/breadcrumbs/Breadcrumbs.test.tsx b/packages/lib/src/breadcrumbs/Breadcrumbs.test.tsx index 4ed7419345..7d0860d4a7 100644 --- a/packages/lib/src/breadcrumbs/Breadcrumbs.test.tsx +++ b/packages/lib/src/breadcrumbs/Breadcrumbs.test.tsx @@ -1,12 +1,12 @@ import { render } from "@testing-library/react"; -import DxcBreadcrumbs from "./Breadcrumbs"; import userEvent from "@testing-library/user-event"; +import DxcBreadcrumbs from "./Breadcrumbs"; -(global as any).ResizeObserver = class ResizeObserver { - observe() {} - unobserve() {} - disconnect() {} -}; +global.ResizeObserver = jest.fn().mockImplementation(() => ({ + observe: jest.fn(), + unobserve: jest.fn(), + disconnect: jest.fn(), +})); const items = [ { @@ -34,16 +34,16 @@ describe("Breadcrumbs component tests", () => { expect(breadcrumbs.getAttribute("aria-label")).toBe("example"); expect(getByText("Dark Mode").parentElement?.getAttribute("aria-current")).toBe("page"); }); - test("Collapsed variant renders all the items inside the dropdown menu except the root and the current page", async () => { + test("Collapsed variant renders all the items inside the dropdown menu except the root and the current page", () => { const { queryByText, getByText, getByRole } = render(); const dropdown = getByRole("button"); expect(queryByText("User Menu")).toBeFalsy(); expect(queryByText("Preferences")).toBeFalsy(); - await userEvent.click(dropdown); + userEvent.click(dropdown); expect(getByText("User Menu")).toBeTruthy(); expect(getByText("Preferences")).toBeTruthy(); }); - test("Collapsed variant, with show root set to false, renders all the items inside the dropdown menu except the current page", async () => { + test("Collapsed variant, with show root set to false, renders all the items inside the dropdown menu except the current page", () => { const { queryByText, getByText, getByRole } = render( ); @@ -51,12 +51,12 @@ describe("Breadcrumbs component tests", () => { expect(queryByText("Home")).toBeFalsy(); expect(queryByText("User Menu")).toBeFalsy(); expect(queryByText("Preferences")).toBeFalsy(); - await userEvent.click(dropdown); + userEvent.click(dropdown); expect(getByText("Home")).toBeTruthy(); expect(getByText("User Menu")).toBeTruthy(); expect(getByText("Preferences")).toBeTruthy(); }); - test("If itemsBeforeCollapse value is below two, ignores it and renders a collapsed variant", async () => { + test("If itemsBeforeCollapse value is below two, ignores it and renders a collapsed variant", () => { const { getByText, getByRole } = render(); expect(getByText("Home")).toBeTruthy(); expect(getByRole("button")).toBeTruthy(); @@ -76,7 +76,7 @@ describe("Breadcrumbs component tests", () => { userEvent.click(getByText("Home")); expect(onItemClick).toHaveBeenCalledWith("/home"); }); - test("The onClick prop from an item is properly called (collapsed)", async () => { + test("The onClick prop from an item is properly called (collapsed)", () => { const onItemClick = jest.fn(); const { getByText, getByRole } = render( { itemsBeforeCollapse={2} /> ); - await userEvent.click(getByRole("button")); - await userEvent.click(getByText("Preferences")); + userEvent.click(getByRole("button")); + userEvent.click(getByText("Preferences")); expect(onItemClick).toHaveBeenCalledWith("/"); }); }); diff --git a/packages/lib/src/bulleted-list/BulletedList.stories.tsx b/packages/lib/src/bulleted-list/BulletedList.stories.tsx index e98d12caee..d3ea7c7e0b 100644 --- a/packages/lib/src/bulleted-list/BulletedList.stories.tsx +++ b/packages/lib/src/bulleted-list/BulletedList.stories.tsx @@ -1,8 +1,8 @@ +import { Meta, StoryObj } from "@storybook/react"; import styled from "@emotion/styled"; import Title from "../../.storybook/components/Title"; import ExampleContainer from "../../.storybook/components/ExampleContainer"; import DxcBulletedList from "./BulletedList"; -import { Meta, StoryObj } from "@storybook/react"; export default { title: "Bulleted List", diff --git a/packages/lib/src/bulleted-list/BulletedList.test.tsx b/packages/lib/src/bulleted-list/BulletedList.test.tsx index 0ac2c5fe7b..dbe805ea2c 100644 --- a/packages/lib/src/bulleted-list/BulletedList.test.tsx +++ b/packages/lib/src/bulleted-list/BulletedList.test.tsx @@ -1,6 +1,5 @@ import { render } from "@testing-library/react"; import DxcBulletedList from "./BulletedList"; -import DxcIcon from "../icon/Icon"; describe("Bulleted list component tests", () => { test("The component renders properly", () => { diff --git a/packages/lib/src/bulleted-list/BulletedList.tsx b/packages/lib/src/bulleted-list/BulletedList.tsx index ecd64b6845..b044ae37f9 100644 --- a/packages/lib/src/bulleted-list/BulletedList.tsx +++ b/packages/lib/src/bulleted-list/BulletedList.tsx @@ -1,4 +1,4 @@ -import { Children, useContext } from "react"; +import { Children } from "react"; import styled from "@emotion/styled"; import DxcFlex from "../flex/Flex"; import DxcTypography from "../typography/Typography"; diff --git a/packages/lib/src/bulleted-list/types.ts b/packages/lib/src/bulleted-list/types.ts index 280d444429..24ae1f1125 100644 --- a/packages/lib/src/bulleted-list/types.ts +++ b/packages/lib/src/bulleted-list/types.ts @@ -33,11 +33,11 @@ type OtherProps = { type Props = IconProps | OtherProps; -export default Props; - export type BulletedListItemPropsType = { /** * Text to be shown in the list. */ children?: ReactNode; }; + +export default Props; diff --git a/packages/lib/src/button/Button.stories.tsx b/packages/lib/src/button/Button.stories.tsx index a91e42c359..7f4e806a4b 100644 --- a/packages/lib/src/button/Button.stories.tsx +++ b/packages/lib/src/button/Button.stories.tsx @@ -1,11 +1,11 @@ +import { Meta, StoryObj } from "@storybook/react"; +import { userEvent, within } from "@storybook/test"; import DxcButton from "./Button"; import DxcFlex from "../flex/Flex"; import Title from "../../.storybook/components/Title"; import ExampleContainer from "../../.storybook/components/ExampleContainer"; import DxcInset from "../inset/Inset"; import DxcTooltip from "../tooltip/Tooltip"; -import { userEvent, within } from "@storybook/test"; -import { Meta, StoryObj } from "@storybook/react"; export default { title: "Button", diff --git a/packages/lib/src/button/Button.test.tsx b/packages/lib/src/button/Button.test.tsx index bb51167def..4e44478090 100644 --- a/packages/lib/src/button/Button.test.tsx +++ b/packages/lib/src/button/Button.test.tsx @@ -4,7 +4,9 @@ import DxcButton from "./Button"; describe("Button component tests", () => { test("Calls correct function on click", () => { const onClick = jest.fn(); - const { getByText } = render(); + const { getByText } = render( + + ); const button = getByText("Button"); fireEvent.click(button); expect(onClick).toHaveBeenCalled(); diff --git a/packages/lib/src/button/utils.ts b/packages/lib/src/button/utils.ts index 13a3b1ec0c..8d6374de3b 100644 --- a/packages/lib/src/button/utils.ts +++ b/packages/lib/src/button/utils.ts @@ -1,11 +1,7 @@ import { getMargin } from "../common/utils"; import ButtonPropsType, { Mode, Semantic, Size } from "./types"; -export const getButtonStyles = ( - mode: Mode, - semantic: Semantic | "unselected" | "selected", - size: Size, -) => { +export const getButtonStyles = (mode: Mode, semantic: Semantic | "unselected" | "selected", size: Size) => { let enabled = ""; let hover = ""; let active = ""; @@ -227,4 +223,4 @@ export const getHeight = (height: Size["height"]) => { default: return "var(--height-xl)"; } -}; \ No newline at end of file +}; diff --git a/packages/lib/src/card/Card.stories.tsx b/packages/lib/src/card/Card.stories.tsx index 5be6aa4e0b..76f516465d 100644 --- a/packages/lib/src/card/Card.stories.tsx +++ b/packages/lib/src/card/Card.stories.tsx @@ -1,8 +1,8 @@ +import { Meta, StoryObj } from "@storybook/react"; +import { userEvent, within } from "@storybook/test"; import Title from "../../.storybook/components/Title"; import ExampleContainer from "../../.storybook/components/ExampleContainer"; import DxcCard from "./Card"; -import { userEvent, within } from "@storybook/test"; -import { Meta, StoryObj } from "@storybook/react"; export default { title: "Card", @@ -159,7 +159,9 @@ export const ActionCardStates: Story = { const canvas = within(canvasElement); await userEvent.tab(); const card = canvas.getAllByText("Hovered default with action")[1]; - card != null && (await userEvent.hover(card)); + if (card != null) { + await userEvent.hover(card); + } }, }; @@ -168,7 +170,11 @@ export const Chromatic: Story = { play: async ({ canvasElement }) => { const canvas = within(canvasElement); const linkCards = canvas.getAllByRole("link"); - linkCards[1] != null && linkCards[1].focus(); - linkCards[2] != null && (await userEvent.hover(linkCards[2])); + if (linkCards[1] != null) { + linkCards[1].focus(); + } + if (linkCards[2] != null) { + await userEvent.hover(linkCards[2]); + } }, }; diff --git a/packages/lib/src/card/Card.tsx b/packages/lib/src/card/Card.tsx index 6de93bc718..1723c5b6c2 100644 --- a/packages/lib/src/card/Card.tsx +++ b/packages/lib/src/card/Card.tsx @@ -42,7 +42,10 @@ const CardContainer = styled.div<{ } `; -const TagImage = styled.img<{ imagePadding: CardPropsType["imagePadding"]; imageCover: CardPropsType["imageCover"] }>` +const TagImage = styled.img<{ + imagePadding: CardPropsType["imagePadding"]; + imageCover: CardPropsType["imageCover"]; +}>` height: ${({ imagePadding }) => !imagePadding ? "100%" @@ -102,7 +105,7 @@ const DxcCard = ({ href={linkHref ? linkHref : undefined} shadowDepth={!outlined ? 0 : isHovered && (onClick || linkHref) ? 2 : 1} > - + {imageSrc && ( diff --git a/packages/lib/src/checkbox/Checkbox.stories.tsx b/packages/lib/src/checkbox/Checkbox.stories.tsx index e32ac5831c..448e996608 100644 --- a/packages/lib/src/checkbox/Checkbox.stories.tsx +++ b/packages/lib/src/checkbox/Checkbox.stories.tsx @@ -1,8 +1,8 @@ +import { Meta, StoryObj } from "@storybook/react"; import styled from "@emotion/styled"; import ExampleContainer from "../../.storybook/components/ExampleContainer"; import Title from "../../.storybook/components/Title"; import DxcCheckbox from "./Checkbox"; -import { Meta, StoryObj } from "@storybook/react"; export default { title: "Checkbox", @@ -193,7 +193,7 @@ type Story = StoryObj; export const Chromatic: Story = { render: Checkbox, - play: async () => { + play: () => { document.getElementById("scroll-container")?.scrollTo({ top: 50 }); }, }; diff --git a/packages/lib/src/checkbox/Checkbox.test.tsx b/packages/lib/src/checkbox/Checkbox.test.tsx index a240718022..a0d9ca6dfd 100644 --- a/packages/lib/src/checkbox/Checkbox.test.tsx +++ b/packages/lib/src/checkbox/Checkbox.test.tsx @@ -35,10 +35,10 @@ describe("Checkbox component tests", () => { fireEvent.click(checkbox); expect(onChange).not.toHaveBeenCalled(); }); - test("Read-only checkbox sends its value on submit", async () => { - const handlerOnSubmit = jest.fn((e) => { + test("Read-only checkbox sends its value on submit", () => { + const handlerOnSubmit = jest.fn((e: React.FormEvent) => { e.preventDefault(); - const formData = new FormData(e.target); + const formData = new FormData(e.currentTarget); const formProps = Object.fromEntries(formData); expect(formProps).toStrictEqual({ data: "checked" }); }); @@ -49,7 +49,7 @@ describe("Checkbox component tests", () => { ); const submit = getByText("Submit"); - await userEvent.click(submit); + userEvent.click(submit); expect(handlerOnSubmit).toHaveBeenCalled(); }); test("Read-only checkbox doesn't change its value with Space key", () => { @@ -58,7 +58,12 @@ describe("Checkbox component tests", () => { const checkbox = getByRole("checkbox"); userEvent.tab(); expect(document.activeElement === checkbox).toBeTruthy(); - fireEvent.keyDown(checkbox, { key: " ", code: "Space", keyCode: 32, charCode: 32 }); + fireEvent.keyDown(checkbox, { + key: " ", + code: "Space", + keyCode: 32, + charCode: 32, + }); expect(onChange).not.toHaveBeenCalled(); }); test("Uncontrolled checkbox", () => { @@ -97,7 +102,7 @@ describe("Checkbox component tests", () => { expect(checkbox.getAttribute("aria-checked")).toBe("true"); expect(submitInput?.checked).toBe(true); }); - test("Test disable keyboard and mouse interactions", () => { + test("Disable keyboard and mouse interactions", () => { const onChange = jest.fn(); const { getByRole, getByText, container } = render( @@ -113,13 +118,18 @@ describe("Checkbox component tests", () => { userEvent.tab(); expect(document.activeElement === input).toBeFalsy(); }); - test("Test keyboard interactions", () => { + test("Keyboard interactions", () => { const onChange = jest.fn(); const { getByRole } = render(); const checkbox = getByRole("checkbox"); userEvent.tab(); expect(document.activeElement === checkbox).toBeTruthy(); - fireEvent.keyDown(checkbox, { key: " ", code: "Space", keyCode: 32, charCode: 32 }); + fireEvent.keyDown(checkbox, { + key: " ", + code: "Space", + keyCode: 32, + charCode: 32, + }); expect(onChange).toHaveBeenCalledWith(true); }); }); diff --git a/packages/lib/src/checkbox/Checkbox.tsx b/packages/lib/src/checkbox/Checkbox.tsx index f2285798e8..7f747f0463 100644 --- a/packages/lib/src/checkbox/Checkbox.tsx +++ b/packages/lib/src/checkbox/Checkbox.tsx @@ -160,4 +160,6 @@ const DxcCheckbox = forwardRef( } ); +DxcCheckbox.displayName = "DxcCheckbox"; + export default DxcCheckbox; diff --git a/packages/lib/src/chip/Chip.stories.tsx b/packages/lib/src/chip/Chip.stories.tsx index c9edd3e333..c3c2ba4f69 100644 --- a/packages/lib/src/chip/Chip.stories.tsx +++ b/packages/lib/src/chip/Chip.stories.tsx @@ -1,8 +1,8 @@ +import { Meta, StoryObj } from "@storybook/react"; import { userEvent } from "@storybook/test"; -import ExampleContainer from "../../.storybook/components/ExampleContainer"; import Title from "../../.storybook/components/Title"; +import ExampleContainer from "../../.storybook/components/ExampleContainer"; import DxcChip from "./Chip"; -import { Meta, StoryObj } from "@storybook/react"; export default { title: "Chip", diff --git a/packages/lib/src/container/Container.stories.tsx b/packages/lib/src/container/Container.stories.tsx index 8846f18be0..cceee3f85c 100644 --- a/packages/lib/src/container/Container.stories.tsx +++ b/packages/lib/src/container/Container.stories.tsx @@ -1,8 +1,8 @@ +import { Meta, StoryObj } from "@storybook/react"; import Title from "../../.storybook/components/Title"; import ExampleContainer from "../../.storybook/components/ExampleContainer"; import DxcContainer from "./Container"; import DxcTypography from "../typography/Typography"; -import { Meta, StoryObj } from "@storybook/react"; export default { title: "Container", @@ -26,7 +26,10 @@ const Listbox = ({ suggestions = [] }: { suggestions: string[] }): JSX.Element = width="250px" > {suggestions.map((suggestion, index) => ( - + ` ${({ border }) => { let styles = ""; if (border != null) { - switch (true) { - case "width" in border: - styles += border.width ? `border-width: ${border.width};` : ""; - case "style" in border: - styles += border.style ? `border-style: ${border.style};` : ""; - case "color" in border: - styles += border.color ? `border-color: ${border.color};` : ""; + if ("width" in border) { + styles += border.width ? `border-width: ${border.width};` : ""; + } + if ("style" in border) { + styles += border.style ? `border-style: ${border.style};` : ""; + } + if ("color" in border) { + styles += border.color ? `border-color: ${border.color};` : ""; } } return styles; @@ -49,15 +50,17 @@ const Container = styled.div` ${({ border }) => { let styles = ""; if (border != null) { - switch (true) { - case "top" in border: - styles += border.top ? getBorderStyles("top", border.top) : ""; - case "right" in border: - styles += border.right ? getBorderStyles("right", border.right) : ""; - case "left" in border: - styles += border.left ? getBorderStyles("left", border.left) : ""; - case "bottom" in border: - styles += border.bottom ? getBorderStyles("bottom", border.bottom) : ""; + if ("top" in border) { + styles += border.top ? getBorderStyles("top", border.top) : ""; + } + if ("right" in border) { + styles += border.right ? getBorderStyles("right", border.right) : ""; + } + if ("left" in border) { + styles += border.left ? getBorderStyles("left", border.left) : ""; + } + if ("bottom" in border) { + styles += border.bottom ? getBorderStyles("bottom", border.bottom) : ""; } } return styles; diff --git a/packages/lib/src/container/types.ts b/packages/lib/src/container/types.ts index 2346f54204..dc27f4b6fd 100644 --- a/packages/lib/src/container/types.ts +++ b/packages/lib/src/container/types.ts @@ -33,11 +33,11 @@ export type BorderProperties = { type Border = | BorderProperties | { - top?: BorderProperties; - right?: BorderProperties; - bottom?: BorderProperties; - left?: BorderProperties; - }; + top?: BorderProperties; + right?: BorderProperties; + bottom?: BorderProperties; + left?: BorderProperties; + }; type Outline = BorderProperties & { offset?: string; diff --git a/packages/lib/src/contextual-menu/ContextualMenu.accessibility.test.tsx b/packages/lib/src/contextual-menu/ContextualMenu.accessibility.test.tsx index 352698ffaf..5f546dbcfa 100644 --- a/packages/lib/src/contextual-menu/ContextualMenu.accessibility.test.tsx +++ b/packages/lib/src/contextual-menu/ContextualMenu.accessibility.test.tsx @@ -3,7 +3,7 @@ import { axe } from "../../test/accessibility/axe-helper"; import DxcBadge from "../badge/Badge"; import DxcContextualMenu from "./ContextualMenu"; -const badge_icon = ( +const badgeIcon = ( @@ -11,13 +11,13 @@ const badge_icon = ( ); -const key_icon = ( +const keyIcon = ( ); -const fav_icon = ( +const favIcon = ( @@ -26,8 +26,8 @@ const fav_icon = ( const itemsWithTruncatedText = [ { label: "Item with a very long label that should be truncated", - slot: , - icon: key_icon, + slot: , + icon: keyIcon, }, { label: "Item 2", @@ -39,7 +39,7 @@ const itemsWithTruncatedText = [ /> ), - icon: fav_icon, + icon: favIcon, }, ]; @@ -74,17 +74,15 @@ const items = [ { label: "Sales performance", }, - { - label: "Key metrics" + { + label: "Key metrics", }, ], }, ], }, { - items: [ - { label: "Support", icon: "support_agent" }, - ], + items: [{ label: "Support", icon: "support_agent" }], }, ]; diff --git a/packages/lib/src/contextual-menu/ContextualMenu.stories.tsx b/packages/lib/src/contextual-menu/ContextualMenu.stories.tsx index 6f7d9f2610..b7f09bbc61 100644 --- a/packages/lib/src/contextual-menu/ContextualMenu.stories.tsx +++ b/packages/lib/src/contextual-menu/ContextualMenu.stories.tsx @@ -1,12 +1,12 @@ +import { Meta, StoryObj } from "@storybook/react"; +import { userEvent, within } from "@storybook/test"; import ExampleContainer from "../../.storybook/components/ExampleContainer"; import Title from "../../.storybook/components/Title"; import DxcBadge from "../badge/Badge"; import DxcContainer from "../container/Container"; import DxcContextualMenu from "./ContextualMenu"; import SingleItem from "./SingleItem"; -import { userEvent, within } from "@storybook/test"; import ContextualMenuContext from "./ContextualMenuContext"; -import { Meta, StoryObj } from "@storybook/react"; export default { title: "Contextual Menu", diff --git a/packages/lib/src/contextual-menu/ContextualMenu.test.tsx b/packages/lib/src/contextual-menu/ContextualMenu.test.tsx index 2270d2ea32..59af5b6fd8 100644 --- a/packages/lib/src/contextual-menu/ContextualMenu.test.tsx +++ b/packages/lib/src/contextual-menu/ContextualMenu.test.tsx @@ -35,7 +35,9 @@ describe("Contextual menu component tests", () => { const { getAllByRole, getByRole } = render(); expect(getAllByRole("menuitem").length).toBe(4); const actions = getAllByRole("button"); - actions[0] != null && userEvent.click(actions[0]); + if (actions[0] != null) { + userEvent.click(actions[0]); + } expect(actions[0]?.getAttribute("aria-pressed")).toBeTruthy(); expect(getByRole("menu")).toBeTruthy(); }); @@ -66,16 +68,24 @@ describe("Contextual menu component tests", () => { test("Group — Renders with correct aria attributes", () => { const { getAllByRole } = render(); const group1 = getAllByRole("button")[0]; - group1 != null && userEvent.click(group1); + if (group1 != null) { + userEvent.click(group1); + } expect(group1?.getAttribute("aria-expanded")).toBeTruthy(); expect(group1?.getAttribute("aria-controls")).toBe(group1?.nextElementSibling?.id); const expandedGroupItem1 = getAllByRole("button")[2]; - expandedGroupItem1 != null && userEvent.click(expandedGroupItem1); + if (expandedGroupItem1 != null) { + userEvent.click(expandedGroupItem1); + } const expandedGroupedItem2 = getAllByRole("button")[6]; - expandedGroupedItem2 != null && userEvent.click(expandedGroupedItem2); + if (expandedGroupedItem2 != null) { + userEvent.click(expandedGroupedItem2); + } expect(getAllByRole("menuitem").length).toBe(10); const optionToBeClicked = getAllByRole("button")[4]; - optionToBeClicked != null && userEvent.click(optionToBeClicked); + if (optionToBeClicked != null) { + userEvent.click(optionToBeClicked); + } expect(optionToBeClicked?.getAttribute("aria-pressed")).toBeTruthy(); }); test("Group — A grouped item, selected by default, must be visible (expanded group) in the first render of the component", () => { @@ -92,17 +102,27 @@ describe("Contextual menu component tests", () => { test("Group — Collapsed groups render as selected when containing a selected item", () => { const { getAllByRole } = render(); const group1 = getAllByRole("button")[0]; - group1 != null && userEvent.click(group1); + if (group1 != null) { + userEvent.click(group1); + } const group2 = getAllByRole("button")[2]; - group2 != null && userEvent.click(group2); + if (group2 != null) { + userEvent.click(group2); + } const item = getAllByRole("button")[3]; - item != null && userEvent.click(item); + if (item != null) { + userEvent.click(item); + } expect(item?.getAttribute("aria-pressed")).toBeTruthy(); expect(group1?.getAttribute("aria-pressed")).toBe("false"); expect(group2?.getAttribute("aria-pressed")).toBe("false"); - group2 != null && userEvent.click(group2); + if (group2 != null) { + userEvent.click(group2); + } expect(group2?.getAttribute("aria-pressed")).toBe("true"); - group1 != null && userEvent.click(group1); + if (group1 != null) { + userEvent.click(group1); + } expect(group1?.getAttribute("aria-pressed")).toBe("true"); }); test("Sections — Renders with correct aria attributes", () => { @@ -110,7 +130,9 @@ describe("Contextual menu component tests", () => { expect(getAllByRole("region").length).toBe(2); expect(getAllByRole("menuitem").length).toBe(6); const actions = getAllByRole("button"); - actions[0] != null && userEvent.click(actions[0]); + if (actions[0] != null) { + userEvent.click(actions[0]); + } expect(actions[0]?.getAttribute("aria-pressed")).toBeTruthy(); expect(getAllByRole("menu").length).toBe(2); expect(getAllByRole("region")[0]?.getAttribute("aria-labelledby")).toBe(getByText("Section title").id); diff --git a/packages/lib/src/contextual-menu/ContextualMenu.tsx b/packages/lib/src/contextual-menu/ContextualMenu.tsx index ae95747ce9..13f58b4172 100644 --- a/packages/lib/src/contextual-menu/ContextualMenu.tsx +++ b/packages/lib/src/contextual-menu/ContextualMenu.tsx @@ -4,7 +4,7 @@ import MenuItem from "./MenuItem"; import ContextualMenuPropsType, { GroupItemWithId, ItemWithId, SectionWithId } from "./types"; import Section from "./Section"; import ContextualMenuContext from "./ContextualMenuContext"; -import { scrollbarStyles } from "../styles/scroll"; +import scrollbarStyles from "../styles/scroll"; import { addIdToItems, isSection } from "./utils"; import SubMenu from "./SubMenu"; @@ -34,10 +34,12 @@ export default function DxcContextualMenu({ items }: ContextualMenuPropsType) { useLayoutEffect(() => { if (selectedItemId !== -1 && firstUpdate) { const contextualMenuEl = contextualMenuRef.current; - const selectedItemEl = contextualMenuEl?.querySelector("[aria-pressed='true']") as HTMLButtonElement; - contextualMenuEl?.scrollTo?.({ - top: (selectedItemEl?.offsetTop ?? 0) - (contextualMenuEl?.clientHeight ?? 0) / 2, - }); + const selectedItemEl = contextualMenuEl?.querySelector("[aria-pressed='true']"); + if (selectedItemEl instanceof HTMLButtonElement) { + contextualMenuEl?.scrollTo?.({ + top: (selectedItemEl?.offsetTop ?? 0) - (contextualMenuEl?.clientHeight ?? 0) / 2, + }); + } setFirstUpdate(false); } }, [firstUpdate, selectedItemId]); diff --git a/packages/lib/src/contextual-menu/GroupItem.tsx b/packages/lib/src/contextual-menu/GroupItem.tsx index ebf2c79f9b..ba794fd617 100644 --- a/packages/lib/src/contextual-menu/GroupItem.tsx +++ b/packages/lib/src/contextual-menu/GroupItem.tsx @@ -1,4 +1,4 @@ -import { useContext, useMemo, useState, memo, useId } from "react"; +import { useContext, useMemo, useState, useId } from "react"; import DxcIcon from "../icon/Icon"; import SubMenu from "./SubMenu"; import ItemAction from "./ItemAction"; @@ -7,7 +7,7 @@ import { GroupItemProps } from "./types"; import ContextualMenuContext from "./ContextualMenuContext"; import { isGroupSelected } from "./utils"; -export default function GroupItem({ items, ...props }: GroupItemProps) { +const GroupItem = ({ items, ...props }: GroupItemProps) => { const groupMenuId = `group-menu-${useId()}`; const { selectedItemId } = useContext(ContextualMenuContext) ?? {}; const groupSelected = useMemo(() => isGroupSelected(items, selectedItemId), [items, selectedItemId]); @@ -20,9 +20,7 @@ export default function GroupItem({ items, ...props }: GroupItemProps) { aria-expanded={isOpen ? true : undefined} aria-pressed={groupSelected && !isOpen} collapseIcon={isOpen ? : } - onClick={() => { - setIsOpen((isCurrentlyOpen) => !isCurrentlyOpen); - }} + onClick={() => setIsOpen((isCurrentlyOpen) => !isCurrentlyOpen)} selected={groupSelected && !isOpen} {...props} /> @@ -36,3 +34,5 @@ export default function GroupItem({ items, ...props }: GroupItemProps) { ); }; + +export default GroupItem; diff --git a/packages/lib/src/contextual-menu/ItemAction.tsx b/packages/lib/src/contextual-menu/ItemAction.tsx index c8c1294926..7476819964 100644 --- a/packages/lib/src/contextual-menu/ItemAction.tsx +++ b/packages/lib/src/contextual-menu/ItemAction.tsx @@ -63,7 +63,7 @@ const Text = styled.span<{ selected: ItemActionProps["selected"] }>` overflow: hidden; `; -export default memo(function ItemAction({ badge, collapseIcon, depthLevel, icon, label, ...props }: ItemActionProps) { +const ItemAction = memo(({ badge, collapseIcon, depthLevel, icon, label, ...props }: ItemActionProps) => { const [hasTooltip, setHasTooltip] = useState(false); const modifiedBadge = badge && cloneElement(badge, { size: "small" }); @@ -88,3 +88,7 @@ export default memo(function ItemAction({ badge, collapseIcon, depthLevel, icon, ); }); + +ItemAction.displayName = "ItemAction"; + +export default ItemAction; diff --git a/packages/lib/src/contextual-menu/Section.tsx b/packages/lib/src/contextual-menu/Section.tsx index ae8afdabfc..8cade2fba0 100644 --- a/packages/lib/src/contextual-menu/Section.tsx +++ b/packages/lib/src/contextual-menu/Section.tsx @@ -1,10 +1,10 @@ +import { useId } from "react"; import styled from "@emotion/styled"; import { DxcInset } from ".."; import DxcDivider from "../divider/Divider"; import SubMenu from "./SubMenu"; import MenuItem from "./MenuItem"; import { SectionProps } from "./types"; -import { useId } from "react"; const SectionContainer = styled.section` display: grid; @@ -27,8 +27,8 @@ export default function Section({ index, length, section }: SectionProps) { {section.title && {section.title}} - {section.items.map((item, index) => ( - + {section.items.map((item, i) => ( + ))} {index !== length - 1 && ( diff --git a/packages/lib/src/contextual-menu/SingleItem.tsx b/packages/lib/src/contextual-menu/SingleItem.tsx index df86ea61da..5fcd304d91 100644 --- a/packages/lib/src/contextual-menu/SingleItem.tsx +++ b/packages/lib/src/contextual-menu/SingleItem.tsx @@ -21,7 +21,9 @@ export default function SingleItem({ id, onSelect, selectedByDefault = false, .. ); diff --git a/packages/lib/src/contextual-menu/types.ts b/packages/lib/src/contextual-menu/types.ts index c6107f8ae6..e9599a7f89 100644 --- a/packages/lib/src/contextual-menu/types.ts +++ b/packages/lib/src/contextual-menu/types.ts @@ -53,7 +53,6 @@ type ContextualMenuContextProps = { setSelectedItemId: Dispatch>; }; -export default Props; export type { ContextualMenuContextProps, GroupItem, @@ -69,3 +68,5 @@ export type { SectionProps, SingleItemProps, }; + +export default Props; diff --git a/packages/lib/src/contextual-menu/utils.ts b/packages/lib/src/contextual-menu/utils.ts index a77c213b0b..3dfe2fb6d8 100644 --- a/packages/lib/src/contextual-menu/utils.ts +++ b/packages/lib/src/contextual-menu/utils.ts @@ -10,21 +10,23 @@ import ContextualMenuPropsType, { export const isGroupItem = (item: Item | GroupItem): item is GroupItem => "items" in item; -export const isSection = (item: SectionType | Item | GroupItem): item is SectionType => "items" in item && !("label" in item); +export const isSection = (item: SectionType | Item | GroupItem): item is SectionType => + "items" in item && !("label" in item); -export const addIdToItems = (items: ContextualMenuPropsType["items"]): (ItemWithId | GroupItemWithId | SectionWithId)[] => { +export const addIdToItems = ( + items: ContextualMenuPropsType["items"] +): (ItemWithId | GroupItemWithId | SectionWithId)[] => { let accId = 0; const innerAddIdToItems = ( items: ContextualMenuPropsType["items"] - ): (ItemWithId | GroupItemWithId | SectionWithId)[] => { - return items.map((item: Item | GroupItem | SectionType) => + ): (ItemWithId | GroupItemWithId | SectionWithId)[] => + items.map((item: Item | GroupItem | SectionType) => isSection(item) ? ({ ...item, items: innerAddIdToItems(item.items) } as SectionWithId) : isGroupItem(item) ? ({ ...item, items: innerAddIdToItems(item.items) } as GroupItemWithId) : { ...item, id: accId++ } ); - }; return innerAddIdToItems(items); }; @@ -32,5 +34,5 @@ export const isGroupSelected = (items: GroupItemProps["items"], selectedItemId?: items.some((item) => { if ("items" in item) return isGroupSelected(item.items, selectedItemId); else if (selectedItemId !== -1) return item.id === selectedItemId; - else return (item as ItemWithId).selectedByDefault; - }); \ No newline at end of file + else return item.selectedByDefault; + }); diff --git a/packages/lib/src/data-grid/DataGrid.stories.tsx b/packages/lib/src/data-grid/DataGrid.stories.tsx index 7f63a8fb41..263dc3a98c 100644 --- a/packages/lib/src/data-grid/DataGrid.stories.tsx +++ b/packages/lib/src/data-grid/DataGrid.stories.tsx @@ -1,15 +1,15 @@ +import { isValidElement, useState } from "react"; +import { Meta, StoryObj } from "@storybook/react"; +import { userEvent, within } from "@storybook/test"; import Title from "../../.storybook/components/Title"; import ExampleContainer from "../../.storybook/components/ExampleContainer"; import DxcDataGrid from "./DataGrid"; import DxcContainer from "../container/Container"; -import { GridColumn, GridRow, HierarchyGridRow } from "./types"; -import { isValidElement, useState } from "react"; -import { disabledRules } from "../../test/accessibility/rules/specific/data-grid/disabledRules"; +import disabledRules from "../../test/accessibility/rules/specific/data-grid/disabledRules"; +import { GridColumn, HierarchyGridRow } from "./types"; import preview from "../../.storybook/preview"; -import { userEvent, within } from "@storybook/test"; import DxcBadge from "../badge/Badge"; import { ActionsCellPropsType } from "../table/types"; -import { Meta, StoryObj } from "@storybook/react"; import { isKeyOfRow } from "./utils"; export default { @@ -20,7 +20,7 @@ export default { config: { rules: [ ...disabledRules.map((ruleId) => ({ id: ruleId, reviewOnFail: true })), - ...preview?.parameters?.a11y?.config?.rules, + ...(preview?.parameters?.a11y?.config?.rules || []), ], }, }, @@ -450,15 +450,15 @@ const childrenTrigger = (open: boolean, triggerRow: HierarchyGridRow) => { setTimeout(() => { resolve([ { - name: `${triggerRow.name} Child 1`, - value: triggerRow.value, - id: `${triggerRow.id}-child-1`, + name: `${triggerRow.name as string} Child 1`, + value: triggerRow.value as string, + id: `${triggerRow.id as string}-child-1`, childrenTrigger, }, { - name: `${triggerRow.name} Child 2`, - value: triggerRow.value, - id: `${triggerRow.id}-child-2`, + name: `${triggerRow.name as string} Child 2`, + value: triggerRow.value as string, + id: `${triggerRow.id as string}-child-2`, childrenTrigger, }, ] as unknown as HierarchyGridRow[]); @@ -643,7 +643,7 @@ const customSortColumns: GridColumn[] = [ summaryKey: "total", sortable: true, sortFn: (a, b) => { - if (isValidElement(a) && isValidElement(b)) { + if (isValidElement<{ label: string }>(a) && isValidElement<{ label: string }>(b)) { return a.props.label < b.props.label ? -1 : a.props.label > b.props.label ? 1 : 0; } return 0; @@ -657,27 +657,27 @@ const customSortRows = [ task: "Task 1", complete: 46, priority: "High", - component: , + component: , }, { id: 2, task: "Task 2", complete: 51, priority: "High", - component: , + component: , }, { id: 3, task: "Task 3", complete: 40, priority: "High", - component: , + component: , }, { id: 4, task: "Task 4", complete: 10, - component: , + component: , priority: "High", }, { @@ -685,21 +685,21 @@ const customSortRows = [ task: "Task 5", complete: 68, priority: "High", - component: , + component: , }, { id: 6, task: "Task 6", complete: 37, priority: "High", - component: , + component: , }, { id: 7, task: "Task 7", complete: 73, priority: "Medium", - component: , + component: , }, ]; @@ -816,8 +816,8 @@ const DataGridControlled = () => { if (sortColumn) { const { columnKey, direction } = sortColumn; console.log(`Sorting the column '${columnKey}' by '${direction}' direction`); - setRowsControlled((currentRows) => { - return currentRows.sort((a, b) => { + setRowsControlled((currentRows) => + currentRows.sort((a, b) => { if (isKeyOfRow(columnKey, a) && isKeyOfRow(columnKey, b)) { const valueA = a[columnKey]; const valueB = b[columnKey]; @@ -833,8 +833,8 @@ const DataGridControlled = () => { } else { return 0; } - }); - }); + }) + ); } else { console.log("Removed sorting criteria"); setRowsControlled(expandableRows.slice(page * itemsPerPage, page * itemsPerPage + itemsPerPage)); @@ -860,16 +860,12 @@ const DataGridControlled = () => { ); }; -const DataGridSort = () => { - return ( - <> - - - <DxcDataGrid columns={customSortColumns} rows={customSortRows} uniqueRowId="id" /> - </ExampleContainer> - </> - ); -}; +const DataGridSort = () => ( + <ExampleContainer> + <Title title="Default" theme="light" level={4} /> + <DxcDataGrid columns={customSortColumns} rows={customSortRows} uniqueRowId="id" /> + </ExampleContainer> +); const DataGridPaginator = () => { const [selectedRows, setSelectedRows] = useState((): Set<number | string> => new Set()); @@ -1083,27 +1079,41 @@ export const DataGridSortedWithChildren: Story = { play: async ({ canvasElement }) => { const canvas = within(canvasElement); const checkbox0 = canvas.getAllByRole("checkbox")[0]; - checkbox0 && (await userEvent.click(checkbox0)); + if (checkbox0) { + await userEvent.click(checkbox0); + } await userEvent.click(canvas.getByText("Root Node 1")); await userEvent.click(canvas.getByText("Root Node 2")); await userEvent.click(canvas.getByText("Child Node 1.1")); await userEvent.click(canvas.getByText("Child Node 2.1")); let columnheader1 = canvas.getAllByRole("columnheader")[1]; - columnheader1 && (await userEvent.click(columnheader1)); + if (columnheader1) { + await userEvent.click(columnheader1); + } columnheader1 = canvas.getAllByRole("columnheader")[1]; - columnheader1 && (await userEvent.click(columnheader1)); + if (columnheader1) { + await userEvent.click(columnheader1); + } const checkbox5 = canvas.getAllByRole("checkbox")[5]; - checkbox5 && (await userEvent.click(checkbox5)); + if (checkbox5) { + await userEvent.click(checkbox5); + } const checkbox13 = canvas.getAllByRole("checkbox")[13]; - checkbox13 && (await userEvent.click(checkbox13)); + if (checkbox13) { + await userEvent.click(checkbox13); + } await userEvent.click(canvas.getByText("Paginated Node 1")); await userEvent.click(canvas.getByText("Paginated Node 2")); await userEvent.click(canvas.getByText("Paginated Node 1.1")); await userEvent.click(canvas.getByText("Paginated Node 2.1")); const columnheader4 = canvas.getAllByRole("columnheader")[4]; - columnheader4 && (await userEvent.click(columnheader4)); + if (columnheader4) { + await userEvent.click(columnheader4); + } const checkbox18 = canvas.getAllByRole("checkbox")[18]; - checkbox18 && (await userEvent.click(checkbox18)); + if (checkbox18) { + await userEvent.click(checkbox18); + } }, }; @@ -1112,25 +1122,45 @@ export const DataGridSortedExpanded: Story = { play: async ({ canvasElement }) => { const canvas = within(canvasElement); const button0 = canvas.getAllByRole("button")[0]; - button0 && (await userEvent.click(button0)); + if (button0) { + await userEvent.click(button0); + } const button1 = canvas.getAllByRole("button")[1]; - button1 && (await userEvent.click(button1)); + if (button1) { + await userEvent.click(button1); + } const columnHeaders4 = canvas.getAllByRole("columnheader")[4]; - columnHeaders4 && (await userEvent.click(columnHeaders4)); + if (columnHeaders4) { + await userEvent.click(columnHeaders4); + } const button9 = canvas.getAllByRole("button")[9]; - button9 && (await userEvent.click(button9)); + if (button9) { + await userEvent.click(button9); + } const button10 = canvas.getAllByRole("button")[10]; - button10 && (await userEvent.click(button10)); + if (button10) { + await userEvent.click(button10); + } const columnHeaders10 = canvas.getAllByRole("columnheader")[10]; - columnHeaders10 && (await userEvent.click(columnHeaders10)); + if (columnHeaders10) { + await userEvent.click(columnHeaders10); + } const button16 = canvas.getAllByRole("button")[16]; - button16 && (await userEvent.click(button16)); + if (button16) { + await userEvent.click(button16); + } const button43 = canvas.getAllByRole("button")[43]; - button43 && (await userEvent.click(button43)); + if (button43) { + await userEvent.click(button43); + } const button36 = canvas.getAllByRole("button")[36]; - button36 && (await userEvent.click(button36)); + if (button36) { + await userEvent.click(button36); + } const button37 = canvas.getAllByRole("button")[37]; - button37 && (await userEvent.click(button37)); + if (button37) { + await userEvent.click(button37); + } }, }; @@ -1139,6 +1169,6 @@ export const UnknownUniqueId: Story = { play: async ({ canvasElement }) => { const canvas = within(canvasElement); const editorCell = canvas.getAllByText("Task 1")[0]; - editorCell && (await userEvent.dblClick(editorCell)); + if (editorCell) await userEvent.dblClick(editorCell); }, }; diff --git a/packages/lib/src/data-grid/DataGrid.test.tsx b/packages/lib/src/data-grid/DataGrid.test.tsx index fd871fdbdc..d1856090b7 100644 --- a/packages/lib/src/data-grid/DataGrid.test.tsx +++ b/packages/lib/src/data-grid/DataGrid.test.tsx @@ -243,14 +243,15 @@ const hierarchyRowsLazy: HierarchyGridRow[] = [ describe("Data grid component tests", () => { beforeAll(() => { + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-explicit-any (global as any).CSS = { - escape: (str: string): string => str, + escape: (str: string) => str, }; window.HTMLElement.prototype.scrollIntoView = jest.fn; }); - test("Renders with correct content", async () => { - const { getByText, getAllByRole } = await render(<DxcDataGrid columns={columns} rows={expandableRows} />); + test("Renders with correct content", () => { + const { getByText, getAllByRole } = render(<DxcDataGrid columns={columns} rows={expandableRows} />); expect(getByText("46")).toBeTruthy(); const rows = getAllByRole("row"); expect(rows.length).toBe(5); @@ -273,7 +274,7 @@ describe("Data grid component tests", () => { expect(rows.length).toBe(5); }); - test("Triggers childrenTrigger when expanding hierarchy row", async () => { + test("Triggers childrenTrigger when expanding hierarchy row", () => { const onSelectRows = jest.fn(); const selectedRows = new Set<number | string>(); @@ -292,7 +293,9 @@ describe("Data grid component tests", () => { const buttons = getAllByRole("button"); - buttons[0] && fireEvent.click(buttons[0]); + if (buttons[0]) { + fireEvent.click(buttons[0]); + } expect(childrenTriggerMock).toHaveBeenCalledWith(true, expect.objectContaining({ id: "lazy-a" })); }); @@ -303,14 +306,18 @@ describe("Data grid component tests", () => { expect(getByText("% Complete")).toBeTruthy(); }); - test("Expands and collapses a row to show custom content", async () => { + test("Expands and collapses a row to show custom content", () => { const { getAllByRole, getByText, queryByText } = render( <DxcDataGrid columns={columns} rows={expandableRows} uniqueRowId="id" expandable /> ); const buttons = getAllByRole("button"); - buttons[0] && fireEvent.click(buttons[0]); + if (buttons[0]) { + fireEvent.click(buttons[0]); + } expect(getByText("Custom content 1")).toBeTruthy(); - buttons[0] && fireEvent.click(buttons[0]); + if (buttons[0]) { + fireEvent.click(buttons[0]); + } expect(queryByText("Custom content 1")).not.toBeTruthy(); }); @@ -321,13 +328,17 @@ describe("Data grid component tests", () => { const headers = getAllByRole("columnheader"); const sortableHeader = headers[1]; - sortableHeader && fireEvent.click(sortableHeader); + if (sortableHeader) { + fireEvent.click(sortableHeader); + } expect(sortableHeader?.getAttribute("aria-sort")).toBe("ascending"); await waitFor(() => { const cells = getAllByRole("gridcell"); expect(cells[1]?.textContent).toBe("1"); }); - sortableHeader && fireEvent.click(sortableHeader); + if (sortableHeader) { + fireEvent.click(sortableHeader); + } expect(sortableHeader?.getAttribute("aria-sort")).toBe("descending"); await waitFor(() => { const cells = getAllByRole("gridcell"); @@ -341,8 +352,12 @@ describe("Data grid component tests", () => { ); const buttons = getAllByRole("button"); - buttons[0] && fireEvent.click(buttons[0]); - buttons[1] && fireEvent.click(buttons[1]); + if (buttons[0]) { + fireEvent.click(buttons[0]); + } + if (buttons[1]) { + fireEvent.click(buttons[1]); + } expect(getByText("Custom content 1")).toBeTruthy(); expect(getByText("Custom content 2")).toBeTruthy(); diff --git a/packages/lib/src/data-grid/DataGrid.tsx b/packages/lib/src/data-grid/DataGrid.tsx index ace21c1f10..82f7d0ba07 100644 --- a/packages/lib/src/data-grid/DataGrid.tsx +++ b/packages/lib/src/data-grid/DataGrid.tsx @@ -1,4 +1,4 @@ -import { useEffect, useMemo, useState, ReactNode } from "react"; +import { useEffect, useMemo, useState } from "react"; import DataGrid, { SortColumn } from "react-data-grid"; import styled from "@emotion/styled"; import DataGridPropsType, { HierarchyGridRow, GridRow, ExpandableGridRow } from "./types"; @@ -22,8 +22,7 @@ import { } from "./utils"; import DxcPaginator from "../paginator/Paginator"; import { DxcActionsCell } from "../table/Table"; -import { scrollbarStyles } from "../styles/scroll"; - +import scrollbarStyles from "../styles/scroll"; const DataGridContainer = styled.div<{ paginatorRendered: boolean; }>` @@ -218,7 +217,7 @@ const DxcDataGrid = ({ renderCell({ row }) { if (row.isExpandedChildContent) { // if it is expanded content - return (row.expandedChildContent as ReactNode) || null; + return row.expandedChildContent || null; } // if row has expandable content return ( @@ -262,7 +261,7 @@ const DxcDataGrid = ({ } return ( <HierarchyContainer level={typeof row.rowLevel === "number" ? row.rowLevel : 0} className="ellipsis-cell"> - {row[firstColumnKey] as ReactNode} + {row[firstColumnKey]} </HierarchyContainer> ); }, @@ -339,7 +338,7 @@ const DxcDataGrid = ({ const reorderedColumns = useMemo( () => - // Array ordered by columnsOrder + // Array sorted by columnsOrder columnsOrder.map((index) => columnsToRender[index]!), [columnsOrder, columnsToRender] ); diff --git a/packages/lib/src/data-grid/types.ts b/packages/lib/src/data-grid/types.ts index 5b85840d3c..1a750971a9 100644 --- a/packages/lib/src/data-grid/types.ts +++ b/packages/lib/src/data-grid/types.ts @@ -54,7 +54,7 @@ export type HierarchyGridRow = GridRow & { */ childRows?: HierarchyGridRow[] | GridRow[]; /** - * Function called when a row with children is expanded or collapsed (based on the value of `open`). + * Function called when a row with children is expanded or collapsed (based on the value of `open`). * Returns (or resolves to) the array of child rows nested under this row to display when expanded. */ childrenTrigger?: ( diff --git a/packages/lib/src/data-grid/utils.tsx b/packages/lib/src/data-grid/utils.tsx index 55fa64b92b..ef5fcc4761 100644 --- a/packages/lib/src/data-grid/utils.tsx +++ b/packages/lib/src/data-grid/utils.tsx @@ -1,7 +1,4 @@ -// TODO: Remove eslint disable -/* eslint-disable no-param-reassign */ - -import { ReactNode, SetStateAction, useState } from "react"; +import { ReactNode, SetStateAction } from "react"; import { Column, RenderSortStatusProps, SortColumn, textEditor } from "react-data-grid"; import DxcActionIcon from "../action-icon/ActionIcon"; import DxcCheckbox from "../checkbox/Checkbox"; @@ -122,6 +119,19 @@ export const renderExpandableTrigger = ( /> ); +/** + * Determines if the given row is a `HierarchyGridRow`. + * + * A `HierarchyGridRow` is identified by having a `childRows` property + * that is an array with at least one element. + * + * @param {GridRow} row - The row to check. + * @returns {row is HierarchyGridRow & { childRows: HierarchyGridRow[] | GridRow[] }} + * Returns `true` if the row is a `HierarchyGridRow` with `childRows` defined, otherwise `false`. + */ +const isHierarchyGridRow = (row: GridRow): row is HierarchyGridRow & { childRows: HierarchyGridRow[] | GridRow[] } => + Array.isArray(row.childRows) && row.childRows.length > 0; + /** * Renders a trigger for hierarchical row expansion in the grid. * @param {HierarchyGridRow[]} rows - List of all hierarchy grid rows. @@ -198,47 +208,53 @@ export const renderHierarchyTrigger = ( triggerRow.childRows?.length && !rows.some((row) => row.parentKey === rowKeyGetter(triggerRow, uniqueRowId)) ) { - expandChildren(); + expandChildren().catch((err) => { + console.error("Children expansion failed:", err); + }); } const onClick = async () => { - if (isLoading) return; // Prevent double clicks while loading - triggerRow.visibleChildren = !triggerRow.visibleChildren; - if (triggerRow.visibleChildren) { - await expandChildren(); - } else { - setRowsToRender((currentRows) => { - // The children of the row that is being collapsed are added to an array - const rowsToRemove: HierarchyGridRow[] = rows.filter( - (rowToRender) => rowToRender.parentKey && rowToRender.parentKey === rowKeyGetter(triggerRow, uniqueRowId) - ); - // The children are checked if any of them has any other children of their own - const rowsToCheck = [...rowsToRemove]; - while (rowsToCheck.length > 0) { - const currentRow = rowsToCheck.pop(); - const childRows = currentRow?.visibleChildren && currentRow?.childRows ? currentRow.childRows : []; - - rowsToRemove.push(...childRows); - rowsToCheck.push(...childRows); - } - - const newRowsToRender = currentRows.filter( - (row) => - !rowsToRemove - .map((rowToRemove) => { - if (rowToRemove.visibleChildren) { - rowToRemove.visibleChildren = false; - } - return rowKeyGetter(rowToRemove, uniqueRowId); - }) - .includes(rowKeyGetter(row, uniqueRowId)) - ); + try { + if (isLoading) return; // Prevent double clicks while loading + triggerRow.visibleChildren = !triggerRow.visibleChildren; + if (triggerRow.visibleChildren) { + await expandChildren(); + } else { + setRowsToRender((currentRows) => { + // The children of the row that is being collapsed are added to an array + const rowsToRemove: HierarchyGridRow[] = rows.filter( + (rowToRender) => rowToRender.parentKey && rowToRender.parentKey === rowKeyGetter(triggerRow, uniqueRowId) + ); + // The children are checked if any of them has any other children of their own + const rowsToCheck = [...rowsToRemove]; + while (rowsToCheck.length > 0) { + const currentRow = rowsToCheck.pop(); + const childRows = currentRow?.visibleChildren && currentRow?.childRows ? currentRow.childRows : []; + + rowsToRemove.push(...childRows); + rowsToCheck.push(...childRows); + } - return newRowsToRender; - }); + const newRowsToRender = currentRows.filter( + (row) => + !rowsToRemove + .map((rowToRemove) => { + if (rowToRemove.visibleChildren) { + rowToRemove.visibleChildren = false; + } + return rowKeyGetter(rowToRemove, uniqueRowId); + }) + .includes(rowKeyGetter(row, uniqueRowId)) + ); + + return newRowsToRender; + }); + } + } catch (err) { + console.error("Error toggling row:", err); } }; return ( - <button type="button" disabled={!rows.some((row) => uniqueRowId in row)} onClick={onClick}> + <button type="button" disabled={!rows.some((row) => uniqueRowId in row)} onClick={() => void onClick()}> {isLoading ? ( <DxcSpinner mode="small" /> ) : ( @@ -304,14 +320,14 @@ export const renderHeaderCheckbox = ( if (checked) { rows.forEach((row) => { updatedSelection.add(rowKeyGetter(row, uniqueRowId)); - if (row.childRows && Array.isArray(row.childRows)) { + if (isHierarchyGridRow(row)) { getChildrenSelection(row.childRows, uniqueRowId, updatedSelection, checked); } }); } else { rows.forEach((row) => { updatedSelection.delete(rowKeyGetter(row, uniqueRowId)); - if (row.childRows && Array.isArray(row.childRows)) { + if (isHierarchyGridRow(row)) { getChildrenSelection(row.childRows, uniqueRowId, updatedSelection, checked); } }); @@ -506,26 +522,25 @@ export const rowFinderBasedOnId = ( if (foundRow) { return foundRow; } - return undefined; }; /** * Recursively selects or deselects children rows based on the checked state. - * @param {HierarchyGridRow[]} rowList - List of child rows that need to be checked/unchecked. + * @param {GridRow[] | HierarchyGridRow[]} rowList - List of child rows that need to be checked/unchecked. * @param {string} uniqueRowId - Key used to uniquely identify each row. * @param {Set<string | number>} selectedRows - Set of selected rows. * @param {boolean} checked - Boolean indicating whether the rows should be selected (true) or deselected (false). * @param {boolean} expandingChildren - Defines children are being expanded or not, used to avoid removing children that were previously set when expanding an unset parent */ const getChildrenSelection = ( - rowList: HierarchyGridRow[], + rowList: GridRow[] | HierarchyGridRow[], uniqueRowId: string, selectedRows: Set<string | number>, checked: boolean, hierarchyValidation?: boolean ) => { rowList.forEach((row) => { - if (row.childRows) { + if (isHierarchyGridRow(row)) { // Recursively select/deselect child rows getChildrenSelection(row.childRows, uniqueRowId, selectedRows, checked, hierarchyValidation); } @@ -560,7 +575,7 @@ const getParentSelectedState = ( } const parentRow = rowFinderBasedOnId(rowList, uniqueRowId, parentKeyValue) as HierarchyGridRow; - if (!parentRow) { + if (!parentRow || !isHierarchyGridRow(parentRow)) { return; } @@ -698,6 +713,4 @@ export const getPaginatedNodes = ( * @returns {boolean} - Returns `true` if `key` is a valid key of `obj`, otherwise `false`. * */ -export const isKeyOfRow = <T extends GridRow>(key: string, obj: T): key is Extract<keyof T, string> => { - return key in obj; -}; +export const isKeyOfRow = <T extends GridRow>(key: string, obj: T): key is Extract<keyof T, string> => key in obj; diff --git a/packages/lib/src/date-input/DateInput.accessibility.test.tsx b/packages/lib/src/date-input/DateInput.accessibility.test.tsx index 21ff46dfc0..4e597ff5b3 100644 --- a/packages/lib/src/date-input/DateInput.accessibility.test.tsx +++ b/packages/lib/src/date-input/DateInput.accessibility.test.tsx @@ -1,26 +1,23 @@ import { render } from "@testing-library/react"; import { axe, formatRules } from "../../test/accessibility/axe-helper"; import DxcDateInput from "./DateInput"; - -// Mocking DOMRect for Radix Primitive Popover -(global as any).globalThis = global; -(global as any).DOMRect = { - fromRect: () => ({ top: 0, left: 0, bottom: 0, right: 0, width: 0, height: 0, x: 0, y: 0 }), -}; -(global as any).ResizeObserver = class ResizeObserver { - observe() {} - unobserve() {} - disconnect() {} -}; +import MockDOMRect from "../../test/mocks/domRectMock"; // TODO: REMOVE -import { disabledRules as rules } from "../../test/accessibility/rules/specific/date-input/disabledRules"; +import rules from "../../test/accessibility/rules/specific/date-input/disabledRules"; + +// Mocking DOMRect for Radix Primitive Popover +global.DOMRect = MockDOMRect; +global.ResizeObserver = jest.fn().mockImplementation(() => ({ + observe: jest.fn(), + unobserve: jest.fn(), + disconnect: jest.fn(), +})); const disabledRules = { rules: formatRules(rules), }; - describe("DateInput component accessibility tests", () => { it("Should not have basic accessibility issues", async () => { // baseElement is needed when using React Portals diff --git a/packages/lib/src/date-input/DateInput.stories.tsx b/packages/lib/src/date-input/DateInput.stories.tsx index 0b16912a31..dce0c29c8e 100644 --- a/packages/lib/src/date-input/DateInput.stories.tsx +++ b/packages/lib/src/date-input/DateInput.stories.tsx @@ -1,16 +1,15 @@ -import { useContext } from "react"; +import { Meta, StoryObj } from "@storybook/react"; import { fireEvent, screen, userEvent, within } from "@storybook/test"; import dayjs from "dayjs"; -import ExampleContainer from "../../.storybook/components/ExampleContainer"; import Title from "../../.storybook/components/Title"; +import ExampleContainer from "../../.storybook/components/ExampleContainer"; import preview from "../../.storybook/preview"; -import { disabledRules } from "../../test/accessibility/rules/specific/date-input/disabledRules"; +import disabledRules from "../../test/accessibility/rules/specific/date-input/disabledRules"; import DxcContainer from "../container/Container"; import Calendar from "./Calendar"; import DxcDateInput from "./DateInput"; import DxcDatePicker from "./DatePicker"; import YearPicker from "./YearPicker"; -import { Meta, StoryObj } from "@storybook/react"; export default { title: "Date Input", @@ -19,8 +18,11 @@ export default { a11y: { config: { rules: [ - ...disabledRules.map((ruleId) => ({ id: ruleId, enabled: false })), - ...preview?.parameters?.a11y?.config?.rules, + ...disabledRules.map((ruleId) => ({ + id: ruleId, + reviewOnFail: true, + })), + ...(preview?.parameters?.a11y?.config?.rules || []), ], }, }, @@ -225,7 +227,9 @@ export const Chromatic: Story = { play: async ({ canvasElement }) => { const canvas = within(canvasElement); const firstDateInput = canvas.getAllByRole("combobox")[0]; - firstDateInput != null && (await userEvent.click(firstDateInput)); + if (firstDateInput != null) { + await userEvent.click(firstDateInput); + } await fireEvent.click(screen.getByText("April 1905")); }, }; @@ -244,7 +248,9 @@ export const DatePickerStates: Story = { play: async ({ canvasElement }) => { const canvas = within(canvasElement); const dateBtn = canvas.getAllByRole("combobox")[0]; - dateBtn != null && (await userEvent.click(dateBtn)); + if (dateBtn != null) { + await userEvent.click(dateBtn); + } }, }; @@ -261,7 +267,9 @@ export const DatePickerTooltipPrevious: Story = { play: async ({ canvasElement }) => { const canvas = within(canvasElement); const previousMonthButton = canvas.getAllByRole("button")[0]; - previousMonthButton != null && (await userEvent.hover(previousMonthButton)); + if (previousMonthButton != null) { + await userEvent.hover(previousMonthButton); + } }, }; @@ -270,6 +278,8 @@ export const DatePickerTooltipAfter: Story = { play: async ({ canvasElement }) => { const canvas = within(canvasElement); const afterMonthButton = canvas.getAllByRole("button")[2]; - afterMonthButton != null && (await userEvent.hover(afterMonthButton)); + if (afterMonthButton != null) { + await userEvent.hover(afterMonthButton); + } }, }; diff --git a/packages/lib/src/date-input/DateInput.test.tsx b/packages/lib/src/date-input/DateInput.test.tsx index 1975837246..f9c7188d97 100644 --- a/packages/lib/src/date-input/DateInput.test.tsx +++ b/packages/lib/src/date-input/DateInput.test.tsx @@ -2,17 +2,15 @@ import { fireEvent, render } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import dayjs from "dayjs"; import DxcDateInput from "./DateInput"; +import MockDOMRect from "../../test/mocks/domRectMock"; // Mocking DOMRect for Radix Primitive Popover -(global as any).globalThis = global; -(global as any).DOMRect = { - fromRect: () => ({ top: 0, left: 0, bottom: 0, right: 0, width: 0, height: 0 }), -}; -(global as any).ResizeObserver = class ResizeObserver { - observe() {} - unobserve() {} - disconnect() {} -}; +global.DOMRect = MockDOMRect; +global.ResizeObserver = jest.fn().mockImplementation(() => ({ + observe: jest.fn(), + unobserve: jest.fn(), + disconnect: jest.fn(), +})); describe("DateInput component tests", () => { test("Renders with correct label, helper text, optional, placeholder and clearable action", () => { @@ -26,7 +24,9 @@ describe("DateInput component tests", () => { expect(input.getAttribute("placeholder")).toBe("DD-MM-YYYY"); userEvent.type(input, "10/10/2010"); const closeAction = getAllByRole("button")[0]; - closeAction != null && userEvent.click(closeAction); + if (closeAction != null) { + userEvent.click(closeAction); + } expect(input.value).toBe(""); }); test("Renders with custom error", () => { @@ -76,7 +76,10 @@ describe("DateInput component tests", () => { userEvent.keyboard("/"); userEvent.keyboard("2010"); expect(onChange).toHaveBeenCalledTimes(10); - expect(onChange).toHaveBeenCalledWith({ value: "10/90/2010", error: "Invalid date." }); + expect(onChange).toHaveBeenCalledWith({ + value: "10/90/2010", + error: "Invalid date.", + }); }); test("Calendar renders with correct date: today's date", () => { const { getByText, getByRole, getAllByText } = render(<DxcDateInput />); @@ -136,12 +139,19 @@ describe("DateInput component tests", () => { const calendarAction = getByRole("combobox"); userEvent.click(calendarAction); const dayButton = getAllByText("10")[0]; - dayButton != null && fireEvent.click(dayButton); + if (dayButton != null) { + fireEvent.click(dayButton); + } let d = dayjs(); d = d.set("date", 10); expect(getAllByText(d.get("date"))[0]?.getAttribute("aria-selected")).toBe("true"); expect(getByText(d.format("MMMM YYYY"))).toBeTruthy(); - fireEvent.keyDown(document, { key: "Escape", code: "Escape", keyCode: 27, charCode: 27 }); + fireEvent.keyDown(document, { + key: "Escape", + code: "Escape", + keyCode: 27, + charCode: 27, + }); expect(input.value).toBe(d.format("M-DD-YYYY")); }); test("Changing months using the arrows", () => { @@ -154,14 +164,14 @@ describe("DateInput component tests", () => { d = d.set("date", 10); expect(getByText(d.format("MMMM YYYY"))).toBeTruthy(); const previousMonthButton = getAllByRole("button")[0]; + expect(previousMonthButton?.getAttribute("aria-label")).toBe("Previous month"); if (previousMonthButton != null) { - expect(previousMonthButton.getAttribute("aria-label")).toBe("Previous month"); userEvent.click(previousMonthButton); } expect(getByText(d.set("month", d.get("month") - 1).format("MMMM YYYY"))).toBeTruthy(); const nextMonthButton = getAllByRole("button")[2]; + expect(nextMonthButton?.getAttribute("aria-label")).toBe("Next month"); if (nextMonthButton != null) { - expect(nextMonthButton.getAttribute("aria-label")).toBe("Next month"); userEvent.click(nextMonthButton); } expect(getByText(d.format("MMMM YYYY"))).toBeTruthy(); @@ -174,12 +184,19 @@ describe("DateInput component tests", () => { const calendarAction = getByRole("combobox"); userEvent.click(calendarAction); const dayButton = getAllByText("31")[0]; - dayButton != null && fireEvent.click(dayButton); + if (dayButton != null) { + fireEvent.click(dayButton); + } let d = dayjs("10-08-2021", "DD-MM-YYYY", true); d = d.set("date", 31).set("month", 6); expect(getAllByText(d.get("date"))[0]?.getAttribute("aria-selected")).toBe("true"); expect(getByText(d.format("MMMM YYYY"))).toBeTruthy(); - fireEvent.keyDown(document, { key: "Escape", code: "Escape", keyCode: 27, charCode: 27 }); + fireEvent.keyDown(document, { + key: "Escape", + code: "Escape", + keyCode: 27, + charCode: 27, + }); expect(input.value).toBe(d.format("DD-MM-YYYY")); }); test("Selecting a year from the calendar year picker", () => { @@ -195,45 +212,48 @@ describe("DateInput component tests", () => { fireEvent.keyDown(document, { key: "Escape", code: "Escape", keyCode: 27, charCode: 27 }); expect(input.value).toBe(d.format("DD-MM-YYYY")); }); - test("Selecting a date from the calendar (using keyboard presses)", async () => { + test("Selecting a date from the calendar (using keyboard presses)", () => { const { getByRole, getAllByText, getByText } = render(<DxcDateInput />); const calendarAction = getByRole("combobox"); const input = getByRole("textbox") as HTMLInputElement; userEvent.type(input, "01-01-2010"); expect(input.value).toBe("01-01-2010"); - await userEvent.click(calendarAction); + userEvent.click(calendarAction); const day1 = getAllByText("1")[0]; expect(document.activeElement === day1).toBeTruthy(); - day1 != null && + if (day1 != null) { fireEvent.keyDown(day1, { key: "ArrowRight", code: "ArrowRight", keyCode: 39, charCode: 39, }); + } let day2 = getAllByText("2")[0]; expect(document.activeElement === day2).toBeTruthy(); - day2 != null && + if (day2 != null) { fireEvent.keyDown(day2, { key: "PageUp", code: "PageUp", keyCode: 33, charCode: 33, }); + } day2 = getAllByText("2")[0]; expect(document.activeElement === day2).toBeTruthy(); expect(getByText("December 2009")).toBeTruthy(); - day2 != null && + if (day2 != null) { fireEvent.keyDown(day2, { key: "PageDown", code: "PageDown", keyCode: 34, charCode: 34, }); + } day2 = getAllByText("2")[0]; expect(document.activeElement === day2).toBeTruthy(); expect(getByText("January 2010")).toBeTruthy(); - day2 != null && + if (day2 != null) { fireEvent.keyDown(day2, { key: "PageDown", code: "PageDown", @@ -241,9 +261,10 @@ describe("DateInput component tests", () => { charCode: 34, shiftKey: true, }); + } day2 = getAllByText("2")[0]; expect(getByText("January 2011")).toBeTruthy(); - day2 != null && + if (day2 != null) { fireEvent.keyDown(day2, { key: "PageUp", code: "PageUp", @@ -251,10 +272,13 @@ describe("DateInput component tests", () => { charCode: 33, shiftKey: true, }); + } day2 = getAllByText("2")[0]; expect(getByText("January 2010")).toBeTruthy(); expect(document.activeElement === day2).toBeTruthy(); - day2 != null && fireEvent.click(day2, { key: " ", code: "Space", keyCode: 32, charCode: 32 }); + if (day2 != null) { + fireEvent.click(day2, { key: " ", code: "Space", keyCode: 32, charCode: 32 }); + } expect(day2?.getAttribute("aria-selected")).toBe("true"); fireEvent.keyDown(document, { key: "Escape", code: "Escape", keyCode: 27, charCode: 27 }); expect(input.value).toBe("02-01-2010"); @@ -271,15 +295,16 @@ describe("DateInput component tests", () => { const day8 = getAllByText("8")[0]; const day10 = getAllByText("10")[0]; const day15 = getAllByText("15")[0]; - day1 != null && + if (day1 != null) { fireEvent.keyDown(day1, { key: "ArrowDown", code: "ArrowDown", keyCode: 40, charCode: 40, }); + } + expect(document.activeElement === day8).toBeTruthy(); if (day8 != null) { - expect(document.activeElement === day8).toBeTruthy(); fireEvent.keyDown(day8, { key: "ArrowDown", code: "ArrowDown", @@ -287,8 +312,8 @@ describe("DateInput component tests", () => { charCode: 40, }); } + expect(document.activeElement === day15).toBeTruthy(); if (day15 != null) { - expect(document.activeElement === day15).toBeTruthy(); fireEvent.keyDown(day15, { key: "ArrowUp", code: "ArrowUp", @@ -296,8 +321,8 @@ describe("DateInput component tests", () => { charCode: 38, }); } + expect(document.activeElement === day8).toBeTruthy(); if (day8 != null) { - expect(document.activeElement === day8).toBeTruthy(); fireEvent.keyDown(day8, { key: "End", code: "End", @@ -305,8 +330,8 @@ describe("DateInput component tests", () => { charCode: 35, }); } + expect(document.activeElement === day10).toBeTruthy(); if (day10 != null) { - expect(document.activeElement === day10).toBeTruthy(); fireEvent.keyDown(day10, { key: "Home", code: "Home", @@ -339,10 +364,16 @@ describe("DateInput component tests", () => { userEvent.type(input, "10-10-"); expect(input.value).toBe("10-10-"); expect(onChange).toHaveBeenCalledTimes(6); - expect(onChange).toHaveBeenCalledWith({ value: "10-10-", error: "Invalid date." }); + expect(onChange).toHaveBeenCalledWith({ + value: "10-10-", + error: "Invalid date.", + }); fireEvent.blur(input); expect(onBlur).toHaveBeenCalled(); - expect(onBlur).toHaveBeenCalledWith({ value: "10-10-", error: "Invalid date." }); + expect(onBlur).toHaveBeenCalledWith({ + value: "10-10-", + error: "Invalid date.", + }); }); test("onBlur function removes the error when it is fixed", () => { const onBlur = jest.fn(); @@ -353,7 +384,10 @@ describe("DateInput component tests", () => { expect(input.value).toBe("test"); fireEvent.blur(input); expect(onBlur).toHaveBeenCalled(); - expect(onBlur).toHaveBeenCalledWith({ value: "test", error: "Invalid date." }); + expect(onBlur).toHaveBeenCalledWith({ + value: "test", + error: "Invalid date.", + }); userEvent.clear(input); userEvent.type(input, "20-02-2002"); expect(input.value).toBe("20-02-2002"); @@ -369,7 +403,10 @@ describe("DateInput component tests", () => { expect(input.value).toBe("test"); fireEvent.blur(input); expect(onBlur).toHaveBeenCalled(); - expect(onBlur).toHaveBeenCalledWith({ value: "test", error: "Invalid date." }); + expect(onBlur).toHaveBeenCalledWith({ + value: "test", + error: "Invalid date.", + }); userEvent.clear(input); fireEvent.blur(input); expect(onBlur).toHaveBeenCalled(); @@ -400,7 +437,11 @@ describe("DateInput component tests", () => { const { getByRole, queryByText } = render(<DxcDateInput disabled />); const calendarAction = getByRole("button"); const d = new Date(); - const options: Intl.DateTimeFormatOptions = { weekday: "short", month: "short", day: "numeric" }; + const options: Intl.DateTimeFormatOptions = { + weekday: "short", + month: "short", + day: "numeric", + }; const input = getByRole("textbox") as HTMLInputElement; expect(input.disabled).toBeTruthy(); userEvent.click(calendarAction); @@ -421,8 +462,8 @@ describe("DateInput component tests", () => { const datePicker = getByRole("dialog"); expect(datePicker.getAttribute("aria-modal")).toBe("true"); expect(calendarAction.getAttribute("aria-expanded")).toBe("true"); - const ariaDescribedBy = calendarAction.getAttribute("aria-describedby"); - ariaDescribedBy != null && expect(document.getElementById(ariaDescribedBy)).toBeTruthy(); + const ariaDescribedBy = calendarAction.getAttribute("aria-describedby") ?? ""; + expect(document.getElementById(ariaDescribedBy)).toBeTruthy(); expect( calendarAction.getAttribute("aria-describedby") === calendarAction.getAttribute("aria-controls") ).toBeTruthy(); @@ -446,7 +487,9 @@ describe("DateInput component tests", () => { userEvent.click(getByText("October 1910")); userEvent.click(getByText("2010")); const day1 = getAllByText("1")[0]; - day1 != null && userEvent.click(day1); + if (day1 != null) { + userEvent.click(day1); + } expect(input.value).toBe("01-10-10"); userEvent.type(calendarAction, "{esc}"); fireEvent.change(input, { target: { value: "21-10-80" } }); diff --git a/packages/lib/src/date-input/DateInput.tsx b/packages/lib/src/date-input/DateInput.tsx index 1e45731940..8175d0b490 100644 --- a/packages/lib/src/date-input/DateInput.tsx +++ b/packages/lib/src/date-input/DateInput.tsx @@ -342,4 +342,6 @@ const DxcDateInput = forwardRef<RefType, DateInputPropsType>( } ); +DxcDateInput.displayName = "DxcDateInput"; + export default DxcDateInput; diff --git a/packages/lib/src/dialog/Dialog.accessibility.test.tsx b/packages/lib/src/dialog/Dialog.accessibility.test.tsx index e61471afdd..78b2edd427 100644 --- a/packages/lib/src/dialog/Dialog.accessibility.test.tsx +++ b/packages/lib/src/dialog/Dialog.accessibility.test.tsx @@ -1,16 +1,14 @@ import { render } from "@testing-library/react"; import { axe } from "../../test/accessibility/axe-helper"; import DxcDialog from "./Dialog"; +import MockDOMRect from "../../test/mocks/domRectMock"; -(global as any).globalThis = global; -(global as any).DOMRect = { - fromRect: () => ({ top: 0, left: 0, bottom: 0, right: 0, width: 0, height: 0 }), -}; -(global as any).ResizeObserver = class ResizeObserver { - observe() {} - unobserve() {} - disconnect() {} -}; +global.DOMRect = MockDOMRect; +global.ResizeObserver = jest.fn().mockImplementation(() => ({ + observe: jest.fn(), + unobserve: jest.fn(), + disconnect: jest.fn(), +})); describe("Dialog component accessibility tests", () => { it("Should not have basic accessibility issues", async () => { diff --git a/packages/lib/src/dialog/Dialog.stories.tsx b/packages/lib/src/dialog/Dialog.stories.tsx index 57ecdb3603..94323ddfe0 100644 --- a/packages/lib/src/dialog/Dialog.stories.tsx +++ b/packages/lib/src/dialog/Dialog.stories.tsx @@ -1,16 +1,16 @@ +import { Meta, StoryObj } from "@storybook/react"; import { INITIAL_VIEWPORTS } from "@storybook/addon-viewport"; -import { screen, userEvent, within } from "@storybook/test"; +import { screen, userEvent } from "@storybook/test"; import ExampleContainer from "../../.storybook/components/ExampleContainer"; import Title from "../../.storybook/components/Title"; -import DxcAlert from "../alert/Alert"; import DxcButton from "../button/Button"; import DxcFlex from "../flex/Flex"; import DxcHeading from "../heading/Heading"; import DxcInset from "../inset/Inset"; import DxcParagraph from "../paragraph/Paragraph"; +import DxcAlert from "../alert/Alert"; import DxcTextInput from "../text-input/TextInput"; import DxcDialog from "./Dialog"; -import { Meta, StoryObj } from "@storybook/react"; import DxcSelect from "../select/Select"; import DxcDateInput from "../date-input/DateInput"; import DxcDropdown from "../dropdown/Dropdown"; @@ -425,7 +425,9 @@ export const DropdownDialog: Story = { render: DialogWithDropdown, play: async () => { const buttons = await screen.findAllByRole("button"); - buttons[0] && (await userEvent.click(buttons[0])); + if (buttons[0]) { + await userEvent.click(buttons[0]); + } }, }; @@ -441,6 +443,8 @@ export const TooltipDialog: Story = { render: DialogWithTooltip, play: async () => { const buttons = await screen.findAllByRole("button"); - buttons[0] && (await userEvent.hover(buttons[0])); + if (buttons[0]) { + await userEvent.hover(buttons[0]); + } }, }; diff --git a/packages/lib/src/dialog/Dialog.test.tsx b/packages/lib/src/dialog/Dialog.test.tsx index 7909c8d977..794597f614 100644 --- a/packages/lib/src/dialog/Dialog.test.tsx +++ b/packages/lib/src/dialog/Dialog.test.tsx @@ -14,16 +14,14 @@ import DxcTextarea from "../textarea/Textarea"; import DxcDialog from "./Dialog"; import DxcTooltip from "../tooltip/Tooltip"; import DxcAlert from "../alert/Alert"; +import MockDOMRect from "../../test/mocks/domRectMock"; -(global as any).globalThis = global; -(global as any).DOMRect = { - fromRect: () => ({ top: 0, left: 0, bottom: 0, right: 0, width: 0, height: 0 }), -}; -(global as any).ResizeObserver = class ResizeObserver { - observe() {} - unobserve() {} - disconnect() {} -}; +global.DOMRect = MockDOMRect; +global.ResizeObserver = jest.fn().mockImplementation(() => ({ + observe: jest.fn(), + unobserve: jest.fn(), + disconnect: jest.fn(), +})); const options = [ { label: "Female", value: "female" }, @@ -62,7 +60,12 @@ describe("Dialog component tests", () => { test("Calls correct function onCloseClick when 'Escape' key is pressed", () => { const onCloseClick = jest.fn(); const { getByRole } = render(<DxcDialog onCloseClick={onCloseClick}>dialog-text</DxcDialog>); - fireEvent.keyDown(getByRole("dialog"), { key: "Escape", code: "Escape", keyCode: 27, charCode: 27 }); + fireEvent.keyDown(getByRole("dialog"), { + key: "Escape", + code: "Escape", + keyCode: 27, + charCode: 27, + }); expect(onCloseClick).toHaveBeenCalled(); }); test("Does not call function onCloseClick when 'Escape' key is pressed while a child popover is opened", () => { @@ -74,8 +77,12 @@ describe("Dialog component tests", () => { ); const calendarAction = getByRole("combobox"); userEvent.click(calendarAction); - document.activeElement != null && - fireEvent.keyDown(document.activeElement, { key: "Escape", code: "Escape", keyCode: 27, charCode: 27 }); + fireEvent.keyDown(document.activeElement!, { + key: "Escape", + code: "Escape", + keyCode: 27, + charCode: 27, + }); expect(onCloseClick).not.toHaveBeenCalled(); }); }); @@ -288,14 +295,18 @@ describe("Dialog component: Focus lock tests", () => { ); const select = getAllByRole("combobox")[0]; expect(document.activeElement).toEqual(select); - select != null && fireEvent.keyDown(select, { key: "ArrowDown", code: "ArrowDown", keyCode: 40, charCode: 40 }); + if (select != null) { + fireEvent.keyDown(select, { key: "ArrowDown", code: "ArrowDown", keyCode: 40, charCode: 40 }); + } expect(queryByRole("listbox")).toBeTruthy(); userEvent.tab(); userEvent.tab(); userEvent.keyboard("{Enter}"); expect(getAllByRole("dialog")[1]).toBeTruthy(); const dialog = getAllByRole("dialog")[0]; - dialog != null && userEvent.click(dialog); + if (dialog != null) { + userEvent.click(dialog); + } userEvent.tab(); userEvent.tab(); userEvent.tab(); diff --git a/packages/lib/src/dialog/types.ts b/packages/lib/src/dialog/types.ts index cbeb075cbd..558068c6e3 100644 --- a/packages/lib/src/dialog/types.ts +++ b/packages/lib/src/dialog/types.ts @@ -37,7 +37,7 @@ type Props = { * If true the focusLock functionality won't work. * @private */ - disableFocusLock?: boolean + disableFocusLock?: boolean; }; export default Props; diff --git a/packages/lib/src/divider/Divider.tsx b/packages/lib/src/divider/Divider.tsx index af67e6f288..17ee8757e9 100644 --- a/packages/lib/src/divider/Divider.tsx +++ b/packages/lib/src/divider/Divider.tsx @@ -14,8 +14,8 @@ const StyledDivider = styled.hr<DividerPropsType>` ${orientation === "horizontal" ? "height" : "width"}: 0; ${ orientation === "horizontal" - ? "border-width: " + (weight === "regular" ? "var(--border-width-s) 0 0 0" : "var(--border-width-m) 0 0 0") - : "border-width: " + (weight === "regular" ? "0 0 0 var(--border-width-s)" : "0 0 0 var(--border-width-m)") + ? `border-width: ${weight === "regular" ? "var(--border-width-s) 0 0 0" : "var(--border-width-m) 0 0 0"}` + : `border-width: ${weight === "regular" ? "0 0 0 var(--border-width-s)" : "0 0 0 var(--border-width-m)"}` }; margin: 0; `} diff --git a/packages/lib/src/dropdown/Dropdown.accessibility.test.tsx b/packages/lib/src/dropdown/Dropdown.accessibility.test.tsx index 4b19d2ceb9..ab3889b944 100644 --- a/packages/lib/src/dropdown/Dropdown.accessibility.test.tsx +++ b/packages/lib/src/dropdown/Dropdown.accessibility.test.tsx @@ -1,6 +1,7 @@ import { render } from "@testing-library/react"; import { axe } from "../../test/accessibility/axe-helper"; import DxcDropdown from "./Dropdown"; +import MockDOMRect from "../../test/mocks/domRectMock"; const iconSVG = ( <svg viewBox="0 0 24 24" height="24" width="24" fill="currentColor"> @@ -12,15 +13,12 @@ const iconSVG = ( const iconUrl = "https://iconape.com/wp-content/files/yd/367773/svg/logo-linkedin-logo-icon-png-svg.png"; // Mocking DOMRect for Radix Primitive Popover -(global as any).globalThis = global; -(global as any).DOMRect = { - fromRect: () => ({ top: 0, left: 0, bottom: 0, right: 0, width: 0, height: 0 }), -}; -(global as any).ResizeObserver = class ResizeObserver { - observe() {} - unobserve() {} - disconnect() {} -}; +global.DOMRect = MockDOMRect; +global.ResizeObserver = jest.fn().mockImplementation(() => ({ + observe: jest.fn(), + unobserve: jest.fn(), + disconnect: jest.fn(), +})); const options = [ { diff --git a/packages/lib/src/dropdown/Dropdown.stories.tsx b/packages/lib/src/dropdown/Dropdown.stories.tsx index 783408a040..6a02758f6f 100644 --- a/packages/lib/src/dropdown/Dropdown.stories.tsx +++ b/packages/lib/src/dropdown/Dropdown.stories.tsx @@ -1,10 +1,10 @@ +import { Meta, StoryObj } from "@storybook/react"; import { userEvent, within } from "@storybook/test"; import ExampleContainer from "../../.storybook/components/ExampleContainer"; import Title from "../../.storybook/components/Title"; import DxcDropdown from "./Dropdown"; import DropdownMenu from "./DropdownMenu"; import { Option } from "./types"; -import { Meta, StoryObj } from "@storybook/react"; export default { title: "Dropdown", @@ -80,7 +80,7 @@ const optionWithIcon: Option[] = [ }, ]; -const optionsIcon: any = options.map((op, i) => ({ ...op, icon: icons[i] })); +const optionsIcon = options.map((op, i) => ({ ...op, icon: icons[i] })); const Dropdown = () => ( <> @@ -234,7 +234,12 @@ const DropdownListStates = () => ( zIndex: "130", }} > - <DxcDropdown label="Select a platform" options={defaultOptions} onSelectOption={(option) => {}} size="medium" /> + <DxcDropdown + label="Select a platform" + options={defaultOptions} + onSelectOption={(_option) => {}} + size="medium" + /> <button style={{ zIndex: "1", width: "100px" }}>Submit</button> </div> </ExampleContainer> @@ -328,7 +333,9 @@ export const Chromatic: Story = { const canvas = within(canvasElement); const buttonList = canvas.getAllByRole("button"); const lastButton = buttonList[buttonList.length - 1]; - lastButton != null && (await userEvent.click(lastButton)); + if (lastButton != null) { + await userEvent.click(lastButton); + } }, }; @@ -337,7 +344,9 @@ export const MenuStates: Story = { play: async ({ canvasElement }) => { const canvas = within(canvasElement); const dropdownTrigger = canvas.getAllByRole("button")[0]; - dropdownTrigger != null && (await userEvent.click(dropdownTrigger)); + if (dropdownTrigger != null) { + await userEvent.click(dropdownTrigger); + } }, }; diff --git a/packages/lib/src/dropdown/Dropdown.test.tsx b/packages/lib/src/dropdown/Dropdown.test.tsx index f2ccc574fd..ba2290eb71 100644 --- a/packages/lib/src/dropdown/Dropdown.test.tsx +++ b/packages/lib/src/dropdown/Dropdown.test.tsx @@ -1,17 +1,15 @@ import { fireEvent, render } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import DxcDropdown from "./Dropdown"; +import MockDOMRect from "../../test/mocks/domRectMock"; // Mocking DOMRect for Radix Primitive Popover -(global as any).globalThis = global; -(global as any).DOMRect = { - fromRect: () => ({ top: 0, left: 0, bottom: 0, right: 0, width: 0, height: 0 }), -}; -(global as any).ResizeObserver = class ResizeObserver { - observe() {} - unobserve() {} - disconnect() {} -}; +global.DOMRect = MockDOMRect; +global.ResizeObserver = jest.fn().mockImplementation(() => ({ + observe: jest.fn(), + unobserve: jest.fn(), + disconnect: jest.fn(), +})); const options = [ { @@ -33,7 +31,7 @@ const options = [ ]; describe("Dropdown component tests", () => { - test("Renders with correct aria attributes", async () => { + test("Renders with correct aria attributes", () => { const onSelectOption = jest.fn(); const { getAllByRole, getByRole } = render( <DxcDropdown options={options} label="dropdown-test" onSelectOption={onSelectOption} /> @@ -42,7 +40,7 @@ describe("Dropdown component tests", () => { expect(dropdown.getAttribute("aria-haspopup")).toBe("true"); expect(dropdown.getAttribute("aria-expanded")).toBeNull(); expect(dropdown.getAttribute("aria-activedescendant")).toBeNull(); - await userEvent.click(dropdown); + userEvent.click(dropdown); const menu = getByRole("menu"); expect(dropdown.getAttribute("aria-controls")).toBe(menu.id); expect(dropdown.getAttribute("aria-expanded")).toBe("true"); @@ -51,45 +49,45 @@ describe("Dropdown component tests", () => { expect(menu.getAttribute("aria-labelledby")).toBe(dropdown.id); expect(getAllByRole("menuitem").length).toBe(4); }); - test("Button trigger opens and closes the menu options when clicked", async () => { + test("Button trigger opens and closes the menu options when clicked", () => { const onSelectOption = jest.fn(); const { getByRole, queryByRole, getByText } = render( <DxcDropdown options={options} label="dropdown-test" onSelectOption={onSelectOption} /> ); const dropdown = getByRole("button"); expect(queryByRole("menu")).toBeFalsy(); - await userEvent.click(dropdown); + userEvent.click(dropdown); expect(queryByRole("menu")).toBeTruthy(); expect(getByText("Amazon")).toBeTruthy(); expect(getByText("Ebay")).toBeTruthy(); expect(getByText("Wallapop")).toBeTruthy(); expect(getByText("Aliexpress")).toBeTruthy(); - await userEvent.click(dropdown); + userEvent.click(dropdown); expect(queryByRole("menu")).toBeFalsy(); }); - test("Button trigger is not interactive when disabled", async () => { + test("Button trigger is not interactive when disabled", () => { const onSelectOption = jest.fn(); const { getByRole, queryByRole, queryByText } = render( <DxcDropdown disabled options={options} label="dropdown-test" onSelectOption={onSelectOption} /> ); const dropdown = getByRole("button"); expect(queryByRole("menu")).toBeFalsy(); - await userEvent.click(dropdown); + userEvent.click(dropdown); expect(queryByRole("menu")).toBeFalsy(); expect(queryByText("Amazon")).toBeFalsy(); - await userEvent.click(dropdown); + userEvent.click(dropdown); expect(queryByRole("menu")).toBeFalsy(); expect(dropdown.getAttribute("aria-expanded")).toBeNull(); }); - test("onSelectOption function is called correctly when an option is clicked", async () => { + test("onSelectOption function is called correctly when an option is clicked", () => { const onSelectOption = jest.fn(); const { getByText } = render( <DxcDropdown options={options} onSelectOption={onSelectOption} label="dropdown-test" /> ); const dropdown = getByText("dropdown-test"); - await userEvent.click(dropdown); + userEvent.click(dropdown); const option = getByText("Aliexpress"); - await userEvent.click(option); + userEvent.click(option); expect(onSelectOption).toHaveBeenCalledWith("4"); }); test("When expandOnHover is true, the dropdown trigger shows and hides the menu when it is hovered", () => { @@ -105,13 +103,13 @@ describe("Dropdown component tests", () => { expect(document.activeElement === menu).toBeTruthy(); expect(menu.getAttribute("aria-activedescendant")).toBe(`${menu.id}-option-0`); }); - test("The menu is closed when the dropdown loses the focus (blur)", async () => { + test("The menu is closed when the dropdown loses the focus (blur)", () => { const onSelectOption = jest.fn(); const { getByRole, queryByRole } = render( <DxcDropdown options={options} label="dropdown-test" onSelectOption={onSelectOption} /> ); const dropdown = getByRole("button"); - await userEvent.click(dropdown); + userEvent.click(dropdown); expect(getByRole("menu")).toBeTruthy(); fireEvent.blur(getByRole("menu")); expect(queryByRole("menu")).toBeFalsy(); @@ -122,7 +120,12 @@ describe("Dropdown component tests", () => { <DxcDropdown options={options} label="dropdown-test" onSelectOption={onSelectOption} /> ); const dropdown = getByRole("button"); - fireEvent.keyDown(dropdown, { key: "ArrowUp", code: "ArrowUp", keyCode: 38, charCode: 38 }); + fireEvent.keyDown(dropdown, { + key: "ArrowUp", + code: "ArrowUp", + keyCode: 38, + charCode: 38, + }); const menu = getByRole("menu"); expect(menu).toBeTruthy(); expect(document.activeElement === menu).toBeTruthy(); @@ -134,7 +137,12 @@ describe("Dropdown component tests", () => { <DxcDropdown options={options} label="dropdown-test" onSelectOption={onSelectOption} /> ); const dropdown = getByRole("button"); - fireEvent.keyDown(dropdown, { key: "ArrowDown", code: "ArrowDown", keyCode: 40, charCode: 40 }); + fireEvent.keyDown(dropdown, { + key: "ArrowDown", + code: "ArrowDown", + keyCode: 40, + charCode: 40, + }); const menu = getByRole("menu"); expect(menu).toBeTruthy(); expect(document.activeElement === menu).toBeTruthy(); @@ -146,7 +154,12 @@ describe("Dropdown component tests", () => { <DxcDropdown options={options} label="dropdown-test" onSelectOption={onSelectOption} /> ); const dropdown = getByRole("button"); - fireEvent.keyDown(dropdown, { key: "Enter", code: "Enter", keyCode: 13, charCode: 13 }); + fireEvent.keyDown(dropdown, { + key: "Enter", + code: "Enter", + keyCode: 13, + charCode: 13, + }); const menu = getByRole("menu"); expect(menu).toBeTruthy(); expect(document.activeElement === menu).toBeTruthy(); @@ -158,7 +171,12 @@ describe("Dropdown component tests", () => { <DxcDropdown options={options} label="dropdown-test" onSelectOption={onSelectOption} /> ); const dropdown = getByRole("button"); - fireEvent.keyDown(dropdown, { key: " ", code: "Space", keyCode: 32, charCode: 32 }); + fireEvent.keyDown(dropdown, { + key: " ", + code: "Space", + keyCode: 32, + charCode: 32, + }); const menu = getByRole("menu"); expect(menu).toBeTruthy(); expect(document.activeElement === menu).toBeTruthy(); @@ -169,72 +187,137 @@ describe("Dropdown component tests", () => { const { getByRole } = render( <DxcDropdown onSelectOption={onSelectOption} options={options} label="dropdown-test" /> ); - fireEvent.keyDown(getByRole("button"), { key: "ArrowUp", code: "ArrowUp", keyCode: 38, charCode: 38 }); + fireEvent.keyDown(getByRole("button"), { + key: "ArrowUp", + code: "ArrowUp", + keyCode: 38, + charCode: 38, + }); const menu = getByRole("menu"); - fireEvent.keyDown(menu, { key: "ArrowUp", code: "ArrowUp", keyCode: 38, charCode: 38 }); + fireEvent.keyDown(menu, { + key: "ArrowUp", + code: "ArrowUp", + keyCode: 38, + charCode: 38, + }); expect(document.activeElement === menu).toBeTruthy(); expect(menu.getAttribute("aria-activedescendant")).toBe(`${menu.id}-option-2`); - fireEvent.keyDown(menu, { key: "Enter", code: "Enter", keyCode: 13, charCode: 13 }); + fireEvent.keyDown(menu, { + key: "Enter", + code: "Enter", + keyCode: 13, + charCode: 13, + }); expect(onSelectOption).toHaveBeenCalledWith("3"); }); - test("Menu key events — Arrow up, if focus is on the first menu item, moves focus to the last menu item.", async () => { + test("Menu key events — Arrow up, if focus is on the first menu item, moves focus to the last menu item.", () => { const onSelectOption = jest.fn(); const { getByRole } = render( <DxcDropdown onSelectOption={onSelectOption} options={options} label="dropdown-test" /> ); - await userEvent.click(getByRole("button")); + userEvent.click(getByRole("button")); const menu = getByRole("menu"); - fireEvent.keyDown(menu, { key: "ArrowUp", code: "ArrowUp", keyCode: 38, charCode: 38 }); + fireEvent.keyDown(menu, { + key: "ArrowUp", + code: "ArrowUp", + keyCode: 38, + charCode: 38, + }); expect(document.activeElement === menu).toBeTruthy(); expect(menu.getAttribute("aria-activedescendant")).toBe(`${menu.id}-option-3`); - fireEvent.keyDown(menu, { key: "Enter", code: "Enter", keyCode: 13, charCode: 13 }); + fireEvent.keyDown(menu, { + key: "Enter", + code: "Enter", + keyCode: 13, + charCode: 13, + }); expect(onSelectOption).toHaveBeenCalledWith("4"); }); - test("Menu key events — Arrow down moves the focus to the next menu item", async () => { + test("Menu key events — Arrow down moves the focus to the next menu item", () => { const onSelectOption = jest.fn(); const { getByRole } = render( <DxcDropdown onSelectOption={onSelectOption} options={options} label="dropdown-test" /> ); - await userEvent.click(getByRole("button")); + userEvent.click(getByRole("button")); const menu = getByRole("menu"); - fireEvent.keyDown(menu, { key: "ArrowDown", code: "ArrowDown", keyCode: 40, charCode: 40 }); - fireEvent.keyDown(menu, { key: "ArrowDown", code: "ArrowDown", keyCode: 40, charCode: 40 }); + fireEvent.keyDown(menu, { + key: "ArrowDown", + code: "ArrowDown", + keyCode: 40, + charCode: 40, + }); + fireEvent.keyDown(menu, { + key: "ArrowDown", + code: "ArrowDown", + keyCode: 40, + charCode: 40, + }); expect(document.activeElement === menu).toBeTruthy(); expect(menu.getAttribute("aria-activedescendant")).toBe(`${menu.id}-option-2`); - fireEvent.keyDown(menu, { key: "Enter", code: "Enter", keyCode: 13, charCode: 13 }); + fireEvent.keyDown(menu, { + key: "Enter", + code: "Enter", + keyCode: 13, + charCode: 13, + }); expect(onSelectOption).toHaveBeenCalledWith("3"); }); - test("Menu key events — Arrow down, if focus is on the last menu item, moves focus to the first menu item. ", () => { + test("Menu key events — Arrow down, if focus is on the last menu item, moves focus to the first menu item.", () => { const onSelectOption = jest.fn(); const { getByRole } = render( <DxcDropdown onSelectOption={onSelectOption} options={options} label="dropdown-test" /> ); - fireEvent.keyDown(getByRole("button"), { key: "ArrowUp", code: "ArrowUp", keyCode: 38, charCode: 38 }); + fireEvent.keyDown(getByRole("button"), { + key: "ArrowUp", + code: "ArrowUp", + keyCode: 38, + charCode: 38, + }); const menu = getByRole("menu"); - fireEvent.keyDown(menu, { key: "ArrowDown", code: "ArrowDown", keyCode: 40, charCode: 40 }); + fireEvent.keyDown(menu, { + key: "ArrowDown", + code: "ArrowDown", + keyCode: 40, + charCode: 40, + }); expect(document.activeElement === menu).toBeTruthy(); expect(menu.getAttribute("aria-activedescendant")).toBe(`${menu.id}-option-0`); - fireEvent.keyDown(menu, { key: "Enter", code: "Enter", keyCode: 13, charCode: 13 }); + fireEvent.keyDown(menu, { + key: "Enter", + code: "Enter", + keyCode: 13, + charCode: 13, + }); expect(onSelectOption).toHaveBeenCalledWith("1"); }); - test("Menu key events — Enter key selects the current focused item and closes the menu", async () => { + test("Menu key events — Enter key selects the current focused item and closes the menu", () => { const onSelectOption = jest.fn(); const { getByRole, queryByRole } = render( <DxcDropdown onSelectOption={onSelectOption} options={options} label="dropdown-test" /> ); - await userEvent.click(getByRole("button")); - fireEvent.keyDown(getByRole("menu"), { key: "Enter", code: "Enter", keyCode: 13, charCode: 13 }); + userEvent.click(getByRole("button")); + fireEvent.keyDown(getByRole("menu"), { + key: "Enter", + code: "Enter", + keyCode: 13, + charCode: 13, + }); expect(onSelectOption).toHaveBeenCalledWith("1"); expect(queryByRole("menu")).toBeFalsy(); expect(document.activeElement === getByRole("button")).toBeTruthy(); }); - test("Menu key events — Esc closes the menu and sets focus on the menu button", async () => { + test("Menu key events — Esc closes the menu and sets focus on the menu button", () => { const onSelectOption = jest.fn(); const { getByRole, queryByRole } = render( <DxcDropdown options={options} label="dropdown-test" onSelectOption={onSelectOption} /> ); - await userEvent.click(getByRole("button")); - fireEvent.keyDown(getByRole("menu"), { key: "Esc", code: "Esc", keyCode: 27, charCode: 27 }); + userEvent.click(getByRole("button")); + fireEvent.keyDown(getByRole("menu"), { + key: "Esc", + code: "Esc", + keyCode: 27, + charCode: 27, + }); expect(queryByRole("menu")).toBeFalsy(); expect(document.activeElement === getByRole("button")).toBeTruthy(); }); @@ -243,21 +326,36 @@ describe("Dropdown component tests", () => { const { getByRole } = render( <DxcDropdown options={options} label="dropdown-test-1" onSelectOption={onSelectOption} /> ); - fireEvent.keyDown(getByRole("button"), { key: "ArrowUp", code: "ArrowUp", keyCode: 38, charCode: 38 }); + fireEvent.keyDown(getByRole("button"), { + key: "ArrowUp", + code: "ArrowUp", + keyCode: 38, + charCode: 38, + }); const menu = getByRole("menu"); expect(menu.getAttribute("aria-activedescendant")).toBe(`${menu.id}-option-3`); - fireEvent.keyDown(menu, { key: "Home", code: "Home", keyCode: 36, charCode: 36 }); + fireEvent.keyDown(menu, { + key: "Home", + code: "Home", + keyCode: 36, + charCode: 36, + }); expect(menu.getAttribute("aria-activedescendant")).toBe(`${menu.id}-option-0`); }); - test("Menu key events — End moves the focus to the last menu item", async () => { + test("Menu key events — End moves the focus to the last menu item", () => { const onSelectOption = jest.fn(); const { getByRole } = render( <DxcDropdown options={options} label="dropdown-test-1" onSelectOption={onSelectOption} /> ); - await userEvent.click(getByRole("button")); + userEvent.click(getByRole("button")); const menu = getByRole("menu"); expect(menu.getAttribute("aria-activedescendant")).toBe(`${menu.id}-option-0`); - fireEvent.keyDown(menu, { key: "End", code: "End", keyCode: 35, charCode: 35 }); + fireEvent.keyDown(menu, { + key: "End", + code: "End", + keyCode: 35, + charCode: 35, + }); expect(menu.getAttribute("aria-activedescendant")).toBe(`${menu.id}-option-3`); }); test("Menu key events — PageUp moves the focus to the first menu item", () => { @@ -265,32 +363,52 @@ describe("Dropdown component tests", () => { const { getByRole } = render( <DxcDropdown options={options} label="dropdown-test-1" onSelectOption={onSelectOption} /> ); - fireEvent.keyDown(getByRole("button"), { key: "ArrowUp", code: "ArrowUp", keyCode: 38, charCode: 38 }); + fireEvent.keyDown(getByRole("button"), { + key: "ArrowUp", + code: "ArrowUp", + keyCode: 38, + charCode: 38, + }); const menu = getByRole("menu"); expect(menu.getAttribute("aria-activedescendant")).toBe(`${menu.id}-option-3`); - fireEvent.keyDown(menu, { key: "PageUp", code: "PageUp", keyCode: 33, charCode: 33 }); + fireEvent.keyDown(menu, { + key: "PageUp", + code: "PageUp", + keyCode: 33, + charCode: 33, + }); expect(menu.getAttribute("aria-activedescendant")).toBe(`${menu.id}-option-0`); }); - test("Menu key events — PageDown moves the focus to the last menu item", async () => { + test("Menu key events — PageDown moves the focus to the last menu item", () => { const onSelectOption = jest.fn(); const { getByRole } = render( <DxcDropdown options={options} label="dropdown-test-1" onSelectOption={onSelectOption} /> ); - await userEvent.click(getByRole("button")); + userEvent.click(getByRole("button")); const menu = getByRole("menu"); expect(menu.getAttribute("aria-activedescendant")).toBe(`${menu.id}-option-0`); - fireEvent.keyDown(menu, { key: "PageDown", code: "PageDown", keyCode: 34, charCode: 34 }); + fireEvent.keyDown(menu, { + key: "PageDown", + code: "PageDown", + keyCode: 34, + charCode: 34, + }); expect(menu.getAttribute("aria-activedescendant")).toBe(`${menu.id}-option-3`); }); - test("Menu key events — Tab closes the menu and sets focus to the next element", async () => { + test("Menu key events — Tab closes the menu and sets focus to the next element", () => { const onSelectOption = jest.fn(); const { getByRole, queryByRole } = render( <DxcDropdown options={options} label="dropdown-test-1" onSelectOption={onSelectOption} /> ); const dropdown = getByRole("button"); - await userEvent.click(dropdown); + userEvent.click(dropdown); expect(getByRole("menu")).toBeTruthy(); - fireEvent.keyDown(getByRole("menu"), { key: "Tab", code: "Tab", keyCode: 9, charCode: 9 }); + fireEvent.keyDown(getByRole("menu"), { + key: "Tab", + code: "Tab", + keyCode: 9, + charCode: 9, + }); expect(queryByRole("menu")).toBeFalsy(); }); }); diff --git a/packages/lib/src/dropdown/Dropdown.tsx b/packages/lib/src/dropdown/Dropdown.tsx index bf3b95918b..905b419b8b 100644 --- a/packages/lib/src/dropdown/Dropdown.tsx +++ b/packages/lib/src/dropdown/Dropdown.tsx @@ -1,5 +1,5 @@ import * as Popover from "@radix-ui/react-popover"; -import { FocusEvent, KeyboardEvent, useCallback, useId, useLayoutEffect, useRef, useState, useContext } from "react"; +import { FocusEvent, KeyboardEvent, useCallback, useId, useLayoutEffect, useRef, useState } from "react"; import styled from "@emotion/styled"; import { getMargin } from "../common/utils"; import { spaces } from "../common/variables"; @@ -145,7 +145,9 @@ const DxcDropdown = ({ }; const handleMenuItemOnClick = useCallback( (value?: string) => { - if (value) onSelectOption(value); + if (value) { + onSelectOption(value); + } handleOnCloseMenu(); triggerRef.current?.focus(); }, diff --git a/packages/lib/src/dropdown/DropdownMenu.tsx b/packages/lib/src/dropdown/DropdownMenu.tsx index 3473196bcb..a1587d9d6b 100644 --- a/packages/lib/src/dropdown/DropdownMenu.tsx +++ b/packages/lib/src/dropdown/DropdownMenu.tsx @@ -2,7 +2,7 @@ import { forwardRef, memo } from "react"; import styled from "@emotion/styled"; import DropdownMenuItem from "./DropdownMenuItem"; import { DropdownMenuProps } from "./types"; -import { scrollbarStyles } from "../styles/scroll"; +import scrollbarStyles from "../styles/scroll"; const DropdownMenuContainer = styled.ul` max-height: 230px; @@ -50,4 +50,6 @@ const DropdownMenu = forwardRef<HTMLUListElement, DropdownMenuProps>( ) ); +DropdownMenu.displayName = "DropdownMenu"; + export default memo(DropdownMenu); diff --git a/packages/lib/src/file-input/FileInput.test.tsx b/packages/lib/src/file-input/FileInput.test.tsx index c8ffb39f49..40aa45ba53 100644 --- a/packages/lib/src/file-input/FileInput.test.tsx +++ b/packages/lib/src/file-input/FileInput.test.tsx @@ -284,7 +284,9 @@ describe("FileInput component tests", () => { expect(getByText("file2.txt")).toBeTruthy(); expect(getByText("Error message")).toBeTruthy(); const removeBtn = getAllByRole("button")[1]; - removeBtn != null && userEvent.click(removeBtn); + if (removeBtn != null) { + userEvent.click(removeBtn); + } expect(callbackFile).toHaveBeenCalledWith([ { error: "Error message", diff --git a/packages/lib/src/file-input/FileInput.tsx b/packages/lib/src/file-input/FileInput.tsx index 08aab3317b..bb05aee54a 100644 --- a/packages/lib/src/file-input/FileInput.tsx +++ b/packages/lib/src/file-input/FileInput.tsx @@ -129,8 +129,11 @@ const DxcFileInput = forwardRef<RefType, FileInputPropsType>( const translatedLabels = useContext(HalstackLanguageContext); const checkFileSize = (file: File) => { - if (minSize && file.size < minSize) return translatedLabels.fileInput.fileSizeGreaterThanErrorMessage; - else if (maxSize && file.size > maxSize) return translatedLabels.fileInput.fileSizeLessThanErrorMessage; + if (minSize && file.size < minSize) { + return translatedLabels.fileInput.fileSizeGreaterThanErrorMessage; + } else if (maxSize && file.size > maxSize) { + return translatedLabels.fileInput.fileSizeLessThanErrorMessage; + } }; const getFilesToAdd = async (selectedFiles: File[]) => { @@ -149,9 +152,7 @@ const DxcFileInput = forwardRef<RefType, FileInputPropsType>( }; const addFile = async (selectedFiles: File[]) => { - const filesToAdd = await getFilesToAdd( - multiple ? selectedFiles : selectedFiles.length === 1 ? selectedFiles : [selectedFiles[0] as File] - ); + const filesToAdd = await getFilesToAdd(multiple ? selectedFiles : selectedFiles.slice(0, 1)); const finalFiles = multiple ? [...files, ...filesToAdd] : filesToAdd; callbackFile?.(finalFiles); }; @@ -160,7 +161,7 @@ const DxcFileInput = forwardRef<RefType, FileInputPropsType>( const selectedFiles = e.target.files; if (selectedFiles) { const filesArray = Array.from(selectedFiles); - addFile(filesArray); + addFile(filesArray).catch((err) => console.error("Error adding files:", err)); e.target.value = ""; } }; @@ -192,7 +193,8 @@ const DxcFileInput = forwardRef<RefType, FileInputPropsType>( }; const handleDragOut = (e: DragEvent<HTMLDivElement>) => { // only if dragged items leave container (outside, not to children) - if (!e.currentTarget.contains(e.relatedTarget as HTMLDivElement)) { + const { relatedTarget } = e; + if (relatedTarget instanceof Node && !e.currentTarget.contains(relatedTarget)) { setIsDragging(false); } }; @@ -203,14 +205,14 @@ const DxcFileInput = forwardRef<RefType, FileInputPropsType>( const filesObject = e.dataTransfer.files; if (filesObject.length > 0) { const filesArray = Array.from(filesObject); - addFile(filesArray); + addFile(filesArray).catch((err) => console.error("Error adding files:", err)); } }; useEffect(() => { const getFiles = async () => { if (value) { - const valueFiles = (await Promise.all( + const valueFiles = await Promise.all( value.map(async (file) => { if (file.preview) { return file; @@ -218,11 +220,13 @@ const DxcFileInput = forwardRef<RefType, FileInputPropsType>( const preview = await getFilePreview(file.file); return { ...file, preview }; }) - )) as FileData[]; + ); setFiles(valueFiles); } }; - getFiles(); + getFiles().catch((err) => { + console.error("Error fetching file previews:", err); + }); }, [value]); return ( @@ -344,4 +348,6 @@ const DxcFileInput = forwardRef<RefType, FileInputPropsType>( } ); +DxcFileInput.displayName = "DxcFileInput"; + export default DxcFileInput; diff --git a/packages/lib/src/flex/Flex.stories.tsx b/packages/lib/src/flex/Flex.stories.tsx index 0b9e02fd96..27ab0bf9cf 100644 --- a/packages/lib/src/flex/Flex.stories.tsx +++ b/packages/lib/src/flex/Flex.stories.tsx @@ -1,7 +1,7 @@ +import { Meta, StoryObj } from "@storybook/react"; import styled from "@emotion/styled"; import Title from "../../.storybook/components/Title"; import DxcFlex from "./Flex"; -import { Meta, StoryObj } from "@storybook/react"; export default { title: "Flex", diff --git a/packages/lib/src/footer/Footer.accessibility.test.tsx b/packages/lib/src/footer/Footer.accessibility.test.tsx index f72c023040..745427a191 100644 --- a/packages/lib/src/footer/Footer.accessibility.test.tsx +++ b/packages/lib/src/footer/Footer.accessibility.test.tsx @@ -1,7 +1,7 @@ import { render } from "@testing-library/react"; import { axe, formatRules } from "../../test/accessibility/axe-helper"; -import { disabledRules as rules } from "../../test/accessibility/rules/specific/footer/disabledRules"; import DxcFooter from "./Footer"; +import rules from "../../test/accessibility/rules/specific/footer/disabledRules"; const disabledRules = { rules: formatRules(rules), diff --git a/packages/lib/src/footer/Footer.stories.tsx b/packages/lib/src/footer/Footer.stories.tsx index e27c6b91e0..40504f708f 100644 --- a/packages/lib/src/footer/Footer.stories.tsx +++ b/packages/lib/src/footer/Footer.stories.tsx @@ -1,12 +1,12 @@ +import { Meta, StoryObj } from "@storybook/react"; import { userEvent, within } from "@storybook/test"; import ExampleContainer from "../../.storybook/components/ExampleContainer"; import Title from "../../.storybook/components/Title"; import preview from "../../.storybook/preview"; -import { disabledRules } from "../../test/accessibility/rules/specific/footer/disabledRules"; +import disabledRules from "../../test/accessibility/rules/specific/footer/disabledRules"; import DxcFlex from "../flex/Flex"; import DxcTypography from "../typography/Typography"; import DxcFooter from "./Footer"; -import { Meta, StoryObj } from "@storybook/react"; import DxcLink from "../link/Link"; const social = [ @@ -115,22 +115,13 @@ export default { config: { rules: [ ...disabledRules.map((ruleId) => ({ id: ruleId, enabled: false })), - ...preview?.parameters?.a11y?.config?.rules, + ...(preview?.parameters?.a11y?.config?.rules || []), ], }, }, }, } as Meta<typeof DxcFooter>; -const opinionatedTheme = { - footer: { - baseColor: "#000000", - fontColor: "#ffffff", - accentColor: "#0095ff", - logo: "https://upload.wikimedia.org/wikipedia/commons/thumb/b/b8/2021_Facebook_icon.svg/2048px-2021_Facebook_icon.svg.png", - }, -}; - const info = [ { label: "Example Label", text: "Example" }, { label: "Example Label", text: "Example" }, @@ -169,7 +160,7 @@ const Footer = () => ( <ExampleContainer> <Title title="Reduced" theme="light" level={4} /> <DxcFooter mode="reduced"> - <DxcFlex justifyContent="center" alignItems="center" gap={"1rem"}> + <DxcFlex justifyContent="center" alignItems="center" gap="1rem"> {info.map((tag, index) => ( <DxcTypography color="white" key={`tag${index}${tag.label}${tag.text}`}> {tag.label}: {tag.text} @@ -218,7 +209,9 @@ export const FooterTooltipFirst: Story = { play: async ({ canvasElement }) => { const canvas = within(canvasElement); const link = canvas.getAllByRole("link")[0]; - link != null && (await userEvent.hover(link)); + if (link != null) { + await userEvent.hover(link); + } }, }; @@ -227,6 +220,8 @@ export const FooterTooltipSecond: Story = { play: async ({ canvasElement }) => { const canvas = within(canvasElement); const link = canvas.getAllByRole("link")[1]; - link != null && (await userEvent.hover(link)); + if (link != null) { + await userEvent.hover(link); + } }, }; diff --git a/packages/lib/src/footer/Footer.test.tsx b/packages/lib/src/footer/Footer.test.tsx index 1a4260d3f0..e0a615d68f 100644 --- a/packages/lib/src/footer/Footer.test.tsx +++ b/packages/lib/src/footer/Footer.test.tsx @@ -36,7 +36,10 @@ describe("Footer component tests", () => { }); test("Footer renders with correct children", () => { // We need to force the offsetWidth value - Object.defineProperty(HTMLElement.prototype, "offsetWidth", { configurable: true, value: 1024 }); + Object.defineProperty(HTMLElement.prototype, "offsetWidth", { + configurable: true, + value: 1024, + }); const { getByText } = render( <DxcFooter> <p>footer-child-text</p> @@ -46,7 +49,10 @@ describe("Footer component tests", () => { }); test("Footer renders with children in mobile", () => { // 425 is mobile width - Object.defineProperty(HTMLElement.prototype, "offsetWidth", { configurable: true, value: 425 }); + Object.defineProperty(HTMLElement.prototype, "offsetWidth", { + configurable: true, + value: 425, + }); const { queryByText } = render( <DxcFooter> @@ -57,7 +63,10 @@ describe("Footer component tests", () => { expect(queryByText("footer-child-text")).toBeTruthy(); }); test("Footer is fully rendered", () => { - Object.defineProperty(HTMLElement.prototype, "offsetWidth", { configurable: true, value: 1024 }); + Object.defineProperty(HTMLElement.prototype, "offsetWidth", { + configurable: true, + value: 1024, + }); const { getAllByRole, getByText } = render( <DxcFooter socialLinks={social} bottomLinks={bottom} copyright="test-copyright"> diff --git a/packages/lib/src/grid/Grid.stories.tsx b/packages/lib/src/grid/Grid.stories.tsx index bc04080c6a..586fac1dd6 100644 --- a/packages/lib/src/grid/Grid.stories.tsx +++ b/packages/lib/src/grid/Grid.stories.tsx @@ -1,9 +1,9 @@ +import { Meta, StoryObj } from "@storybook/react"; import styled from "@emotion/styled"; import ExampleContainer from "../../.storybook/components/ExampleContainer"; import Title from "../../.storybook/components/Title"; import DxcInset from "../inset/Inset"; import DxcGrid from "./Grid"; -import { Meta, StoryObj } from "@storybook/react"; export default { title: "Grid", diff --git a/packages/lib/src/grid/Grid.tsx b/packages/lib/src/grid/Grid.tsx index 00921dd181..7e1797a76b 100644 --- a/packages/lib/src/grid/Grid.tsx +++ b/packages/lib/src/grid/Grid.tsx @@ -93,4 +93,5 @@ const GridItem = styled.div<GridItemProps>` const DxcGrid = (props: GridPropsType) => <Grid {...props} />; DxcGrid.Item = GridItem; + export default DxcGrid; diff --git a/packages/lib/src/grid/types.ts b/packages/lib/src/grid/types.ts index 49fd152de4..81fb8b58d6 100644 --- a/packages/lib/src/grid/types.ts +++ b/packages/lib/src/grid/types.ts @@ -1,7 +1,7 @@ import { ReactNode } from "react"; -type Gap = string | { columnGap?: string; rowGap: string; } | { columnGap: string; rowGap?: string; }; -type GridCell = { end: number | string; start: number | string; }; +type Gap = string | { columnGap?: string; rowGap: string } | { columnGap: string; rowGap?: string }; +type GridCell = { end: number | string; start: number | string }; type PlaceSelfValues = "auto" | "baseline" | "center" | "end" | "start" | "stretch"; type PlaceContentValues = | "baseline" @@ -18,8 +18,8 @@ type PlaceObject<Type, Suffix extends string> = { [Property in keyof Type as `${string & Property}${Capitalize<string & Suffix>}`]: Type[Property]; }; type PlaceGeneric<PlaceValues, Element extends string> = - | PlaceObject<{ align: PlaceValues; justify?: PlaceValues; }, Element> - | PlaceObject<{ align?: PlaceValues; justify: PlaceValues; }, Element> + | PlaceObject<{ align: PlaceValues; justify?: PlaceValues }, Element> + | PlaceObject<{ align?: PlaceValues; justify: PlaceValues }, Element> | PlaceValues; export type GridItemProps = { diff --git a/packages/lib/src/header/Header.accessibility.test.tsx b/packages/lib/src/header/Header.accessibility.test.tsx index d31e16d361..cce7a37ecc 100644 --- a/packages/lib/src/header/Header.accessibility.test.tsx +++ b/packages/lib/src/header/Header.accessibility.test.tsx @@ -1,17 +1,15 @@ import { render } from "@testing-library/react"; import { axe, formatRules } from "../../test/accessibility/axe-helper"; -import { disabledRules as rules } from "../../test/accessibility/rules/specific/header/disabledRules"; +import DxcHeader from "./Header"; import DxcFlex from "../flex/Flex"; import DxcLink from "../link/Link"; -import DxcHeader from "./Header"; - -(global as any).ResizeObserver = class ResizeObserver { - observe() {} +import rules from "../../test/accessibility/rules/specific/header/disabledRules"; - unobserve() {} - - disconnect() {} -}; +global.ResizeObserver = jest.fn().mockImplementation(() => ({ + observe: jest.fn(), + unobserve: jest.fn(), + disconnect: jest.fn(), +})); const disabledRules = { rules: formatRules(rules), diff --git a/packages/lib/src/header/Header.stories.tsx b/packages/lib/src/header/Header.stories.tsx index 63c51cb6f0..f636136561 100644 --- a/packages/lib/src/header/Header.stories.tsx +++ b/packages/lib/src/header/Header.stories.tsx @@ -1,14 +1,13 @@ -import { INITIAL_VIEWPORTS } from "@storybook/addon-viewport"; +import { Meta, StoryObj } from "@storybook/react"; import { userEvent, waitFor, within } from "@storybook/test"; -import ExampleContainer from "../../.storybook/components/ExampleContainer"; +import { INITIAL_VIEWPORTS } from "@storybook/addon-viewport"; import Title from "../../.storybook/components/Title"; +import ExampleContainer from "../../.storybook/components/ExampleContainer"; +import disabledRules from "../../test/accessibility/rules/specific/header/disabledRules"; import preview from "../../.storybook/preview"; -import { disabledRules } from "../../test/accessibility/rules/specific/header/disabledRules"; -import DxcButton from "../button/Button"; import DxcFlex from "../flex/Flex"; import DxcLink from "../link/Link"; import DxcHeader from "./Header"; -import { Meta, StoryObj } from "@storybook/react"; export default { title: "Header", @@ -18,7 +17,7 @@ export default { config: { rules: [ ...disabledRules.map((ruleId) => ({ id: ruleId, enabled: false })), - ...preview?.parameters?.a11y?.config?.rules, + ...(preview?.parameters?.a11y?.config?.rules || []), ], }, }, @@ -28,28 +27,30 @@ export default { }, } as Meta<typeof DxcHeader>; -const options: any = [ +const options = [ { - value: 1, + value: "1", label: "Amazon", }, ]; -const options2: any = [ +const options2 = [ { - value: 1, + value: "1", label: "Home", }, { - value: 2, + value: "2", label: "Release notes", }, { - value: 3, + value: "3", label: "Sign out", }, ]; +const responsiveContentFunction = () => <p>Lorem ipsum dolor sit amet.</p>; + const Header = () => ( <> <ExampleContainer> @@ -136,9 +137,7 @@ const Responsive = () => ( <Title title="Responsive" theme="light" level={4} /> <DxcHeader content={<DxcHeader.Dropdown options={options} label="Default Dropdown" onSelectOption={() => {}} />} - responsiveContent={(closeHandler) => ( - <DxcHeader.Dropdown options={options} label="Default Dropdown" onSelectOption={() => {}} /> - )} + responsiveContent={responsiveContentFunction} underlined /> </ExampleContainer> @@ -147,28 +146,26 @@ const Responsive = () => ( const RespHeaderFocus = () => ( <ExampleContainer pseudoState="pseudo-focus"> <Title title="Responsive focus" theme="light" level={4} /> - <DxcHeader responsiveContent={(closeHandler) => <p>Lorem ipsum dolor sit amet.</p>} underlined /> + <DxcHeader responsiveContent={responsiveContentFunction} underlined /> </ExampleContainer> ); - const RespHeaderHover = () => ( <ExampleContainer pseudoState="pseudo-hover"> <Title title="Responsive hover" theme="light" level={4} /> - <DxcHeader responsiveContent={(closeHandler) => <p>Lorem ipsum dolor sit amet.</p>} underlined /> + <DxcHeader responsiveContent={responsiveContentFunction} underlined /> </ExampleContainer> ); - const RespHeaderMenuMobile = () => ( <ExampleContainer> <Title title="Responsive menu" theme="light" level={4} /> - <DxcHeader responsiveContent={(closeHandler) => <p>Lorem ipsum dolor sit amet.</p>} underlined /> + <DxcHeader responsiveContent={responsiveContentFunction} underlined /> </ExampleContainer> ); const RespHeaderMenuTablet = () => ( <ExampleContainer> <Title title="Responsive menu" theme="light" level={4} /> - <DxcHeader responsiveContent={(closeHandler) => <p>Lorem ipsum dolor sit amet.</p>} underlined /> + <DxcHeader responsiveContent={responsiveContentFunction} underlined /> </ExampleContainer> ); @@ -263,6 +260,8 @@ export const ResponsiveHeaderTooltip: Story = { await waitFor(() => canvas.findByText("Menu")); await userEvent.click(canvas.getByText("Menu")); const closeButton = canvas.getAllByRole("button")[1]; - closeButton != null && (await userEvent.hover(closeButton)); + if (closeButton != null) { + await userEvent.hover(closeButton); + } }, }; diff --git a/packages/lib/src/header/Header.test.tsx b/packages/lib/src/header/Header.test.tsx index 2c49ada42f..c280117d09 100644 --- a/packages/lib/src/header/Header.test.tsx +++ b/packages/lib/src/header/Header.test.tsx @@ -23,13 +23,19 @@ describe("Header component tests", () => { }); test("Header renders with correct children", () => { // We need to force the offsetWidth value - Object.defineProperty(HTMLElement.prototype, "offsetWidth", { configurable: true, value: 1024 }); + Object.defineProperty(HTMLElement.prototype, "offsetWidth", { + configurable: true, + value: 1024, + }); const { getByText } = render(<DxcHeader content={<p>header-child-text</p>} />); expect(getByText("header-child-text")).toBeTruthy(); }); test("Header renders menu button in mobile", () => { - Object.defineProperty(HTMLElement.prototype, "offsetWidth", { configurable: true, value: 425 }); + Object.defineProperty(HTMLElement.prototype, "offsetWidth", { + configurable: true, + value: 425, + }); Object.defineProperty(window, "matchMedia", { writable: true, value: jest.fn().mockImplementation(() => ({ diff --git a/packages/lib/src/header/Header.tsx b/packages/lib/src/header/Header.tsx index 5480ebc2ec..6c8c934c01 100644 --- a/packages/lib/src/header/Header.tsx +++ b/packages/lib/src/header/Header.tsx @@ -1,14 +1,13 @@ -import { ComponentProps, useEffect, useRef, useState } from "react"; -import styled from "@emotion/styled"; +import { ComponentProps, useContext, useEffect, useRef, useState } from "react"; import { responsiveSizes, spaces } from "../common/variables"; import DxcDropdown from "../dropdown/Dropdown"; import DxcIcon from "../icon/Icon"; import HeaderPropsType, { Logo } from "./types"; import DxcFlex from "../flex/Flex"; -import { useContext } from "react"; import { HalstackLanguageContext } from "../HalstackContext"; import ActionIcon from "../action-icon/ActionIcon"; import { dxcLogo } from "./Icons"; +import styled from "@emotion/styled"; const HeaderDropdown = styled.div` display: flex; diff --git a/packages/lib/src/header/types.ts b/packages/lib/src/header/types.ts index c175455d24..05857fa7f7 100644 --- a/packages/lib/src/header/types.ts +++ b/packages/lib/src/header/types.ts @@ -23,7 +23,7 @@ type Props = { underlined?: boolean; /** * Content shown in the header. Take into account that the component applies styles - * for the first child in the content, so we recommend the use of React.Fragment + * for the first child in the content, so we recommend the use of Fragment * to be applied correctly. Otherwise, the styles can be modified. */ content?: ReactNode; diff --git a/packages/lib/src/heading/utils.ts b/packages/lib/src/heading/utils.ts index 046152e97b..02771a9e70 100644 --- a/packages/lib/src/heading/utils.ts +++ b/packages/lib/src/heading/utils.ts @@ -26,4 +26,4 @@ export const getHeadingWeight = (weight: HeadingPropsType["weight"]) => { case "light": return "var(--typography-heading-light)"; } -}; \ No newline at end of file +}; diff --git a/packages/lib/src/icon/Icon.stories.tsx b/packages/lib/src/icon/Icon.stories.tsx index 6c82ef5066..6ef1f7ae92 100644 --- a/packages/lib/src/icon/Icon.stories.tsx +++ b/packages/lib/src/icon/Icon.stories.tsx @@ -1,8 +1,8 @@ +import { Meta, StoryObj } from "@storybook/react"; import DxcIcon from "./Icon"; import Title from "../../.storybook/components/Title"; import ExampleContainer from "../../.storybook/components/ExampleContainer"; import DxcTypography from "../typography/Typography"; -import { Meta, StoryObj } from "@storybook/react"; export default { title: "Icon", diff --git a/packages/lib/src/inset/Inset.stories.tsx b/packages/lib/src/inset/Inset.stories.tsx index 435975e35d..27dcf83808 100644 --- a/packages/lib/src/inset/Inset.stories.tsx +++ b/packages/lib/src/inset/Inset.stories.tsx @@ -1,9 +1,9 @@ +import { ReactNode } from "react"; +import { Meta, StoryObj } from "@storybook/react"; import Title from "../../.storybook/components/Title"; -import DxcFlex from "./../flex/Flex"; +import DxcFlex from "../flex/Flex"; import DxcInset from "./Inset"; -import { Meta, StoryObj } from "@storybook/react"; import DxcContainer from "../container/Container"; -import { ReactNode } from "react"; export default { title: "Inset", diff --git a/packages/lib/src/layout/ApplicationLayout.stories.tsx b/packages/lib/src/layout/ApplicationLayout.stories.tsx index 09d0a621b0..7f6e3415ac 100644 --- a/packages/lib/src/layout/ApplicationLayout.stories.tsx +++ b/packages/lib/src/layout/ApplicationLayout.stories.tsx @@ -1,8 +1,8 @@ +import { Meta, StoryObj } from "@storybook/react"; +import { userEvent, within } from "@storybook/test"; import { INITIAL_VIEWPORTS } from "@storybook/addon-viewport"; import Title from "../../.storybook/components/Title"; import DxcApplicationLayout from "./ApplicationLayout"; -import { userEvent, within } from "@storybook/test"; -import { Meta, StoryObj } from "@storybook/react"; export default { title: "Application Layout", @@ -156,21 +156,19 @@ const ApplicationLayoutCustomFooter = () => ( ); const Tooltip = () => ( - <> - <DxcApplicationLayout - sidenav={ - <DxcApplicationLayout.SideNav> - <DxcApplicationLayout.SideNav.Section> - <p>SideNav Content</p> - </DxcApplicationLayout.SideNav.Section> - </DxcApplicationLayout.SideNav> - } - > - <DxcApplicationLayout.Main> - <p>Main Content</p> - </DxcApplicationLayout.Main> - </DxcApplicationLayout> - </> + <DxcApplicationLayout + sidenav={ + <DxcApplicationLayout.SideNav> + <DxcApplicationLayout.SideNav.Section> + <p>SideNav Content</p> + </DxcApplicationLayout.SideNav.Section> + </DxcApplicationLayout.SideNav> + } + > + <DxcApplicationLayout.Main> + <p>Main Content</p> + </DxcApplicationLayout.Main> + </DxcApplicationLayout> ); type Story = StoryObj<typeof DxcApplicationLayout>; diff --git a/packages/lib/src/link/Link.stories.tsx b/packages/lib/src/link/Link.stories.tsx index 6f5cfc0326..98f4d035be 100644 --- a/packages/lib/src/link/Link.stories.tsx +++ b/packages/lib/src/link/Link.stories.tsx @@ -1,7 +1,7 @@ +import { Meta, StoryObj } from "@storybook/react"; import ExampleContainer from "../../.storybook/components/ExampleContainer"; import Title from "../../.storybook/components/Title"; import DxcLink from "./Link"; -import { Meta, StoryObj } from "@storybook/react"; export default { title: "Link", diff --git a/packages/lib/src/link/Link.tsx b/packages/lib/src/link/Link.tsx index 1075930bee..5bb8a37b9b 100644 --- a/packages/lib/src/link/Link.tsx +++ b/packages/lib/src/link/Link.tsx @@ -106,4 +106,6 @@ const DxcLink = forwardRef( ) ); +DxcLink.displayName = "DxcLink"; + export default DxcLink; diff --git a/packages/lib/src/nav-tabs/NavTabs.tsx b/packages/lib/src/nav-tabs/NavTabs.tsx index 158bd2d143..d0c6b0e47d 100644 --- a/packages/lib/src/nav-tabs/NavTabs.tsx +++ b/packages/lib/src/nav-tabs/NavTabs.tsx @@ -1,4 +1,4 @@ -import { Children, KeyboardEvent, ReactElement, useMemo, useState } from "react"; +import { Children, KeyboardEvent, useMemo, useState } from "react"; import styled from "@emotion/styled"; import NavTabsPropsType from "./types"; import Tab from "./Tab"; @@ -22,9 +22,7 @@ const Underline = styled.div` const DxcNavTabs = ({ iconPosition = "left", tabIndex = 0, children }: NavTabsPropsType): JSX.Element => { const [innerFocusIndex, setInnerFocusIndex] = useState<number | null>(null); - const childArray = Children.toArray(children).filter( - (child) => typeof child === "object" && "props" in child - ) as ReactElement[]; + const childArray = Children.toArray(children).filter((child) => typeof child === "object" && "props" in child); const contextValue = useMemo( () => ({ diff --git a/packages/lib/src/nav-tabs/Tab.tsx b/packages/lib/src/nav-tabs/Tab.tsx index aa67645156..ac262c18ce 100644 --- a/packages/lib/src/nav-tabs/Tab.tsx +++ b/packages/lib/src/nav-tabs/Tab.tsx @@ -166,4 +166,6 @@ const Tab = forwardRef( } ); +Tab.displayName = "Tab"; + export default Tab; diff --git a/packages/lib/src/nav-tabs/types.ts b/packages/lib/src/nav-tabs/types.ts index abde4211db..83fafddc2b 100644 --- a/packages/lib/src/nav-tabs/types.ts +++ b/packages/lib/src/nav-tabs/types.ts @@ -1,4 +1,4 @@ -import { ReactNode, SVGProps } from "react"; +import { ReactNode } from "react"; import { SVG } from "../common/utils"; export type NavTabsContextProps = { diff --git a/packages/lib/src/nav-tabs/utils.ts b/packages/lib/src/nav-tabs/utils.ts index 1cc2452f85..74cd3f2f60 100644 --- a/packages/lib/src/nav-tabs/utils.ts +++ b/packages/lib/src/nav-tabs/utils.ts @@ -1,12 +1,18 @@ -import { ReactNode, ReactElement } from "react"; - -export const getPropInChild = (child: ReactNode, propName: string): string | undefined => { - if (child && typeof child === "object" && "props" in child) { - const childWithProps = child as ReactElement; - if (childWithProps.props[propName]) { - return childWithProps.props[propName]; - } else if (childWithProps.props.children) { - return getPropInChild(childWithProps.props.children, propName); +import { ReactNode, ReactElement, isValidElement } from "react"; + +type ElementWithChildren = ReactElement<{ children?: ReactNode; [key: string]: unknown }>; + +export const getPropInChild = (child: ReactNode, propName: string): string | boolean | undefined => { + if (isValidElement(child)) { + const el = child as ElementWithChildren; + const value = el.props[propName]; + + if (typeof value === "string" || typeof value === "boolean") { + return value; + } + + if (el.props.children) { + return getPropInChild(el.props.children, propName); } } }; @@ -14,11 +20,16 @@ export const getPropInChild = (child: ReactNode, propName: string): string | und export const getLabelFromTab = (child: ReactNode): string | undefined => { if (typeof child === "string") { return child; - } else if (child && typeof child === "object" && "props" in child) { - const childWithProps = child as ReactElement; - return Array.isArray(childWithProps.props.children) - ? getLabelFromTab(childWithProps.props.children[0]) - : getLabelFromTab(childWithProps.props.children); + } + if (isValidElement(child)) { + const el = child as ElementWithChildren; + const children = el.props.children; + + if (Array.isArray(children) && isValidElement(children[0] as ReactNode)) { + return getLabelFromTab(children[0] as ReactNode); + } + + return getLabelFromTab(children); } }; diff --git a/packages/lib/src/number-input/NumberInput.accessibility.test.tsx b/packages/lib/src/number-input/NumberInput.accessibility.test.tsx index 71a8823ece..1177ddf2f7 100644 --- a/packages/lib/src/number-input/NumberInput.accessibility.test.tsx +++ b/packages/lib/src/number-input/NumberInput.accessibility.test.tsx @@ -1,17 +1,15 @@ import { render } from "@testing-library/react"; import { axe } from "../../test/accessibility/axe-helper"; import DxcNumberInput from "./NumberInput"; +import MockDOMRect from "../../test/mocks/domRectMock"; // Mocking DOMRect for Radix Primitive Popover -(global as any).globalThis = global; -(global as any).DOMRect = { - fromRect: () => ({ top: 0, left: 0, bottom: 0, right: 0, width: 0, height: 0 }), -}; -(global as any).ResizeObserver = class ResizeObserver { - observe() {} - unobserve() {} - disconnect() {} -}; +global.DOMRect = MockDOMRect; +global.ResizeObserver = jest.fn().mockImplementation(() => ({ + observe: jest.fn(), + unobserve: jest.fn(), + disconnect: jest.fn(), +})); describe("Number input component accessibility tests", () => { it("Should not have basic accessibility issues", async () => { diff --git a/packages/lib/src/number-input/NumberInput.test.tsx b/packages/lib/src/number-input/NumberInput.test.tsx index c49170b2b3..2b584cd0c2 100644 --- a/packages/lib/src/number-input/NumberInput.test.tsx +++ b/packages/lib/src/number-input/NumberInput.test.tsx @@ -1,17 +1,15 @@ import { fireEvent, render } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import DxcNumberInput from "./NumberInput"; +import MockDOMRect from "../../test/mocks/domRectMock"; // Mocking DOMRect for Radix Primitive Popover -(global as any).globalThis = global; -(global as any).DOMRect = { - fromRect: () => ({ top: 0, left: 0, bottom: 0, right: 0, width: 0, height: 0 }), -}; -(global as any).ResizeObserver = class ResizeObserver { - observe() {} - unobserve() {} - disconnect() {} -}; +global.DOMRect = MockDOMRect; +global.ResizeObserver = jest.fn().mockImplementation(() => ({ + observe: jest.fn(), + unobserve: jest.fn(), + disconnect: jest.fn(), +})); describe("Number input component tests", () => { test("Number input renders with label, helper text, placeholder and increment/decrement action buttons", () => { @@ -29,15 +27,19 @@ describe("Number input component tests", () => { const number = getByLabelText("Number label") as HTMLInputElement; expect(number.disabled).toBeTruthy(); }); - test("Number input is read only and cannot be incremented or decremented using the actions", async () => { + test("Number input is read only and cannot be incremented or decremented using the actions", () => { const { getByLabelText, getAllByRole } = render(<DxcNumberInput label="Number label" readOnly />); const number = getByLabelText("Number label") as HTMLInputElement; expect(number.readOnly).toBeTruthy(); const decrement = getAllByRole("button")[0]; - decrement && (await userEvent.click(decrement)); + if (decrement) { + userEvent.click(decrement); + } expect(number.value).toBe(""); const increment = getAllByRole("button")[1]; - increment && (await userEvent.click(increment)); + if (increment) { + userEvent.click(increment); + } expect(number.value).toBe(""); }); test("Number input is read only and cannot be incremented or decremented using the arrow keys", () => { @@ -64,9 +66,15 @@ describe("Number input component tests", () => { userEvent.clear(number); fireEvent.blur(number); expect(onBlur).toHaveBeenCalled(); - expect(onBlur).toHaveBeenCalledWith({ value: "", error: "This field is required. Please, enter a value." }); + expect(onBlur).toHaveBeenCalledWith({ + value: "", + error: "This field is required. Please, enter a value.", + }); expect(onChange).toHaveBeenCalled(); - expect(onChange).toHaveBeenCalledWith({ value: "", error: "This field is required. Please, enter a value." }); + expect(onChange).toHaveBeenCalledWith({ + value: "", + error: "This field is required. Please, enter a value.", + }); }); test("Hiding number input controls", () => { const { queryByRole } = render(<DxcNumberInput label="Number label" showControls={false} />); @@ -108,24 +116,28 @@ describe("Number input component tests", () => { userEvent.type(number, "-1"); fireEvent.blur(number); }); - test("Cannot decrement the value if it is less than the min value", async () => { + test("Cannot decrement the value if it is less than the min value", () => { const { getByLabelText, getAllByRole } = render(<DxcNumberInput label="Number input label" min={5} />); const number = getByLabelText("Number input label") as HTMLInputElement; userEvent.type(number, "1"); fireEvent.blur(number); expect(number.value).toBe("1"); const decrement = getAllByRole("button")[0]; - decrement && (await userEvent.click(decrement)); + if (decrement) { + userEvent.click(decrement); + } expect(number.value).toBe("1"); }); - test("Increment the value when it is less than the min value", async () => { + test("Increment the value when it is less than the min value", () => { const { getByLabelText, getAllByRole } = render(<DxcNumberInput label="Number input label" min={5} />); const number = getByLabelText("Number input label") as HTMLInputElement; userEvent.type(number, "1"); fireEvent.blur(number); expect(number.value).toBe("1"); const increment = getAllByRole("button")[1]; - increment && (await userEvent.click(increment)); + if (increment) { + userEvent.click(increment); + } expect(number.value).toBe("5"); }); test("Error message is shown if the typed value is greater than the max value", () => { @@ -137,89 +149,123 @@ describe("Number input component tests", () => { const number = getByLabelText("Number input label"); userEvent.type(number, "12"); expect(onChange).toHaveBeenCalledTimes(2); - expect(onChange).toHaveBeenCalledWith({ value: "12", error: "Value must be less than or equal to 10." }); + expect(onChange).toHaveBeenCalledWith({ + value: "12", + error: "Value must be less than or equal to 10.", + }); fireEvent.blur(number); expect(onBlur).toHaveBeenCalled(); - expect(onBlur).toHaveBeenCalledWith({ value: "12", error: "Value must be less than or equal to 10." }); + expect(onBlur).toHaveBeenCalledWith({ + value: "12", + error: "Value must be less than or equal to 10.", + }); }); - test("Cannot increment the value if it is greater than the max value", async () => { + test("Cannot increment the value if it is greater than the max value", () => { const { getByLabelText, getAllByRole } = render(<DxcNumberInput label="Number input label" max={10} />); const number = getByLabelText("Number input label") as HTMLInputElement; userEvent.type(number, "12"); fireEvent.blur(number); expect(number.value).toBe("12"); const decrement = getAllByRole("button")[1]; - decrement && (await userEvent.click(decrement)); + if (decrement) { + userEvent.click(decrement); + } expect(number.value).toBe("12"); }); - test("Decrement the value when it is greater than the max value", async () => { + test("Decrement the value when it is greater than the max value", () => { const { getByLabelText, getAllByRole } = render(<DxcNumberInput label="Number input label" max={10} />); const number = getByLabelText("Number input label") as HTMLInputElement; userEvent.type(number, "120"); fireEvent.blur(number); expect(number.value).toBe("120"); const decrement = getAllByRole("button")[0]; - decrement && (await userEvent.click(decrement)); + if (decrement) { + userEvent.click(decrement); + } expect(number.value).toBe("10"); }); - test("Increment and decrement the value with min and max values", async () => { + test("Increment and decrement the value with min and max values", () => { const { getByLabelText, getAllByRole } = render(<DxcNumberInput label="Number input label" min={5} max={10} />); const number = getByLabelText("Number input label") as HTMLInputElement; userEvent.type(number, "1"); fireEvent.blur(number); expect(number.value).toBe("1"); const decrement = getAllByRole("button")[0]; - decrement && (await userEvent.click(decrement)); + if (decrement) { + userEvent.click(decrement); + } expect(number.value).toBe("1"); const increment = getAllByRole("button")[1]; - increment && (await userEvent.click(increment)); + if (increment) { + userEvent.click(increment); + } expect(number.value).toBe("5"); - increment && (await userEvent.click(increment)); - increment && (await userEvent.click(increment)); - increment && (await userEvent.click(increment)); - increment && (await userEvent.click(increment)); - increment && (await userEvent.click(increment)); + if (increment) { + userEvent.click(increment); + userEvent.click(increment); + userEvent.click(increment); + userEvent.click(increment); + userEvent.click(increment); + } expect(number.value).toBe("10"); - increment && (await userEvent.click(increment)); + if (increment) { + userEvent.click(increment); + } expect(number.value).toBe("10"); }); - test("Increment and decrement the value with an integer step", async () => { + test("Increment and decrement the value with an integer step", () => { const { getByLabelText, getAllByRole } = render(<DxcNumberInput label="Number input label" step={5} />); const number = getByLabelText("Number input label") as HTMLInputElement; userEvent.type(number, "10"); fireEvent.blur(number); expect(number.value).toBe("10"); const increment = getAllByRole("button")[1]; - increment && (await userEvent.click(increment)); + if (increment) { + userEvent.click(increment); + } expect(number.value).toBe("15"); - increment && (await userEvent.click(increment)); + if (increment) { + userEvent.click(increment); + } expect(number.value).toBe("20"); const decrement = getAllByRole("button")[0]; - decrement && (await userEvent.click(decrement)); + if (decrement) { + userEvent.click(decrement); + } expect(number.value).toBe("15"); - decrement && (await userEvent.click(decrement)); + if (decrement) { + userEvent.click(decrement); + } expect(number.value).toBe("10"); }); - test("Increment and decrement the value with a decimal step", async () => { + test("Increment and decrement the value with a decimal step", () => { const { getByLabelText, getAllByRole } = render(<DxcNumberInput label="Number input label" step={0.5} />); const number = getByLabelText("Number input label") as HTMLInputElement; userEvent.type(number, "-9"); fireEvent.blur(number); expect(number.value).toBe("-9"); const increment = getAllByRole("button")[1]; - increment && (await userEvent.click(increment)); + if (increment) { + userEvent.click(increment); + } expect(number.value).toBe("-8.5"); - increment && (await userEvent.click(increment)); + if (increment) { + userEvent.click(increment); + } expect(number.value).toBe("-8"); const decrement = getAllByRole("button")[0]; - decrement && (await userEvent.click(decrement)); - decrement && (await userEvent.click(decrement)); - decrement && (await userEvent.click(decrement)); + if (decrement) { + userEvent.click(decrement); + userEvent.click(decrement); + userEvent.click(decrement); + } expect(number.value).toBe("-9.5"); - decrement && (await userEvent.click(decrement)); + if (decrement) { + userEvent.click(decrement); + } expect(number.value).toBe("-10"); }); - test("Increment and decrement the value with min, max and step", async () => { + test("Increment and decrement the value with min, max and step", () => { const onBlur = jest.fn(); const { getByLabelText, getAllByRole } = render( <DxcNumberInput label="Number input label" min={5} max={20} step={8} onBlur={onBlur} /> @@ -227,106 +273,151 @@ describe("Number input component tests", () => { const number = getByLabelText("Number input label") as HTMLInputElement; userEvent.type(number, "1"); fireEvent.blur(number); - expect(onBlur).toHaveBeenCalledWith({ value: "1", error: "Value must be greater than or equal to 5." }); + expect(onBlur).toHaveBeenCalledWith({ + value: "1", + error: "Value must be greater than or equal to 5.", + }); const increment = getAllByRole("button")[1]; - increment && (await userEvent.click(increment)); + if (increment) { + userEvent.click(increment); + } expect(number.value).toBe("5"); - increment && (await userEvent.click(increment)); + if (increment) { + userEvent.click(increment); + } expect(number.value).toBe("13"); - increment && (await userEvent.click(increment)); + if (increment) { + userEvent.click(increment); + } expect(number.value).toBe("13"); - increment && (await userEvent.click(increment)); + if (increment) { + userEvent.click(increment); + } expect(number.value).toBe("13"); const decrement = getAllByRole("button")[0]; - decrement && (await userEvent.click(decrement)); + if (decrement) { + userEvent.click(decrement); + } expect(number.value).toBe("5"); - decrement && (await userEvent.click(decrement)); + if (decrement) { + userEvent.click(decrement); + } expect(number.value).toBe("5"); - decrement && (await userEvent.click(decrement)); + if (decrement) { + userEvent.click(decrement); + } }); - test("Start incrementing from 0 when the min value is less than 0 and the max value is bigger than 0", async () => { + test("Start incrementing from 0 when the min value is less than 0 and the max value is bigger than 0", () => { const onBlur = jest.fn(); const { getByLabelText, getAllByRole } = render( <DxcNumberInput label="Number input label" min={-10} max={10} step={1} onBlur={onBlur} /> ); const number = getByLabelText("Number input label") as HTMLInputElement; const increment = getAllByRole("button")[1]; - increment && (await userEvent.click(increment)); + if (increment) { + userEvent.click(increment); + } expect(number.value).toBe("1"); - increment && (await userEvent.click(increment)); + if (increment) { + userEvent.click(increment); + } expect(number.value).toBe("2"); }); - test("Start incrementing from 0 when the min value is less than 0 and the max is 0", async () => { + test("Start incrementing from 0 when the min value is less than 0 and the max is 0", () => { const { getByLabelText, getAllByRole } = render( <DxcNumberInput label="Number input label" min={-10} max={0} step={1} /> ); const number = getByLabelText("Number input label") as HTMLInputElement; const increment = getAllByRole("button")[1]; - increment && (await userEvent.click(increment)); + if (increment) { + userEvent.click(increment); + } expect(number.value).toBe("0"); - increment && (await userEvent.click(increment)); + if (increment) { + userEvent.click(increment); + } expect(number.value).toBe("0"); }); - test("Start incrementing from the min value when it is bigger than 0", async () => { + test("Start incrementing from the min value when it is bigger than 0", () => { const { getByLabelText, getAllByRole } = render( <DxcNumberInput label="Number input label" min={2} max={10} step={0.5} /> ); const number = getByLabelText("Number input label") as HTMLInputElement; const increment = getAllByRole("button")[1]; - increment && (await userEvent.click(increment)); + if (increment) { + userEvent.click(increment); + } expect(number.value).toBe("2"); - increment && (await userEvent.click(increment)); + if (increment) { + userEvent.click(increment); + } expect(number.value).toBe("2.5"); }); - test("Start incrementing from the max value when it is less than 0", async () => { + test("Start incrementing from the max value when it is less than 0", () => { const { getByLabelText, getAllByRole } = render( <DxcNumberInput label="Number input label" min={-10} max={-1} step={0.5} /> ); const number = getByLabelText("Number input label") as HTMLInputElement; const increment = getAllByRole("button")[1]; - increment && (await userEvent.click(increment)); + if (increment) { + userEvent.click(increment); + } expect(number.value).toBe("-1"); - increment && (await userEvent.click(increment)); + if (increment) { + userEvent.click(increment); + } expect(number.value).toBe("-1"); }); - test("Start decrementing from 0 when the min value is less than 0 and the max value is bigger than 0", async () => { + test("Start decrementing from 0 when the min value is less than 0 and the max value is bigger than 0", () => { const { getByLabelText, getAllByRole } = render( <DxcNumberInput label="Number input label" min={-10} max={10} step={1} /> ); const number = getByLabelText("Number input label") as HTMLInputElement; const decrement = getAllByRole("button")[0]; - decrement && (await userEvent.click(decrement)); + if (decrement) { + userEvent.click(decrement); + } expect(number.value).toBe("-1"); }); - test("Start decrementing from 0 when the min value is 0 and the max value is bigger than 0", async () => { + test("Start decrementing from 0 when the min value is 0 and the max value is bigger than 0", () => { const { getByLabelText, getAllByRole } = render( <DxcNumberInput label="Number input label" min={0} max={10} step={1} /> ); const number = getByLabelText("Number input label") as HTMLInputElement; const decrement = getAllByRole("button")[0]; - decrement && (await userEvent.click(decrement)); + if (decrement) { + userEvent.click(decrement); + } expect(number.value).toBe("0"); }); - test("Start decrementing from the min value when it is bigger than 0", async () => { + test("Start decrementing from the min value when it is bigger than 0", () => { const { getByLabelText, getAllByRole } = render( <DxcNumberInput label="Number input label" min={2} max={10} step={0.5} /> ); const number = getByLabelText("Number input label") as HTMLInputElement; const decrement = getAllByRole("button")[0]; - decrement && (await userEvent.click(decrement)); + if (decrement) { + userEvent.click(decrement); + } expect(number.value).toBe("2"); - decrement && (await userEvent.click(decrement)); + if (decrement) { + userEvent.click(decrement); + } expect(number.value).toBe("2"); }); - test("Start decrementing from the max value when it is less than 0", async () => { + test("Start decrementing from the max value when it is less than 0", () => { const { getByLabelText, getAllByRole } = render( <DxcNumberInput label="Number input label" min={-10} max={-1} step={0.5} /> ); const number = getByLabelText("Number input label") as HTMLInputElement; const decrement = getAllByRole("button")[0]; - decrement && (await userEvent.click(decrement)); + if (decrement) { + userEvent.click(decrement); + } expect(number.value).toBe("-1"); - decrement && (await userEvent.click(decrement)); + if (decrement) { + userEvent.click(decrement); + } expect(number.value).toBe("-1.5"); }); test("Increment and decrement the value with min, max and step using the arrows in keyboard", () => { @@ -443,10 +534,10 @@ describe("Number input component tests", () => { const increment = getAllByRole("button")[1]; expect(increment?.getAttribute("aria-label")).toBe("Increment value"); }); - test("Number input submits correct values inside a form and actions don't trigger the submit event", async () => { - const handlerOnSubmit = jest.fn((e) => { + test("Number input submits correct values inside a form and actions don't trigger the submit event", () => { + const handlerOnSubmit = jest.fn((e: React.FormEvent<HTMLFormElement>) => { e.preventDefault(); - const formData = new FormData(e.target); + const formData = new FormData(e.currentTarget); const formProps = Object.fromEntries(formData); expect(formProps).toStrictEqual({ data: "0" }); }); @@ -459,11 +550,17 @@ describe("Number input component tests", () => { const less = getAllByRole("button")[0]; const more = getAllByRole("button")[1]; const submit = getByText("Submit"); - more && (await userEvent.click(more)); + if (more) { + userEvent.click(more); + } expect(handlerOnSubmit).not.toHaveBeenCalled(); - less && (await userEvent.click(less)); + if (less) { + userEvent.click(less); + } expect(handlerOnSubmit).not.toHaveBeenCalled(); - submit && (await userEvent.click(submit)); + if (submit) { + userEvent.click(submit); + } expect(handlerOnSubmit).toHaveBeenCalled(); }); }); diff --git a/packages/lib/src/number-input/NumberInput.tsx b/packages/lib/src/number-input/NumberInput.tsx index 748594276d..372ffdd14c 100644 --- a/packages/lib/src/number-input/NumberInput.tsx +++ b/packages/lib/src/number-input/NumberInput.tsx @@ -63,7 +63,7 @@ const DxcNumberInput = forwardRef<RefType, NumberInputPropsType>( ); useEffect(() => { - const input = numberInputRef.current?.getElementsByTagName("input")[0] as HTMLInputElement; + const input = numberInputRef.current?.getElementsByTagName("input")[0]; const preventDefault = (event: WheelEvent) => { event.preventDefault(); }; @@ -104,4 +104,6 @@ const DxcNumberInput = forwardRef<RefType, NumberInputPropsType>( } ); +DxcNumberInput.displayName = "DxcNumberInput"; + export default DxcNumberInput; diff --git a/packages/lib/src/paginator/Paginator.accessibility.test.tsx b/packages/lib/src/paginator/Paginator.accessibility.test.tsx index aa86a22377..02ee7e41af 100644 --- a/packages/lib/src/paginator/Paginator.accessibility.test.tsx +++ b/packages/lib/src/paginator/Paginator.accessibility.test.tsx @@ -2,22 +2,11 @@ import { render } from "@testing-library/react"; import { axe } from "../../test/accessibility/axe-helper"; import DxcPaginator from "./Paginator"; -// Mocking DOMRect for Radix Primitive Popover -(global as any).globalThis = global; -(global as any).DOMRect = { - fromRect: () => ({ top: 0, left: 0, bottom: 0, right: 0, width: 0, height: 0 }), -}; -(global as any).ResizeObserver = class ResizeObserver { - observe() {} - - unobserve() {} - - disconnect() {} -}; - -(global as any).DOMRect = { - fromRect: () => ({ top: 0, left: 0, bottom: 0, right: 0, width: 0, height: 0 }), -}; +global.ResizeObserver = jest.fn().mockImplementation(() => ({ + observe: jest.fn(), + unobserve: jest.fn(), + disconnect: jest.fn(), +})); describe("Paginator component accessibility tests", () => { it("Should not have basic accessibility issues", async () => { diff --git a/packages/lib/src/paginator/Paginator.test.tsx b/packages/lib/src/paginator/Paginator.test.tsx index 4902eba8b9..9087431b9b 100644 --- a/packages/lib/src/paginator/Paginator.test.tsx +++ b/packages/lib/src/paginator/Paginator.test.tsx @@ -2,22 +2,11 @@ import { render } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import DxcPaginator from "./Paginator"; -// Mocking DOMRect for Radix Primitive Popover -(global as any).globalThis = global; -(global as any).DOMRect = { - fromRect: () => ({ top: 0, left: 0, bottom: 0, right: 0, width: 0, height: 0 }), -}; -(global as any).ResizeObserver = class ResizeObserver { - observe() {} - - unobserve() {} - - disconnect() {} -}; - -(global as any).DOMRect = { - fromRect: () => ({ top: 0, left: 0, bottom: 0, right: 0, width: 0, height: 0 }), -}; +global.ResizeObserver = jest.fn().mockImplementation(() => ({ + observe: jest.fn(), + unobserve: jest.fn(), + disconnect: jest.fn(), +})); describe("Paginator component tests", () => { test("Paginator renders with default values", () => { @@ -57,7 +46,7 @@ describe("Paginator component tests", () => { expect(getByText("Go to page:")).toBeTruthy(); }); - test("Paginator goToPage call correct function", async () => { + test("Paginator goToPage call correct function", () => { const onClick = jest.fn(); window.HTMLElement.prototype.scrollIntoView = () => {}; window.HTMLElement.prototype.scrollTo = () => {}; @@ -65,23 +54,29 @@ describe("Paginator component tests", () => { <DxcPaginator currentPage={1} itemsPerPage={10} totalItems={27} showGoToPage onPageChange={onClick} /> ); const goToPageSelect = getAllByRole("combobox")[0]; - goToPageSelect && (await userEvent.click(goToPageSelect)); + if (goToPageSelect) { + userEvent.click(goToPageSelect); + } const goToPageOption = getByText("2"); - goToPageOption && (await userEvent.click(goToPageOption)); + if (goToPageOption) { + userEvent.click(goToPageOption); + } expect(onClick).toHaveBeenCalledWith(2); }); - test("Call correct goToPageFunction", async () => { + test("Call correct goToPageFunction", () => { const onClick = jest.fn(); const { getAllByRole } = render( <DxcPaginator onPageChange={onClick} currentPage={1} itemsPerPage={10} totalItems={20} /> ); const nextButton = getAllByRole("button")[2]; - nextButton && (await userEvent.click(nextButton)); + if (nextButton) { + userEvent.click(nextButton); + } expect(onClick).toHaveBeenCalled(); }); - test("Call correct itemsPerPageFunction", async () => { + test("Call correct itemsPerPageFunction", () => { const onClick = jest.fn(); window.HTMLElement.prototype.scrollIntoView = () => {}; window.HTMLElement.prototype.scrollTo = () => {}; @@ -95,53 +90,65 @@ describe("Paginator component tests", () => { /> ); const select = getAllByText("10")[0]; - select && (await userEvent.click(select)); + if (select) { + userEvent.click(select); + } const itemPerPageOption = getByText("15"); - itemPerPageOption && (await userEvent.click(itemPerPageOption)); + if (itemPerPageOption) { + userEvent.click(itemPerPageOption); + } expect(onClick).toHaveBeenCalledWith(15); }); - test("Next button is disable in last page", async () => { + test("Next button is disable in last page", () => { const onClick = jest.fn(); const { getAllByRole } = render( <DxcPaginator onPageChange={onClick} currentPage={2} itemsPerPage={10} totalItems={20} /> ); const nextButton = getAllByRole("button")[2]; expect(nextButton?.hasAttribute("disabled")).toBeTruthy(); - nextButton && (await userEvent.click(nextButton)); + if (nextButton) { + userEvent.click(nextButton); + } expect(onClick).toHaveBeenCalledTimes(0); }); - test("Last button is disable in last page", async () => { + test("Last button is disable in last page", () => { const onClick = jest.fn(); const { getAllByRole } = render( <DxcPaginator onPageChange={onClick} currentPage={2} itemsPerPage={10} totalItems={20} /> ); const lastButton = getAllByRole("button")[3]; expect(lastButton?.hasAttribute("disabled")).toBeTruthy(); - lastButton && (await userEvent.click(lastButton)); + if (lastButton) { + userEvent.click(lastButton); + } expect(onClick).toHaveBeenCalledTimes(0); }); - test("First button is disable in first page", async () => { + test("First button is disable in first page", () => { const onClick = jest.fn(); const { getAllByRole } = render( <DxcPaginator onPageChange={onClick} currentPage={1} itemsPerPage={10} totalItems={20} /> ); const lastButton = getAllByRole("button")[0]; expect(lastButton?.hasAttribute("disabled")).toBeTruthy(); - lastButton && (await userEvent.click(lastButton)); + if (lastButton) { + userEvent.click(lastButton); + } expect(onClick).toHaveBeenCalledTimes(0); }); - test("Previous button is disable in first page", async () => { + test("Previous button is disable in first page", () => { const onClick = jest.fn(); const { getAllByRole } = render( <DxcPaginator onPageChange={onClick} currentPage={1} itemsPerPage={10} totalItems={20} /> ); const lastButton = getAllByRole("button")[1]; expect(lastButton?.hasAttribute("disabled")).toBeTruthy(); - lastButton && (await userEvent.click(lastButton)); + if (lastButton) { + userEvent.click(lastButton); + } expect(onClick).toHaveBeenCalledTimes(0); }); diff --git a/packages/lib/src/password-input/PasswordInput.accessibility.test.tsx b/packages/lib/src/password-input/PasswordInput.accessibility.test.tsx index 36491e870f..18962a0f35 100644 --- a/packages/lib/src/password-input/PasswordInput.accessibility.test.tsx +++ b/packages/lib/src/password-input/PasswordInput.accessibility.test.tsx @@ -2,16 +2,11 @@ import { render } from "@testing-library/react"; import { axe } from "../../test/accessibility/axe-helper"; import DxcPasswordInput from "./PasswordInput"; -// Mocking DOMRect for Radix Primitive Popover -(global as any).globalThis = global; -(global as any).DOMRect = { - fromRect: () => ({ top: 0, left: 0, bottom: 0, right: 0, width: 0, height: 0 }), -}; -(global as any).ResizeObserver = class ResizeObserver { - observe() {} - unobserve() {} - disconnect() {} -}; +global.ResizeObserver = jest.fn().mockImplementation(() => ({ + observe: jest.fn(), + unobserve: jest.fn(), + disconnect: jest.fn(), +})); describe("Password input component accessibility tests", () => { it("Should not have basic accessibility issues", async () => { diff --git a/packages/lib/src/password-input/PasswordInput.stories.tsx b/packages/lib/src/password-input/PasswordInput.stories.tsx index eebf6602ce..fca39f385c 100644 --- a/packages/lib/src/password-input/PasswordInput.stories.tsx +++ b/packages/lib/src/password-input/PasswordInput.stories.tsx @@ -1,9 +1,9 @@ +import { Meta, StoryObj } from "@storybook/react"; import { userEvent, within } from "@storybook/test"; import ExampleContainer from "../../.storybook/components/ExampleContainer"; import Title from "../../.storybook/components/Title"; import DxcFlex from "../flex/Flex"; import DxcPasswordInput from "./PasswordInput"; -import { Meta, StoryObj } from "@storybook/react"; export default { title: "Password Input", diff --git a/packages/lib/src/password-input/PasswordInput.test.tsx b/packages/lib/src/password-input/PasswordInput.test.tsx index 06b82f8583..71ef613e40 100644 --- a/packages/lib/src/password-input/PasswordInput.test.tsx +++ b/packages/lib/src/password-input/PasswordInput.test.tsx @@ -2,16 +2,11 @@ import { fireEvent, render } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import DxcPasswordInput from "./PasswordInput"; -// Mocking DOMRect for Radix Primitive Popover -(global as any).globalThis = global; -(global as any).DOMRect = { - fromRect: () => ({ top: 0, left: 0, bottom: 0, right: 0, width: 0, height: 0 }), -}; -(global as any).ResizeObserver = class ResizeObserver { - observe() {} - unobserve() {} - disconnect() {} -}; +global.ResizeObserver = jest.fn().mockImplementation(() => ({ + observe: jest.fn(), + unobserve: jest.fn(), + disconnect: jest.fn(), +})); describe("Password input component tests", () => { test("Password input renders with label and helper text", () => { @@ -44,17 +39,19 @@ describe("Password input component tests", () => { expect(passwordInput.value).toBe("Pa$$w0rd"); }); - test("Clear password input value", async () => { + test("Clear password input value", () => { const { getAllByRole, getByLabelText } = render(<DxcPasswordInput label="Password input" clearable />); const passwordInput = getByLabelText("Password input") as HTMLInputElement; userEvent.type(passwordInput, "Pa$$w0rd"); expect(passwordInput.value).toBe("Pa$$w0rd"); const clearButton = getAllByRole("button")[0]; - clearButton && await userEvent.click(clearButton); + if (clearButton) { + userEvent.click(clearButton); + } expect(passwordInput.value).toBe(""); }); - test("Non clearable password input has no clear icon", async () => { + test("Non clearable password input has no clear icon", () => { const { getAllByRole, getByLabelText } = render(<DxcPasswordInput label="Password input" />); const passwordInput = getByLabelText("Password input") as HTMLInputElement; userEvent.type(passwordInput, "Pa$$w0rd"); @@ -63,24 +60,26 @@ describe("Password input component tests", () => { expect(buttons.length).toBe(1); }); - test("Show/hide password input button works correctly", async () => { + test("Show/hide password input button works correctly", () => { const { getAllByRole, getByLabelText } = render(<DxcPasswordInput label="Password input" clearable />); const passwordInput = getByLabelText("Password input") as HTMLInputElement; userEvent.type(passwordInput, "Pa$$w0rd"); expect(passwordInput.value).toBe("Pa$$w0rd"); expect(passwordInput.type).toBe("password"); const showButton = getAllByRole("button")[1]; - showButton && await userEvent.click(showButton); + if (showButton) { + userEvent.click(showButton); + } expect(passwordInput.type).toBe("text"); }); - test("Password input has correct accessibility attributes", async () => { + test("Password input has correct accessibility attributes", () => { const { getByRole, getByLabelText } = render(<DxcPasswordInput label="Password input" />); const showButton = getByRole("button"); expect(getByLabelText("Password input")).toBeTruthy(); expect(showButton.getAttribute("aria-expanded")).toBe("false"); expect(showButton.getAttribute("aria-label")).toBe("Show password"); - await userEvent.click(showButton); + userEvent.click(showButton); expect(showButton.getAttribute("aria-expanded")).toBe("true"); expect(showButton.getAttribute("aria-label")).toBe("Hide password"); }); diff --git a/packages/lib/src/password-input/PasswordInput.tsx b/packages/lib/src/password-input/PasswordInput.tsx index f5ee9280d3..cd3a040fea 100644 --- a/packages/lib/src/password-input/PasswordInput.tsx +++ b/packages/lib/src/password-input/PasswordInput.tsx @@ -74,7 +74,7 @@ const DxcPasswordInput = forwardRef<RefType, PasswordInputPropsType>( setIsPasswordVisible((isPasswordCurrentlyVisible) => !isPasswordCurrentlyVisible); }, icon: isPasswordVisible ? "Visibility_Off" : "Visibility", - title: isPasswordVisible ? passwordInput.inputHidePasswordTitle : passwordInput.inputShowPasswordTitle, + title: isPasswordVisible ? passwordInput?.inputHidePasswordTitle : passwordInput?.inputShowPasswordTitle, }} error={error} clearable={clearable} @@ -95,4 +95,6 @@ const DxcPasswordInput = forwardRef<RefType, PasswordInputPropsType>( } ); +DxcPasswordInput.displayName = "DxcPasswordInput"; + export default DxcPasswordInput; diff --git a/packages/lib/src/quick-nav/QuickNav.stories.tsx b/packages/lib/src/quick-nav/QuickNav.stories.tsx index da5925c582..6e9b90bcc5 100644 --- a/packages/lib/src/quick-nav/QuickNav.stories.tsx +++ b/packages/lib/src/quick-nav/QuickNav.stories.tsx @@ -1,10 +1,10 @@ +import { Meta, StoryObj } from "@storybook/react"; import styled from "@emotion/styled"; import ExampleContainer from "../../.storybook/components/ExampleContainer"; import Title from "../../.storybook/components/Title"; import DxcHeading from "../heading/Heading"; import DxcParagraph from "../paragraph/Paragraph"; import DxcQuickNav from "./QuickNav"; -import { Meta, StoryObj } from "@storybook/react"; export default { title: "Quick Nav", @@ -126,37 +126,38 @@ const QuickNav = () => ( <Content id="overview"> <DxcHeading level={1} text="Overview" margin={{ bottom: "small" }} /> <DxcParagraph> - Halstack is the DXC Technology's open source design system for insurance products and digital experiences. - Our system provides all the tools and resources needed to create superior, beautiful but above all, - functional user experiences. Halstack is the DXC Technology's open source design system for insurance - products and digital experiences. Our system provides all the tools and resources needed to create - superior, beautiful but above all, functional user experiences.Halstack is the DXC Technology's open - source design system for insurance products and digital experiences. Our system provides all the tools and - resources needed to create superior, beautiful but above all, functional user experiences.Halstack is the - DXC Technology's open source design system for insurance products and digital experiences. Our system - provides all the tools and resources needed to create superior, beautiful but above all, functional user - experiences.Halstack is the DXC Technology's open source design system for insurance products and digital + Halstack is the DXC Technology's open source design system for insurance products and digital experiences. Our system provides all the tools and resources needed to create superior, beautiful but - above all, functional user experiences.Halstack is the DXC Technology's open source design system for - insurance products and digital experiences. Our system provides all the tools and resources needed to - create superior, beautiful but above all, functional user experiences.Halstack is the DXC Technology's - open source design system for insurance products and digital experiences. Our system provides all the - tools and resources needed to create superior, beautiful but above all, functional user experiences. + above all, functional user experiences. Halstack is the DXC Technology's open source design system + for insurance products and digital experiences. Our system provides all the tools and resources needed to + create superior, beautiful but above all, functional user experiences.Halstack is the DXC + Technology's open source design system for insurance products and digital experiences. Our system + provides all the tools and resources needed to create superior, beautiful but above all, functional user + experiences.Halstack is the DXC Technology's open source design system for insurance products and + digital experiences. Our system provides all the tools and resources needed to create superior, beautiful + but above all, functional user experiences.Halstack is the DXC Technology's open source design system + for insurance products and digital experiences. Our system provides all the tools and resources needed to + create superior, beautiful but above all, functional user experiences.Halstack is the DXC + Technology's open source design system for insurance products and digital experiences. Our system + provides all the tools and resources needed to create superior, beautiful but above all, functional user + experiences.Halstack is the DXC Technology's open source design system for insurance products and + digital experiences. Our system provides all the tools and resources needed to create superior, beautiful + but above all, functional user experiences. </DxcParagraph> <Content id="overview-introduction"> <DxcHeading level={2} text="Introduction" margin={{ top: "xsmall", bottom: "xsmall" }} /> <DxcParagraph> - Design principles Halstack design principles are the fundamental part of DXC Technology's approach to - provide guidance for development teams in order to deliver delightful and consistent user experiences to - our customers: Balance Consistency Visual hierarchy All our components, design tokens, accessibility + Design principles Halstack design principles are the fundamental part of DXC Technology's approach + to provide guidance for development teams in order to deliver delightful and consistent user experiences + to our customers: Balance Consistency Visual hierarchy All our components, design tokens, accessibility guidelines, responsive design techniques, and layout proposals have been carefully curated by DXC design and engineering teams with the objective of creating a unique visual language and ecosystem for our applications. This is the DXC way of creating User Experiences. Open Source Halstack is an open source design system, this means that we work towards DXC Technology bussines needs, but it is open for anyone to use and contribute back to. We are charmed to receive external contributions to help us find bugs, - design new features, or help us improve the project documentation. If you're interested, definitely + design new features, or help us improve the project documentation. If you're interested, definitely check out our contribution guidelines.Design principles Halstack design principles are the fundamental - part of DXC Technology's approach to provide guidance for development teams in order to deliver + part of DXC Technology's approach to provide guidance for development teams in order to deliver delightful and consistent user experiences to our customers: Balance Consistency Visual hierarchy All our components, design tokens, accessibility guidelines, responsive design techniques, and layout proposals have been carefully curated by DXC design and engineering teams with the objective of creating @@ -164,16 +165,16 @@ const QuickNav = () => ( Experiences. Open Source Halstack is an open source design system, this means that we work towards DXC Technology bussines needs, but it is open for anyone to use and contribute back to. We are charmed to receive external contributions to help us find bugs, design new features, or help us improve the project - documentation. If you're interested, definitely check out our contribution guidelines.Design principles - Halstack design principles are the fundamental part of DXC Technology's approach to provide guidance for - development teams in order to deliver delightful and consistent user experiences to our customers: - Balance Consistency Visual hierarchy All our components, design tokens, accessibility guidelines, - responsive design techniques, and layout proposals have been carefully curated by DXC design and - engineering teams with the objective of creating a unique visual language and ecosystem for our + documentation. If you're interested, definitely check out our contribution guidelines.Design + principles Halstack design principles are the fundamental part of DXC Technology's approach to + provide guidance for development teams in order to deliver delightful and consistent user experiences to + our customers: Balance Consistency Visual hierarchy All our components, design tokens, accessibility + guidelines, responsive design techniques, and layout proposals have been carefully curated by DXC design + and engineering teams with the objective of creating a unique visual language and ecosystem for our applications. This is the DXC way of creating User Experiences. Open Source Halstack is an open source design system, this means that we work towards DXC Technology bussines needs, but it is open for anyone to use and contribute back to. We are charmed to receive external contributions to help us find bugs, - design new features, or help us improve the project documentation. If you're interested, definitely + design new features, or help us improve the project documentation. If you're interested, definitely check out our contribution guidelines. </DxcParagraph> </Content> @@ -183,17 +184,17 @@ const QuickNav = () => ( <Content id="components-introduction"> <DxcHeading level={2} text="Introduction" margin={{ top: "xsmall", bottom: "xsmall" }} /> <DxcParagraph> - Design principles Halstack design principles are the fundamental part of DXC Technology's approach to - provide guidance for development teams in order to deliver delightful and consistent user experiences to - our customers: Balance Consistency Visual hierarchy All our components, design tokens, accessibility + Design principles Halstack design principles are the fundamental part of DXC Technology's approach + to provide guidance for development teams in order to deliver delightful and consistent user experiences + to our customers: Balance Consistency Visual hierarchy All our components, design tokens, accessibility guidelines, responsive design techniques, and layout proposals have been carefully curated by DXC design and engineering teams with the objective of creating a unique visual language and ecosystem for our applications. This is the DXC way of creating User Experiences. Open Source Halstack is an open source design system, this means that we work towards DXC Technology bussines needs, but it is open for anyone to use and contribute back to. We are charmed to receive external contributions to help us find bugs, - design new features, or help us improve the project documentation. If you're interested, definitely + design new features, or help us improve the project documentation. If you're interested, definitely check out our contribution guidelines.Design principles Halstack design principles are the fundamental - part of DXC Technology's approach to provide guidance for development teams in order to deliver + part of DXC Technology's approach to provide guidance for development teams in order to deliver delightful and consistent user experiences to our customers: Balance Consistency Visual hierarchy All our components, design tokens, accessibility guidelines, responsive design techniques, and layout proposals have been carefully curated by DXC design and engineering teams with the objective of creating @@ -201,16 +202,16 @@ const QuickNav = () => ( Experiences. Open Source Halstack is an open source design system, this means that we work towards DXC Technology bussines needs, but it is open for anyone to use and contribute back to. We are charmed to receive external contributions to help us find bugs, design new features, or help us improve the project - documentation. If you're interested, definitely check out our contribution guidelines.Design principles - Halstack design principles are the fundamental part of DXC Technology's approach to provide guidance for - development teams in order to deliver delightful and consistent user experiences to our customers: - Balance Consistency Visual hierarchy All our components, design tokens, accessibility guidelines, - responsive design techniques, and layout proposals have been carefully curated by DXC design and - engineering teams with the objective of creating a unique visual language and ecosystem for our + documentation. If you're interested, definitely check out our contribution guidelines.Design + principles Halstack design principles are the fundamental part of DXC Technology's approach to + provide guidance for development teams in order to deliver delightful and consistent user experiences to + our customers: Balance Consistency Visual hierarchy All our components, design tokens, accessibility + guidelines, responsive design techniques, and layout proposals have been carefully curated by DXC design + and engineering teams with the objective of creating a unique visual language and ecosystem for our applications. This is the DXC way of creating User Experiences. Open Source Halstack is an open source design system, this means that we work towards DXC Technology bussines needs, but it is open for anyone to use and contribute back to. We are charmed to receive external contributions to help us find bugs, - design new features, or help us improve the project documentation. If you're interested, definitely + design new features, or help us improve the project documentation. If you're interested, definitely check out our contribution guidelines. </DxcParagraph> </Content> @@ -229,37 +230,37 @@ const QuickNav = () => ( <DxcHeading level={2} text="Color" margin={{ top: "xsmall", bottom: "xsmall" }} /> <DxcParagraph> The color palette is an essential asset as a communication resource of our design system. Halstack color - palette brings a unified consistency and helps in guiding the user's perception order. Our color palette - is based in the HSL model . All our color families are calculated using the lightness value of the - standard DXC palette colors. Color Tokens Halstack uses tokens to manage color. Appart from a + palette brings a unified consistency and helps in guiding the user's perception order. Our color + palette is based in the HSL model . All our color families are calculated using the lightness value of + the standard DXC palette colors. Color Tokens Halstack uses tokens to manage color. Appart from a multi-purpose greyscale family, purple and blue are the core color families used in our set of components. Additional families as red, green and yellow help as feedback role-based color palettes and must not be used outside this context.The color palette is an essential asset as a communication resource of our design system. Halstack color palette brings a unified consistency and helps in guiding - the user's perception order. Our color palette is based in the HSL model . All our color families are - calculated using the lightness value of the standard DXC palette colors. Color Tokens Halstack uses + the user's perception order. Our color palette is based in the HSL model . All our color families + are calculated using the lightness value of the standard DXC palette colors. Color Tokens Halstack uses tokens to manage color. Appart from a multi-purpose greyscale family, purple and blue are the core color families used in our set of components. Additional families as red, green and yellow help as feedback role-based color palettes and must not be used outside this context.The color palette is an essential asset as a communication resource of our design system. Halstack color palette brings a unified - consistency and helps in guiding the user's perception order. Our color palette is based in the HSL + consistency and helps in guiding the user's perception order. Our color palette is based in the HSL model . All our color families are calculated using the lightness value of the standard DXC palette colors. Color Tokens Halstack uses tokens to manage color. Appart from a multi-purpose greyscale family, purple and blue are the core color families used in our set of components. Additional families as red, green and yellow help as feedback role-based color palettes and must not be used outside this context.The color palette is an essential asset as a communication resource of our design system. - Halstack color palette brings a unified consistency and helps in guiding the user's perception order. - Our color palette is based in the HSL model . All our color families are calculated using the lightness - value of the standard DXC palette colors. Color Tokens Halstack uses tokens to manage color. Appart from - a multi-purpose greyscale family, purple and blue are the core color families used in our set of - components. Additional families as red, green and yellow help as feedback role-based color palettes and - must not be used outside this context.The color palette is an essential asset as a communication - resource of our design system. Halstack color palette brings a unified consistency and helps in guiding - the user's perception order. Our color palette is based in the HSL model . All our color families are - calculated using the lightness value of the standard DXC palette colors. Color Tokens Halstack uses - tokens to manage color. Appart from a multi-purpose greyscale family, purple and blue are the core color - families used in our set of components. Additional families as red, green and yellow help as feedback - role-based color palettes and must not be used outside this context. + Halstack color palette brings a unified consistency and helps in guiding the user's perception + order. Our color palette is based in the HSL model . All our color families are calculated using the + lightness value of the standard DXC palette colors. Color Tokens Halstack uses tokens to manage color. + Appart from a multi-purpose greyscale family, purple and blue are the core color families used in our + set of components. Additional families as red, green and yellow help as feedback role-based color + palettes and must not be used outside this context.The color palette is an essential asset as a + communication resource of our design system. Halstack color palette brings a unified consistency and + helps in guiding the user's perception order. Our color palette is based in the HSL model . All our + color families are calculated using the lightness value of the standard DXC palette colors. Color Tokens + Halstack uses tokens to manage color. Appart from a multi-purpose greyscale family, purple and blue are + the core color families used in our set of components. Additional families as red, green and yellow help + as feedback role-based color palettes and must not be used outside this context. </DxcParagraph> </Content> <Content id="principles-very-very-very-very-very-very-very-very-long-spacingveryveryveryveryveryveryveryverylong"> @@ -287,37 +288,38 @@ const QuickNav = () => ( <Content id="principles-very-very-very-very-very-very-very-very-long-typography"> <DxcHeading level={2} text="Typography" margin={{ top: "xsmall", bottom: "xsmall" }} /> <DxcParagraph> - Our selected typography helps in structuring our user's experience based on the visual impact that it - has on the user interface content. It defines what is the first noticeable piece of information or data - based on the font shape, size, color, or type and it highlights some pieces of text over the rest. Some - typographic elements used in Halstack Design System include headers, body, taglines, captions, and + Our selected typography helps in structuring our user's experience based on the visual impact that + it has on the user interface content. It defines what is the first noticeable piece of information or + data based on the font shape, size, color, or type and it highlights some pieces of text over the rest. + Some typographic elements used in Halstack Design System include headers, body, taglines, captions, and labels. Make sure you include all the different typographic variants in order to enhance the - application's content structure, including the Heading component which defines different levels of page - and section titles.Our selected typography helps in structuring our user's experience based on the - visual impact that it has on the user interface content. It defines what is the first noticeable piece - of information or data based on the font shape, size, color, or type and it highlights some pieces of - text over the rest. Some typographic elements used in Halstack Design System include headers, body, + application's content structure, including the Heading component which defines different levels of + page and section titles.Our selected typography helps in structuring our user's experience based on + the visual impact that it has on the user interface content. It defines what is the first noticeable + piece of information or data based on the font shape, size, color, or type and it highlights some pieces + of text over the rest. Some typographic elements used in Halstack Design System include headers, body, taglines, captions, and labels. Make sure you include all the different typographic variants in order to - enhance the application's content structure, including the Heading component which defines different - levels of page and section titles.Our selected typography helps in structuring our user's experience - based on the visual impact that it has on the user interface content. It defines what is the first - noticeable piece of information or data based on the font shape, size, color, or type and it highlights - some pieces of text over the rest. Some typographic elements used in Halstack Design System include - headers, body, taglines, captions, and labels. Make sure you include all the different typographic - variants in order to enhance the application's content structure, including the Heading component which - defines different levels of page and section titles.Our selected typography helps in structuring our - user's experience based on the visual impact that it has on the user interface content. It defines what - is the first noticeable piece of information or data based on the font shape, size, color, or type and - it highlights some pieces of text over the rest. Some typographic elements used in Halstack Design - System include headers, body, taglines, captions, and labels. Make sure you include all the different - typographic variants in order to enhance the application's content structure, including the Heading + enhance the application's content structure, including the Heading component which defines + different levels of page and section titles.Our selected typography helps in structuring our user's + experience based on the visual impact that it has on the user interface content. It defines what is the + first noticeable piece of information or data based on the font shape, size, color, or type and it + highlights some pieces of text over the rest. Some typographic elements used in Halstack Design System + include headers, body, taglines, captions, and labels. Make sure you include all the different + typographic variants in order to enhance the application's content structure, including the Heading component which defines different levels of page and section titles.Our selected typography helps in - structuring our user's experience based on the visual impact that it has on the user interface content. - It defines what is the first noticeable piece of information or data based on the font shape, size, - color, or type and it highlights some pieces of text over the rest. Some typographic elements used in - Halstack Design System include headers, body, taglines, captions, and labels. Make sure you include all - the different typographic variants in order to enhance the application's content structure, including - the Heading component which defines different levels of page and section titles. + structuring our user's experience based on the visual impact that it has on the user interface + content. It defines what is the first noticeable piece of information or data based on the font shape, + size, color, or type and it highlights some pieces of text over the rest. Some typographic elements used + in Halstack Design System include headers, body, taglines, captions, and labels. Make sure you include + all the different typographic variants in order to enhance the application's content structure, + including the Heading component which defines different levels of page and section titles.Our selected + typography helps in structuring our user's experience based on the visual impact that it has on the + user interface content. It defines what is the first noticeable piece of information or data based on + the font shape, size, color, or type and it highlights some pieces of text over the rest. Some + typographic elements used in Halstack Design System include headers, body, taglines, captions, and + labels. Make sure you include all the different typographic variants in order to enhance the + application's content structure, including the Heading component which defines different levels of + page and section titles. </DxcParagraph> </Content> </Content> diff --git a/packages/lib/src/radio-group/RadioGroup.stories.tsx b/packages/lib/src/radio-group/RadioGroup.stories.tsx index ab8a93d340..add1cedeac 100644 --- a/packages/lib/src/radio-group/RadioGroup.stories.tsx +++ b/packages/lib/src/radio-group/RadioGroup.stories.tsx @@ -8,7 +8,7 @@ export default { component: DxcRadioGroup, } as Meta<typeof DxcRadioGroup>; -const single_option = [{ label: "Option A", value: "A" }]; +const singleOption = [{ label: "Option A", value: "A" }]; const options = [ { label: "Option 1", value: "1" }, @@ -17,44 +17,44 @@ const options = [ { label: "Option 4", value: "4" }, ]; -const single_disabled_options = [{ label: "Option A", value: "A", disabled: true }]; +const singleDisabledOptions = [{ label: "Option A", value: "A", disabled: true }]; const RadioGroup = () => ( <> <Title title="Enabled" theme="light" level={2} /> <ExampleContainer> <Title title="Default" theme="light" level={4} /> - <DxcRadioGroup label="Label" helperText="Helper text" defaultValue="A" options={single_option} /> + <DxcRadioGroup label="Label" helperText="Helper text" defaultValue="A" options={singleOption} /> </ExampleContainer> <ExampleContainer pseudoState="pseudo-hover"> <Title title="Hovered" theme="light" level={4} /> - <DxcRadioGroup label="Label" helperText="Helper text" defaultValue="A" options={single_option} /> + <DxcRadioGroup label="Label" helperText="Helper text" defaultValue="A" options={singleOption} /> </ExampleContainer> <ExampleContainer pseudoState="pseudo-active"> <Title title="Active" theme="light" level={4} /> - <DxcRadioGroup label="Label" helperText="Helper text" defaultValue="A" options={single_option} /> + <DxcRadioGroup label="Label" helperText="Helper text" defaultValue="A" options={singleOption} /> </ExampleContainer> <ExampleContainer pseudoState="pseudo-focus"> <Title title="Focused" theme="light" level={4} /> - <DxcRadioGroup label="Label" helperText="Helper text" defaultValue="A" options={single_option} /> + <DxcRadioGroup label="Label" helperText="Helper text" defaultValue="A" options={singleOption} /> </ExampleContainer> <Title title="Disabled" theme="light" level={2} /> <ExampleContainer> <Title title="Disabled" theme="light" level={4} /> - <DxcRadioGroup label="Label" helperText="Helper text" options={single_disabled_options} defaultValue="A" /> + <DxcRadioGroup label="Label" helperText="Helper text" options={singleDisabledOptions} defaultValue="A" /> </ExampleContainer> <Title title="Readonly" theme="light" level={2} /> <ExampleContainer> <Title title="Default" theme="light" level={4} /> - <DxcRadioGroup label="Label" helperText="Helper text" options={single_option} defaultValue="A" readOnly /> + <DxcRadioGroup label="Label" helperText="Helper text" options={singleOption} defaultValue="A" readOnly /> </ExampleContainer> <ExampleContainer pseudoState="pseudo-hover"> <Title title="Hovered" theme="light" level={4} /> - <DxcRadioGroup label="Label" helperText="Helper text" options={single_option} defaultValue="A" readOnly /> + <DxcRadioGroup label="Label" helperText="Helper text" options={singleOption} defaultValue="A" readOnly /> </ExampleContainer> <ExampleContainer pseudoState="pseudo-active"> <Title title="Active" theme="light" level={4} /> - <DxcRadioGroup label="Label" helperText="Helper text" options={single_option} defaultValue="A" readOnly /> + <DxcRadioGroup label="Label" helperText="Helper text" options={singleOption} defaultValue="A" readOnly /> </ExampleContainer> <Title title="Error" theme="light" level={2} /> <ExampleContainer> @@ -62,7 +62,7 @@ const RadioGroup = () => ( <DxcRadioGroup label="Label" helperText="Helper text" - options={single_option} + options={singleOption} defaultValue="A" error="Error message" /> @@ -72,7 +72,7 @@ const RadioGroup = () => ( <DxcRadioGroup label="Label" helperText="Helper text" - options={single_option} + options={singleOption} defaultValue="A" readOnly error="Error message" @@ -83,7 +83,7 @@ const RadioGroup = () => ( <DxcRadioGroup label="Label" helperText="Helper text" - options={single_option} + options={singleOption} defaultValue="A" readOnly error="Error message" diff --git a/packages/lib/src/radio-group/RadioGroup.test.tsx b/packages/lib/src/radio-group/RadioGroup.test.tsx index 333defaca7..d2a5b4f203 100644 --- a/packages/lib/src/radio-group/RadioGroup.test.tsx +++ b/packages/lib/src/radio-group/RadioGroup.test.tsx @@ -36,8 +36,7 @@ describe("Radio Group component tests", () => { expect(error.getAttribute("aria-live")).toBe("off"); radios.forEach((radio, index) => { // if no option was previously selected, first option is the focusable one - if (index === 0) expect(radio.tabIndex).toBe(0); - else expect(radio.tabIndex).toBe(-1); + expect(radio.tabIndex).toBe(index === 0 ? 0 : -1); expect(radio.getAttribute("aria-checked")).toBe("false"); expect(radio.getAttribute("aria-disabled")).toBe("false"); }); @@ -55,10 +54,10 @@ describe("Radio Group component tests", () => { expect(radioGroup.getAttribute("aria-orientation")).toBe("horizontal"); }); - test("Sends its value when submitted", async () => { - const handlerOnSubmit = jest.fn((e) => { + test("Sends its value when submitted", () => { + const handlerOnSubmit = jest.fn((e: React.FormEvent<HTMLFormElement>) => { e.preventDefault(); - const formData = new FormData(e.target); + const formData = new FormData(e.currentTarget); const formProps = Object.fromEntries(formData); expect(formProps).toStrictEqual({ radiogroup: "5" }); }); @@ -71,9 +70,11 @@ describe("Radio Group component tests", () => { const radioGroup = getByRole("radiogroup"); const submit = getByText("Submit"); const radio = getAllByRole("radio")[4]; - await userEvent.click(radioGroup); - radio && (await userEvent.click(radio)); - await userEvent.click(submit); + userEvent.click(radioGroup); + if (radio) { + userEvent.click(radio); + } + userEvent.click(submit); }); test("Disabled state renders with correct aria attribute, correct tabIndex values and it is not focusable by keyboard", () => { @@ -86,9 +87,24 @@ describe("Radio Group component tests", () => { radios.forEach((radio) => { expect(radio.tabIndex).toBe(-1); }); - fireEvent.keyDown(radioGroup, { key: " ", code: "Space", keyCode: 13, charCode: 13 }); - fireEvent.keyDown(radioGroup, { key: "ArrowLeft", code: "ArrowLeft", keyCode: 37, charCode: 37 }); - fireEvent.keyDown(radioGroup, { key: "ArrowDown", code: "ArrowDown", keyCode: 40, charCode: 40 }); + fireEvent.keyDown(radioGroup, { + key: " ", + code: "Space", + keyCode: 13, + charCode: 13, + }); + fireEvent.keyDown(radioGroup, { + key: "ArrowLeft", + code: "ArrowLeft", + keyCode: 37, + charCode: 37, + }); + fireEvent.keyDown(radioGroup, { + key: "ArrowDown", + code: "ArrowDown", + keyCode: 40, + charCode: 40, + }); radios.forEach((radio) => { expect(radio.tabIndex).toBe(-1); }); @@ -111,10 +127,10 @@ describe("Radio Group component tests", () => { expect(radios[2]?.tabIndex).toBe(-1); }); - test("Disabled radio group doesn't send its value when submitted", async () => { - const handlerOnSubmit = jest.fn((e) => { + test("Disabled radio group doesn't send its value when submitted", () => { + const handlerOnSubmit = jest.fn((e: React.FormEvent<HTMLFormElement>) => { e.preventDefault(); - const formData = new FormData(e.target); + const formData = new FormData(e.currentTarget); const formProps = Object.fromEntries(formData); expect(formProps).toStrictEqual({}); }); @@ -125,7 +141,7 @@ describe("Radio Group component tests", () => { </form> ); const submit = getByText("Submit"); - await userEvent.click(submit); + userEvent.click(submit); }); test("Error state renders with correct aria attributes", () => { @@ -139,7 +155,7 @@ describe("Radio Group component tests", () => { expect(errorMessage.getAttribute("aria-live")).toBe("assertive"); }); - test("Radio group with required constraint and 'undefined' as value, sends an error", async () => { + test("Radio group with required constraint and 'undefined' as value, sends an error", () => { const onChange = jest.fn(); const onBlur = jest.fn(); const { getByRole, getAllByRole } = render( @@ -149,15 +165,19 @@ describe("Radio Group component tests", () => { const radio = getAllByRole("radio")[0]; expect(radioGroup.getAttribute("aria-required")).toBe("true"); fireEvent.blur(radioGroup); - expect(onBlur).toHaveBeenCalledWith({ error: "This field is required. Please, choose an option." }); - await userEvent.click(radioGroup); - radio && (await userEvent.click(radio)); + expect(onBlur).toHaveBeenCalledWith({ + error: "This field is required. Please, choose an option.", + }); + userEvent.click(radioGroup); + if (radio) { + userEvent.click(radio); + } expect(onChange).toHaveBeenCalledWith("1"); fireEvent.blur(radioGroup); expect(onBlur).toHaveBeenCalledWith({ value: "1" }); }); - test("Radio group with required constraint and empty string as value, sends an error", async () => { + test("Radio group with required constraint and empty string as value, sends an error", () => { const onChange = jest.fn(); const onBlur = jest.fn(); const { getByRole, getAllByRole } = render( @@ -168,7 +188,9 @@ describe("Radio Group component tests", () => { expect(radioGroup.getAttribute("aria-required")).toBe("true"); fireEvent.blur(radioGroup); expect(onBlur).toHaveBeenCalledWith({ value: "", error: "This field is required. Please, choose an option." }); - radio && (await userEvent.click(radio)); + if (radio) { + userEvent.click(radio); + } expect(onChange).toHaveBeenCalledWith("1"); }); @@ -191,7 +213,7 @@ describe("Radio Group component tests", () => { expect(submitInput?.value).toBe("2"); }); - test("Optional radio group conditions: onBlur event doesn't send an error when no radio was checked, has correct aria attributes, custom label and its value is the empty string", async () => { + test("Optional radio group conditions: onBlur event doesn't send an error when no radio was checked, has correct aria attributes, custom label and its value is the empty string", () => { const onChange = jest.fn(); const onBlur = jest.fn(); const { getByRole, getByText, container } = render( @@ -213,12 +235,12 @@ describe("Radio Group component tests", () => { expect(radioGroup.getAttribute("aria-invalid")).toBe("false"); const optionalLabel = getByText("No selection"); const submitInput = container.querySelector<HTMLInputElement>(`input[name="test"]`); - await userEvent.click(optionalLabel); + userEvent.click(optionalLabel); expect(onChange).toHaveBeenCalledWith(""); expect(submitInput?.value).toBe(""); }); - test("Controlled radio group", async () => { + test("Controlled radio group", () => { const onChange = jest.fn(); const onBlur = jest.fn(); const { getByRole, getAllByRole, container } = render( @@ -238,13 +260,15 @@ describe("Radio Group component tests", () => { expect(submitInput?.value).toBe("2"); expect(radios[1]?.tabIndex).toBe(0); expect(radios[1]?.getAttribute("aria-checked")).toBe("true"); - radios[6] && (await userEvent.click(radios[6])); + if (radios[6]) { + userEvent.click(radios[6]); + } expect(onChange).toHaveBeenCalledWith("7"); fireEvent.blur(radioGroup); expect(onBlur).toHaveBeenCalledWith({ value: "2" }); }); - test("Select an option by clicking on its label", async () => { + test("Select an option by clicking on its label", () => { const onChange = jest.fn(); const { getByText, getAllByRole, container } = render( <DxcRadioGroup @@ -259,7 +283,7 @@ describe("Radio Group component tests", () => { const checkedRadio = getAllByRole("radio")[8]; const submitInput = container.querySelector<HTMLInputElement>(`input[name="test"]`); expect(checkedRadio?.tabIndex).toBe(-1); - await userEvent.click(radioLabel); + userEvent.click(radioLabel); expect(onChange).toHaveBeenCalledWith("9"); expect(checkedRadio?.getAttribute("aria-checked")).toBe("true"); expect(checkedRadio?.tabIndex).toBe(0); @@ -267,7 +291,7 @@ describe("Radio Group component tests", () => { expect(submitInput?.value).toBe("9"); }); - test("Select an option by clicking on its radio input", async () => { + test("Select an option by clicking on its radio input", () => { const onChange = jest.fn(); const { getAllByRole, container } = render( <DxcRadioGroup @@ -281,7 +305,9 @@ describe("Radio Group component tests", () => { const checkedRadio = getAllByRole("radio")[6]; const submitInput = container.querySelector<HTMLInputElement>(`input[name="test"]`); expect(checkedRadio?.tabIndex).toBe(-1); - checkedRadio && (await userEvent.click(checkedRadio)); + if (checkedRadio) { + userEvent.click(checkedRadio); + } expect(onChange).toHaveBeenCalledWith("7"); expect(checkedRadio?.getAttribute("aria-checked")).toBe("true"); expect(checkedRadio?.tabIndex).toBe(0); @@ -289,7 +315,7 @@ describe("Radio Group component tests", () => { expect(submitInput?.value).toBe("7"); }); - test("Select an option that is already checked does not call onChange event but gives the focus", async () => { + test("Select an option that is already checked does not call onChange event but gives the focus", () => { const onChange = jest.fn(); const { getAllByRole } = render( <DxcRadioGroup @@ -304,7 +330,9 @@ describe("Radio Group component tests", () => { const checkedRadio = getAllByRole("radio")[1]; expect(checkedRadio?.tabIndex).toBe(0); expect(checkedRadio?.getAttribute("aria-checked")).toBe("true"); - checkedRadio && (await userEvent.click(checkedRadio)); + if (checkedRadio) { + userEvent.click(checkedRadio); + } expect(onChange).not.toHaveBeenCalled(); expect(document.activeElement).toEqual(checkedRadio); }); @@ -323,7 +351,12 @@ describe("Radio Group component tests", () => { const radioGroup = getByRole("radiogroup"); const checkedRadio = getAllByRole("radio")[0]; const submitInput = container.querySelector<HTMLInputElement>(`input[name="test"]`); - fireEvent.keyDown(radioGroup, { key: " ", code: "Space", keyCode: 32, charCode: 32 }); + fireEvent.keyDown(radioGroup, { + key: " ", + code: "Space", + keyCode: 32, + charCode: 32, + }); expect(onChange).toHaveBeenCalledWith("1"); expect(checkedRadio?.getAttribute("aria-checked")).toBe("true"); expect(checkedRadio?.tabIndex).toBe(0); @@ -353,7 +386,12 @@ describe("Radio Group component tests", () => { expect(checkedRadio?.tabIndex).toBe(0); expect(checkedRadio?.getAttribute("aria-checked")).toBe("false"); expect(document.activeElement).toEqual(checkedRadio); - fireEvent.keyDown(radioGroup, { key: "ArrowRight", code: "ArrowRight", keyCode: 39, charCode: 39 }); + fireEvent.keyDown(radioGroup, { + key: "ArrowRight", + code: "ArrowRight", + keyCode: 39, + charCode: 39, + }); expect(onBlur).not.toHaveBeenCalled(); expect(onChange).toHaveBeenCalledTimes(1); expect(radios[1]?.getAttribute("aria-checked")).toBe("true"); @@ -379,7 +417,12 @@ describe("Radio Group component tests", () => { const radioGroup = getByRole("radiogroup"); const radios = getAllByRole("radio"); const submitInput = container.querySelector<HTMLInputElement>(`input[name="test"]`); - fireEvent.keyDown(radioGroup, { key: "ArrowDown", code: "ArrowDown", keyCode: 40, charCode: 40 }); + fireEvent.keyDown(radioGroup, { + key: "ArrowDown", + code: "ArrowDown", + keyCode: 40, + charCode: 40, + }); expect(onBlur).not.toHaveBeenCalled(); expect(onChange).toHaveBeenCalledTimes(1); expect(radios[8]?.getAttribute("aria-checked")).toBe("true"); @@ -412,7 +455,12 @@ describe("Radio Group component tests", () => { const radioGroup = getByRole("radiogroup"); const radios = getAllByRole("radio"); const submitInput = container.querySelector<HTMLInputElement>(`input[name="test"]`); - fireEvent.keyDown(radioGroup, { key: "ArrowUp", code: "ArrowUp", keyCode: 38, charCode: 38 }); + fireEvent.keyDown(radioGroup, { + key: "ArrowUp", + code: "ArrowUp", + keyCode: 38, + charCode: 38, + }); expect(onBlur).not.toHaveBeenCalled(); expect(onChange).toHaveBeenCalledTimes(1); expect(radios[0]?.getAttribute("aria-checked")).toBe("true"); @@ -428,7 +476,7 @@ describe("Radio Group component tests", () => { expect(submitInput?.value).toBe("9"); }); - test("Keyboard focus movement continues from the last radio input clicked", async () => { + test("Keyboard focus movement continues from the last radio input clicked", () => { const onChange = jest.fn(); const { getByRole, getAllByRole, container } = render( <DxcRadioGroup @@ -442,14 +490,18 @@ describe("Radio Group component tests", () => { const radioGroup = getByRole("radiogroup"); const radios = getAllByRole("radio"); const submitInput = container.querySelector<HTMLInputElement>(`input[name="test"]`); - radios[3] && (await userEvent.click(radios[3])); + if (radios[3]) { + userEvent.click(radios[3]); + } fireEvent.keyDown(radioGroup, { key: "ArrowDown", code: "ArrowDown", keyCode: 40, charCode: 40 }); expect(onChange).toHaveBeenCalledWith("5"); expect(radios[4]?.getAttribute("aria-checked")).toBe("true"); expect(document.activeElement).toEqual(radios[4]); expect(radios[4]?.tabIndex).toBe(0); expect(submitInput?.value).toBe("5"); - radios[8] && (await userEvent.click(radios[8])); + if (radios[8]) { + userEvent.click(radios[8]); + } fireEvent.keyDown(radioGroup, { key: "ArrowLeft", code: "ArrowLeft", keyCode: 37, charCode: 37 }); expect(onChange).toHaveBeenCalledWith("8"); expect(radios[7]?.getAttribute("aria-checked")).toBe("true"); @@ -458,7 +510,7 @@ describe("Radio Group component tests", () => { expect(submitInput?.value).toBe("8"); }); - test("Read-only radio group lets the user move the focus, but neither click nor keyboard press changes the value", async () => { + test("Read-only radio group lets the user move the focus, but neither click nor keyboard press changes the value", () => { const onChange = jest.fn(); const { getByRole, getAllByRole, container } = render( <DxcRadioGroup @@ -473,7 +525,9 @@ describe("Radio Group component tests", () => { const radioGroup = getByRole("radiogroup"); const radios = getAllByRole("radio"); const submitInput = container.querySelector<HTMLInputElement>(`input[name="test"]`); - radios[5] && (await userEvent.click(radios[5])); + if (radios[5]) { + userEvent.click(radios[5]); + } expect(onChange).not.toHaveBeenCalled(); expect(radios[5]?.getAttribute("aria-checked")).toBe("false"); expect(document.activeElement).toEqual(radios[5]); @@ -487,10 +541,10 @@ describe("Radio Group component tests", () => { expect(submitInput?.value).toBe(""); }); - test("Read-only radio group sends its value on submit", async () => { - const handlerOnSubmit = jest.fn((e) => { + test("Read-only radio group sends its value on submit", () => { + const handlerOnSubmit = jest.fn((e: React.FormEvent<HTMLFormElement>) => { e.preventDefault(); - const formData = new FormData(e.target); + const formData = new FormData(e.currentTarget); const formProps = Object.fromEntries(formData); expect(formProps).toStrictEqual({ radiogroup: "data" }); }); @@ -501,6 +555,6 @@ describe("Radio Group component tests", () => { </form> ); const submit = getByText("Submit"); - await userEvent.click(submit); + userEvent.click(submit); }); }); diff --git a/packages/lib/src/radio-group/RadioGroup.tsx b/packages/lib/src/radio-group/RadioGroup.tsx index 7d83f25cb1..357a276ca6 100644 --- a/packages/lib/src/radio-group/RadioGroup.tsx +++ b/packages/lib/src/radio-group/RadioGroup.tsx @@ -81,7 +81,7 @@ const DxcRadioGroup = forwardRef<RefType, RadioGroupPropsType>( const handleOnBlur = (event: FocusEvent<HTMLDivElement>) => { // If the radio group loses the focus to an element not contained inside it... - if (!event.currentTarget.contains(event.relatedTarget as Node)) { + if (!event.currentTarget.contains(event.relatedTarget)) { setFirstTimeFocus(true); const currentValue = value ?? innerValue; onBlur?.({ @@ -196,4 +196,6 @@ const DxcRadioGroup = forwardRef<RefType, RadioGroupPropsType>( } ); +DxcRadioGroup.displayName = "DxcRadioGroup"; + export default DxcRadioGroup; diff --git a/packages/lib/src/radio-group/RadioInput.tsx b/packages/lib/src/radio-group/RadioInput.tsx index 876df95466..c48be3d49d 100644 --- a/packages/lib/src/radio-group/RadioInput.tsx +++ b/packages/lib/src/radio-group/RadioInput.tsx @@ -56,12 +56,12 @@ const RadioInput = ({ checked, disabled, error, focused, label, onClick, readOnl setFirstUpdate(false); return; } - focused && ref.current?.focus(); + if (focused) ref.current?.focus(); }, [focused]); const handleOnClick = () => { onClick(); - document.activeElement !== ref.current && ref.current?.focus(); + if (document.activeElement !== ref.current) ref.current?.focus(); }; return ( diff --git a/packages/lib/src/resultset-table/ResultsetTable.accessibility.test.tsx b/packages/lib/src/resultset-table/ResultsetTable.accessibility.test.tsx index 521062051a..8083335520 100644 --- a/packages/lib/src/resultset-table/ResultsetTable.accessibility.test.tsx +++ b/packages/lib/src/resultset-table/ResultsetTable.accessibility.test.tsx @@ -3,7 +3,7 @@ import { axe, formatRules } from "../../test/accessibility/axe-helper"; import DxcResultsetTable from "./ResultsetTable"; // TODO: REMOVE -import { disabledRules as rules } from "../../test/accessibility/rules/specific/resultset-table/disabledRules"; +import rules from "../../test/accessibility/rules/specific/resultset-table/disabledRules"; const disabledRules = { rules: formatRules(rules), @@ -16,16 +16,11 @@ const deleteIcon = ( </svg> ); -// Mocking DOMRect for Radix Primitive Popover -(global as any).globalThis = global; -(global as any).DOMRect = { - fromRect: () => ({ top: 0, left: 0, bottom: 0, right: 0, width: 0, height: 0 }), -}; -(global as any).ResizeObserver = class ResizeObserver { - observe() {} - unobserve() {} - disconnect() {} -}; +global.ResizeObserver = jest.fn().mockImplementation(() => ({ + observe: jest.fn(), + unobserve: jest.fn(), + disconnect: jest.fn(), +})); const actions = [ { diff --git a/packages/lib/src/resultset-table/ResultsetTable.stories.tsx b/packages/lib/src/resultset-table/ResultsetTable.stories.tsx index 5b661e2591..e7e00d1c41 100644 --- a/packages/lib/src/resultset-table/ResultsetTable.stories.tsx +++ b/packages/lib/src/resultset-table/ResultsetTable.stories.tsx @@ -1,11 +1,11 @@ +import { Meta, StoryObj } from "@storybook/react"; import { userEvent, within } from "@storybook/test"; import styled from "@emotion/styled"; -import ExampleContainer from "../../.storybook/components/ExampleContainer"; import Title from "../../.storybook/components/Title"; +import ExampleContainer from "../../.storybook/components/ExampleContainer"; +import disabledRules from "../../test/accessibility/rules/specific/resultset-table/disabledRules"; import preview from "../../.storybook/preview"; -import { disabledRules } from "../../test/accessibility/rules/specific/resultset-table/disabledRules"; import DxcResultsetTable from "./ResultsetTable"; -import { Meta, StoryObj } from "@storybook/react"; import DxcFlex from "../flex/Flex"; export default { @@ -15,8 +15,11 @@ export default { a11y: { config: { rules: [ - ...disabledRules.map((ruleId) => ({ id: ruleId, reviewOnFail: true })), - ...preview?.parameters?.a11y?.config?.rules, + ...disabledRules.map((ruleId) => ({ + id: ruleId, + reviewOnFail: true, + })), + ...(preview?.parameters?.a11y?.config?.rules || []), ], }, }, @@ -632,31 +635,31 @@ const ResultsetTable = () => ( <Title title="Margins" theme="light" level={2} /> <ExampleContainer> <Title title="Xxsmall" theme="light" level={4} /> - <DxcResultsetTable columns={columns} rows={rows} margin={"xxsmall"} /> + <DxcResultsetTable columns={columns} rows={rows} margin="xxsmall" /> </ExampleContainer> <ExampleContainer> <Title title="Xsmall" theme="light" level={4} /> - <DxcResultsetTable columns={columns} rows={rows} margin={"xsmall"} /> + <DxcResultsetTable columns={columns} rows={rows} margin="xsmall" /> </ExampleContainer> <ExampleContainer> <Title title="Small" theme="light" level={4} /> - <DxcResultsetTable columns={columns} rows={rows} margin={"small"} /> + <DxcResultsetTable columns={columns} rows={rows} margin="small" /> </ExampleContainer> <ExampleContainer> <Title title="Medium" theme="light" level={4} /> - <DxcResultsetTable columns={columns} rows={rows} margin={"medium"} /> + <DxcResultsetTable columns={columns} rows={rows} margin="medium" /> </ExampleContainer> <ExampleContainer> <Title title="Large" theme="light" level={4} /> - <DxcResultsetTable columns={columns} rows={rows} margin={"large"} /> + <DxcResultsetTable columns={columns} rows={rows} margin="large" /> </ExampleContainer> <ExampleContainer> <Title title="Xlarge" theme="light" level={4} /> - <DxcResultsetTable columns={columns} rows={rows} margin={"xlarge"} /> + <DxcResultsetTable columns={columns} rows={rows} margin="xlarge" /> </ExampleContainer> <ExampleContainer expanded> <Title title="Xxlarge" theme="light" level={4} /> - <DxcResultsetTable columns={columns} rows={rows} margin={"xxlarge"} /> + <DxcResultsetTable columns={columns} rows={rows} margin="xxlarge" /> </ExampleContainer> </> ); @@ -722,8 +725,8 @@ export const AscendentSorting: Story = { const canvas = within(canvasElement); const idHeader = canvas.getAllByRole("button")[0]; const idHeader2 = canvas.getAllByRole("button")[2]; - idHeader && (await userEvent.click(idHeader)); - idHeader2 && (await userEvent.click(idHeader2)); + if (idHeader) await userEvent.click(idHeader); + if (idHeader2) await userEvent.click(idHeader2); }, }; @@ -733,10 +736,10 @@ export const DescendantSorting: Story = { const canvas = within(canvasElement); const nameHeader = canvas.getAllByRole("button")[1]; const nameHeader2 = canvas.getAllByRole("button")[3]; - nameHeader && (await userEvent.click(nameHeader)); - nameHeader && (await userEvent.click(nameHeader)); - nameHeader2 && (await userEvent.click(nameHeader2)); - nameHeader2 && (await userEvent.click(nameHeader2)); + if (nameHeader) await userEvent.click(nameHeader); + if (nameHeader) await userEvent.click(nameHeader); + if (nameHeader2) await userEvent.click(nameHeader2); + if (nameHeader2) await userEvent.click(nameHeader2); }, }; @@ -745,7 +748,9 @@ export const MiddlePage: Story = { play: async ({ canvasElement }) => { const canvas = within(canvasElement); const nextButton = canvas.getAllByRole("button")[2]; - nextButton && (await userEvent.click(nextButton)); + if (nextButton) { + await userEvent.click(nextButton); + } }, }; @@ -754,7 +759,9 @@ export const LastPage: Story = { play: async ({ canvasElement }) => { const canvas = within(canvasElement); const nextButton = canvas.getAllByRole("button")[3]; - nextButton && (await userEvent.click(nextButton)); + if (nextButton) { + await userEvent.click(nextButton); + } }, }; @@ -763,7 +770,9 @@ export const DropdownAction: Story = { play: async ({ canvasElement }) => { const canvas = within(canvasElement); const dropdown = canvas.getAllByRole("button")[5]; - dropdown && userEvent.click(dropdown); + if (dropdown) { + await userEvent.click(dropdown); + } }, }; diff --git a/packages/lib/src/resultset-table/ResultsetTable.test.tsx b/packages/lib/src/resultset-table/ResultsetTable.test.tsx index 92cb387853..8a63e1ee9e 100644 --- a/packages/lib/src/resultset-table/ResultsetTable.test.tsx +++ b/packages/lib/src/resultset-table/ResultsetTable.test.tsx @@ -3,16 +3,11 @@ import userEvent from "@testing-library/user-event"; import DxcCheckbox from "../checkbox/Checkbox"; import DxcResultsetTable from "./ResultsetTable"; -// Mocking DOMRect for Radix Primitive Popover -(global as any).globalThis = global; -(global as any).DOMRect = { - fromRect: () => ({ top: 0, left: 0, bottom: 0, right: 0, width: 0, height: 0 }), -}; -(global as any).ResizeObserver = class ResizeObserver { - observe() {} - unobserve() {} - disconnect() {} -}; +global.ResizeObserver = jest.fn().mockImplementation(() => ({ + observe: jest.fn(), + unobserve: jest.fn(), + disconnect: jest.fn(), +})); const icon = ( <svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="currentColor"> @@ -360,7 +355,9 @@ describe("Resultset table component tests", () => { expect(getByText("Lana")).toBeTruthy(); expect(getAllByRole("row").length - 1).toEqual(3); const nextButton = getAllByRole("button")[3]; - nextButton && fireEvent.click(nextButton); + if (nextButton) { + fireEvent.click(nextButton); + } expect(getByText("4 to 6 of 10")).toBeTruthy(); expect(getByText("Rick")).toBeTruthy(); expect(getByText("Mark")).toBeTruthy(); @@ -368,7 +365,7 @@ describe("Resultset table component tests", () => { expect(getAllByRole("row").length - 1).toEqual(3); }); - test("Resultset table goToPage works as expected", async () => { + test("Resultset table goToPage works as expected", () => { window.HTMLElement.prototype.scrollIntoView = () => {}; window.HTMLElement.prototype.scrollTo = () => {}; const { getByText, getAllByRole } = render( @@ -379,9 +376,11 @@ describe("Resultset table component tests", () => { expect(getByText("Lana")).toBeTruthy(); expect(getAllByRole("row").length - 1).toEqual(3); const goToPageSelect = getAllByRole("button")[3]; - goToPageSelect && (await userEvent.click(goToPageSelect)); + if (goToPageSelect) { + userEvent.click(goToPageSelect); + } const goToPageOption = getByText("2"); - await userEvent.click(goToPageOption); + userEvent.click(goToPageOption); expect(getByText("4 to 6 of 10")).toBeTruthy(); expect(getByText("Rick")).toBeTruthy(); expect(getByText("Mark")).toBeTruthy(); @@ -392,7 +391,9 @@ describe("Resultset table component tests", () => { test("Resultset table going to the last page shows only one row", () => { const { getByText, getAllByRole } = render(<DxcResultsetTable columns={columns} rows={rows} itemsPerPage={3} />); const lastButton = getAllByRole("button")[4]; - lastButton && fireEvent.click(lastButton); + if (lastButton) { + fireEvent.click(lastButton); + } expect(getByText("10 to 10 of 10")).toBeTruthy(); expect(getAllByRole("row")).toHaveLength(2); expect(getByText("Cosmin")).toBeTruthy(); @@ -402,11 +403,15 @@ describe("Resultset table component tests", () => { const component = render(<DxcResultsetTable columns={columns} rows={rows} itemsPerPage={3} />); const name = component.queryByText("Name"); expect(component.queryByText("Peter")).toBeTruthy(); - name && fireEvent.click(name); + if (name) { + fireEvent.click(name); + } expect(component.queryByText("Tina")).not.toBeTruthy(); expect(component.queryByText("Cosmin")).toBeTruthy(); - name && fireEvent.click(name); + if (name) { + fireEvent.click(name); + } expect(component.queryByText("Tina")).toBeTruthy(); expect(component.queryByText("Cosmin")).not.toBeTruthy(); }); @@ -415,11 +420,15 @@ describe("Resultset table component tests", () => { const component = render(<DxcResultsetTable columns={columns} rows={rowsMissingSortValues} itemsPerPage={3} />); const name = component.queryByText("Name"); expect(component.queryByText("Peter")).toBeTruthy(); - name && fireEvent.click(name); + if (name) { + fireEvent.click(name); + } expect(component.queryByText("Tina")).not.toBeTruthy(); expect(component.queryByText("Cosmin")).toBeTruthy(); - name && fireEvent.click(name); + if (name) { + fireEvent.click(name); + } expect(component.queryByText("Tina")).toBeTruthy(); expect(component.queryByText("Cosmin")).not.toBeTruthy(); }); @@ -431,7 +440,9 @@ describe("Resultset table component tests", () => { expect(queryByText("1 to 3 of 10")).toBeTruthy(); const lastButton = getAllByRole("button")[4]; expect(queryByText("Peter")).toBeTruthy(); - lastButton && fireEvent.click(lastButton); + if (lastButton) { + fireEvent.click(lastButton); + } expect(queryByText("10 to 10 of 10")).toBeTruthy(); rerender(<DxcResultsetTable columns={columns} rows={rows2} itemsPerPage={3} />); expect(queryByText("7 to 9 of 9")).toBeTruthy(); @@ -444,13 +455,15 @@ describe("Resultset table component tests", () => { expect(queryByText("1 to 2 of 10")).toBeTruthy(); const lastButton = getAllByRole("button")[4]; expect(queryByText("Peter")).toBeTruthy(); - lastButton && fireEvent.click(lastButton); + if (lastButton) { + fireEvent.click(lastButton); + } expect(queryByText("9 to 10 of 10")).toBeTruthy(); rerender(<DxcResultsetTable columns={columns} rows={rows2} itemsPerPage={2} />); expect(queryByText("9 to 9 of 9")).toBeTruthy(); }); - test("Resultset table uncontrolled components maintain its value when sorting", async () => { + test("Resultset table uncontrolled components maintain its value when sorting", () => { const { getAllByRole } = render( <DxcResultsetTable columns={columnsWithCheckbox} rows={rowsWithCheckbox} itemsPerPage={3} /> ); @@ -461,11 +474,15 @@ describe("Resultset table component tests", () => { expect(columnHeader?.getAttribute("aria-sort")).toBe("none"); - sortButton && fireEvent.click(sortButton); + if (sortButton) { + fireEvent.click(sortButton); + } expect(columnHeader?.getAttribute("aria-sort")).toBe("ascending"); - sortButton && fireEvent.click(sortButton); + if (sortButton) { + fireEvent.click(sortButton); + } expect(columnHeader?.getAttribute("aria-sort")).toBe("descending"); @@ -478,7 +495,9 @@ describe("Resultset table component tests", () => { ); const lastButton = getAllByRole("button")[4]; expect(getAllByRole("row").length - 1).toEqual(3); - lastButton && fireEvent.click(lastButton); + if (lastButton) { + fireEvent.click(lastButton); + } expect(getAllByRole("row").length - 1).toEqual(1); }); @@ -511,9 +530,9 @@ describe("Resultset table component tests", () => { ], }, { - icon: icon, + icon, title: "icon2", - onClick: onClick, + onClick, }, ]; const actionRows = [ @@ -537,14 +556,18 @@ describe("Resultset table component tests", () => { ); const dropdown = getAllByRole("button")[2]; act(() => { - dropdown && userEvent.click(dropdown); + if (dropdown) { + userEvent.click(dropdown); + } }); expect(getByRole("menu")).toBeTruthy(); const option = getByText("Aliexpress"); userEvent.click(option); expect(onSelectOption).toHaveBeenCalledWith("3"); const action = getAllByRole("button")[1]; - action && userEvent.click(action); + if (action) { + userEvent.click(action); + } expect(onClick).toHaveBeenCalled(); }); }); diff --git a/packages/lib/src/select/ListOption.tsx b/packages/lib/src/select/ListOption.tsx index d312e6d45a..a495d53a99 100644 --- a/packages/lib/src/select/ListOption.tsx +++ b/packages/lib/src/select/ListOption.tsx @@ -1,8 +1,8 @@ +import { MouseEvent, useEffect, useRef, useState } from "react"; import styled from "@emotion/styled"; import { OptionProps } from "./types"; import DxcCheckbox from "../checkbox/Checkbox"; import DxcIcon from "../icon/Icon"; -import { MouseEvent, useEffect, useRef, useState } from "react"; import { TooltipWrapper } from "../tooltip/Tooltip"; const OptionItem = styled.li<{ diff --git a/packages/lib/src/select/Listbox.tsx b/packages/lib/src/select/Listbox.tsx index c01baf9f60..9e0dbfe511 100644 --- a/packages/lib/src/select/Listbox.tsx +++ b/packages/lib/src/select/Listbox.tsx @@ -4,8 +4,8 @@ import DxcIcon from "../icon/Icon"; import { HalstackLanguageContext } from "../HalstackContext"; import ListOption from "./ListOption"; import { getGroupSelectionType, groupsHaveOptions } from "./utils"; +import scrollbarStyles from "../styles/scroll"; import { FlattenedItem, ListboxProps, ListOptionGroupType, ListOptionType } from "./types"; -import { scrollbarStyles } from "../styles/scroll"; import CheckboxContext from "../checkbox/CheckboxContext"; import { Virtuoso, VirtuosoHandle } from "react-virtuoso"; @@ -417,10 +417,12 @@ const NonVirtualizedListbox = ({ useLayoutEffect(() => { if (currentValue && !multiple) { const listEl = listboxRef?.current; - const selectedListOptionEl = listEl?.querySelector("[aria-selected='true']") as HTMLUListElement; - listEl?.scrollTo?.({ - top: (selectedListOptionEl.offsetTop ?? 0) - (listEl.clientHeight ?? 0) / 2, - }); + const selectedListOptionEl = listEl?.querySelector("[aria-selected='true']"); + if (selectedListOptionEl instanceof HTMLUListElement) { + listEl?.scrollTo?.({ + top: (selectedListOptionEl.offsetTop ?? 0) - (listEl.clientHeight ?? 0) / 2, + }); + } } }, [currentValue, multiple]); diff --git a/packages/lib/src/select/Select.accessibility.test.tsx b/packages/lib/src/select/Select.accessibility.test.tsx index d9600d3220..1834bff259 100644 --- a/packages/lib/src/select/Select.accessibility.test.tsx +++ b/packages/lib/src/select/Select.accessibility.test.tsx @@ -2,6 +2,7 @@ import { render } from "@testing-library/react"; import { axe } from "../../test/accessibility/axe-helper"; import DxcFlex from "../flex/Flex"; import DxcSelect from "./Select"; +import MockDOMRect from "../../test/mocks/domRectMock"; const iconSVG = ( <svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="currentColor"> @@ -16,7 +17,7 @@ const iconSVG = ( </svg> ); -const group_options = [ +const groupOptions = [ { label: "Group 001", options: [ @@ -59,7 +60,7 @@ const group_options = [ }, ]; -const single_options = [ +const singleOptions = [ { label: "Option 01", value: "1", icon: iconSVG }, { label: "Option 02", value: "2", icon: iconSVG }, { label: "Option 03", value: "3", icon: iconSVG }, @@ -67,15 +68,12 @@ const single_options = [ ]; // Mocking DOMRect for Radix Primitive Popover -(global as any).globalThis = global; -(global as any).DOMRect = { - fromRect: () => ({ top: 0, left: 0, bottom: 0, right: 0, width: 0, height: 0 }), -}; -(global as any).ResizeObserver = class ResizeObserver { - observe() {} - unobserve() {} - disconnect() {} -}; +global.DOMRect = MockDOMRect; +global.ResizeObserver = jest.fn().mockImplementation(() => ({ + observe: jest.fn(), + unobserve: jest.fn(), + disconnect: jest.fn(), +})); describe("Select component accessibility tests", () => { it("Should not have basic accessibility issues", async () => { @@ -86,7 +84,7 @@ describe("Select component accessibility tests", () => { label="test-select-label" helperText="test-select-helper-text" placeholder="Example text" - options={single_options} + options={singleOptions} defaultValue="1" margin="medium" name="Name" @@ -97,7 +95,7 @@ describe("Select component accessibility tests", () => { label="test-select-label" helperText="test-select-helper-text" placeholder="Example text" - options={single_options} + options={singleOptions} defaultValue={["4", "2", "6"]} margin="medium" name="Name" @@ -119,7 +117,7 @@ describe("Select component accessibility tests", () => { label="test-select-label" helperText="test-select-helper-text" placeholder="Example text" - options={group_options} + options={groupOptions} defaultValue={["4", "2", "6"]} error="Error" margin="medium" @@ -132,7 +130,7 @@ describe("Select component accessibility tests", () => { label="test-select-label" helperText="test-select-helper-text" placeholder="Example text" - options={group_options} + options={groupOptions} defaultValue={["4", "2", "6"]} margin="medium" name="Name" diff --git a/packages/lib/src/select/Select.stories.tsx b/packages/lib/src/select/Select.stories.tsx index efe71ca724..885841378a 100644 --- a/packages/lib/src/select/Select.stories.tsx +++ b/packages/lib/src/select/Select.stories.tsx @@ -1,13 +1,12 @@ -import { fireEvent, userEvent, within } from "@storybook/test"; +import { Meta, StoryObj } from "@storybook/react"; +import { userEvent, within, waitFor } from "@storybook/test"; import ExampleContainer from "../../.storybook/components/ExampleContainer"; import Title from "../../.storybook/components/Title"; import preview from "../../.storybook/preview"; -import { disabledRules } from "../../test/accessibility/rules/specific/select/disabledRules"; +import disabledRules from "../../test/accessibility/rules/specific/select/disabledRules"; import DxcFlex from "../flex/Flex"; import Listbox from "./Listbox"; import DxcSelect from "./Select"; -import { Meta, StoryObj } from "@storybook/react"; -import { waitFor } from "@testing-library/react"; export default { title: "Select", @@ -16,17 +15,20 @@ export default { a11y: { config: { rules: [ - ...disabledRules.map((ruleId) => ({ id: ruleId, reviewOnFail: true })), - ...preview?.parameters?.a11y?.config?.rules, + ...disabledRules.map((ruleId) => ({ + id: ruleId, + reviewOnFail: true, + })), + ...(preview?.parameters?.a11y?.config?.rules || []), ], }, }, }, } as Meta<typeof DxcSelect>; -const one_option = [{ label: "Option 01", value: "1" }]; +const oneOption = [{ label: "Option 01", value: "1" }]; -const single_options = [ +const singleOptions = [ { label: "Option 01", value: "1" }, { label: "Option 02", value: "2" }, { label: "Option 03", value: "3" }, @@ -40,7 +42,7 @@ const single_options_virtualized = [ })), ]; -const group_options = [ +const groupOptions = [ { label: "Group 001", options: [ @@ -83,7 +85,7 @@ const group_options = [ }, ]; -const icon_options_grouped_material = [ +const iconOptionsGroupedMaterial = [ { label: "Group 001", options: [ @@ -121,7 +123,7 @@ const icon_options_grouped_material = [ }, ]; -const icon_options = [ +const iconOptions = [ { label: "3G Mobile", value: "1", @@ -170,7 +172,7 @@ const icon_options = [ }, ]; -const options_material = [ +const optionsMaterial = [ { label: "Transport", options: [ @@ -228,15 +230,15 @@ const Select = () => ( <> <ExampleContainer> <Title title="Default" theme="light" level={4} /> - <DxcSelect label="Default" options={single_options} /> + <DxcSelect options={singleOptions} /> </ExampleContainer> <ExampleContainer pseudoState="pseudo-hover"> <Title title="Hovered" theme="light" level={4} /> - <DxcSelect label="Hovered" options={single_options} /> + <DxcSelect label="Hovered" options={singleOptions} /> </ExampleContainer> <ExampleContainer pseudoState="pseudo-focus-within"> <Title title="Focused" theme="light" level={4} /> - <DxcSelect label="Focused" options={single_options} /> + <DxcSelect label="Focused" options={singleOptions} /> </ExampleContainer> <ExampleContainer> <Title title="Disabled" theme="light" level={4} /> @@ -246,18 +248,18 @@ const Select = () => ( helperText="Helper text" optional disabled - options={single_options} + options={singleOptions} /> </ExampleContainer> <ExampleContainer> <Title title="Disabled with value" theme="light" level={4} /> - <DxcSelect label="Label" disabled helperText="Helper text" optional options={single_options} defaultValue="1" /> + <DxcSelect label="Label" disabled helperText="Helper text" optional options={singleOptions} defaultValue="1" /> </ExampleContainer> <ExampleContainer> <Title title="Error" theme="light" level={4} /> <DxcSelect label="Label" - options={single_options} + options={singleOptions} error="Error message." helperText="Helper text" placeholder="Placeholder" @@ -267,7 +269,7 @@ const Select = () => ( <Title title="Hovered error" theme="light" level={4} /> <DxcSelect label="Label" - options={single_options} + options={singleOptions} error="Error message." helperText="Helper text" placeholder="Placeholder" @@ -276,83 +278,83 @@ const Select = () => ( <Title title="Anatomy" theme="light" level={2} /> <ExampleContainer> <Title title="Label, placeholder and helper text" theme="light" level={4} /> - <DxcSelect label="Label" options={single_options} helperText="Helper text" placeholder="Placeholder" optional /> + <DxcSelect label="Label" options={singleOptions} helperText="Helper text" placeholder="Placeholder" optional /> </ExampleContainer> <Title title="Variants" theme="light" level={2} /> <ExampleContainer> <Title title="Simple selection" theme="light" level={4} /> - <DxcSelect label="Simple selection" searchable options={single_options} defaultValue="2" /> + <DxcSelect label="Simple selection" searchable options={singleOptions} defaultValue="2" /> </ExampleContainer> <ExampleContainer> <Title title="Multiple selection" theme="light" level={4} /> - <DxcSelect label="Multiple select" searchable options={single_options} multiple defaultValue={["1", "2"]} /> + <DxcSelect label="Multiple select" searchable options={singleOptions} multiple defaultValue={["1", "2"]} /> </ExampleContainer> <ExampleContainer pseudoState="pseudo-hover"> <Title title="Multiple clear hovered" theme="light" level={4} /> - <DxcSelect label="Multiple select" options={single_options} multiple defaultValue={["1", "2"]} /> + <DxcSelect label="Multiple select" options={singleOptions} multiple defaultValue={["1", "2"]} /> </ExampleContainer> <ExampleContainer pseudoState="pseudo-active"> <Title title="Multiple clear actived" theme="light" level={4} /> - <DxcSelect label="Multiple select" options={single_options} multiple defaultValue={["1", "2"]} /> + <DxcSelect label="Multiple select" options={singleOptions} multiple defaultValue={["1", "2"]} /> </ExampleContainer> <Title title="Sizes" theme="light" level={2} /> <ExampleContainer> <Title title="Small size" theme="light" level={4} /> - <DxcSelect label="Small" options={single_options} size="small" /> + <DxcSelect label="Small" options={singleOptions} size="small" /> </ExampleContainer> <ExampleContainer> <Title title="Medium size" theme="light" level={4} /> - <DxcSelect label="Medium" options={single_options} size="medium" /> + <DxcSelect label="Medium" options={singleOptions} size="medium" /> </ExampleContainer> <ExampleContainer> <Title title="Large size" theme="light" level={4} /> - <DxcSelect label="Large" options={single_options} size="large" /> + <DxcSelect label="Large" options={singleOptions} size="large" /> </ExampleContainer> <ExampleContainer> <Title title="Fillparent size" theme="light" level={4} /> - <DxcSelect label="Fillparent" options={single_options} size="fillParent" /> + <DxcSelect label="Fillparent" options={singleOptions} size="fillParent" /> </ExampleContainer> <ExampleContainer> <Title title="Different sizes inside a flex" theme="light" level={4} /> <DxcFlex justifyContent="space-between" gap="var(--spacing-gap-ml)"> - <DxcSelect label="fillParent" size="fillParent" options={single_options} /> - <DxcSelect label="medium" size="medium" options={single_options} /> - <DxcSelect label="large" size="large" options={single_options} /> + <DxcSelect label="fillParent" size="fillParent" options={singleOptions} /> + <DxcSelect label="medium" size="medium" options={singleOptions} /> + <DxcSelect label="large" size="large" options={singleOptions} /> </DxcFlex> </ExampleContainer> <Title title="Margins" theme="light" level={2} /> <ExampleContainer> <Title title="xxsmall margin" theme="light" level={4} /> - <DxcSelect label="xxSmall" options={single_options} margin="xxsmall" /> + <DxcSelect label="xxSmall" options={singleOptions} margin="xxsmall" /> </ExampleContainer> <ExampleContainer> <Title title="xsmall margin" theme="light" level={4} /> - <DxcSelect label="xSmall" options={single_options} margin="xsmall" /> + <DxcSelect label="xSmall" options={singleOptions} margin="xsmall" /> </ExampleContainer> <ExampleContainer> <Title title="small margin" theme="light" level={4} /> - <DxcSelect label="Small" options={single_options} margin="small" /> + <DxcSelect label="Small" options={singleOptions} margin="small" /> </ExampleContainer> <ExampleContainer> <Title title="medium margin" theme="light" level={4} /> - <DxcSelect label="Medium" options={single_options} margin="medium" /> + <DxcSelect label="Medium" options={singleOptions} margin="medium" /> </ExampleContainer> <ExampleContainer> <Title title="large margin" theme="light" level={4} /> - <DxcSelect label="Large" options={single_options} margin="large" /> + <DxcSelect label="Large" options={singleOptions} margin="large" /> </ExampleContainer> <ExampleContainer> <Title title="xlarge margin" theme="light" level={4} /> - <DxcSelect label="xLarge" options={single_options} margin="xlarge" /> + <DxcSelect label="xLarge" options={singleOptions} margin="xlarge" /> </ExampleContainer> <ExampleContainer> <Title title="xxlarge margin" theme="light" level={4} /> - <DxcSelect label="xxLarge" options={single_options} margin="xxlarge" /> + <DxcSelect label="xxLarge" options={singleOptions} margin="xxlarge" /> </ExampleContainer> <ExampleContainer expanded> <Title title="Ellipsis" theme="light" level={2} /> <Title title="Multiple selection with ellipsis" theme="light" level={4} /> - <DxcSelect label="Label" options={single_options} multiple defaultValue={["1", "2", "3", "4"]} /> + <DxcSelect label="Label" options={singleOptions} multiple defaultValue={["1", "2", "3", "4"]} /> <Title title="Value with ellipsis" theme="light" level={4} /> <DxcSelect label="Label" options={optionsWithEllipsis} defaultValue="1" /> <Title title="Options with ellipsis" theme="light" level={4} /> @@ -399,7 +401,7 @@ const SelectListbox = () => ( zIndex: "130", }} > - <DxcSelect label="Label" options={single_options} optional placeholder="Choose an option" /> + <DxcSelect label="Label" options={singleOptions} optional placeholder="Choose an option" /> <button style={{ zIndex: "1", width: "100px" }}>Submit</button> </div> </ExampleContainer> @@ -411,7 +413,7 @@ const SelectListbox = () => ( ariaLabelledBy="x8-label" id="x8" currentValue="" - options={one_option} + options={oneOption} visualFocusIndex={-1} lastOptionIndex={0} multiple={false} @@ -433,7 +435,7 @@ const SelectListbox = () => ( ariaLabelledBy="x9-label" id="x9" currentValue="" - options={one_option} + options={oneOption} visualFocusIndex={-1} lastOptionIndex={0} multiple={false} @@ -455,7 +457,7 @@ const SelectListbox = () => ( ariaLabelledBy="x10-label" id="x10" currentValue="" - options={one_option} + options={oneOption} visualFocusIndex={0} lastOptionIndex={0} multiple={false} @@ -477,7 +479,7 @@ const SelectListbox = () => ( ariaLabelledBy="x11-label" id="x11" currentValue="1" - options={single_options} + options={singleOptions} visualFocusIndex={-1} lastOptionIndex={3} multiple={false} @@ -499,7 +501,7 @@ const SelectListbox = () => ( ariaLabelledBy="x12-label" id="x12" currentValue="2" - options={single_options} + options={singleOptions} visualFocusIndex={0} lastOptionIndex={3} multiple={false} @@ -522,7 +524,7 @@ const SelectListbox = () => ( ariaLabelledBy="x13-label" id="x13" currentValue="3" - options={icon_options} + options={iconOptions} visualFocusIndex={-1} lastOptionIndex={3} multiple={false} @@ -543,8 +545,8 @@ const SelectListbox = () => ( <Listbox ariaLabelledBy="x14-label" id="x14" - currentValue={"4"} - options={icon_options_grouped_material} + currentValue="4" + options={iconOptionsGroupedMaterial} visualFocusIndex={-1} lastOptionIndex={3} multiple={false} @@ -566,7 +568,7 @@ const SelectListbox = () => ( ariaLabelledBy="x15-label" id="x15" currentValue={["car", "motorcycle", "train"]} - options={options_material} + options={optionsMaterial} visualFocusIndex={-1} lastOptionIndex={6} multiple @@ -587,7 +589,7 @@ const SelectListbox = () => ( const SearchableSelect = () => ( <ExampleContainer expanded> <Title title="Searchable select" theme="light" level={4} /> - <DxcSelect label="Select Label" searchable optional options={single_options} placeholder="Choose an option" /> + <DxcSelect label="Select Label" searchable options={singleOptions} placeholder="Choose an option" /> </ExampleContainer> ); @@ -598,44 +600,36 @@ const SearchValue = () => ( label="Select Label" searchable defaultValue="1" - options={single_options} + options={singleOptions} placeholder="Choose an option" /> </ExampleContainer> ); const MultipleSelect = () => ( - <> - <ExampleContainer expanded> - <Title title="Multiple select" theme="light" level={4} /> - <DxcSelect - label="Select label" - options={single_options} - defaultValue={["1", "4"]} - multiple - placeholder="Choose an option" - /> - </ExampleContainer> - </> + <ExampleContainer expanded> + <Title title="Multiple select" theme="light" level={4} /> + <DxcSelect + label="Select label" + options={singleOptions} + defaultValue={["1", "4"]} + multiple + placeholder="Choose an option" + /> + </ExampleContainer> ); const DefaultGroupedOptionsSelect = () => ( <ExampleContainer expanded> <Title title="Grouped options simple select" theme="light" level={4} /> - <DxcSelect label="Label" options={group_options} defaultValue="9" placeholder="Choose an option" /> + <DxcSelect label="Label" options={groupOptions} defaultValue="9" placeholder="Choose an option" /> </ExampleContainer> ); const MultipleGroupedOptionsSelect = () => ( <ExampleContainer expanded> <Title title="Grouped options multiple select" theme="light" level={4} /> - <DxcSelect - label="Label" - options={group_options} - defaultValue={["0", "2"]} - multiple - placeholder="Choose an option" - /> + <DxcSelect label="Label" options={groupOptions} defaultValue={["0", "2"]} multiple placeholder="Choose an option" /> </ExampleContainer> ); @@ -647,7 +641,7 @@ const MultipleSearchable = () => ( searchable multiple defaultValue={["1", "4"]} - options={single_options} + options={singleOptions} placeholder="Choose an option" /> </ExampleContainer> @@ -656,7 +650,7 @@ const MultipleSearchable = () => ( const TooltipValue = () => ( <ExampleContainer expanded> <Title title="Selected value(s) have tooltip when they overflow" theme="light" level={4} /> - <DxcSelect label="Label" options={single_options} multiple defaultValue={["1", "2", "3", "4"]} /> + <DxcSelect label="Label" options={singleOptions} multiple defaultValue={["1", "2", "3", "4"]} /> </ExampleContainer> ); @@ -688,7 +682,7 @@ const TooltipOption = () => ( const TooltipClear = () => ( <ExampleContainer expanded> <Title title="Clear action tooltip" theme="light" level={4} /> - <DxcSelect label="Label" options={single_options} multiple defaultValue={["1", "2", "3", "4"]} /> + <DxcSelect label="Label" options={singleOptions} multiple defaultValue={["1", "2", "3", "4"]} /> </ExampleContainer> ); @@ -700,7 +694,7 @@ const SelectAll = () => ( enableSelectAll label="Select an option" multiple - options={group_options} + options={groupOptions} placeholder="Select an available option" searchable /> @@ -714,7 +708,9 @@ export const Chromatic: Story = { play: async ({ canvasElement }) => { const canvas = within(canvasElement); const combobox = canvas.getAllByRole("combobox")[24]; - combobox && (await userEvent.click(combobox)); + if (combobox) { + await userEvent.click(combobox); + } }, }; @@ -757,7 +753,7 @@ export const MultipleSearchableWithValue: Story = { play: async ({ canvasElement }) => { const canvas = within(canvasElement); const combobox = canvas.getAllByRole("combobox")[0]; - combobox && (await userEvent.click(combobox)); + if (combobox) await userEvent.click(combobox); }, }; @@ -775,7 +771,7 @@ export const MultipleOptionsDisplayed: Story = { play: async ({ canvasElement }) => { const canvas = within(canvasElement); const combobox = canvas.getAllByRole("combobox")[0]; - combobox && (await userEvent.click(combobox)); + if (combobox) await userEvent.click(combobox); }, }; diff --git a/packages/lib/src/select/Select.test.tsx b/packages/lib/src/select/Select.test.tsx index e07a320e51..6278b8696e 100644 --- a/packages/lib/src/select/Select.test.tsx +++ b/packages/lib/src/select/Select.test.tsx @@ -1,17 +1,15 @@ import { act, fireEvent, render } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import DxcSelect from "./Select"; +import MockDOMRect from "../../test/mocks/domRectMock"; // Mocking DOMRect for Radix Primitive Popover -(global as any).globalThis = global; -(global as any).DOMRect = { - fromRect: () => ({ top: 0, left: 0, bottom: 0, right: 0, width: 0, height: 0 }), -}; -(global as any).ResizeObserver = class ResizeObserver { - observe() {} - unobserve() {} - disconnect() {} -}; +global.DOMRect = MockDOMRect; +global.ResizeObserver = jest.fn().mockImplementation(() => ({ + observe: jest.fn(), + unobserve: jest.fn(), + disconnect: jest.fn(), +})); const reducedSingleOptions = [ { label: "Option 01", value: "1" }, @@ -170,7 +168,9 @@ describe("Select component tests", () => { userEvent.click(select); const options = getAllByRole("option"); expect(options[3]?.getAttribute("aria-selected")).toBe("true"); - options[7] && userEvent.click(options[7]); + if (options[7]) { + userEvent.click(options[7]); + } expect(getByText("Option 08")).toBeTruthy(); expect(submitInput?.value).toBe("8"); }); @@ -191,14 +191,16 @@ describe("Select component tests", () => { expect(submitInput?.value).toBe("4,2,6"); userEvent.click(select); const options = getAllByRole("option"); - options[2] && userEvent.click(options[2]); + if (options[2]) { + userEvent.click(options[2]); + } expect(getByText("Option 02, Option 03, Option 04, Option 06")).toBeTruthy(); expect(submitInput?.value).toBe("4,2,6,3"); }); test("Sends its value when submitted", () => { - const handlerOnSubmit = jest.fn((e) => { + const handlerOnSubmit = jest.fn((e: React.FormEvent<HTMLFormElement>) => { e.preventDefault(); - const formData = new FormData(e.target); + const formData = new FormData(e.currentTarget); const formProps = Object.fromEntries(formData); expect(formProps).toStrictEqual({ options: "1,5,3" }); }); @@ -218,7 +220,9 @@ describe("Select component tests", () => { const submit = getByText("Submit"); userEvent.click(select); const options = getAllByRole("option"); - options[2] && userEvent.click(options[2]); + if (options[2]) { + userEvent.click(options[2]); + } userEvent.click(submit); }); test("Searching for a value with an empty list of options passed doesn't open the listbox", () => { @@ -229,7 +233,9 @@ describe("Select component tests", () => { const searchInput = container.querySelectorAll("input")[1]; userEvent.click(select); act(() => { - searchInput && userEvent.type(searchInput, "test"); + if (searchInput) { + userEvent.type(searchInput, "test"); + } }); expect(queryByRole("listbox")).toBeFalsy(); expect(select.getAttribute("aria-expanded")).toBe("false"); @@ -271,9 +277,9 @@ describe("Select component tests", () => { expect(document.activeElement === select).toBeFalsy(); }); test("Disabled select — Doesn't send its value when submitted", () => { - const handlerOnSubmit = jest.fn((e) => { + const handlerOnSubmit = jest.fn((e: React.FormEvent<HTMLFormElement>) => { e.preventDefault(); - const formData = new FormData(e.target); + const formData = new FormData(e.currentTarget); const formProps = Object.fromEntries(formData); expect(formProps).toStrictEqual({}); }); @@ -297,10 +303,15 @@ describe("Select component tests", () => { fireEvent.focus(select); fireEvent.blur(select); expect(onBlur).toHaveBeenCalled(); - expect(onBlur).toHaveBeenCalledWith({ value: "", error: "This field is required. Please, enter a value." }); + expect(onBlur).toHaveBeenCalledWith({ + value: "", + error: "This field is required. Please, enter a value.", + }); userEvent.click(select); const options = getAllByRole("option"); - options[0] && userEvent.click(options[0]); + if (options[0]) { + userEvent.click(options[0]); + } expect(onChange).toHaveBeenCalled(); expect(onChange).toHaveBeenCalledWith({ value: "1" }); fireEvent.blur(select); @@ -318,11 +329,18 @@ describe("Select component tests", () => { fireEvent.focus(select); fireEvent.blur(select); expect(onBlur).toHaveBeenCalled(); - expect(onBlur).toHaveBeenCalledWith({ value: [], error: "This field is required. Please, enter a value." }); + expect(onBlur).toHaveBeenCalledWith({ + value: [], + error: "This field is required. Please, enter a value.", + }); userEvent.click(select); let options = getAllByRole("option"); - options[0] && userEvent.click(options[0]); - options[1] && userEvent.click(options[1]); + if (options[0]) { + userEvent.click(options[0]); + } + if (options[1]) { + userEvent.click(options[1]); + } expect(onChange).toHaveBeenCalled(); expect(onChange).toHaveBeenCalledWith({ value: ["1", "2"] }); fireEvent.blur(select); @@ -330,13 +348,23 @@ describe("Select component tests", () => { expect(onBlur).toHaveBeenCalledWith({ value: ["1", "2"] }); userEvent.click(select); options = getAllByRole("option"); - options[0] && userEvent.click(options[0]); - options[1] && userEvent.click(options[1]); + if (options[0]) { + userEvent.click(options[0]); + } + if (options[1]) { + userEvent.click(options[1]); + } expect(onChange).toHaveBeenCalled(); - expect(onChange).toHaveBeenCalledWith({ value: [], error: "This field is required. Please, enter a value." }); + expect(onChange).toHaveBeenCalledWith({ + value: [], + error: "This field is required. Please, enter a value.", + }); fireEvent.blur(select); expect(onBlur).toHaveBeenCalled(); - expect(onBlur).toHaveBeenCalledWith({ value: [], error: "This field is required. Please, enter a value." }); + expect(onBlur).toHaveBeenCalledWith({ + value: [], + error: "This field is required. Please, enter a value.", + }); }); test("Controlled — Optional constraint", () => { const onChange = jest.fn(); @@ -385,7 +413,9 @@ describe("Select component tests", () => { const submitInput = container.querySelector<HTMLInputElement>(`input[name="test"]`); userEvent.click(select); let options = getAllByRole("option"); - options[2] && userEvent.click(options[2]); + if (options[2]) { + userEvent.click(options[2]); + } expect(onChange).toHaveBeenCalledWith({ value: "3" }); expect(queryByRole("listbox")).toBeFalsy(); expect(getByText("Option 03")).toBeTruthy(); @@ -410,15 +440,32 @@ describe("Select component tests", () => { expect(getAllByText("Choose an option").length).toBe(2); const options = getAllByRole("option"); expect(options[0]?.getAttribute("aria-selected")).toBe("true"); - options[0] && userEvent.click(options[0]); + if (options[0]) { + userEvent.click(options[0]); + } expect(onChange).toHaveBeenCalledWith({ value: "" }); expect(getAllByText("Choose an option").length).toBe(1); - fireEvent.keyDown(select, { key: "ArrowDown", code: "ArrowDown", keyCode: 40, charCode: 40 }); + fireEvent.keyDown(select, { + key: "ArrowDown", + code: "ArrowDown", + keyCode: 40, + charCode: 40, + }); expect(select.getAttribute("aria-activedescendant")).toBe("option-0"); - fireEvent.keyDown(select, { key: "Enter", code: "Enter", keyCode: 13, charCode: 13 }); + fireEvent.keyDown(select, { + key: "Enter", + code: "Enter", + keyCode: 13, + charCode: 13, + }); expect(onChange).toHaveBeenCalledWith({ value: "" }); expect(getAllByText("Choose an option").length).toBe(1); - fireEvent.keyDown(select, { key: "ArrowUp", code: "ArrowUp", keyCode: 38, charCode: 38 }); + fireEvent.keyDown(select, { + key: "ArrowUp", + code: "ArrowUp", + keyCode: 38, + charCode: 38, + }); expect(select.getAttribute("aria-activedescendant")).toBe("option-0"); }); test("Non-Grouped Options — Filtering options never affects the optional item until there are no coincidences", () => { @@ -433,12 +480,16 @@ describe("Select component tests", () => { ); const searchInput = container.querySelectorAll("input")[1]; act(() => { - searchInput && userEvent.type(searchInput, "1"); + if (searchInput) { + userEvent.type(searchInput, "1"); + } }); expect(getByText("Placeholder example")).toBeTruthy(); expect(getAllByRole("option").length).toBe(12); act(() => { - searchInput && userEvent.type(searchInput, "123"); + if (searchInput) { + userEvent.type(searchInput, "123"); + } }); expect(queryByText("Placeholder example")).toBeFalsy(); expect(getByText("No matches found")).toBeTruthy(); @@ -446,30 +497,60 @@ describe("Select component tests", () => { test("Non-Grouped Options: Arrow up key — Opens the listbox and visually focus the last option", () => { const { getByRole, queryByRole } = render(<DxcSelect label="test-select-label" options={singleOptions} />); const select = getByRole("combobox"); - fireEvent.keyDown(select, { key: "ArrowUp", code: "ArrowUp", keyCode: 38, charCode: 38 }); + fireEvent.keyDown(select, { + key: "ArrowUp", + code: "ArrowUp", + keyCode: 38, + charCode: 38, + }); expect(queryByRole("listbox")).toBeTruthy(); expect(select.getAttribute("aria-activedescendant")).toBe("option-19"); }); test("Non-Grouped Options: Arrow up key — Puts the focus in last option when the first one is visually focused", () => { const { getByRole, queryByRole } = render(<DxcSelect label="test-select-label" options={singleOptions} />); const select = getByRole("combobox"); - fireEvent.keyDown(select, { key: "ArrowDown", code: "ArrowDown", keyCode: 40, charCode: 40 }); - fireEvent.keyDown(select, { key: "ArrowUp", code: "ArrowUp", keyCode: 38, charCode: 38 }); + fireEvent.keyDown(select, { + key: "ArrowDown", + code: "ArrowDown", + keyCode: 40, + charCode: 40, + }); + fireEvent.keyDown(select, { + key: "ArrowUp", + code: "ArrowUp", + keyCode: 38, + charCode: 38, + }); expect(queryByRole("listbox")).toBeTruthy(); expect(select.getAttribute("aria-activedescendant")).toBe("option-19"); }); test("Non-Grouped Options: Arrow down key — Opens the listbox and visually focus the first option", () => { const { getByRole, queryByRole } = render(<DxcSelect label="test-select-label" options={singleOptions} />); const select = getByRole("combobox"); - fireEvent.keyDown(select, { key: "ArrowDown", code: "ArrowDown", keyCode: 40, charCode: 40 }); + fireEvent.keyDown(select, { + key: "ArrowDown", + code: "ArrowDown", + keyCode: 40, + charCode: 40, + }); expect(queryByRole("listbox")).toBeTruthy(); expect(select.getAttribute("aria-activedescendant")).toBe("option-0"); }); test("Non-Grouped Options: Arrow down key — Puts the focus in the first option when the last one is visually focused", () => { const { getByRole, queryByRole } = render(<DxcSelect label="test-select-label" options={singleOptions} />); const select = getByRole("combobox"); - fireEvent.keyDown(select, { key: "ArrowUp", code: "ArrowUp", keyCode: 38, charCode: 38 }); - fireEvent.keyDown(select, { key: "ArrowDown", code: "ArrowDown", keyCode: 40, charCode: 40 }); + fireEvent.keyDown(select, { + key: "ArrowUp", + code: "ArrowUp", + keyCode: 38, + charCode: 38, + }); + fireEvent.keyDown(select, { + key: "ArrowDown", + code: "ArrowDown", + keyCode: 40, + charCode: 40, + }); expect(queryByRole("listbox")).toBeTruthy(); expect(select.getAttribute("aria-activedescendant")).toBe("option-0"); }); @@ -479,11 +560,36 @@ describe("Select component tests", () => { <DxcSelect label="test-select-label" options={singleOptions} onChange={onChange} optional /> ); const select = getByRole("combobox"); - fireEvent.keyDown(select, { key: "ArrowUp", code: "ArrowUp", keyCode: 38, charCode: 38 }); - fireEvent.keyDown(select, { key: "ArrowUp", code: "ArrowUp", keyCode: 38, charCode: 38 }); - fireEvent.keyDown(select, { key: "ArrowUp", code: "ArrowUp", keyCode: 38, charCode: 38 }); - fireEvent.keyDown(select, { key: "ArrowDown", code: "ArrowDown", keyCode: 40, charCode: 40 }); - fireEvent.keyDown(select, { key: "Enter", code: "Enter", keyCode: 13, charCode: 13 }); + fireEvent.keyDown(select, { + key: "ArrowUp", + code: "ArrowUp", + keyCode: 38, + charCode: 38, + }); + fireEvent.keyDown(select, { + key: "ArrowUp", + code: "ArrowUp", + keyCode: 38, + charCode: 38, + }); + fireEvent.keyDown(select, { + key: "ArrowUp", + code: "ArrowUp", + keyCode: 38, + charCode: 38, + }); + fireEvent.keyDown(select, { + key: "ArrowDown", + code: "ArrowDown", + keyCode: 40, + charCode: 40, + }); + fireEvent.keyDown(select, { + key: "Enter", + code: "Enter", + keyCode: 13, + charCode: 13, + }); expect(onChange).toHaveBeenCalledWith({ value: "20" }); expect(queryByRole("listbox")).toBeFalsy(); expect(getByText("Option 20")).toBeTruthy(); @@ -500,7 +606,9 @@ describe("Select component tests", () => { const searchInput = container.querySelectorAll("input")[1]; userEvent.click(select); expect(getByRole("listbox")).toBeTruthy(); - searchInput && userEvent.type(searchInput, "08"); + if (searchInput) { + userEvent.type(searchInput, "08"); + } userEvent.click(getByRole("option")); expect(onChange).toHaveBeenCalledWith({ value: "8" }); expect(queryByRole("listbox")).toBeFalsy(); @@ -519,7 +627,9 @@ describe("Select component tests", () => { const searchInput = container.querySelectorAll("input")[1]; userEvent.click(select); expect(getByRole("listbox")).toBeTruthy(); - searchInput && userEvent.type(searchInput, "abc"); + if (searchInput) { + userEvent.type(searchInput, "abc"); + } expect(getByText("No matches found")).toBeTruthy(); }); test("Non-Grouped Options: Searchable — Clicking the select, when the list is open, clears the search value", () => { @@ -530,7 +640,9 @@ describe("Select component tests", () => { const select = getByRole("combobox"); const searchInput = container.querySelectorAll("input")[1]; act(() => { - searchInput && userEvent.type(searchInput, "2"); + if (searchInput) { + userEvent.type(searchInput, "2"); + } }); expect(getByRole("listbox")).toBeTruthy(); expect(getByText("Option 02")).toBeTruthy(); @@ -552,7 +664,9 @@ describe("Select component tests", () => { userEvent.click(select); userEvent.click(select); expect(queryByRole("listbox")).toBeFalsy(); - searchInput && userEvent.type(searchInput, "2"); + if (searchInput) { + userEvent.type(searchInput, "2"); + } expect(getByRole("listbox")).toBeTruthy(); }); test("Non-Grouped Options: Searchable — Key Esc cleans the search value and closes the options", () => { @@ -562,7 +676,9 @@ describe("Select component tests", () => { ); const select = getByRole("combobox"); const searchInput = container.querySelectorAll("input")[1]; - searchInput && userEvent.type(searchInput, "Option 02"); + if (searchInput) { + userEvent.type(searchInput, "Option 02"); + } fireEvent.keyDown(select, { key: "Esc", code: "Esc", keyCode: 27, charCode: 27 }); expect(searchInput?.value).toBe(""); expect(queryByRole("listbox")).toBeFalsy(); @@ -573,7 +689,9 @@ describe("Select component tests", () => { <DxcSelect label="test-select-label" options={singleOptions} onChange={onChange} searchable /> ); const searchInput = container.querySelectorAll("input")[1]; - searchInput && userEvent.type(searchInput, "Option 02"); + if (searchInput) { + userEvent.type(searchInput, "Option 02"); + } expect(getAllByRole("option").length).toBe(1); const clearSearchButton = getByRole("button"); expect(clearSearchButton.getAttribute("aria-label")).toBe("Clear search"); @@ -592,13 +710,30 @@ describe("Select component tests", () => { userEvent.click(select); expect(getByRole("listbox").getAttribute("aria-multiselectable")).toBe("true"); const options = getAllByRole("option"); - options[10] && userEvent.click(options[10]); + if (options[10]) { + userEvent.click(options[10]); + } expect(onChange).toHaveBeenCalledWith({ value: ["11"] }); expect(queryByRole("listbox")).toBeTruthy(); expect(getAllByText("Option 11").length).toBe(2); - fireEvent.keyDown(select, { key: "ArrowUp", code: "ArrowUp", keyCode: 38, charCode: 38 }); - fireEvent.keyDown(select, { key: "ArrowUp", code: "ArrowUp", keyCode: 38, charCode: 38 }); - fireEvent.keyDown(select, { key: "Enter", code: "Enter", keyCode: 13, charCode: 13 }); + fireEvent.keyDown(select, { + key: "ArrowUp", + code: "ArrowUp", + keyCode: 38, + charCode: 38, + }); + fireEvent.keyDown(select, { + key: "ArrowUp", + code: "ArrowUp", + keyCode: 38, + charCode: 38, + }); + fireEvent.keyDown(select, { + key: "Enter", + code: "Enter", + keyCode: 13, + charCode: 13, + }); expect(onChange).toHaveBeenCalledWith({ value: ["11", "19"] }); expect(queryByRole("listbox")).toBeTruthy(); expect(getByText("Option 11, Option 19")).toBeTruthy(); @@ -612,9 +747,15 @@ describe("Select component tests", () => { const select = getByRole("combobox"); userEvent.click(select); const options = getAllByRole("option"); - options[5] && userEvent.click(options[5]); - options[8] && userEvent.click(options[8]); - options[13] && userEvent.click(options[13]); + if (options[5]) { + userEvent.click(options[5]); + } + if (options[8]) { + userEvent.click(options[8]); + } + if (options[13]) { + userEvent.click(options[13]); + } expect(onChange).toHaveBeenCalledWith({ value: ["6", "9", "14"] }); expect(queryByRole("listbox")).toBeTruthy(); expect(getByText("Option 06, Option 09, Option 14")).toBeTruthy(); @@ -645,7 +786,9 @@ describe("Select component tests", () => { userEvent.click(select); expect(getAllByText("Choose an option").length).toBe(1); const options = getAllByRole("option"); - options[0] && userEvent.click(options[0]); + if (options[0]) { + userEvent.click(options[0]); + } expect(onChange).toHaveBeenCalledWith({ value: ["1"] }); expect(getAllByText("Option 01").length).toBe(2); }); @@ -656,18 +799,55 @@ describe("Select component tests", () => { const select = getByRole("combobox"); userEvent.click(select); const options = getAllByRole("option"); - options[4] && userEvent.click(options[4]); + if (options[4]) { + userEvent.click(options[4]); + } expect(getByText("Option 05")).toBeTruthy(); - fireEvent.keyDown(select, { key: "ArrowUp", code: "ArrowUp", keyCode: 38, charCode: 38 }); + fireEvent.keyDown(select, { + key: "ArrowUp", + code: "ArrowUp", + keyCode: 38, + charCode: 38, + }); expect(select.getAttribute("aria-activedescendant")).toBe("option-4"); - fireEvent.keyDown(select, { key: "ArrowUp", code: "ArrowUp", keyCode: 38, charCode: 38 }); - fireEvent.keyDown(select, { key: "Enter", code: "Enter", keyCode: 13, charCode: 13 }); + fireEvent.keyDown(select, { + key: "ArrowUp", + code: "ArrowUp", + keyCode: 38, + charCode: 38, + }); + fireEvent.keyDown(select, { + key: "Enter", + code: "Enter", + keyCode: 13, + charCode: 13, + }); expect(getByText("Option 04")).toBeTruthy(); - fireEvent.keyDown(select, { key: "ArrowDown", code: "ArrowDown", keyCode: 40, charCode: 40 }); + fireEvent.keyDown(select, { + key: "ArrowDown", + code: "ArrowDown", + keyCode: 40, + charCode: 40, + }); expect(select.getAttribute("aria-activedescendant")).toBe("option-3"); - fireEvent.keyDown(select, { key: "ArrowDown", code: "ArrowDown", keyCode: 40, charCode: 40 }); - fireEvent.keyDown(select, { key: "ArrowDown", code: "ArrowDown", keyCode: 40, charCode: 40 }); - fireEvent.keyDown(select, { key: "Enter", code: "Enter", keyCode: 13, charCode: 13 }); + fireEvent.keyDown(select, { + key: "ArrowDown", + code: "ArrowDown", + keyCode: 40, + charCode: 40, + }); + fireEvent.keyDown(select, { + key: "ArrowDown", + code: "ArrowDown", + keyCode: 40, + charCode: 40, + }); + fireEvent.keyDown(select, { + key: "Enter", + code: "Enter", + keyCode: 13, + charCode: 13, + }); expect(getByText("Option 06")).toBeTruthy(); }); test("Non-Grouped Options — If an options was previously selected when its opened (by click and key press), the visual focus appears always in the selected option", () => { @@ -677,21 +857,53 @@ describe("Select component tests", () => { const select = getByRole("combobox"); userEvent.click(select); const options = getAllByRole("option"); - options[15] && userEvent.click(options[15]); + if (options[15]) { + userEvent.click(options[15]); + } expect(queryByRole("listbox")).toBeFalsy(); expect(getByText("Option 16")).toBeTruthy(); userEvent.click(select); expect(select.getAttribute("aria-activedescendant")).toBeNull(); - fireEvent.keyDown(select, { key: "ArrowUp", code: "ArrowUp", keyCode: 38, charCode: 38 }); + fireEvent.keyDown(select, { + key: "ArrowUp", + code: "ArrowUp", + keyCode: 38, + charCode: 38, + }); expect(select.getAttribute("aria-activedescendant")).toBe("option-15"); userEvent.click(select); expect(queryByRole("listbox")).toBeFalsy(); - fireEvent.keyDown(select, { key: "ArrowDown", code: "ArrowDown", keyCode: 40, charCode: 40 }); + fireEvent.keyDown(select, { + key: "ArrowDown", + code: "ArrowDown", + keyCode: 40, + charCode: 40, + }); expect(select.getAttribute("aria-activedescendant")).toBe("option-15"); - fireEvent.keyDown(select, { key: "ArrowDown", code: "ArrowDown", keyCode: 40, charCode: 40 }); - fireEvent.keyDown(select, { key: "ArrowDown", code: "ArrowDown", keyCode: 40, charCode: 40 }); - fireEvent.keyDown(select, { key: "ArrowUp", code: "ArrowUp", keyCode: 38, charCode: 38 }); - fireEvent.keyDown(select, { key: "Enter", code: "Enter", keyCode: 13, charCode: 13 }); + fireEvent.keyDown(select, { + key: "ArrowDown", + code: "ArrowDown", + keyCode: 40, + charCode: 40, + }); + fireEvent.keyDown(select, { + key: "ArrowDown", + code: "ArrowDown", + keyCode: 40, + charCode: 40, + }); + fireEvent.keyDown(select, { + key: "ArrowUp", + code: "ArrowUp", + keyCode: 38, + charCode: 38, + }); + fireEvent.keyDown(select, { + key: "Enter", + code: "Enter", + keyCode: 13, + charCode: 13, + }); expect(getByText("Option 17")).toBeTruthy(); }); test("Grouped Options — Opens listbox and renders it correctly or closes it with a click on select", () => { @@ -745,7 +957,9 @@ describe("Select component tests", () => { const submitInput = container.querySelector<HTMLInputElement>(`input[name="test"]`); userEvent.click(select); let options = getAllByRole("option"); - options[8] && userEvent.click(options[8]); + if (options[8]) { + userEvent.click(options[8]); + } expect(onChange).toHaveBeenCalledWith({ value: "oviedo" }); expect(queryByRole("listbox")).toBeFalsy(); expect(getByText("Oviedo")).toBeTruthy(); @@ -770,15 +984,32 @@ describe("Select component tests", () => { expect(getAllByText("Placeholder example").length).toBe(2); const options = getAllByRole("option"); expect(options[0]?.getAttribute("aria-selected")).toBe("true"); - options[0] && userEvent.click(options[0]); + if (options[0]) { + userEvent.click(options[0]); + } expect(onChange).toHaveBeenCalledWith({ value: "" }); expect(getAllByText("Placeholder example").length).toBe(1); - fireEvent.keyDown(select, { key: "ArrowDown", code: "ArrowDown", keyCode: 40, charCode: 40 }); + fireEvent.keyDown(select, { + key: "ArrowDown", + code: "ArrowDown", + keyCode: 40, + charCode: 40, + }); expect(select.getAttribute("aria-activedescendant")).toBe("option-0"); - fireEvent.keyDown(select, { key: "Enter", code: "Enter", keyCode: 13, charCode: 13 }); + fireEvent.keyDown(select, { + key: "Enter", + code: "Enter", + keyCode: 13, + charCode: 13, + }); expect(onChange).toHaveBeenCalledWith({ value: "" }); expect(getAllByText("Placeholder example").length).toBe(1); - fireEvent.keyDown(select, { key: "ArrowUp", code: "ArrowUp", keyCode: 38, charCode: 38 }); + fireEvent.keyDown(select, { + key: "ArrowUp", + code: "ArrowUp", + keyCode: 38, + charCode: 38, + }); expect(select.getAttribute("aria-activedescendant")).toBe("option-0"); }); test("Grouped Options — Filtering options never affects the optional item until there are no coincidence", () => { @@ -794,10 +1025,14 @@ describe("Select component tests", () => { const select = getByRole("combobox"); const searchInput = container.querySelectorAll("input")[1]; userEvent.click(select); - searchInput && userEvent.type(searchInput, "ro"); + if (searchInput) { + userEvent.type(searchInput, "ro"); + } expect(getByText("Placeholder example")).toBeTruthy(); expect(getAllByRole("option").length).toBe(6); - searchInput && userEvent.type(searchInput, "roro"); + if (searchInput) { + userEvent.type(searchInput, "roro"); + } expect(queryByText("Placeholder example")).toBeFalsy(); expect(getByText("No matches found")).toBeTruthy(); }); @@ -837,11 +1072,36 @@ describe("Select component tests", () => { <DxcSelect label="test-select-label" options={groupedOptions} onChange={onChange} optional /> ); const select = getByRole("combobox"); - fireEvent.keyDown(select, { key: "ArrowUp", code: "ArrowUp", keyCode: 38, charCode: 38 }); - fireEvent.keyDown(select, { key: "ArrowUp", code: "ArrowUp", keyCode: 38, charCode: 38 }); - fireEvent.keyDown(select, { key: "ArrowUp", code: "ArrowUp", keyCode: 38, charCode: 38 }); - fireEvent.keyDown(select, { key: "ArrowDown", code: "ArrowDown", keyCode: 40, charCode: 40 }); - fireEvent.keyDown(select, { key: "Enter", code: "Enter", keyCode: 13, charCode: 13 }); + fireEvent.keyDown(select, { + key: "ArrowUp", + code: "ArrowUp", + keyCode: 38, + charCode: 38, + }); + fireEvent.keyDown(select, { + key: "ArrowUp", + code: "ArrowUp", + keyCode: 38, + charCode: 38, + }); + fireEvent.keyDown(select, { + key: "ArrowUp", + code: "ArrowUp", + keyCode: 38, + charCode: 38, + }); + fireEvent.keyDown(select, { + key: "ArrowDown", + code: "ArrowDown", + keyCode: 40, + charCode: 40, + }); + fireEvent.keyDown(select, { + key: "Enter", + code: "Enter", + keyCode: 13, + charCode: 13, + }); expect(onChange).toHaveBeenCalledWith({ value: "ebro" }); expect(queryByRole("listbox")).toBeFalsy(); expect(getByText("Ebro")).toBeTruthy(); @@ -858,13 +1118,17 @@ describe("Select component tests", () => { const searchInput = container.querySelectorAll("input")[1]; userEvent.click(select); expect(getByRole("listbox")).toBeTruthy(); - searchInput && userEvent.type(searchInput, "ro"); + if (searchInput) { + userEvent.type(searchInput, "ro"); + } expect(getAllByRole("presentation").length).toBe(2); expect(getAllByRole("option").length).toBe(5); expect(getByText("Colores")).toBeTruthy(); expect(getByText("Ríos españoles")).toBeTruthy(); let options = getAllByRole("option"); - options[4] && userEvent.click(options[4]); + if (options[4]) { + userEvent.click(options[4]); + } expect(onChange).toHaveBeenCalledWith({ value: "ebro" }); expect(queryByRole("listbox")).toBeFalsy(); expect(getByText("Ebro")).toBeTruthy(); @@ -882,7 +1146,9 @@ describe("Select component tests", () => { const searchInput = container.querySelectorAll("input")[1]; userEvent.click(select); expect(getByRole("listbox")).toBeTruthy(); - searchInput && userEvent.type(searchInput, "very long string"); + if (searchInput) { + userEvent.type(searchInput, "very long string"); + } expect(getByText("No matches found")).toBeTruthy(); }); test("Grouped Options: Multiple selection — Displays a checkbox per option and enables the multi-selection", () => { @@ -894,7 +1160,9 @@ describe("Select component tests", () => { const submitInput = container.querySelector<HTMLInputElement>(`input[name="test"]`); userEvent.click(select); const options = getAllByRole("option"); - options[10] && userEvent.click(options[10]); + if (options[10]) { + userEvent.click(options[10]); + } expect(onChange).toHaveBeenCalledWith({ value: ["bilbao"] }); expect(queryByRole("listbox")).toBeTruthy(); expect(getAllByText("Bilbao").length).toBe(2); @@ -914,10 +1182,18 @@ describe("Select component tests", () => { const select = getByRole("combobox"); userEvent.click(select); const options = getAllByRole("option"); - options[5] && userEvent.click(options[5]); - options[8] && userEvent.click(options[8]); - options[13] && userEvent.click(options[13]); - options[17] && userEvent.click(options[17]); + if (options[5]) { + userEvent.click(options[5]); + } + if (options[8]) { + userEvent.click(options[8]); + } + if (options[13]) { + userEvent.click(options[13]); + } + if (options[17]) { + userEvent.click(options[17]); + } expect(onChange).toHaveBeenCalledWith({ value: ["blanco", "oviedo", "duero", "ebro"] }); expect(queryByRole("listbox")).toBeTruthy(); expect(getByText("Blanco, Oviedo, Duero, Ebro")).toBeTruthy(); @@ -947,7 +1223,9 @@ describe("Select component tests", () => { userEvent.click(select); expect(getAllByText("Choose an option").length).toBe(1); const options = getAllByRole("option"); - options[0] && userEvent.click(options[0]); + if (options[0]) { + userEvent.click(options[0]); + } expect(onChange).toHaveBeenCalledWith({ value: ["azul"] }); expect(getAllByText("Azul").length).toBe(2); }); @@ -958,18 +1236,55 @@ describe("Select component tests", () => { const select = getByRole("combobox"); userEvent.click(select); const options = getAllByRole("option"); - options[2] && userEvent.click(options[2]); + if (options[2]) { + userEvent.click(options[2]); + } expect(getByText("Rosa")).toBeTruthy(); - fireEvent.keyDown(select, { key: "ArrowUp", code: "ArrowUp", keyCode: 38, charCode: 38 }); + fireEvent.keyDown(select, { + key: "ArrowUp", + code: "ArrowUp", + keyCode: 38, + charCode: 38, + }); expect(select.getAttribute("aria-activedescendant")).toBe("option-2"); - fireEvent.keyDown(select, { key: "ArrowUp", code: "ArrowUp", keyCode: 38, charCode: 38 }); - fireEvent.keyDown(select, { key: "Enter", code: "Enter", keyCode: 13, charCode: 13 }); + fireEvent.keyDown(select, { + key: "ArrowUp", + code: "ArrowUp", + keyCode: 38, + charCode: 38, + }); + fireEvent.keyDown(select, { + key: "Enter", + code: "Enter", + keyCode: 13, + charCode: 13, + }); expect(getByText("Rojo")).toBeTruthy(); - fireEvent.keyDown(select, { key: "ArrowDown", code: "ArrowDown", keyCode: 40, charCode: 40 }); + fireEvent.keyDown(select, { + key: "ArrowDown", + code: "ArrowDown", + keyCode: 40, + charCode: 40, + }); expect(select.getAttribute("aria-activedescendant")).toBe("option-1"); - fireEvent.keyDown(select, { key: "ArrowDown", code: "ArrowDown", keyCode: 40, charCode: 40 }); - fireEvent.keyDown(select, { key: "ArrowDown", code: "ArrowDown", keyCode: 40, charCode: 40 }); - fireEvent.keyDown(select, { key: "Enter", code: "Enter", keyCode: 13, charCode: 13 }); + fireEvent.keyDown(select, { + key: "ArrowDown", + code: "ArrowDown", + keyCode: 40, + charCode: 40, + }); + fireEvent.keyDown(select, { + key: "ArrowDown", + code: "ArrowDown", + keyCode: 40, + charCode: 40, + }); + fireEvent.keyDown(select, { + key: "Enter", + code: "Enter", + keyCode: 13, + charCode: 13, + }); expect(getByText("Verde")).toBeTruthy(); }); test("Grouped Options — If an options was previously selected when its opened (by click and key press), the visual focus appears always in the selected option", () => { @@ -979,19 +1294,46 @@ describe("Select component tests", () => { const select = getByRole("combobox"); userEvent.click(select); const options = getAllByRole("option"); - options[17] && userEvent.click(options[17]); + if (options[17]) { + userEvent.click(options[17]); + } expect(getByText("Ebro")).toBeTruthy(); userEvent.click(select); expect(select.getAttribute("aria-activedescendant")).toBeNull(); - fireEvent.keyDown(select, { key: "ArrowUp", code: "ArrowUp", keyCode: 38, charCode: 38 }); + fireEvent.keyDown(select, { + key: "ArrowUp", + code: "ArrowUp", + keyCode: 38, + charCode: 38, + }); expect(select.getAttribute("aria-activedescendant")).toBe("option-17"); userEvent.click(select); fireEvent.keyDown(select, { key: "ArrowDown", code: "ArrowDown", keyCode: 40, charCode: 40 }); expect(select.getAttribute("aria-activedescendant")).toBe("option-17"); - fireEvent.keyDown(select, { key: "ArrowDown", code: "ArrowDown", keyCode: 40, charCode: 40 }); - fireEvent.keyDown(select, { key: "ArrowDown", code: "ArrowDown", keyCode: 40, charCode: 40 }); - fireEvent.keyDown(select, { key: "ArrowUp", code: "ArrowUp", keyCode: 38, charCode: 38 }); - fireEvent.keyDown(select, { key: "Enter", code: "Enter", keyCode: 13, charCode: 13 }); + fireEvent.keyDown(select, { + key: "ArrowDown", + code: "ArrowDown", + keyCode: 40, + charCode: 40, + }); + fireEvent.keyDown(select, { + key: "ArrowDown", + code: "ArrowDown", + keyCode: 40, + charCode: 40, + }); + fireEvent.keyDown(select, { + key: "ArrowUp", + code: "ArrowUp", + keyCode: 38, + charCode: 38, + }); + fireEvent.keyDown(select, { + key: "Enter", + code: "Enter", + keyCode: 13, + charCode: 13, + }); expect(getByText("Azul")).toBeTruthy(); }); test("Multiple selection and optional — Clear action cleans every selected option but does not display an error", () => { @@ -1002,9 +1344,15 @@ describe("Select component tests", () => { const select = getByRole("combobox"); userEvent.click(select); const options = getAllByRole("option"); - options[5] && userEvent.click(options[5]); - options[8] && userEvent.click(options[8]); - options[13] && userEvent.click(options[13]); + if (options[5]) { + userEvent.click(options[5]); + } + if (options[8]) { + userEvent.click(options[8]); + } + if (options[13]) { + userEvent.click(options[13]); + } expect(onChange).toHaveBeenCalledWith({ value: ["6", "9", "14"] }); const clearSelectionButton = getByRole("button"); expect(clearSelectionButton.getAttribute("aria-label")).toBe("Clear selection"); @@ -1027,9 +1375,13 @@ describe("Select component tests", () => { const select = getByRole("combobox"); userEvent.click(select); const selectAllOption = getByText("Select all"); - selectAllOption && userEvent.click(selectAllOption); + if (selectAllOption) { + userEvent.click(selectAllOption); + } expect(onChange).toHaveBeenCalledWith({ value: ["1", "2", "3", "4"] }); - selectAllOption && userEvent.click(selectAllOption); + if (selectAllOption) { + userEvent.click(selectAllOption); + } expect(onChange).toHaveBeenCalledWith({ value: [] }); }); test("Select all (groups) — 'Select all' option is included and (un)selects all the options available", () => { @@ -1047,11 +1399,15 @@ describe("Select component tests", () => { const select = getByRole("combobox"); userEvent.click(select); const selectAllOption = getByText("Select all"); - selectAllOption && userEvent.click(selectAllOption); + if (selectAllOption) { + userEvent.click(selectAllOption); + } expect(onChange).toHaveBeenCalledWith({ value: ["azul", "rojo", "rosa", "madrid", "oviedo", "sevilla", "miño", "duero", "tajo"], }); - selectAllOption && userEvent.click(selectAllOption); + if (selectAllOption) { + userEvent.click(selectAllOption); + } expect(onChange).toHaveBeenCalledWith({ error: "This field is required. Please, enter a value.", value: [] }); }); test("Select all — Keyboard navigation is correct", () => { @@ -1092,11 +1448,15 @@ describe("Select component tests", () => { const select = getByRole("combobox"); userEvent.click(select); const selectAllOption = getByText("Select all"); - selectAllOption && userEvent.click(selectAllOption); + if (selectAllOption) { + userEvent.click(selectAllOption); + } expect(onChange).toHaveBeenCalledWith({ value: ["azul", "rojo", "rosa", "madrid", "oviedo", "sevilla", "miño", "duero", "tajo"], }); - selectAllOption && userEvent.click(selectAllOption); + if (selectAllOption) { + userEvent.click(selectAllOption); + } expect(onChange).toHaveBeenCalledWith({ error: "This field is required. Please, enter a value.", value: [] }); }); test("Select all options from a group — The header of a group is selectable and (un)selects all the options from its group", () => { @@ -1114,11 +1474,15 @@ describe("Select component tests", () => { const select = getByRole("combobox"); userEvent.click(select); const thirdGroupHeader = getByText("Ríos españoles"); - thirdGroupHeader && userEvent.click(thirdGroupHeader); + if (thirdGroupHeader) { + userEvent.click(thirdGroupHeader); + } expect(onChange).toHaveBeenCalledWith({ value: ["miño", "duero", "tajo"], }); - thirdGroupHeader && userEvent.click(thirdGroupHeader); + if (thirdGroupHeader) { + userEvent.click(thirdGroupHeader); + } expect(onChange).toHaveBeenCalledWith({ error: "This field is required. Please, enter a value.", value: [] }); }); test("Select all options from a group — The header of a group selects all the options when there's a partial selection", () => { @@ -1137,11 +1501,15 @@ describe("Select component tests", () => { const select = getByRole("combobox"); userEvent.click(select); const thirdGroupHeader = getByText("Ríos españoles"); - thirdGroupHeader && userEvent.click(thirdGroupHeader); + if (thirdGroupHeader) { + userEvent.click(thirdGroupHeader); + } expect(onChange).toHaveBeenCalledWith({ value: ["miño", "duero", "tajo"], }); - thirdGroupHeader && userEvent.click(thirdGroupHeader); + if (thirdGroupHeader) { + userEvent.click(thirdGroupHeader); + } expect(onChange).toHaveBeenCalledWith({ error: "This field is required. Please, enter a value.", value: [] }); }); test("Select all options from a group — Keyboard navigation is correct", () => { diff --git a/packages/lib/src/select/Select.tsx b/packages/lib/src/select/Select.tsx index 2740777052..2dd32f93b6 100644 --- a/packages/lib/src/select/Select.tsx +++ b/packages/lib/src/select/Select.tsx @@ -40,7 +40,7 @@ import DxcFlex from "../flex/Flex"; import ErrorMessage from "../styles/forms/ErrorMessage"; import HelperText from "../styles/forms/HelperText"; import Label from "../styles/forms/Label"; -import { inputStylesByState } from "../styles/forms/inputStylesByState"; +import inputStylesByState from "../styles/forms/inputStylesByState"; const SelectContainer = styled.div<{ margin: SelectPropsType["margin"]; @@ -634,4 +634,6 @@ const DxcSelect = forwardRef<RefType, SelectPropsType>( } ); +DxcSelect.displayName = "DxcSelect"; + export default DxcSelect; diff --git a/packages/lib/src/select/utils.ts b/packages/lib/src/select/utils.ts index c73e248bdd..83af7d14bb 100644 --- a/packages/lib/src/select/utils.ts +++ b/packages/lib/src/select/utils.ts @@ -102,7 +102,7 @@ export const getSelectedOption = ( optional: boolean, optionalItem: ListOptionType ) => { - let selectedOption: ListOptionType | ListOptionType[] = multiple ? [] : ({} as ListOptionType); + let selectedOption: ListOptionType | ListOptionType[] | null = multiple ? [] : null; let singleSelectionIndex: number | null = null; if (multiple) { @@ -135,12 +135,14 @@ export const getSelectedOption = ( groupIndex++; return false; }); + return false; } else if (option.value === value) { selectedOption = option; singleSelectionIndex = optional ? index + 1 : index; return true; + } else { + return false; } - return false; }); } @@ -153,12 +155,15 @@ export const getSelectedOption = ( /** * Return the label or labels of the selected option(s), separated by commas. */ -export const getSelectedOptionLabel = (placeholder: string, selectedOption: ListOptionType | ListOptionType[]) => +export const getSelectedOptionLabel = ( + placeholder: string, + selectedOption: ListOptionType | ListOptionType[] | null +) => Array.isArray(selectedOption) ? selectedOption.length === 0 ? placeholder : selectedOption.map((option) => option.label).join(", ") - : (selectedOption.label ?? placeholder); + : (selectedOption?.label ?? placeholder); /** * Returns a determined string value depending on the amount of options selected: @@ -209,9 +214,9 @@ export const getSelectableOptionsValues = (options: Props["options"]) => /** * (Un)Selects the option passed as parameter. - * @param currentValue - * @param newOption - * @returns + * @param currentValue + * @param newOption + * @returns */ export const computeNewValue = (currentValue: string[], newOption: ListOptionType) => currentValue.includes(newOption.value) diff --git a/packages/lib/src/sidenav/Sidenav.stories.tsx b/packages/lib/src/sidenav/Sidenav.stories.tsx index 8549eaec3c..2902a38129 100644 --- a/packages/lib/src/sidenav/Sidenav.stories.tsx +++ b/packages/lib/src/sidenav/Sidenav.stories.tsx @@ -1,10 +1,10 @@ +import { Meta, StoryObj } from "@storybook/react"; import { userEvent, within } from "@storybook/test"; -import ExampleContainer from "../../.storybook/components/ExampleContainer"; import Title from "../../.storybook/components/Title"; +import ExampleContainer from "../../.storybook/components/ExampleContainer"; import DxcInset from "../inset/Inset"; import DxcSelect from "../select/Select"; import DxcSidenav from "./Sidenav"; -import { Meta, StoryObj } from "@storybook/react"; export default { title: "Sidenav", @@ -34,16 +34,6 @@ c-10.663,0-17.467,1.853-20.417,5.568c-2.949,3.711-4.428,10.23-4.428,19.558v31.11 </svg> ); -const TitleComponent = () => { - return <DxcSidenav.Title>Dxc technology</DxcSidenav.Title>; -}; - -const opinionatedTheme = { - sidenav: { - baseColor: "#f2f2f2", - }, -}; - const SideNav = () => ( <> <ExampleContainer> @@ -112,32 +102,30 @@ const SideNav = () => ( ); const CollapsedGroupSidenav = () => ( - <> - <ExampleContainer> - <Title title="Collapsed group with a selected link" theme="light" level={4} /> - <DxcSidenav title={<DxcSidenav.Title>Dxc technology</DxcSidenav.Title>}> - <DxcSidenav.Section> - <DxcSidenav.Group collapsable title="Collapsed Group" icon={iconSVG}> - <DxcSidenav.Link>Group Link</DxcSidenav.Link> - <DxcSidenav.Link>Group Link</DxcSidenav.Link> - <DxcSidenav.Link>Group Link</DxcSidenav.Link> - <DxcSidenav.Link>Group Link</DxcSidenav.Link> - </DxcSidenav.Group> - </DxcSidenav.Section> - <DxcSidenav.Section> - <DxcSidenav.Group collapsable title="Collapsed Group"> - <DxcSidenav.Link>Group Link</DxcSidenav.Link> - <DxcSidenav.Link selected>Group Link</DxcSidenav.Link> - </DxcSidenav.Group> - <DxcSidenav.Group collapsable title="Section Group"> - <DxcSidenav.Link>Group Link</DxcSidenav.Link> - <DxcSidenav.Link>Group Link</DxcSidenav.Link> - <DxcSidenav.Link>Group Link</DxcSidenav.Link> - </DxcSidenav.Group> - </DxcSidenav.Section> - </DxcSidenav> - </ExampleContainer> - </> + <ExampleContainer> + <Title title="Collapsed group with a selected link" theme="light" level={4} /> + <DxcSidenav title={<DxcSidenav.Title>Dxc technology</DxcSidenav.Title>}> + <DxcSidenav.Section> + <DxcSidenav.Group collapsable title="Collapsed Group" icon={iconSVG}> + <DxcSidenav.Link>Group Link</DxcSidenav.Link> + <DxcSidenav.Link>Group Link</DxcSidenav.Link> + <DxcSidenav.Link>Group Link</DxcSidenav.Link> + <DxcSidenav.Link>Group Link</DxcSidenav.Link> + </DxcSidenav.Group> + </DxcSidenav.Section> + <DxcSidenav.Section> + <DxcSidenav.Group collapsable title="Collapsed Group"> + <DxcSidenav.Link>Group Link</DxcSidenav.Link> + <DxcSidenav.Link selected>Group Link</DxcSidenav.Link> + </DxcSidenav.Group> + <DxcSidenav.Group collapsable title="Section Group"> + <DxcSidenav.Link>Group Link</DxcSidenav.Link> + <DxcSidenav.Link>Group Link</DxcSidenav.Link> + <DxcSidenav.Link>Group Link</DxcSidenav.Link> + </DxcSidenav.Group> + </DxcSidenav.Section> + </DxcSidenav> + </ExampleContainer> ); const HoveredGroupSidenav = () => ( @@ -225,9 +213,9 @@ export const CollapsableGroup: Story = { play: async ({ canvasElement }) => { const canvas = within(canvasElement); const collapsableGroups = canvas.getAllByText("Collapsed Group"); - collapsableGroups.forEach((group) => { - userEvent.click(group); - }); + for (const group of collapsableGroups) { + await userEvent.click(group); + } }, }; @@ -236,10 +224,12 @@ export const CollapsedHoverGroup: Story = { play: async ({ canvasElement }) => { const canvas = within(canvasElement); const collapsableGroups = canvas.getAllByText("Collapsed Group"); - collapsableGroups.forEach((group) => { - userEvent.click(group); + for (const group of collapsableGroups) { + await userEvent.click(group); + } + await new Promise((resolve) => { + setTimeout(resolve, 1000); }); - await new Promise((resolve) => setTimeout(resolve, 1000)); }, }; @@ -248,6 +238,8 @@ export const CollapsedActiveGroup: Story = { play: async ({ canvasElement }) => { const canvas = within(canvasElement); const collapsableGroups = canvas.getAllByText("Collapsed Group"); - collapsableGroups[0] && userEvent.click(collapsableGroups[0]); + if (collapsableGroups[0]) { + await userEvent.click(collapsableGroups[0]); + } }, }; diff --git a/packages/lib/src/sidenav/Sidenav.tsx b/packages/lib/src/sidenav/Sidenav.tsx index 843a4ea4d1..814dd5b576 100644 --- a/packages/lib/src/sidenav/Sidenav.tsx +++ b/packages/lib/src/sidenav/Sidenav.tsx @@ -10,7 +10,7 @@ import SidenavPropsType, { SidenavSectionPropsType, SidenavTitlePropsType, } from "./types"; -import { scrollbarStyles } from "../styles/scroll"; +import scrollbarStyles from "../styles/scroll"; import DxcDivider from "../divider/Divider"; import DxcInset from "../inset/Inset"; @@ -221,7 +221,7 @@ const Link = forwardRef<HTMLAnchorElement, SidenavLinkPropsType>( return ( <SidenavLink selected={selected} - href={href ? href : undefined} + href={href || undefined} target={href ? (newWindow ? "_blank" : "_self") : undefined} ref={ref} tabIndex={tabIndex} diff --git a/packages/lib/src/slider/Slider.accessibility.test.tsx b/packages/lib/src/slider/Slider.accessibility.test.tsx index 1f92d94437..c89b89a386 100644 --- a/packages/lib/src/slider/Slider.accessibility.test.tsx +++ b/packages/lib/src/slider/Slider.accessibility.test.tsx @@ -2,16 +2,11 @@ import { render } from "@testing-library/react"; import { axe } from "../../test/accessibility/axe-helper"; import DxcSlider from "./Slider"; -// Mocking DOMRect for Radix Primitive Popover -(global as any).globalThis = global; -(global as any).DOMRect = { - fromRect: () => ({ top: 0, left: 0, bottom: 0, right: 0, width: 0, height: 0 }), -}; -(global as any).ResizeObserver = class ResizeObserver { - observe() {} - unobserve() {} - disconnect() {} -}; +global.ResizeObserver = jest.fn().mockImplementation(() => ({ + observe: jest.fn(), + unobserve: jest.fn(), + disconnect: jest.fn(), +})); describe("Slider component accessibility tests", () => { it("Should not have basic accessibility issues", async () => { diff --git a/packages/lib/src/slider/Slider.test.tsx b/packages/lib/src/slider/Slider.test.tsx index e872b29bec..b2ead8feb2 100644 --- a/packages/lib/src/slider/Slider.test.tsx +++ b/packages/lib/src/slider/Slider.test.tsx @@ -1,16 +1,11 @@ -import { act, fireEvent, render, waitFor } from "@testing-library/react"; +import { act, fireEvent, render } from "@testing-library/react"; import DxcSlider from "./Slider"; -// Mocking DOMRect for Radix Primitive Popover -(global as any).globalThis = global; -(global as any).DOMRect = { - fromRect: () => ({ top: 0, left: 0, bottom: 0, right: 0, width: 0, height: 0 }), -}; -(global as any).ResizeObserver = class ResizeObserver { - observe() {} - unobserve() {} - disconnect() {} -}; +global.ResizeObserver = jest.fn().mockImplementation(() => ({ + observe: jest.fn(), + unobserve: jest.fn(), + disconnect: jest.fn(), +})); describe("Slider component tests", () => { test("Slider renders with correct text and label id", () => { @@ -34,7 +29,7 @@ describe("Slider component tests", () => { expect(slider.getAttribute("aria-valuenow")).toBe("30"); expect(input.value).toBe("30"); }); - test("Slider correct limit values", async () => { + test("Slider correct limit values", () => { const { getByRole, getByText } = render( <DxcSlider defaultValue={-30} minValue={-30} maxValue={125} showLimitsValues /> ); diff --git a/packages/lib/src/slider/Slider.tsx b/packages/lib/src/slider/Slider.tsx index bf53352b0d..626a2c01e1 100644 --- a/packages/lib/src/slider/Slider.tsx +++ b/packages/lib/src/slider/Slider.tsx @@ -272,4 +272,6 @@ const DxcSlider = forwardRef<RefType, SliderPropsType>( } ); +DxcSlider.displayName = "DxcSlider"; + export default DxcSlider; diff --git a/packages/lib/src/slider/utils.ts b/packages/lib/src/slider/utils.ts index f6572672b0..a1345438a6 100644 --- a/packages/lib/src/slider/utils.ts +++ b/packages/lib/src/slider/utils.ts @@ -15,9 +15,9 @@ export const calculateWidth = (margin: SliderPropsType["margin"], size: SliderPr /** * Rounds a number to a specific number of decimal places. * this function tries to avoid floating point inaccuracies, present in JS: - * + * * 0.1 + 0.2 === 0.3 // false - * + * * @param number the number to round * @param step slider step value that defines the number of decimal places * @returns the rounded number @@ -29,7 +29,7 @@ export const stepPrecision = (target: number, step: number) => { /** * This function calculates the closest tick value to the target value within the range [min, max]. - * + * * @param target the target value to round up * @param step the step value that defines the ticks from the range * @param min the minimum value of the range @@ -42,17 +42,18 @@ export const roundUp = (target: number, step: number, min: number, max: number): else if (target >= max) return max; else if (step === 1) return Math.round(target); - const ticks = Array.from({ length: Math.floor((max - min) / step) + 1 }, (_, index) => stepPrecision(min + index * step, step)); + const ticks = Array.from({ length: Math.floor((max - min) / step) + 1 }, (_, index) => + stepPrecision(min + index * step, step) + ); if (ticks.includes(target)) return target; - let rounded = 0; - let acc = Infinity; - for (const tick of ticks) { - const diff = Math.abs(stepPrecision(target - tick, target)); - if (diff < Math.abs(acc) || (diff === Math.abs(acc) && target > 0)) { - rounded = tick; - acc = diff; - } else break; - }; - return rounded; + return ticks?.reduce((closest, tick) => { + const currentDiff = Math.abs(stepPrecision(target - tick, target)); + const closestDiff = Math.abs(stepPrecision(target - closest, target)); + + if (currentDiff < closestDiff || (currentDiff === closestDiff && target > 0)) { + return tick; + } + return closest; + }, ticks[0] ?? 0); }; diff --git a/packages/lib/src/spinner/Spinner.stories.tsx b/packages/lib/src/spinner/Spinner.stories.tsx index 1499c9f792..9c1e1a6364 100644 --- a/packages/lib/src/spinner/Spinner.stories.tsx +++ b/packages/lib/src/spinner/Spinner.stories.tsx @@ -1,8 +1,8 @@ import { Meta, StoryObj } from "@storybook/react"; +import { userEvent, within } from "@storybook/test"; import ExampleContainer from "../../.storybook/components/ExampleContainer"; import Title from "../../.storybook/components/Title"; import DxcSpinner from "./Spinner"; -import { userEvent, within } from "@storybook/test"; export default { title: "Spinner", diff --git a/packages/lib/src/spinner/Spinner.test.tsx b/packages/lib/src/spinner/Spinner.test.tsx index 3d1f5a8456..0c952049ce 100644 --- a/packages/lib/src/spinner/Spinner.test.tsx +++ b/packages/lib/src/spinner/Spinner.test.tsx @@ -25,13 +25,13 @@ describe("Spinner component tests", () => { const { getByRole } = render(<DxcSpinner label="test-loading" value={75} showValue />); expect(getByRole("progressbar")).toBeTruthy(); }); - test("Test spinner aria-label to be undefined", () => { + test("Spinner aria-label must be undefined for large size", () => { const { getByRole } = render(<DxcSpinner label="test-loading" value={75} showValue />); const spinner = getByRole("progressbar"); expect(spinner.getAttribute("aria-label")).toBeNull(); expect(spinner.getAttribute("aria-labelledby")).toBeTruthy(); }); - test("Test spinner aria-label to be applied correctly when mode is small", () => { + test("Spinner aria-label is applied correctly when mode is small", () => { const { getByRole } = render( <DxcSpinner label="test-loading" ariaLabel="Example aria label" value={75} mode="small" showValue /> ); diff --git a/packages/lib/src/spinner/types.ts b/packages/lib/src/spinner/types.ts index d10d8f7c3c..129d70d81c 100644 --- a/packages/lib/src/spinner/types.ts +++ b/packages/lib/src/spinner/types.ts @@ -6,7 +6,7 @@ type Props = { */ ariaLabel?: string; /** - * If true, the color is inherited from the closest parent with a defined color. This allows users to adapt the spinner + * If true, the color is inherited from the closest parent with a defined color. This allows users to adapt the spinner * to the semantic color of the use case in which it is used. */ inheritColor?: boolean; diff --git a/packages/lib/src/status-light/StatusLight.test.tsx b/packages/lib/src/status-light/StatusLight.test.tsx index 4403f4e29d..81177e3bd5 100644 --- a/packages/lib/src/status-light/StatusLight.test.tsx +++ b/packages/lib/src/status-light/StatusLight.test.tsx @@ -10,7 +10,7 @@ describe("StatusLight component tests", () => { const { getByRole } = render(<DxcStatusLight label="Status Light Test" />); expect(getByRole("status")).toBeTruthy(); }); - + test("StatusLight dot is aria-hidden", () => { const { container } = render(<DxcStatusLight label="Hidden Dot Test" />); const dot = container.querySelector("div[aria-hidden='true']"); diff --git a/packages/lib/src/styles/forms/inputStylesByState.tsx b/packages/lib/src/styles/forms/inputStylesByState.tsx index 39d5ac7067..5804cc20e2 100644 --- a/packages/lib/src/styles/forms/inputStylesByState.tsx +++ b/packages/lib/src/styles/forms/inputStylesByState.tsx @@ -1,6 +1,6 @@ import { css } from "@emotion/react"; -export const inputStylesByState = (disabled: boolean, error: boolean, readOnly: boolean) => css` +const inputStylesByState = (disabled: boolean, error: boolean, readOnly: boolean) => css` background-color: ${disabled ? `var(--color-bg-neutral-lighter)` : `transparent`}; border-radius: var(--border-radius-s); border: ${!disabled && error ? "var(--border-width-m)" : "var(--border-width-s)"} var(--border-style-default) @@ -27,3 +27,5 @@ export const inputStylesByState = (disabled: boolean, error: boolean, readOnly: }` : "cursor: not-allowed;"}; `; + +export default inputStylesByState; diff --git a/packages/lib/src/styles/scroll.tsx b/packages/lib/src/styles/scroll.tsx index 5cfd9358c5..87442e74d1 100644 --- a/packages/lib/src/styles/scroll.tsx +++ b/packages/lib/src/styles/scroll.tsx @@ -1,6 +1,6 @@ import { css } from "@emotion/react"; -export const scrollbarStyles = css` +const scrollbarStyles = css` &::-webkit-scrollbar { width: 8px; height: 8px; @@ -14,3 +14,5 @@ export const scrollbarStyles = css` border-radius: var(--border-radius-s); } `; + +export default scrollbarStyles; diff --git a/packages/lib/src/styles/tables/tablesStyles.tsx b/packages/lib/src/styles/tables/tablesStyles.tsx index 5e4cab0ab2..e6d4eaa8ce 100644 --- a/packages/lib/src/styles/tables/tablesStyles.tsx +++ b/packages/lib/src/styles/tables/tablesStyles.tsx @@ -1,6 +1,6 @@ import styled from "@emotion/styled"; import TablePropsType from "../../table/types"; -import { scrollbarStyles } from "../scroll"; +import scrollbarStyles from "../scroll"; import { calculateWidth } from "../../table/utils"; import { spaces } from "../../common/variables"; diff --git a/packages/lib/src/styles/variables.css b/packages/lib/src/styles/variables.css index 1c4661cda9..685c6887e2 100644 --- a/packages/lib/src/styles/variables.css +++ b/packages/lib/src/styles/variables.css @@ -288,9 +288,12 @@ --height-xxl: var(--dimensions-48); --height-xxxl: var(--dimensions-56); --shadow-100: var(--dimensions-0) var(--dimensions-2) var(--dimensions-2) var(--dimensions-0) var(--color-alpha-400-a); - --shadow-200: var(--dimensions-0) var(--dimensions-12) var(--dimensions-12) var(--dimensions-0) var(--color-alpha-300-a); - --shadow-300: var(--dimensions-0) var(--dimensions-24) var(--dimensions-24) var(--dimensions-0) var(--color-alpha-300-a); - --shadow-400: var(--dimensions-0) var(--dimensions-48) var(--dimensions-48) var(--dimensions-0) var(--color-alpha-300-a); + --shadow-200: var(--dimensions-0) var(--dimensions-12) var(--dimensions-12) var(--dimensions-0) + var(--color-alpha-300-a); + --shadow-300: var(--dimensions-0) var(--dimensions-24) var(--dimensions-24) var(--dimensions-0) + var(--color-alpha-300-a); + --shadow-400: var(--dimensions-0) var(--dimensions-48) var(--dimensions-48) var(--dimensions-0) + var(--color-alpha-300-a); --spacing-gap-none: var(--dimensions-0); --spacing-gap-xxs: var(--dimensions-2); --spacing-gap-xs: var(--dimensions-4); diff --git a/packages/lib/src/switch/Switch.accessibility.test.tsx b/packages/lib/src/switch/Switch.accessibility.test.tsx index 49445e1c95..1840669207 100644 --- a/packages/lib/src/switch/Switch.accessibility.test.tsx +++ b/packages/lib/src/switch/Switch.accessibility.test.tsx @@ -1,7 +1,7 @@ import { render } from "@testing-library/react"; import { axe, formatRules } from "../../test/accessibility/axe-helper"; -import { disabledRules as rules } from "../../test/accessibility/rules/specific/switch/disabledRules"; import DxcSwitch from "./Switch"; +import rules from "../../test/accessibility/rules/specific/switch/disabledRules"; const disabledRules = { rules: formatRules(rules), diff --git a/packages/lib/src/switch/Switch.stories.tsx b/packages/lib/src/switch/Switch.stories.tsx index 7eaa4cca8e..575b6585e9 100644 --- a/packages/lib/src/switch/Switch.stories.tsx +++ b/packages/lib/src/switch/Switch.stories.tsx @@ -1,8 +1,8 @@ import { Meta, StoryObj } from "@storybook/react"; import ExampleContainer from "../../.storybook/components/ExampleContainer"; +import disabledRules from "../../test/accessibility/rules/specific/switch/disabledRules"; import Title from "../../.storybook/components/Title"; import preview from "../../.storybook/preview"; -import { disabledRules } from "../../test/accessibility/rules/specific/switch/disabledRules"; import DxcSwitch from "./Switch"; export default { @@ -13,7 +13,7 @@ export default { config: { rules: [ ...disabledRules.map((ruleId) => ({ id: ruleId, enabled: false })), - ...preview?.parameters?.a11y?.config?.rules, + ...(preview?.parameters?.a11y?.config?.rules || []), ], }, }, diff --git a/packages/lib/src/switch/Switch.test.tsx b/packages/lib/src/switch/Switch.test.tsx index 23fbfd0c34..b8fd6aef6a 100644 --- a/packages/lib/src/switch/Switch.test.tsx +++ b/packages/lib/src/switch/Switch.test.tsx @@ -29,8 +29,12 @@ describe("Switch component tests", () => { const { getByText } = render(<DxcSwitch label="SwitchComponent" checked={false} onChange={onChange} />); fireEvent.click(getByText("SwitchComponent")); fireEvent.click(getByText("SwitchComponent")); - expect(onChange.mock.calls[0][0]).toBe(true); - expect(onChange.mock.calls[1][0]).toBe(true); + + const firstCall = onChange.mock.calls[0] as [boolean]; + const secondCall = onChange.mock.calls[1] as [boolean]; + + expect(firstCall[0]).toBe(true); + expect(secondCall[0]).toBe(true); }); test("Every time the user use enter in the component, the onchange function is called with the correct value CONTROLLED COMPONENT", () => { const onChange = jest.fn(); @@ -38,8 +42,12 @@ describe("Switch component tests", () => { fireEvent.focus(getByText("SwitchComponent")); fireEvent.keyDown(getByText("SwitchComponent"), { key: "Enter" }); fireEvent.keyDown(getByText("SwitchComponent"), { key: "Enter" }); - expect(onChange.mock.calls[0][0]).toBe(true); - expect(onChange.mock.calls[1][0]).toBe(true); + + const firstCall = onChange.mock.calls[0] as [boolean]; + const secondCall = onChange.mock.calls[1] as [boolean]; + + expect(firstCall[0]).toBe(true); + expect(secondCall[0]).toBe(true); }); test("Every time the user use space in the component, the onchange function is called with the correct value CONTROLLED COMPONENT", () => { const onChange = jest.fn(); @@ -47,16 +55,24 @@ describe("Switch component tests", () => { fireEvent.focus(getByText("SwitchComponent")); fireEvent.keyDown(getByText("SwitchComponent"), { key: " " }); fireEvent.keyDown(getByText("SwitchComponent"), { key: " " }); - expect(onChange.mock.calls[0][0]).toBe(true); - expect(onChange.mock.calls[1][0]).toBe(true); + + const firstCall = onChange.mock.calls[0] as [boolean]; + const secondCall = onChange.mock.calls[1] as [boolean]; + + expect(firstCall[0]).toBe(true); + expect(secondCall[0]).toBe(true); }); test("Every time the user clicks the component the onchange function is called with the correct value UNCONTROLLED COMPONENT", () => { const onChange = jest.fn(); const { getByText } = render(<DxcSwitch label="SwitchComponent" onChange={onChange} />); fireEvent.click(getByText("SwitchComponent")); fireEvent.click(getByText("SwitchComponent")); - expect(onChange.mock.calls[0][0]).toBe(true); - expect(onChange.mock.calls[1][0]).toBe(false); + + const firstCall = onChange.mock.calls[0] as [boolean]; + const secondCall = onChange.mock.calls[1] as [boolean]; + + expect(firstCall[0]).toBe(true); + expect(secondCall[0]).toBe(false); }); test("Every time the user use enter in the component, the onchange function is called with the correct value UNCONTROLLED COMPONENT", () => { const onChange = jest.fn(); @@ -64,8 +80,12 @@ describe("Switch component tests", () => { fireEvent.focus(getByText("SwitchComponent")); fireEvent.keyDown(getByText("SwitchComponent"), { key: "Enter" }); fireEvent.keyDown(getByText("SwitchComponent"), { key: "Enter" }); - expect(onChange.mock.calls[0][0]).toBe(true); - expect(onChange.mock.calls[1][0]).toBe(false); + + const firstCall = onChange.mock.calls[0] as [boolean]; + const secondCall = onChange.mock.calls[1] as [boolean]; + + expect(firstCall[0]).toBe(true); + expect(secondCall[0]).toBe(false); }); test("Every time the user use space in the component, the onchange function is called with the correct value UNCONTROLLED COMPONENT", () => { const onChange = jest.fn(); @@ -73,8 +93,12 @@ describe("Switch component tests", () => { fireEvent.focus(getByText("SwitchComponent")); fireEvent.keyDown(getByText("SwitchComponent"), { key: " " }); fireEvent.keyDown(getByText("SwitchComponent"), { key: " " }); - expect(onChange.mock.calls[0][0]).toBe(true); - expect(onChange.mock.calls[1][0]).toBe(false); + + const firstCall = onChange.mock.calls[0] as [boolean]; + const secondCall = onChange.mock.calls[1] as [boolean]; + + expect(firstCall[0]).toBe(true); + expect(secondCall[0]).toBe(false); }); test("Renders with correct initial value and initial state when it is uncontrolled", () => { const component = render( diff --git a/packages/lib/src/switch/Switch.tsx b/packages/lib/src/switch/Switch.tsx index 5d3629c016..704db66dcc 100644 --- a/packages/lib/src/switch/Switch.tsx +++ b/packages/lib/src/switch/Switch.tsx @@ -188,4 +188,6 @@ const DxcSwitch = forwardRef<RefType, SwitchPropsType>( } ); +DxcSwitch.displayName = "DxcSwitch"; + export default DxcSwitch; diff --git a/packages/lib/src/table/Table.accessibility.test.tsx b/packages/lib/src/table/Table.accessibility.test.tsx index 9b59f0dcee..5cbceedeeb 100644 --- a/packages/lib/src/table/Table.accessibility.test.tsx +++ b/packages/lib/src/table/Table.accessibility.test.tsx @@ -3,22 +3,17 @@ import { axe, formatRules } from "../../test/accessibility/axe-helper"; import DxcTable from "./Table"; // TODO: REMOVE -import { disabledRules as rules } from "../../test/accessibility/rules/specific/table/disabledRules"; +import rules from "../../test/accessibility/rules/specific/table/disabledRules"; const disabledRules = { rules: formatRules(rules), }; -// Mocking DOMRect for Radix Primitive Popover -(global as any).globalThis = global; -(global as any).DOMRect = { - fromRect: () => ({ top: 0, left: 0, bottom: 0, right: 0, width: 0, height: 0 }), -}; -(global as any).ResizeObserver = class ResizeObserver { - observe() {} - unobserve() {} - disconnect() {} -}; +global.ResizeObserver = jest.fn().mockImplementation(() => ({ + observe: jest.fn(), + unobserve: jest.fn(), + disconnect: jest.fn(), +})); describe("Table component accessibility tests", () => { it("Should not have basic accessibility issues", async () => { diff --git a/packages/lib/src/table/Table.stories.tsx b/packages/lib/src/table/Table.stories.tsx index 8f6afd8356..fc7fdf0181 100644 --- a/packages/lib/src/table/Table.stories.tsx +++ b/packages/lib/src/table/Table.stories.tsx @@ -1,10 +1,10 @@ +import { Meta, StoryObj } from "@storybook/react"; import { userEvent, within } from "@storybook/test"; +import disabledRules from "../../test/accessibility/rules/specific/table/disabledRules"; import ExampleContainer from "../../.storybook/components/ExampleContainer"; import Title from "../../.storybook/components/Title"; import preview from "../../.storybook/preview"; -import { disabledRules } from "../../test/accessibility/rules/specific/table/disabledRules"; import DxcTable from "./Table"; -import { Meta, StoryObj } from "@storybook/react"; export default { title: "Table", @@ -13,8 +13,11 @@ export default { a11y: { config: { rules: [ - ...disabledRules.map((ruleId) => ({ id: ruleId, reviewOnFail: true })), - ...preview?.parameters?.a11y?.config?.rules, + ...disabledRules.map((ruleId) => ({ + id: ruleId, + reviewOnFail: true, + })), + ...(preview?.parameters?.a11y?.config?.rules || []), ], }, }, @@ -101,69 +104,83 @@ const Table = () => ( <ExampleContainer> <Title title="Default" theme="light" level={4} /> <DxcTable> - <tr> - <th>header 1</th> - <th>header 2</th> - <th>actions</th> - </tr> - <tr> - <td>cell 1</td> - <td>cell 2</td> - <td> - <DxcTable.ActionsCell actions={actions} /> - </td> - </tr> - <tr> - <td>cell 4</td> - <td>cell 5</td> - <td> - <DxcTable.ActionsCell actions={actions} /> - </td> - </tr> - <tr> - <td>cell 7</td> - <td>cell 8</td> - <td> - <DxcTable.ActionsCell actions={actions} /> - </td> - </tr> + <thead> + <tr> + <th>header 1</th> + <th>header 2</th> + <th>actions</th> + </tr> + </thead> + <tbody> + <tr> + <td>cell 1</td> + <td>cell 2</td> + <td aria-label="actions"> + <DxcTable.ActionsCell actions={actions} /> + </td> + </tr> + <tr> + <td>cell 4</td> + <td>cell 5</td> + <td aria-label="actions"> + <DxcTable.ActionsCell actions={actions} /> + </td> + </tr> + <tr> + <td>cell 7</td> + <td>cell 8</td> + <td aria-label="actions"> + <DxcTable.ActionsCell actions={actions} /> + </td> + </tr> + </tbody> </DxcTable> </ExampleContainer> <ExampleContainer> <Title title="Custom actionsCell theme" theme="light" level={4} /> <DxcTable> - <tr> - <th>header 1</th> - <th>header 2</th> - <th>actions</th> - </tr> - <tr> - <td>cell 1</td> - <td>cell 2</td> - <td> - <DxcTable.ActionsCell actions={actions} /> - </td> - </tr> - <tr> - <td>cell 4</td> - <td>cell 5</td> - <td> - <DxcTable.ActionsCell actions={actions} /> - </td> - </tr> - <tr> - <td>cell 7</td> - <td>cell 8</td> - <td> - <DxcTable.ActionsCell actions={actions} /> - </td> - </tr> + <thead> + <tr> + <th>header 1</th> + <th>header 2</th> + <th>actions</th> + </tr> + </thead> + <tbody> + <tr> + <td>cell 1</td> + <td>cell 2</td> + <td> + <DxcTable.ActionsCell actions={actions} /> + </td> + </tr> + <tr> + <td>cell 4</td> + <td>cell 5</td> + <td> + <DxcTable.ActionsCell actions={actions} /> + </td> + </tr> + <tr> + <td>cell 7</td> + <td>cell 8</td> + <td> + <DxcTable.ActionsCell actions={actions} /> + </td> + </tr> + </tbody> </DxcTable> </ExampleContainer> <ExampleContainer> <Title title="With scrollbar" theme="light" level={4} /> <div - style={{ height: 200 + "px", display: "flex", flexDirection: "row", width: 100 + "%", marginBottom: 50 + "px" }} + style={{ + height: `${200}px`, + display: "flex", + flexDirection: "row", + width: `${100}%`, + marginBottom: `${50}px`, + }} > <DxcTable> <tr> @@ -267,7 +284,13 @@ const Table = () => ( <ExampleContainer> <Title title="Reduced with scrollbar" theme="light" level={4} /> <div - style={{ height: 200 + "px", display: "flex", flexDirection: "row", width: 100 + "%", marginBottom: 50 + "px" }} + style={{ + height: `${200}px`, + display: "flex", + flexDirection: "row", + width: `${100}%`, + marginBottom: `${50}px`, + }} > <DxcTable mode="reduced"> <tr> @@ -354,21 +377,21 @@ const Table = () => ( <tr> <td>cell 1</td> <td>cell 2</td> - <td> + <td aria-label="actions"> <DxcTable.ActionsCell actions={actions} /> </td> </tr> <tr> <td>cell 4</td> <td>cell 5</td> - <td> + <td aria-label="actions"> <DxcTable.ActionsCell actions={actions} /> </td> </tr> <tr> <td>cell 7</td> <td>cell 8</td> - <td> + <td aria-label="actions"> <DxcTable.ActionsCell actions={actions} /> </td> </tr> @@ -553,21 +576,21 @@ const ActionsCellDropdown = () => ( <tr> <td>cell 1</td> <td>cell 2</td> - <td> + <td aria-label="actions"> <DxcTable.ActionsCell actions={actions} /> </td> </tr> <tr> <td>cell 4</td> <td>cell 5</td> - <td> + <td aria-label="actions"> <DxcTable.ActionsCell actions={actions} /> </td> </tr> <tr> <td>cell 7</td> <td>cell 8</td> - <td> + <td aria-label="actions"> <DxcTable.ActionsCell actions={actions} /> </td> </tr> @@ -586,6 +609,8 @@ export const DropdownAction: Story = { play: async ({ canvasElement }) => { const canvas = within(canvasElement); const nextButton = canvas.getAllByRole("button")[8]; - nextButton && (await userEvent.click(nextButton)); + if (nextButton) { + await userEvent.click(nextButton); + } }, }; diff --git a/packages/lib/src/table/Table.test.tsx b/packages/lib/src/table/Table.test.tsx index b92c9bbb2a..3c0f4fc56f 100644 --- a/packages/lib/src/table/Table.test.tsx +++ b/packages/lib/src/table/Table.test.tsx @@ -2,16 +2,11 @@ import { act, render } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import DxcTable from "./Table"; -// Mocking DOMRect for Radix Primitive Popover -(global as any).globalThis = global; -(global as any).DOMRect = { - fromRect: () => ({ top: 0, left: 0, bottom: 0, right: 0, width: 0, height: 0 }), -}; -(global as any).ResizeObserver = class ResizeObserver { - observe() {} - unobserve() {} - disconnect() {} -}; +global.ResizeObserver = jest.fn().mockImplementation(() => ({ + observe: jest.fn(), + unobserve: jest.fn(), + disconnect: jest.fn(), +})); const icon = ( <svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="currentColor"> @@ -55,7 +50,7 @@ describe("Table component tests", () => { expect(getByText("cell-6")).toBeTruthy(); }); - test("Table ActionsCell", async () => { + test("Table ActionsCell", () => { const onSelectOption = jest.fn(); const onClick = jest.fn(); const actions = [ @@ -78,9 +73,9 @@ describe("Table component tests", () => { ], }, { - icon: icon, + icon, title: "icon2", - onClick: onClick, + onClick, }, ]; const { getAllByRole, getByRole, getByText } = render( @@ -101,7 +96,7 @@ describe("Table component tests", () => { <tr> <td>cell-4</td> <td>cell-5</td> - <td> + <td aria-label="actions"> <DxcTable.ActionsCell actions={actions} /> </td> </tr> @@ -111,14 +106,18 @@ describe("Table component tests", () => { const dropdown = getAllByRole("button")[1]; act(() => { - dropdown && userEvent.click(dropdown); + if (dropdown) { + userEvent.click(dropdown); + } }); expect(getByRole("menu")).toBeTruthy(); const option = getByText("Aliexpress"); userEvent.click(option); expect(onSelectOption).toHaveBeenCalledWith("3"); const action = getAllByRole("button")[0]; - action && userEvent.click(action); + if (action) { + userEvent.click(action); + } expect(onClick).toHaveBeenCalled(); }); }); diff --git a/packages/lib/src/table/Table.tsx b/packages/lib/src/table/Table.tsx index 2e812ceac5..e2bb019158 100644 --- a/packages/lib/src/table/Table.tsx +++ b/packages/lib/src/table/Table.tsx @@ -1,9 +1,72 @@ import styled from "@emotion/styled"; +import { spaces } from "../common/variables"; +import { getMargin } from "../common/utils"; import DxcDropdown from "../dropdown/Dropdown"; import DxcActionIcon from "../action-icon/ActionIcon"; import TablePropsType, { ActionsCellPropsType } from "./types"; +import scrollbarStyles from "../styles/scroll"; import { useMemo } from "react"; -import { Table, TableContainer } from "../styles/tables/tablesStyles"; + +const calculateWidth = (margin: TablePropsType["margin"]) => + `calc(100% - ${getMargin(margin, "left")} - ${getMargin(margin, "right")})`; + +const TableContainer = styled.div<{ margin: TablePropsType["margin"] }>` + width: ${({ margin }) => calculateWidth(margin)}; + margin: ${({ margin }) => (margin && typeof margin !== "object" ? spaces[margin] : "0px")}; + margin-top: ${({ margin }) => (margin && typeof margin === "object" && margin.top ? spaces[margin.top] : "")}; + margin-right: ${({ margin }) => (margin && typeof margin === "object" && margin.right ? spaces[margin.right] : "")}; + margin-bottom: ${({ margin }) => + margin && typeof margin === "object" && margin.bottom ? spaces[margin.bottom] : ""}; + margin-left: ${({ margin }) => (margin && typeof margin === "object" && margin.left ? spaces[margin.left] : "")}; + overflow: auto; + ${scrollbarStyles} +`; + +const Table = styled.table<{ mode: TablePropsType["mode"] }>` + border-collapse: collapse; + width: 100%; + + & tr { + border-bottom: var(--border-width-s) solid var(--border-color-neutral-lighter); + height: ${({ mode }) => (mode === "default" ? "var(--height-xxl)" : "var(--height-l)")}; + } + & td { + background-color: var(--color-fg-neutral-bright); + color: var(--color-fg-neutral-dark); + font-family: var(--typography-font-family); + font-size: var(--typography-label-m); + font-style: normal; + font-weight: var(--typography-label-regular); + line-height: normal; + padding: var(--spacing-padding-s) var(--spacing-padding-m); + text-align: start; + } + & th { + background-color: var(--color-fg-primary-strong); + color: var(--color-fg-neutral-bright); + font-family: var(--typography-font-family); + font-size: var(--typography-label-m); + font-style: normal; + font-weight: var(--typography-label-regular); + line-height: normal; + padding: var(--spacing-padding-s) var(--spacing-padding-m); + text-align: start; + } + & th:first-child { + border-top-left-radius: var(--border-radius-s); + padding-left: var(--spacing-padding-ml); + } + & th:last-child { + border-top-right-radius: var(--border-radius-s); + padding-right: var(--spacing-padding-ml); + } + & td:first-child { + padding-left: var(--spacing-padding-ml); + } + & td:last-child { + padding-right: var(--spacing-padding-ml); + } +`; const ActionsContainer = styled.div` display: flex; diff --git a/packages/lib/src/tabs/Tab.tsx b/packages/lib/src/tabs/Tab.tsx index 54973d282e..2f97d91919 100644 --- a/packages/lib/src/tabs/Tab.tsx +++ b/packages/lib/src/tabs/Tab.tsx @@ -177,4 +177,6 @@ const DxcTab = forwardRef( } ); +DxcTab.displayName = "DxcTab"; + export default DxcTab; diff --git a/packages/lib/src/tabs/Tabs.accessibility.test.tsx b/packages/lib/src/tabs/Tabs.accessibility.test.tsx index 27a8f562ed..6ed057616c 100644 --- a/packages/lib/src/tabs/Tabs.accessibility.test.tsx +++ b/packages/lib/src/tabs/Tabs.accessibility.test.tsx @@ -2,11 +2,11 @@ import { render } from "@testing-library/react"; import { axe } from "../../test/accessibility/axe-helper"; import DxcTabs from "./Tabs"; -(global as any).ResizeObserver = class ResizeObserver { - observe() {} - unobserve() {} - disconnect() {} -}; +global.ResizeObserver = jest.fn().mockImplementation(() => ({ + observe: jest.fn(), + unobserve: jest.fn(), + disconnect: jest.fn(), +})); const iconSVG = ( <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" height="20" width="20" fill="currentColor"> diff --git a/packages/lib/src/tabs/Tabs.stories.tsx b/packages/lib/src/tabs/Tabs.stories.tsx index 7ede59472d..44d44861af 100644 --- a/packages/lib/src/tabs/Tabs.stories.tsx +++ b/packages/lib/src/tabs/Tabs.stories.tsx @@ -1,10 +1,10 @@ +import { Meta, StoryObj } from "@storybook/react"; import { INITIAL_VIEWPORTS } from "@storybook/addon-viewport"; +import { userEvent, within } from "@storybook/test"; import ExampleContainer from "../../.storybook/components/ExampleContainer"; import Title from "../../.storybook/components/Title"; import DxcTabs from "./Tabs"; import type { Margin, Space } from "../common/utils"; -import { Meta, StoryObj } from "@storybook/react/*"; -import { userEvent, within } from "@storybook/test"; export default { title: "Tabs", diff --git a/packages/lib/src/tabs/Tabs.test.tsx b/packages/lib/src/tabs/Tabs.test.tsx index 6a2e59658b..66f4f4815c 100644 --- a/packages/lib/src/tabs/Tabs.test.tsx +++ b/packages/lib/src/tabs/Tabs.test.tsx @@ -2,11 +2,11 @@ import "@testing-library/jest-dom"; import { fireEvent, render } from "@testing-library/react"; import DxcTabs from "./Tabs"; -(global as any).ResizeObserver = class ResizeObserver { - observe() {} - unobserve() {} - disconnect() {} -}; +global.ResizeObserver = jest.fn().mockImplementation(() => ({ + observe: jest.fn(), + unobserve: jest.fn(), + disconnect: jest.fn(), +})); const sampleTabs = ( <DxcTabs> @@ -154,17 +154,23 @@ describe("Tabs component tests", () => { const onTabClick = [jest.fn(), jest.fn(), jest.fn()]; const { getAllByRole } = render(sampleTabsInteraction(onTabClick)); const tabs = getAllByRole("tab"); - tabs[0] && fireEvent.click(tabs[0]); + if (tabs[0]) { + fireEvent.click(tabs[0]); + } expect(onTabClick[0]).toHaveBeenCalled(); expect(tabs[0]?.getAttribute("aria-selected")).toBe("true"); expect(tabs[1]?.getAttribute("aria-selected")).toBe("false"); expect(tabs[2]?.getAttribute("aria-selected")).toBe("false"); - tabs[1] && fireEvent.click(tabs[1]); + if (tabs[1]) { + fireEvent.click(tabs[1]); + } expect(onTabClick[1]).toHaveBeenCalled(); expect(tabs[0]?.getAttribute("aria-selected")).toBe("false"); expect(tabs[1]?.getAttribute("aria-selected")).toBe("true"); expect(tabs[2]?.getAttribute("aria-selected")).toBe("false"); - tabs[2] && fireEvent.click(tabs[2]); + if (tabs[2]) { + fireEvent.click(tabs[2]); + } expect(onTabClick[2]).toHaveBeenCalled(); expect(tabs[0]?.getAttribute("aria-selected")).toBe("false"); expect(tabs[1]?.getAttribute("aria-selected")).toBe("false"); @@ -196,42 +202,54 @@ describe("Tabs component tests", () => { expect(onTabClick[0]).toHaveBeenCalled(); fireEvent.keyDown(tabList, { key: "ArrowRight" }); expect(tabs[1]).toHaveFocus(); - tabs[1] && fireEvent.keyDown(tabs[1], { key: "Enter" }); + if (tabs[1]) { + fireEvent.keyDown(tabs[1], { key: "Enter" }); + } expect(tabs[0]?.getAttribute("aria-selected")).toBe("false"); expect(tabs[1]?.getAttribute("aria-selected")).toBe("true"); expect(tabs[2]?.getAttribute("aria-selected")).toBe("false"); expect(onTabClick[1]).toHaveBeenCalled(); fireEvent.keyDown(tabList, { key: "ArrowRight" }); expect(tabs[2]).toHaveFocus(); - tabs[2] && fireEvent.keyDown(tabs[2], { key: "Enter" }); + if (tabs[2]) { + fireEvent.keyDown(tabs[2], { key: "Enter" }); + } expect(tabs[0]?.getAttribute("aria-selected")).toBe("false"); expect(tabs[1]?.getAttribute("aria-selected")).toBe("false"); expect(tabs[2]?.getAttribute("aria-selected")).toBe("true"); expect(onTabClick[2]).toHaveBeenCalled(); fireEvent.keyDown(tabList, { key: "ArrowLeft" }); expect(tabs[1]).toHaveFocus(); - tabs[1] && fireEvent.keyDown(tabs[1], { key: "Enter" }); + if (tabs[1]) { + fireEvent.keyDown(tabs[1], { key: "Enter" }); + } expect(tabs[0]?.getAttribute("aria-selected")).toBe("false"); expect(tabs[1]?.getAttribute("aria-selected")).toBe("true"); expect(tabs[2]?.getAttribute("aria-selected")).toBe("false"); expect(onTabClick[1]).toHaveBeenCalled(); fireEvent.keyDown(tabList, { key: "ArrowLeft" }); expect(tabs[0]).toHaveFocus(); - tabs[0] && fireEvent.keyDown(tabs[0], { key: "Enter" }); + if (tabs[0]) { + fireEvent.keyDown(tabs[0], { key: "Enter" }); + } expect(tabs[0]?.getAttribute("aria-selected")).toBe("true"); expect(tabs[1]?.getAttribute("aria-selected")).toBe("false"); expect(tabs[2]?.getAttribute("aria-selected")).toBe("false"); expect(onTabClick[0]).toHaveBeenCalled(); fireEvent.keyDown(tabList, { key: "ArrowLeft" }); expect(tabs[2]).toHaveFocus(); - tabs[2] && fireEvent.keyDown(tabs[2], { key: "Enter" }); + if (tabs[2]) { + fireEvent.keyDown(tabs[2], { key: "Enter" }); + } expect(tabs[0]?.getAttribute("aria-selected")).toBe("false"); expect(tabs[1]?.getAttribute("aria-selected")).toBe("false"); expect(tabs[2]?.getAttribute("aria-selected")).toBe("true"); expect(onTabClick[2]).toHaveBeenCalled(); fireEvent.keyDown(tabList, { key: "ArrowRight" }); expect(tabs[0]).toHaveFocus(); - tabs[0] && fireEvent.keyDown(tabs[0], { key: "Enter" }); + if (tabs[0]) { + fireEvent.keyDown(tabs[0], { key: "Enter" }); + } expect(tabs[0]?.getAttribute("aria-selected")).toBe("true"); expect(tabs[1]?.getAttribute("aria-selected")).toBe("false"); expect(tabs[2]?.getAttribute("aria-selected")).toBe("false"); @@ -251,7 +269,9 @@ describe("Tabs component tests", () => { expect(onTabClick[0]).toHaveBeenCalled(); fireEvent.keyDown(tabList, { key: "ArrowRight" }); expect(tabs[2]).toHaveFocus(); - tabs[2] && fireEvent.keyDown(tabs[2], { key: " " }); + if (tabs[2]) { + fireEvent.keyDown(tabs[2], { key: " " }); + } expect(tabs[0]?.getAttribute("aria-selected")).toBe("false"); expect(tabs[1]?.getAttribute("aria-selected")).toBe("false"); expect(tabs[2]?.getAttribute("aria-selected")).toBe("true"); @@ -262,17 +282,23 @@ describe("Tabs component tests", () => { const onTabClick = [jest.fn(), jest.fn(), jest.fn()]; const { getAllByRole } = render(sampleControlledTabsInteraction(onTabClick)); const tabs = getAllByRole("tab"); - tabs[0] && fireEvent.click(tabs[0]); + if (tabs[0]) { + fireEvent.click(tabs[0]); + } expect(onTabClick[0]).toHaveBeenCalled(); expect(tabs[0]?.getAttribute("aria-selected")).toBe("true"); expect(tabs[1]?.getAttribute("aria-selected")).toBe("false"); expect(tabs[2]?.getAttribute("aria-selected")).toBe("false"); - tabs[1] && fireEvent.click(tabs[1]); + if (tabs[1]) { + fireEvent.click(tabs[1]); + } expect(onTabClick[1]).toHaveBeenCalled(); expect(tabs[0]?.getAttribute("aria-selected")).toBe("true"); expect(tabs[1]?.getAttribute("aria-selected")).toBe("false"); expect(tabs[2]?.getAttribute("aria-selected")).toBe("false"); - tabs[2] && fireEvent.click(tabs[2]); + if (tabs[2]) { + fireEvent.click(tabs[2]); + } expect(onTabClick[2]).toHaveBeenCalled(); expect(tabs[0]?.getAttribute("aria-selected")).toBe("true"); expect(tabs[1]?.getAttribute("aria-selected")).toBe("false"); @@ -283,17 +309,23 @@ describe("Tabs component tests", () => { const onTabClick = [jest.fn(), jest.fn(), jest.fn()]; const { getAllByRole } = render(sampleTabsWithoutLabel(onTabClick)); const tabs = getAllByRole("tab"); - tabs[0] && fireEvent.click(tabs[0]); + if (tabs[0]) { + fireEvent.click(tabs[0]); + } expect(onTabClick[0]).toHaveBeenCalled(); expect(tabs[0]?.getAttribute("aria-selected")).toBe("true"); expect(tabs[1]?.getAttribute("aria-selected")).toBe("false"); expect(tabs[2]?.getAttribute("aria-selected")).toBe("false"); - tabs[1] && fireEvent.click(tabs[1]); + if (tabs[1]) { + fireEvent.click(tabs[1]); + } expect(onTabClick[1]).toHaveBeenCalled(); expect(tabs[0]?.getAttribute("aria-selected")).toBe("false"); expect(tabs[1]?.getAttribute("aria-selected")).toBe("true"); expect(tabs[2]?.getAttribute("aria-selected")).toBe("false"); - tabs[2] && fireEvent.click(tabs[2]); + if (tabs[2]) { + fireEvent.click(tabs[2]); + } expect(onTabClick[2]).toHaveBeenCalled(); expect(tabs[0]?.getAttribute("aria-selected")).toBe("false"); expect(tabs[1]?.getAttribute("aria-selected")).toBe("false"); diff --git a/packages/lib/src/tabs/Tabs.tsx b/packages/lib/src/tabs/Tabs.tsx index 06b6831a9a..2d028d83d6 100644 --- a/packages/lib/src/tabs/Tabs.tsx +++ b/packages/lib/src/tabs/Tabs.tsx @@ -3,9 +3,9 @@ import { isValidElement, KeyboardEvent, ReactElement, + ReactNode, useContext, useEffect, - useLayoutEffect, useMemo, useRef, useState, @@ -89,21 +89,14 @@ const ScrollableTabsList = styled.div<{ `; const DxcTabs = ({ children, iconPosition = "left", margin, tabIndex = 0 }: TabsPropsType) => { - const childrenArray: ReactElement<TabProps>[] = useMemo( - () => Children.toArray(children) as ReactElement<TabProps>[], - [children] - ); - const [activeTabId, setActiveTabId] = useState(() => { - const hasActiveChild = childrenArray.some( - (child) => isValidElement(child) && (child.props.active || child.props.defaultActive) && !child.props.disabled + const isTabElement = (child: ReactNode): child is ReactElement<TabProps> => isValidElement<TabProps>(child); + const childrenArray = useMemo(() => Children.toArray(children).filter(isTabElement), [children]); + const [activeTabId, setActiveTabId] = useState<string>(() => { + const activeChild = childrenArray.find( + (child) => (child.props.active || child.props.defaultActive) && !child.props.disabled ); - const initialActiveTab = hasActiveChild - ? childrenArray.find( - (child) => isValidElement(child) && (child.props.active || child.props.defaultActive) && !child.props.disabled - ) - : childrenArray.find((child) => isValidElement(child) && !child.props.disabled); - - return isValidElement(initialActiveTab) ? (initialActiveTab.props.label ?? initialActiveTab.props.tabId) : ""; + const initialTab = activeChild ?? childrenArray.find((child) => !child.props.disabled); + return initialTab?.props.label ?? initialTab?.props.tabId ?? ""; }); const [innerFocusIndex, setInnerFocusIndex] = useState<number | null>(null); const [scrollLeftEnabled, setScrollLeftEnabled] = useState(false); @@ -119,7 +112,7 @@ const DxcTabs = ({ children, iconPosition = "left", margin, tabIndex = 0 }: Tabs activeTabId: activeTabId, focusedTabId: isValidElement(focusedChild) ? (focusedChild.props.label ?? focusedChild.props.tabId) : "", iconPosition, - isControlled: childrenArray.some((child) => isValidElement(child) && typeof child.props.active !== "undefined"), + isControlled: childrenArray.some((child) => typeof child.props.active !== "undefined"), setActiveTabId: setActiveTabId, tabIndex, }; @@ -153,9 +146,7 @@ const DxcTabs = ({ children, iconPosition = "left", margin, tabIndex = 0 }: Tabs }; const handleOnKeyDown = (event: KeyboardEvent<HTMLDivElement>) => { - const activeTab = childrenArray.findIndex( - (child: ReactElement) => (child.props.label ?? child.props.tabId) === activeTabId - ); + const activeTab = childrenArray.findIndex((child) => (child.props.label ?? child.props.tabId) === activeTabId); let index; switch (event.key) { case "Left": @@ -238,7 +229,7 @@ const DxcTabs = ({ children, iconPosition = "left", margin, tabIndex = 0 }: Tabs </Tabs> </TabsContainer> {Children.map(children, (child) => - isValidElement(child) && child.props.tabId === activeTabId ? child.props.children : null + isTabElement(child) && child.props.tabId === activeTabId ? child.props.children : null )} </> ); diff --git a/packages/lib/src/text-input/Suggestion.tsx b/packages/lib/src/text-input/Suggestion.tsx index f680ebbfc3..20cdc03761 100644 --- a/packages/lib/src/text-input/Suggestion.tsx +++ b/packages/lib/src/text-input/Suggestion.tsx @@ -36,7 +36,10 @@ const StyledSuggestion = styled.span` const Suggestion = ({ highlighted, id, isLast, onClick, suggestion, value, visuallyFocused }: SuggestionProps) => { const matchedSuggestion = useMemo(() => { const regEx = new RegExp(transformSpecialChars(value), "i"); - return { matchedWords: suggestion.match(regEx), noMatchedWords: suggestion.replace(regEx, "") }; + return { + matchedWords: suggestion.match(regEx), + noMatchedWords: suggestion.replace(regEx, ""), + }; }, [value, suggestion]); return ( diff --git a/packages/lib/src/text-input/Suggestions.tsx b/packages/lib/src/text-input/Suggestions.tsx index cd6563c32a..7757d106b0 100644 --- a/packages/lib/src/text-input/Suggestions.tsx +++ b/packages/lib/src/text-input/Suggestions.tsx @@ -4,7 +4,7 @@ import { HalstackLanguageContext } from "../HalstackContext"; import Suggestion from "./Suggestion"; import { SuggestionsProps } from "./types"; import DxcIcon from "../icon/Icon"; -import { scrollbarStyles } from "../styles/scroll"; +import scrollbarStyles from "../styles/scroll"; const SuggestionsContainer = styled.div` box-sizing: border-box; diff --git a/packages/lib/src/text-input/TextInput.accessibility.test.tsx b/packages/lib/src/text-input/TextInput.accessibility.test.tsx index 4045e1b50f..d7d3a68a98 100644 --- a/packages/lib/src/text-input/TextInput.accessibility.test.tsx +++ b/packages/lib/src/text-input/TextInput.accessibility.test.tsx @@ -1,6 +1,7 @@ import { render } from "@testing-library/react"; import { axe } from "../../test/accessibility/axe-helper"; import DxcTextInput from "./TextInput"; +import MockDOMRect from "../../test/mocks/domRectMock"; const countries = [ "Afghanistan", @@ -39,15 +40,12 @@ const action = { }; // Mocking DOMRect for Radix Primitive Popover -(global as any).globalThis = global; -(global as any).DOMRect = { - fromRect: () => ({ top: 0, left: 0, bottom: 0, right: 0, width: 0, height: 0 }), -}; -(global as any).ResizeObserver = class ResizeObserver { - observe() {} - unobserve() {} - disconnect() {} -}; +global.DOMRect = MockDOMRect; +global.ResizeObserver = jest.fn().mockImplementation(() => ({ + observe: jest.fn(), + unobserve: jest.fn(), + disconnect: jest.fn(), +})); describe("TextInput component accessibility tests", () => { it("Should not have basic accessibility issues", async () => { diff --git a/packages/lib/src/text-input/TextInput.stories.tsx b/packages/lib/src/text-input/TextInput.stories.tsx index 0f09c5f9c0..88c61e4d0c 100644 --- a/packages/lib/src/text-input/TextInput.stories.tsx +++ b/packages/lib/src/text-input/TextInput.stories.tsx @@ -1,10 +1,11 @@ +import { Meta, StoryObj } from "@storybook/react"; import { userEvent, within } from "@storybook/test"; import ExampleContainer from "../../.storybook/components/ExampleContainer"; import Title from "../../.storybook/components/Title"; import DxcFlex from "../flex/Flex"; import Suggestions from "./Suggestions"; import DxcTextInput from "./TextInput"; -import { Meta, StoryObj } from "@storybook/react"; + export default { title: "Text Input", component: DxcTextInput, @@ -262,7 +263,9 @@ const AutosuggestListbox = () => ( placeholder="Choose an option" size="fillParent" /> - <button style={{ zIndex: "1", width: "100px" }}>Submit</button> + <button type="submit" style={{ zIndex: "1", width: "100px" }}> + Submit + </button> </div> </ExampleContainer> <Title title="Listbox suggestion states" theme="light" level={3} /> diff --git a/packages/lib/src/text-input/TextInput.test.tsx b/packages/lib/src/text-input/TextInput.test.tsx index 7b8a6d2100..9f8dc8140b 100644 --- a/packages/lib/src/text-input/TextInput.test.tsx +++ b/packages/lib/src/text-input/TextInput.test.tsx @@ -1,17 +1,15 @@ import { act, fireEvent, render, waitForElementToBeRemoved } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import DxcTextInput from "./TextInput"; +import MockDOMRect from "../../test/mocks/domRectMock"; // Mocking DOMRect for Radix Primitive Popover -(global as any).globalThis = global; -(global as any).DOMRect = { - fromRect: () => ({ top: 0, left: 0, bottom: 0, right: 0, width: 0, height: 0 }), -}; -(global as any).ResizeObserver = class ResizeObserver { - observe() {} - unobserve() {} - disconnect() {} -}; +global.DOMRect = MockDOMRect; +global.ResizeObserver = jest.fn().mockImplementation(() => ({ + observe: jest.fn(), + unobserve: jest.fn(), + disconnect: jest.fn(), +})); const countries = [ "Afghanistan", @@ -79,7 +77,10 @@ describe("TextInput component tests", () => { fireEvent.focus(input); fireEvent.blur(input); expect(onBlur).toHaveBeenCalled(); - expect(onBlur).toHaveBeenCalledWith({ value: "", error: "This field is required. Please, enter a value." }); + expect(onBlur).toHaveBeenCalledWith({ + value: "", + error: "This field is required. Please, enter a value.", + }); fireEvent.change(input, { target: { value: "Test" } }); fireEvent.blur(input); expect(onBlur).toHaveBeenCalled(); @@ -97,7 +98,10 @@ describe("TextInput component tests", () => { expect(onChange).toHaveBeenCalledWith({ value: "Test" }); userEvent.clear(input); expect(onChange).toHaveBeenCalled(); - expect(onChange).toHaveBeenCalledWith({ value: "", error: "This field is required. Please, enter a value." }); + expect(onChange).toHaveBeenCalledWith({ + value: "", + error: "This field is required. Please, enter a value.", + }); }); test("Pattern constraint", () => { @@ -117,10 +121,16 @@ describe("TextInput component tests", () => { const input = getByRole("textbox"); fireEvent.change(input, { target: { value: "pattern test" } }); expect(onChange).toHaveBeenCalled(); - expect(onChange).toHaveBeenCalledWith({ value: "pattern test", error: "Please match the format requested." }); + expect(onChange).toHaveBeenCalledWith({ + value: "pattern test", + error: "Please match the format requested.", + }); fireEvent.blur(input); expect(onBlur).toHaveBeenCalled(); - expect(onBlur).toHaveBeenCalledWith({ value: "pattern test", error: "Please match the format requested." }); + expect(onBlur).toHaveBeenCalledWith({ + value: "pattern test", + error: "Please match the format requested.", + }); userEvent.clear(input); fireEvent.change(input, { target: { value: "pattern4&" } }); expect(onChange).toHaveBeenCalled(); @@ -148,10 +158,16 @@ describe("TextInput component tests", () => { const input = getByRole("textbox"); fireEvent.change(input, { target: { value: "test" } }); expect(onChange).toHaveBeenCalled(); - expect(onChange).toHaveBeenCalledWith({ value: "test", error: "Min length 5, max length 10." }); + expect(onChange).toHaveBeenCalledWith({ + value: "test", + error: "Min length 5, max length 10.", + }); fireEvent.blur(input); expect(onBlur).toHaveBeenCalled(); - expect(onBlur).toHaveBeenCalledWith({ value: "test", error: "Min length 5, max length 10." }); + expect(onBlur).toHaveBeenCalledWith({ + value: "test", + error: "Min length 5, max length 10.", + }); userEvent.clear(input); fireEvent.change(input, { target: { value: "length" } }); expect(onChange).toHaveBeenCalled(); @@ -180,16 +196,28 @@ describe("TextInput component tests", () => { const input = getByRole("textbox"); fireEvent.change(input, { target: { value: "test" } }); expect(onChange).toHaveBeenCalled(); - expect(onChange).toHaveBeenCalledWith({ value: "test", error: "Min length 5, max length 10." }); + expect(onChange).toHaveBeenCalledWith({ + value: "test", + error: "Min length 5, max length 10.", + }); fireEvent.blur(input); expect(onBlur).toHaveBeenCalled(); - expect(onBlur).toHaveBeenCalledWith({ value: "test", error: "Min length 5, max length 10." }); + expect(onBlur).toHaveBeenCalledWith({ + value: "test", + error: "Min length 5, max length 10.", + }); fireEvent.change(input, { target: { value: "tests" } }); expect(onChange).toHaveBeenCalled(); - expect(onChange).toHaveBeenCalledWith({ value: "tests", error: "Please match the format requested." }); + expect(onChange).toHaveBeenCalledWith({ + value: "tests", + error: "Please match the format requested.", + }); fireEvent.blur(input); expect(onBlur).toHaveBeenCalled(); - expect(onBlur).toHaveBeenCalledWith({ value: "tests", error: "Please match the format requested." }); + expect(onBlur).toHaveBeenCalledWith({ + value: "tests", + error: "Please match the format requested.", + }); fireEvent.change(input, { target: { value: "tests4&" } }); expect(onChange).toHaveBeenCalled(); expect(onChange).toHaveBeenCalledWith({ value: "tests4&" }); @@ -219,12 +247,12 @@ describe("TextInput component tests", () => { expect(onBlur).toHaveBeenCalledWith({ value: "Blur test" }); }); - test("Clear action onClick cleans the input", async () => { + test("Clear action onClick cleans the input", () => { const { getByRole } = render(<DxcTextInput label="Input label" clearable />); const input = getByRole("textbox") as HTMLInputElement; userEvent.type(input, "Test"); const closeAction = getByRole("button"); - await userEvent.click(closeAction); + userEvent.click(closeAction); expect(input.value).toBe(""); }); @@ -236,10 +264,10 @@ describe("TextInput component tests", () => { expect(onChange).not.toHaveBeenCalled(); }); - test("Disabled text input (action must be shown but not clickable)", async () => { + test("Disabled text input (action must be shown but not clickable)", () => { const onClick = jest.fn(); const action = { - onClick: onClick, + onClick, icon: ( <svg data-testid="image" @@ -257,7 +285,7 @@ describe("TextInput component tests", () => { const { getByRole } = render(<DxcTextInput label="Disabled input label" action={action} disabled />); const input = getByRole("textbox") as HTMLInputElement; expect(input.disabled).toBeTruthy(); - await userEvent.click(getByRole("button")); + userEvent.click(getByRole("button")); expect(onClick).not.toHaveBeenCalled(); }); @@ -297,10 +325,10 @@ describe("TextInput component tests", () => { expect(onChange).not.toHaveBeenCalled(); }); - test("Read-only text input sends its value on submit", async () => { - const handlerOnSubmit = jest.fn((e) => { + test("Read-only text input sends its value on submit", () => { + const handlerOnSubmit = jest.fn((e: React.FormEvent<HTMLFormElement>) => { e.preventDefault(); - const formData = new FormData(e.target); + const formData = new FormData(e.currentTarget); const formProps = Object.fromEntries(formData); expect(formProps).toStrictEqual({ data: "Text" }); }); @@ -311,14 +339,14 @@ describe("TextInput component tests", () => { </form> ); const submit = getByText("Submit"); - await userEvent.click(submit); + userEvent.click(submit); expect(handlerOnSubmit).toHaveBeenCalled(); }); - test("Read-only text input doesn't trigger custom action's onClick event", async () => { + test("Read-only text input doesn't trigger custom action's onClick event", () => { const onClick = jest.fn(); const action = { - onClick: onClick, + onClick, icon: ( <svg data-testid="image" @@ -335,14 +363,14 @@ describe("TextInput component tests", () => { title: "Search", }; const { getByRole } = render(<DxcTextInput label="Input label" action={action} readOnly />); - await userEvent.click(getByRole("button")); + userEvent.click(getByRole("button")); expect(onClick).not.toHaveBeenCalled(); }); - test("Action prop: image displayed and onClick event", async () => { + test("Action prop: image displayed and onClick event", () => { const onClick = jest.fn(); const action = { - onClick: onClick, + onClick, icon: ( <svg data-testid="image" @@ -360,14 +388,14 @@ describe("TextInput component tests", () => { }; const { getByRole, getByTestId } = render(<DxcTextInput label="Input label" action={action} />); expect(getByTestId("image")).toBeTruthy(); - await userEvent.click(getByRole("button")); + userEvent.click(getByRole("button")); expect(onClick).toHaveBeenCalled(); }); - test("Text input submit correctly value in form", async () => { + test("Text input submit correctly value in form", () => { const onClick = jest.fn(); const action = { - onClick: onClick, + onClick, icon: ( <svg data-testid="image" @@ -383,9 +411,9 @@ describe("TextInput component tests", () => { ), title: "Search", }; - const handlerOnSubmit = jest.fn((e) => { + const handlerOnSubmit = jest.fn((e: React.FormEvent<HTMLFormElement>) => { e.preventDefault(); - const formData = new FormData(e.target); + const formData = new FormData(e.currentTarget); const formProps = Object.fromEntries(formData); expect(formProps).toStrictEqual({ data: "test" }); }); @@ -400,9 +428,11 @@ describe("TextInput component tests", () => { const input = getByRole("textbox") as HTMLInputElement; userEvent.type(input, "test"); expect(input.value).toBe("test"); - search && (await userEvent.click(search)); + if (search) { + userEvent.click(search); + } expect(handlerOnSubmit).not.toHaveBeenCalled(); - await userEvent.click(submit); + userEvent.click(submit); expect(handlerOnSubmit).toHaveBeenCalled(); }); @@ -415,7 +445,7 @@ describe("TextInput component tests", () => { test("Text Input has correct aria accessibility attributes", () => { const onClick = jest.fn(); const action = { - onClick: onClick, + onClick, icon: ( <svg data-testid="image" @@ -443,9 +473,9 @@ describe("TextInput component tests", () => { expect(input.getAttribute("aria-required")).toBe("true"); userEvent.type(input, "Text"); const clear = getAllByRole("button")[0]; - clear && expect(clear.getAttribute("aria-label")).toBe("Clear field"); + expect(clear?.getAttribute("aria-label")).toBe("Clear field"); const search = getAllByRole("button")[1]; - search && expect(search.getAttribute("aria-label")).toBe("Search"); + expect(search?.getAttribute("aria-label")).toBe("Search"); }); test("Autosuggest has correct accessibility attributes", () => { @@ -462,7 +492,7 @@ describe("TextInput component tests", () => { expect(input.getAttribute("aria-controls")).toBe(list.id); expect(input.getAttribute("aria-expanded")).toBe("true"); const options = getAllByRole("option"); - options[0] && expect(options[0].getAttribute("aria-selected")).toBeNull(); + expect(options[0]?.getAttribute("aria-selected")).toBeNull(); }); test("Mouse wheel interaction does not affect the text value", () => { @@ -493,13 +523,13 @@ describe("TextInput component synchronous autosuggest tests", () => { expect(getByText("Andorra")).toBeTruthy(); }); - test("Autosuggest is displayed when the user clicks the input", async () => { + test("Autosuggest is displayed when the user clicks the input", () => { const onChange = jest.fn(); const { getByRole, getByText } = render( <DxcTextInput label="Autocomplete Countries" suggestions={countries} onChange={onChange} /> ); const input = getByRole("combobox"); - await userEvent.click(input); + userEvent.click(input); const list = getByRole("listbox"); expect(list).toBeTruthy(); expect(getByText("Afghanistan")).toBeTruthy(); @@ -508,19 +538,19 @@ describe("TextInput component synchronous autosuggest tests", () => { expect(getByText("Andorra")).toBeTruthy(); }); - test("Autosuggest is displayed while the user is writing (if closed previously, if it is open stays open)", async () => { + test("Autosuggest is displayed while the user is writing (if closed previously, if it is open stays open)", () => { const { getByRole, getByText, getAllByText } = render( <DxcTextInput label="Autocomplete Countries" suggestions={countries} /> ); const input = getByRole("combobox"); - await userEvent.type(input, "Bah"); + userEvent.type(input, "Bah"); expect(getByRole("listbox")).toBeTruthy(); expect(getAllByText("Bah").length).toBe(2); expect(getByText("amas")).toBeTruthy(); expect(getByText("rain")).toBeTruthy(); }); - test("Read-only text input does not open the suggestions list", async () => { + test("Read-only text input does not open the suggestions list", () => { const onChange = jest.fn(); const { getByRole, queryByRole } = render( <DxcTextInput label="Autocomplete Countries" suggestions={countries} onChange={onChange} readOnly /> @@ -528,7 +558,7 @@ describe("TextInput component synchronous autosuggest tests", () => { const input = getByRole("combobox"); fireEvent.focus(input); expect(queryByRole("listbox")).toBeFalsy(); - await userEvent.click(input); + userEvent.click(input); expect(queryByRole("listbox")).toBeFalsy(); }); @@ -558,76 +588,88 @@ describe("TextInput component synchronous autosuggest tests", () => { <DxcTextInput label="Autocomplete Countries" suggestions={[]} onChange={onChange} /> ); const input = queryByRole("textbox"); - input && fireEvent.focus(input); + if (input) { + fireEvent.focus(input); + } expect(queryByRole("listbox")).toBeFalsy(); }); - test("Autosuggest closes the listbox when there are no matches for the user's input", async () => { + test("Autosuggest closes the listbox when there are no matches for the user's input", () => { const onChange = jest.fn(); const { getByRole, queryByRole } = render( <DxcTextInput label="Autocomplete Countries" suggestions={countries} onChange={onChange} /> ); const input = getByRole("combobox"); - await act(async () => { + act(() => { userEvent.type(input, "x"); }); expect(queryByRole("listbox")).toBeFalsy(); }); - test("Autosuggest with no matches founded doesn't let the listbox to be opened", async () => { + test("Autosuggest with no matches founded doesn't let the listbox to be opened", () => { const onChange = jest.fn(); const { getByRole, queryByRole } = render( <DxcTextInput label="Autocomplete Countries" suggestions={countries} onChange={onChange} /> ); const input = getByRole("combobox"); - await act(async () => { + act(() => { userEvent.type(input, "x"); }); expect(queryByRole("listbox")).toBeFalsy(); fireEvent.focus(input); expect(queryByRole("listbox")).toBeFalsy(); - fireEvent.keyDown(input, { key: "ArrowUp", code: "ArrowUp", keyCode: 38, charCode: 38 }); + fireEvent.keyDown(input, { + key: "ArrowUp", + code: "ArrowUp", + keyCode: 38, + charCode: 38, + }); expect(queryByRole("listbox")).toBeFalsy(); - fireEvent.keyDown(input, { key: "ArrowDown", code: "ArrowDown", keyCode: 40, charCode: 40 }); + fireEvent.keyDown(input, { + key: "ArrowDown", + code: "ArrowDown", + keyCode: 40, + charCode: 40, + }); expect(queryByRole("listbox")).toBeFalsy(); }); - test("Autosuggest uncontrolled — Suggestion selected by click", async () => { + test("Autosuggest uncontrolled — Suggestion selected by click", () => { const onChange = jest.fn(); const { getByRole, getByText, queryByRole } = render( <DxcTextInput label="Autocomplete Countries" suggestions={countries} onChange={onChange} /> ); const input = getByRole("combobox") as HTMLInputElement; fireEvent.focus(input); - await act(async () => { + act(() => { userEvent.type(input, "Alba"); }); expect(onChange).toHaveBeenCalled(); expect(getByText("Alba")).toBeTruthy(); expect(getByText("nia")).toBeTruthy(); - await act(async () => { + act(() => { userEvent.click(getByRole("option")); }); expect(input.value).toBe("Albania"); expect(queryByRole("listbox")).toBeFalsy(); }); - test("Autosuggest controlled — Suggestion selected by click", async () => { + test("Autosuggest controlled — Suggestion selected by click", () => { const onChange = jest.fn(); const { getByRole, getByText, queryByRole } = render( <DxcTextInput label="Autocomplete Countries" value="Andor" suggestions={countries} onChange={onChange} /> ); const input = getByRole("combobox") as HTMLInputElement; - await userEvent.click(getByText("Autocomplete Countries")); + userEvent.click(getByText("Autocomplete Countries")); expect(input.value).toBe("Andor"); expect(getByText("Andor")).toBeTruthy(); expect(getByText("ra")).toBeTruthy(); - await userEvent.click(getByRole("option")); + userEvent.click(getByRole("option")); expect(onChange).toHaveBeenCalledWith({ value: "Andorra" }); expect(queryByRole("listbox")).toBeFalsy(); }); - test("Autosuggest — Pattern constraint", async () => { + test("Autosuggest — Pattern constraint", () => { const onChange = jest.fn(); const onBlur = jest.fn(); const { getByRole, getByText } = render( @@ -641,20 +683,26 @@ describe("TextInput component synchronous autosuggest tests", () => { ); const input = getByRole("combobox"); fireEvent.focus(input); - await act(async () => { + act(() => { userEvent.type(input, "Andor"); }); expect(getByText("Andor")).toBeTruthy(); expect(getByText("ra")).toBeTruthy(); - await act(async () => { + act(() => { userEvent.click(getByRole("option")); }); - expect(onChange).toHaveBeenCalledWith({ value: "Andorra", error: "Please match the format requested." }); + expect(onChange).toHaveBeenCalledWith({ + value: "Andorra", + error: "Please match the format requested.", + }); fireEvent.blur(input); - expect(onBlur).toHaveBeenCalledWith({ value: "Andorra", error: "Please match the format requested." }); + expect(onBlur).toHaveBeenCalledWith({ + value: "Andorra", + error: "Please match the format requested.", + }); }); - test("Autosuggest — Length constraint", async () => { + test("Autosuggest — Length constraint", () => { const onChange = jest.fn(); const onBlur = jest.fn(); const { getByText, getByRole } = render( @@ -669,17 +717,23 @@ describe("TextInput component synchronous autosuggest tests", () => { ); const input = getByRole("combobox"); fireEvent.focus(input); - await act(async () => { + act(() => { userEvent.type(input, "Cha"); }); expect(getByText("Cha")).toBeTruthy(); expect(getByText("d")).toBeTruthy(); - await act(async () => { + act(() => { userEvent.click(getByRole("option")); }); - expect(onChange).toHaveBeenCalledWith({ value: "Cha", error: "Min length 5, max length 10." }); + expect(onChange).toHaveBeenCalledWith({ + value: "Cha", + error: "Min length 5, max length 10.", + }); fireEvent.blur(input); - expect(onBlur).toHaveBeenCalledWith({ value: "Chad", error: "Min length 5, max length 10." }); + expect(onBlur).toHaveBeenCalledWith({ + value: "Chad", + error: "Min length 5, max length 10.", + }); }); test("Autosuggest keys: arrow down key opens autosuggest, active first option is selected with Enter and closes the autosuggest", () => { @@ -688,10 +742,20 @@ describe("TextInput component synchronous autosuggest tests", () => { <DxcTextInput label="Autocomplete Countries" suggestions={countries} onChange={onChange} /> ); const input = getByRole("combobox") as HTMLInputElement; - fireEvent.keyDown(input, { key: "ArrowDown", code: "ArrowDown", keyCode: 40, charCode: 40 }); + fireEvent.keyDown(input, { + key: "ArrowDown", + code: "ArrowDown", + keyCode: 40, + charCode: 40, + }); const list = getByRole("listbox"); expect(list).toBeTruthy(); - fireEvent.keyDown(input, { key: "Enter", code: "Enter", keyCode: 13, charCode: 13 }); + fireEvent.keyDown(input, { + key: "Enter", + code: "Enter", + keyCode: 13, + charCode: 13, + }); expect(input.value).toBe("Afghanistan"); expect(queryByRole("list")).toBeFalsy(); }); @@ -702,10 +766,20 @@ describe("TextInput component synchronous autosuggest tests", () => { <DxcTextInput label="Autocomplete Countries" suggestions={countries} onChange={onChange} /> ); const input = getByRole("combobox") as HTMLInputElement; - fireEvent.keyDown(input, { key: "ArrowUp", code: "ArrowUp", keyCode: 38, charCode: 38 }); + fireEvent.keyDown(input, { + key: "ArrowUp", + code: "ArrowUp", + keyCode: 38, + charCode: 38, + }); const list = getByRole("listbox"); expect(list).toBeTruthy(); - fireEvent.keyDown(input, { key: "Enter", code: "Enter", keyCode: 13, charCode: 13 }); + fireEvent.keyDown(input, { + key: "Enter", + code: "Enter", + keyCode: 13, + charCode: 13, + }); expect(input.value).toBe("Djibouti"); expect(queryByRole("list")).toBeFalsy(); }); @@ -720,7 +794,12 @@ describe("TextInput component synchronous autosuggest tests", () => { userEvent.type(input, "Bangla"); const list = getByRole("listbox"); expect(list).toBeTruthy(); - fireEvent.keyDown(input, { key: "Esc", code: "Esc", keyCode: 27, charCode: 27 }); + fireEvent.keyDown(input, { + key: "Esc", + code: "Esc", + keyCode: 27, + charCode: 27, + }); expect(input.value).toBe(""); expect(queryByRole("listbox")).toBeFalsy(); }); @@ -734,28 +813,58 @@ describe("TextInput component synchronous autosuggest tests", () => { fireEvent.focus(input); const list = getByRole("listbox"); expect(list).toBeTruthy(); - fireEvent.keyDown(input, { key: "Enter", code: "Enter", keyCode: 27, charCode: 27 }); + fireEvent.keyDown(input, { + key: "Enter", + code: "Enter", + keyCode: 27, + charCode: 27, + }); expect(input.value).toBe(""); expect(queryByRole("list")).toBeFalsy(); }); - test("Autosuggest complex key sequence: write, arrow up two times, arrow down and select with Enter. Then, clean with Esc.", async () => { + test("Autosuggest complex key sequence: write, arrow up two times, arrow down and select with Enter. Then, clean with Esc.", () => { const onChange = jest.fn(); const { getByRole, queryByRole } = render( <DxcTextInput label="Autocomplete Countries" suggestions={countries} onChange={onChange} /> ); const input = getByRole("combobox") as HTMLInputElement; fireEvent.focus(input); - await act(async () => { + act(() => { userEvent.type(input, "Ba"); }); - fireEvent.keyDown(input, { key: "ArrowUp", code: "ArrowUp", keyCode: 38, charCode: 38 }); - fireEvent.keyDown(input, { key: "ArrowUp", code: "ArrowUpp", keyCode: 38, charCode: 38 }); - fireEvent.keyDown(input, { key: "ArrowDown", code: "ArrowDown", keyCode: 40, charCode: 40 }); - fireEvent.keyDown(input, { key: "Enter", code: "Enter", keyCode: 13, charCode: 13 }); + fireEvent.keyDown(input, { + key: "ArrowUp", + code: "ArrowUp", + keyCode: 38, + charCode: 38, + }); + fireEvent.keyDown(input, { + key: "ArrowUp", + code: "ArrowUpp", + keyCode: 38, + charCode: 38, + }); + fireEvent.keyDown(input, { + key: "ArrowDown", + code: "ArrowDown", + keyCode: 40, + charCode: 40, + }); + fireEvent.keyDown(input, { + key: "Enter", + code: "Enter", + keyCode: 13, + charCode: 13, + }); expect(input.value).toBe("Barbados"); expect(queryByRole("listbox")).toBeFalsy(); - fireEvent.keyDown(input, { key: "Esc", code: "Esp", keyCode: 27, charCode: 27 }); + fireEvent.keyDown(input, { + key: "Esc", + code: "Esc", + keyCode: 27, + charCode: 27, + }); expect(input.value).toBe(""); expect(queryByRole("listbox")).toBeFalsy(); }); @@ -800,16 +909,16 @@ describe("TextInput component synchronous autosuggest tests", () => { describe("TextInput component asynchronous autosuggest tests", () => { test("Autosuggest 'Searching...' message is shown", async () => { - const callbackFunc = jest.fn((newValue) => { - const result = new Promise<string[]>((resolve) => - setTimeout(() => { - resolve( - newValue ? countries.filter((option) => option.toUpperCase().includes(newValue.toUpperCase())) : countries - ); - }, 100) - ); - return result; - }); + const callbackFunc = jest.fn( + (newValue: string) => + new Promise<string[]>((resolve) => { + setTimeout(() => { + resolve( + newValue ? countries.filter((option) => option.toUpperCase().includes(newValue.toUpperCase())) : countries + ); + }, 100); + }) + ); const onChange = jest.fn(); const { getByRole, getByText } = render( <DxcTextInput label="Autosuggest Countries" suggestions={callbackFunc} onChange={onChange} /> @@ -824,27 +933,37 @@ describe("TextInput component asynchronous autosuggest tests", () => { expect(getByText("Albania")).toBeTruthy(); expect(getByText("Algeria")).toBeTruthy(); expect(getByText("Andorra")).toBeTruthy(); - await act(async () => { + act(() => { userEvent.type(input, "Ab"); }); await waitForElementToBeRemoved(() => getByText("Searching...")); expect(getByText("Cabo Verde")).toBeTruthy(); - fireEvent.keyDown(input, { key: "ArrowUp", code: "ArrowUp", keyCode: 38, charCode: 38 }); - fireEvent.keyDown(input, { key: "Enter", code: "Enter", keyCode: 13, charCode: 13 }); + fireEvent.keyDown(input, { + key: "ArrowUp", + code: "ArrowUp", + keyCode: 38, + charCode: 38, + }); + fireEvent.keyDown(input, { + key: "Enter", + code: "Enter", + keyCode: 13, + charCode: 13, + }); expect(input.value).toBe("Cabo Verde"); }); test("Autosuggest Esc key works while 'Searching...' message is shown", () => { - const callbackFunc = jest.fn((newValue) => { - const result = new Promise<string[]>((resolve) => - setTimeout(() => { - resolve( - newValue ? countries.filter((option) => option.toUpperCase().includes(newValue.toUpperCase())) : countries - ); - }, 100) - ); - return result; - }); + const callbackFunc = jest.fn( + (newValue: string) => + new Promise<string[]>((resolve) => { + setTimeout(() => { + resolve( + newValue ? countries.filter((option) => option.toUpperCase().includes(newValue.toUpperCase())) : countries + ); + }, 100); + }) + ); const onChange = jest.fn(); const { getByRole, getByText, queryByText, queryByRole } = render( <DxcTextInput label="Autosuggest Countries" suggestions={callbackFunc} onChange={onChange} /> @@ -853,23 +972,28 @@ describe("TextInput component asynchronous autosuggest tests", () => { fireEvent.focus(input); expect(getByText("Searching...")).toBeTruthy(); userEvent.type(input, "Ab"); - fireEvent.keyDown(input, { key: "Esc", code: "Esc", keyCode: 27, charCode: 27 }); + fireEvent.keyDown(input, { + key: "Esc", + code: "Esc", + keyCode: 27, + charCode: 27, + }); expect(queryByRole("listbox")).toBeFalsy(); expect(queryByText("Searching...")).toBeFalsy(); expect(input.value).toBe(""); }); test("Autosuggest Esc + arrow down working while 'Searching...' message is shown", async () => { - const callbackFunc = jest.fn((newValue) => { - const result = new Promise<string[]>((resolve) => - setTimeout(() => { - resolve( - newValue ? countries.filter((option) => option.toUpperCase().includes(newValue.toUpperCase())) : countries - ); - }, 100) - ); - return result; - }); + const callbackFunc = jest.fn( + (newValue: string) => + new Promise<string[]>((resolve) => { + setTimeout(() => { + resolve( + newValue ? countries.filter((option) => option.toUpperCase().includes(newValue.toUpperCase())) : countries + ); + }, 100); + }) + ); const onChange = jest.fn(); const { getByRole, getByText, queryByText, queryByRole } = render( <DxcTextInput label="Autosuggest Countries" suggestions={callbackFunc} onChange={onChange} /> @@ -878,7 +1002,12 @@ describe("TextInput component asynchronous autosuggest tests", () => { fireEvent.focus(input); expect(getByText("Searching...")).toBeTruthy(); userEvent.type(input, "Ab"); - fireEvent.keyDown(input, { key: "Esc", code: "Esc", keyCode: 27, charCode: 27 }); + fireEvent.keyDown(input, { + key: "Esc", + code: "Esc", + keyCode: 27, + charCode: 27, + }); expect(queryByRole("listbox")).toBeFalsy(); expect(queryByText("Searching...")).toBeFalsy(); expect(input.value).toBe(""); @@ -892,16 +1021,16 @@ describe("TextInput component asynchronous autosuggest tests", () => { }); test("Asynchronous uncontrolled autosuggest test", async () => { - const callbackFunc = jest.fn((newValue) => { - const result = new Promise<string[]>((resolve) => - setTimeout(() => { - resolve( - newValue ? countries.filter((option) => option.toUpperCase().includes(newValue.toUpperCase())) : countries - ); - }, 100) - ); - return result; - }); + const callbackFunc = jest.fn( + (newValue: string) => + new Promise<string[]>((resolve) => { + setTimeout(() => { + resolve( + newValue ? countries.filter((option) => option.toUpperCase().includes(newValue.toUpperCase())) : countries + ); + }, 100); + }) + ); const onChange = jest.fn(); const { getByRole, getByText } = render( <DxcTextInput label="Autosuggest Countries" onChange={onChange} suggestions={callbackFunc} /> @@ -911,55 +1040,55 @@ describe("TextInput component asynchronous autosuggest tests", () => { userEvent.type(input, "Den"); await waitForElementToBeRemoved(() => getByText("Searching...")); expect(getByText("Denmark")).toBeTruthy(); - await userEvent.click(getByRole("option")); + userEvent.click(getByRole("option")); expect(onChange).toHaveBeenCalledWith({ value: "Denmark" }); expect(input.value).toBe("Denmark"); }); test("Asynchronous controlled autosuggest test", async () => { - const callbackFunc = jest.fn((newValue) => { - const result = new Promise<string[]>((resolve) => - setTimeout(() => { - resolve( - newValue ? countries.filter((option) => option.toUpperCase().includes(newValue.toUpperCase())) : countries - ); - }, 100) - ); - return result; - }); + const callbackFunc = jest.fn( + (newValue: string) => + new Promise<string[]>((resolve) => { + setTimeout(() => { + resolve( + newValue ? countries.filter((option) => option.toUpperCase().includes(newValue.toUpperCase())) : countries + ); + }, 100); + }) + ); const onChange = jest.fn(); const { getByRole, getByText, queryByRole } = render( <DxcTextInput label="Autosuggest Countries" value="Denm" onChange={onChange} suggestions={callbackFunc} /> ); const input = getByRole("combobox") as HTMLInputElement; expect(input.value).toBe("Denm"); - await userEvent.click(getByText("Autosuggest Countries")); + userEvent.click(getByText("Autosuggest Countries")); await waitForElementToBeRemoved(() => getByText("Searching...")); expect(getByText("Denmark")).toBeTruthy(); fireEvent.focus(getByRole("option")); - await userEvent.click(getByText("Denmark")); + userEvent.click(getByText("Denmark")); expect(onChange).toHaveBeenCalledWith({ value: "Denmark" }); expect(queryByRole("listbox")).toBeFalsy(); }); test("Asynchronous autosuggest closes the listbox after finishing no matches search", async () => { - const callbackFunc = jest.fn((newValue) => { - const result = new Promise<string[]>((resolve) => - setTimeout(() => { - resolve( - newValue ? countries.filter((option) => option.toUpperCase().includes(newValue.toUpperCase())) : countries - ); - }, 100) - ); - return result; - }); + const callbackFunc = jest.fn( + (newValue: string) => + new Promise<string[]>((resolve) => { + setTimeout(() => { + resolve( + newValue ? countries.filter((option) => option.toUpperCase().includes(newValue.toUpperCase())) : countries + ); + }, 100); + }) + ); const onChange = jest.fn(); const { getByText, getByRole, queryByRole } = render( <DxcTextInput label="Autosuggest Countries" onChange={onChange} suggestions={callbackFunc} /> ); const input = getByRole("combobox"); fireEvent.focus(input); - await act(async () => { + act(() => { userEvent.type(input, "Example text"); }); await waitForElementToBeRemoved(() => getByText("Searching...")); @@ -967,16 +1096,16 @@ describe("TextInput component asynchronous autosuggest tests", () => { }); test("Asynchronous autosuggest with no matches founded doesn't let the listbox to be opened", async () => { - const callbackFunc = jest.fn((newValue) => { - const result = new Promise<string[]>((resolve) => - setTimeout(() => { - resolve( - newValue ? countries.filter((option) => option.toUpperCase().includes(newValue.toUpperCase())) : countries - ); - }, 100) - ); - return result; - }); + const callbackFunc = jest.fn( + (newValue: string) => + new Promise<string[]>((resolve) => { + setTimeout(() => { + resolve( + newValue ? countries.filter((option) => option.toUpperCase().includes(newValue.toUpperCase())) : countries + ); + }, 100); + }) + ); const onChange = jest.fn(); const { getByText, getByRole, queryByRole } = render( <DxcTextInput label="Autosuggest Countries" onChange={onChange} suggestions={callbackFunc} /> @@ -988,21 +1117,31 @@ describe("TextInput component asynchronous autosuggest tests", () => { expect(queryByRole("listbox")).toBeFalsy(); fireEvent.focus(input); expect(queryByRole("listbox")).toBeFalsy(); - fireEvent.keyDown(input, { key: "ArrowUp", code: "ArrowUp", keyCode: 38, charCode: 38 }); + fireEvent.keyDown(input, { + key: "ArrowUp", + code: "ArrowUp", + keyCode: 38, + charCode: 38, + }); expect(queryByRole("listbox")).toBeFalsy(); - fireEvent.keyDown(input, { key: "ArrowDown", code: "ArrowDown", keyCode: 40, charCode: 40 }); + fireEvent.keyDown(input, { + key: "ArrowDown", + code: "ArrowDown", + keyCode: 40, + charCode: 40, + }); expect(queryByRole("listbox")).toBeFalsy(); }); test("Asynchronous autosuggest request failed, shows 'Error fetching data' message", async () => { - const errorCallbackFunc = jest.fn(() => { - const result = new Promise<string[]>((resolve, reject) => - setTimeout(() => { - reject("err"); - }, 100) - ); - return result; - }); + const errorCallbackFunc = jest.fn( + () => + new Promise<string[]>((resolve, reject) => { + setTimeout(() => { + reject(new Error("err")); + }, 100); + }) + ); const onChange = jest.fn(); const { getByRole, getByText } = render( <DxcTextInput label="Autosuggest Countries" onChange={onChange} suggestions={errorCallbackFunc} /> diff --git a/packages/lib/src/text-input/TextInput.tsx b/packages/lib/src/text-input/TextInput.tsx index 9f685bccd2..579f765957 100644 --- a/packages/lib/src/text-input/TextInput.tsx +++ b/packages/lib/src/text-input/TextInput.tsx @@ -5,6 +5,7 @@ import { forwardRef, KeyboardEvent, MouseEvent, + ReactNode, useContext, useEffect, useId, @@ -33,7 +34,7 @@ import { import HelperText from "../styles/forms/HelperText"; import Label from "../styles/forms/Label"; import ErrorMessage from "../styles/forms/ErrorMessage"; -import { inputStylesByState } from "../styles/forms/inputStylesByState"; +import inputStylesByState from "../styles/forms/inputStylesByState"; const TextInputContainer = styled.div<{ margin: TextInputPropsType["margin"]; @@ -153,6 +154,50 @@ const DxcTextInput = forwardRef<RefType, TextInputPropsType>( const [visualFocusIndex, changeVisualFocusIndex] = useState(-1); const width = useWidth(inputContainerRef); + const autosuggestWrapperFunction = (children: ReactNode) => ( + <Popover.Root open={isOpen && (filteredSuggestions.length > 0 || isSearching || isAutosuggestError)}> + <Popover.Trigger + aria-controls={undefined} + aria-expanded={undefined} + aria-haspopup={undefined} + asChild + type={undefined} + > + {children} + </Popover.Trigger> + <Popover.Portal> + <Popover.Content + aria-label="Suggestions" + onCloseAutoFocus={(event) => { + // Avoid select to lose focus when the list is closed + event.preventDefault(); + }} + onOpenAutoFocus={(event) => { + // Avoid select to lose focus when the list is opened + event.preventDefault(); + }} + sideOffset={4} + style={{ zIndex: "var(--z-textinput)" }} + > + <Suggestions + highlightedSuggestions={typeof suggestions !== "function"} + id={autosuggestId} + isSearching={isSearching} + searchHasErrors={isAutosuggestError} + suggestionOnClick={(suggestion) => { + changeValue(suggestion); + closeSuggestions(); + }} + suggestions={filteredSuggestions} + styles={{ width }} + value={value ?? innerValue} + visualFocusIndex={visualFocusIndex} + /> + </Popover.Content> + </Popover.Portal> + </Popover.Root> + ); + const getNumberErrorMessage = (checkedValue: number) => numberInputContext?.minNumber != null && checkedValue < numberInputContext?.minNumber ? translatedLabels.numberInput.valueGreaterThanOrEqualToErrorMessage?.(numberInputContext.minNumber) @@ -410,7 +455,7 @@ const DxcTextInput = forwardRef<RefType, TextInputPropsType>( changeIsAutosuggestError(false); changeFilteredSuggestions(promiseResponse); }) - .catch((err) => { + .catch((err: Error) => { if (err.message !== "Is canceled") { changeIsSearching(false); changeIsAutosuggestError(true); @@ -435,7 +480,6 @@ const DxcTextInput = forwardRef<RefType, TextInputPropsType>( numberInputContext.stepNumber ); } - return undefined; }, [value, innerValue, suggestions, numberInputContext]); return ( @@ -450,52 +494,7 @@ const DxcTextInput = forwardRef<RefType, TextInputPropsType>( {helperText} </HelperText> )} - <AutosuggestWrapper - condition={hasSuggestions(suggestions)} - wrapper={(children) => ( - <Popover.Root open={isOpen && (filteredSuggestions.length > 0 || isSearching || isAutosuggestError)}> - <Popover.Trigger - aria-controls={undefined} - aria-expanded={undefined} - aria-haspopup={undefined} - asChild - type={undefined} - > - {children} - </Popover.Trigger> - <Popover.Portal> - <Popover.Content - aria-label="Suggestions" - onCloseAutoFocus={(event) => { - // Avoid select to lose focus when the list is closed - event.preventDefault(); - }} - onOpenAutoFocus={(event) => { - // Avoid select to lose focus when the list is opened - event.preventDefault(); - }} - sideOffset={4} - style={{ zIndex: "var(--z-textinput)" }} - > - <Suggestions - highlightedSuggestions={typeof suggestions !== "function"} - id={autosuggestId} - isSearching={isSearching} - searchHasErrors={isAutosuggestError} - suggestionOnClick={(suggestion) => { - changeValue(suggestion); - closeSuggestions(); - }} - suggestions={filteredSuggestions} - styles={{ width }} - value={value ?? innerValue} - visualFocusIndex={visualFocusIndex} - /> - </Popover.Content> - </Popover.Portal> - </Popover.Root> - )} - > + <AutosuggestWrapper condition={hasSuggestions(suggestions)} wrapper={autosuggestWrapperFunction}> <TextInput disabled={disabled} error={!!error} @@ -600,4 +599,6 @@ const DxcTextInput = forwardRef<RefType, TextInputPropsType>( } ); +DxcTextInput.displayName = "DxcTextInput"; + export default DxcTextInput; diff --git a/packages/lib/src/text-input/types.ts b/packages/lib/src/text-input/types.ts index 5a14901e54..2444e036ae 100644 --- a/packages/lib/src/text-input/types.ts +++ b/packages/lib/src/text-input/types.ts @@ -166,7 +166,7 @@ type Props = { }; /** - * List of suggestions of an Text Input component. + * List of suggestions of a Text Input component. */ export type SuggestionsProps = { id: string; @@ -186,7 +186,7 @@ export type SuggestionsProps = { export type RefType = HTMLDivElement; /** - * Single suggestion of an Text Input component. + * Single suggestion of a Text Input component. */ export type SuggestionProps = { id: string; diff --git a/packages/lib/src/text-input/utils.ts b/packages/lib/src/text-input/utils.ts index 3e7bb102f7..202e0f56c0 100644 --- a/packages/lib/src/text-input/utils.ts +++ b/packages/lib/src/text-input/utils.ts @@ -17,8 +17,22 @@ export const makeCancelable = (promise: Promise<string[]>) => { let hasCanceled_ = false; const wrappedPromise = new Promise<string[]>((resolve, reject) => { promise.then( - (val) => (hasCanceled_ ? reject(Error("Is canceled")) : resolve(val)), - (promiseError) => (hasCanceled_ ? reject(Error("Is canceled")) : reject(promiseError)) + (val) => { + if (hasCanceled_) { + reject(new Error("Is canceled")); + } else { + resolve(val); + } + }, + (promiseError) => { + if (hasCanceled_) { + reject(new Error("Is canceled")); + } else if (promiseError instanceof Error) { + reject(promiseError); + } else { + reject(new Error(String(promiseError))); + } + } ); }); return { @@ -57,8 +71,10 @@ export const transformSpecialChars = (str: string) => { const regexAsString = specialCharsRegex.toString().split(""); const uniqueSpecialChars = regexAsString.filter((item, index) => regexAsString.indexOf(item) === index); uniqueSpecialChars.forEach((specialChar) => { - if (str.includes(specialChar)) value = value.replace(specialChar, "\\" + specialChar); + if (str.includes(specialChar)) { + value = value.replace(specialChar, `\\${specialChar}`); + } }); } return value; -}; \ No newline at end of file +}; diff --git a/packages/lib/src/textarea/Textarea.test.tsx b/packages/lib/src/textarea/Textarea.test.tsx index 2a01439207..89a315c53c 100644 --- a/packages/lib/src/textarea/Textarea.test.tsx +++ b/packages/lib/src/textarea/Textarea.test.tsx @@ -86,10 +86,10 @@ describe("Textarea component tests", () => { expect(onChange).not.toHaveBeenCalled(); }); - test("Read-only textarea sends its value on submit", async () => { - const handlerOnSubmit = jest.fn((e) => { + test("Read-only textarea sends its value on submit", () => { + const handlerOnSubmit = jest.fn((e: React.FormEvent<HTMLFormElement>) => { e.preventDefault(); - const formData = new FormData(e.target); + const formData = new FormData(e.currentTarget); const formProps = Object.fromEntries(formData); expect(formProps).toStrictEqual({ data: "Comments" }); }); @@ -100,7 +100,7 @@ describe("Textarea component tests", () => { </form> ); const submit = getByText("Submit"); - await userEvent.click(submit); + userEvent.click(submit); expect(handlerOnSubmit).toHaveBeenCalled(); }); @@ -114,7 +114,10 @@ describe("Textarea component tests", () => { fireEvent.focus(textarea); fireEvent.blur(textarea); expect(onBlur).toHaveBeenCalled(); - expect(onBlur).toHaveBeenCalledWith({ value: "", error: "This field is required. Please, enter a value." }); + expect(onBlur).toHaveBeenCalledWith({ + value: "", + error: "This field is required. Please, enter a value.", + }); fireEvent.change(textarea, { target: { value: "Test" } }); fireEvent.blur(textarea); expect(onBlur).toHaveBeenCalled(); @@ -133,7 +136,10 @@ describe("Textarea component tests", () => { expect(onChange).toHaveBeenCalledWith({ value: "Test" }); userEvent.clear(textarea); expect(onChange).toHaveBeenCalled(); - expect(onChange).toHaveBeenCalledWith({ value: "", error: "This field is required. Please, enter a value." }); + expect(onChange).toHaveBeenCalledWith({ + value: "", + error: "This field is required. Please, enter a value.", + }); }); test("Pattern constraint", () => { @@ -152,10 +158,16 @@ describe("Textarea component tests", () => { const textarea = getByLabelText("Example label"); fireEvent.change(textarea, { target: { value: "pattern test" } }); expect(onChange).toHaveBeenCalled(); - expect(onChange).toHaveBeenCalledWith({ value: "pattern test", error: "Please match the format requested." }); + expect(onChange).toHaveBeenCalledWith({ + value: "pattern test", + error: "Please match the format requested.", + }); fireEvent.blur(textarea); expect(onBlur).toHaveBeenCalled(); - expect(onBlur).toHaveBeenCalledWith({ value: "pattern test", error: "Please match the format requested." }); + expect(onBlur).toHaveBeenCalledWith({ + value: "pattern test", + error: "Please match the format requested.", + }); userEvent.clear(textarea); fireEvent.change(textarea, { target: { value: "pattern4&" } }); expect(onChange).toHaveBeenCalled(); @@ -182,10 +194,16 @@ describe("Textarea component tests", () => { const textarea = getByLabelText("Example label"); fireEvent.change(textarea, { target: { value: "test" } }); expect(onChange).toHaveBeenCalled(); - expect(onChange).toHaveBeenCalledWith({ value: "test", error: "Min length 5, max length 10." }); + expect(onChange).toHaveBeenCalledWith({ + value: "test", + error: "Min length 5, max length 10.", + }); fireEvent.blur(textarea); expect(onBlur).toHaveBeenCalled(); - expect(onBlur).toHaveBeenCalledWith({ value: "test", error: "Min length 5, max length 10." }); + expect(onBlur).toHaveBeenCalledWith({ + value: "test", + error: "Min length 5, max length 10.", + }); userEvent.clear(textarea); fireEvent.change(textarea, { target: { value: "length" } }); expect(onChange).toHaveBeenCalled(); @@ -213,16 +231,28 @@ describe("Textarea component tests", () => { const textarea = getByLabelText("Example label"); fireEvent.change(textarea, { target: { value: "test" } }); expect(onChange).toHaveBeenCalled(); - expect(onChange).toHaveBeenCalledWith({ value: "test", error: "Min length 5, max length 10." }); + expect(onChange).toHaveBeenCalledWith({ + value: "test", + error: "Min length 5, max length 10.", + }); fireEvent.blur(textarea); expect(onBlur).toHaveBeenCalled(); - expect(onBlur).toHaveBeenCalledWith({ value: "test", error: "Min length 5, max length 10." }); + expect(onBlur).toHaveBeenCalledWith({ + value: "test", + error: "Min length 5, max length 10.", + }); fireEvent.change(textarea, { target: { value: "tests" } }); expect(onChange).toHaveBeenCalled(); - expect(onChange).toHaveBeenCalledWith({ value: "tests", error: "Please match the format requested." }); + expect(onChange).toHaveBeenCalledWith({ + value: "tests", + error: "Please match the format requested.", + }); fireEvent.blur(textarea); expect(onBlur).toHaveBeenCalled(); - expect(onBlur).toHaveBeenCalledWith({ value: "tests", error: "Please match the format requested." }); + expect(onBlur).toHaveBeenCalledWith({ + value: "tests", + error: "Please match the format requested.", + }); fireEvent.change(textarea, { target: { value: "tests4&" } }); expect(onChange).toHaveBeenCalled(); expect(onChange).toHaveBeenCalledWith({ value: "tests4&" }); diff --git a/packages/lib/src/textarea/Textarea.tsx b/packages/lib/src/textarea/Textarea.tsx index 4f2684bcf9..c2013092fc 100644 --- a/packages/lib/src/textarea/Textarea.tsx +++ b/packages/lib/src/textarea/Textarea.tsx @@ -4,11 +4,11 @@ import { getMargin } from "../common/utils"; import { spaces } from "../common/variables"; import { HalstackLanguageContext } from "../HalstackContext"; import TextareaPropsType, { RefType } from "./types"; -import { scrollbarStyles } from "../styles/scroll"; +import scrollbarStyles from "../styles/scroll"; import ErrorMessage from "../styles/forms/ErrorMessage"; import Label from "../styles/forms/Label"; import HelperText from "../styles/forms/HelperText"; -import { inputStylesByState } from "../styles/forms/inputStylesByState"; +import inputStylesByState from "../styles/forms/inputStylesByState"; const sizes = { small: "240px", @@ -198,4 +198,6 @@ const DxcTextarea = forwardRef<RefType, TextareaPropsType>( } ); +DxcTextarea.displayName = "DxcTextarea"; + export default DxcTextarea; diff --git a/packages/lib/src/toast/Toast.accessibility.test.tsx b/packages/lib/src/toast/Toast.accessibility.test.tsx index d89d603fee..6ba0f8f7f0 100644 --- a/packages/lib/src/toast/Toast.accessibility.test.tsx +++ b/packages/lib/src/toast/Toast.accessibility.test.tsx @@ -1,10 +1,10 @@ import { render } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; import { axe } from "../../test/accessibility/axe-helper"; import DxcToast from "./Toast"; import DxcToastsQueue from "./ToastsQueue"; import useToast from "./useToast"; import DxcButton from "../button/Button"; -import userEvent from "@testing-library/user-event"; const actionIcon = { label: "Action", @@ -37,7 +37,9 @@ describe("Toast component accessibility tests", () => { const { container } = render(<TestExample />); const results = await axe(container); const button = container.querySelector("button"); - button && userEvent.click(button); + if (button) { + userEvent.click(button); + } expect(results).toHaveNoViolations(); }); it("Should not have basic accessibility issues", async () => { diff --git a/packages/lib/src/toast/Toast.stories.tsx b/packages/lib/src/toast/Toast.stories.tsx index 8d2e0e234b..47c0612e1f 100644 --- a/packages/lib/src/toast/Toast.stories.tsx +++ b/packages/lib/src/toast/Toast.stories.tsx @@ -1,4 +1,6 @@ +import { Meta, StoryObj } from "@storybook/react"; import { userEvent, within } from "@storybook/test"; +import { INITIAL_VIEWPORTS } from "@storybook/addon-viewport"; import ExampleContainer from "../../.storybook/components/ExampleContainer"; import Title from "../../.storybook/components/Title"; import DxcButton from "../button/Button"; @@ -6,8 +8,6 @@ import DxcFlex from "../flex/Flex"; import DxcToast from "./Toast"; import DxcToastsQueue from "./ToastsQueue"; import useToast from "./useToast"; -import { INITIAL_VIEWPORTS } from "@storybook/addon-viewport"; -import { Meta, StoryObj } from "@storybook/react"; import DxcDialog from "../dialog/Dialog"; import DxcInset from "../inset/Inset"; import { screen } from "@testing-library/react"; @@ -245,7 +245,7 @@ const Screens = () => { toast.success({ message: "This is another very long label for a Toast. Please, always try to avoid this king of messages, be brief and concise.", - action: action, + action, }); }} /> diff --git a/packages/lib/src/toast/Toast.test.tsx b/packages/lib/src/toast/Toast.test.tsx index ab2f39b3cb..79709a7bc4 100644 --- a/packages/lib/src/toast/Toast.test.tsx +++ b/packages/lib/src/toast/Toast.test.tsx @@ -1,9 +1,8 @@ +import { act, render, waitFor } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import DxcButton from "../button/Button"; import DxcToastsQueue from "./ToastsQueue"; import useToast from "./useToast"; -import { render, waitFor } from "@testing-library/react"; -import { act } from "@testing-library/react"; const ToastPage = ({ onClick }: { onClick?: () => void }) => { const toast = useToast(); @@ -34,9 +33,11 @@ const ToastPage = ({ onClick }: { onClick?: () => void }) => { <DxcButton label="Show toast" onClick={() => { - onClick - ? toast.default({ message: "This is a simple toast.", action: { label: "Action", onClick } }) - : toast.default({ message: "This is a simple toast." }); + if (onClick) { + toast.default({ message: "This is a simple toast.", action: { label: "Action", onClick } }); + } else { + toast.default({ message: "This is a simple toast." }); + } }} /> <DxcButton label="Load process" onClick={loadingFunc} /> @@ -57,7 +58,7 @@ describe("Toast component tests", () => { expect(getByText("This is a simple toast.")).toBeTruthy(); }); }); - test("Toast disappears after the specified duration", async () => { + test("Toast disappears after the specified duration", () => { jest.useFakeTimers(); const { getByText, queryByText } = render( <DxcToastsQueue duration={4250}> @@ -79,7 +80,7 @@ describe("Toast component tests", () => { jest.useRealTimers(); }); - test("If duration > 5000, the toast disappears at 5000ms", async () => { + test("If duration > 5000, the toast disappears at 5000ms", () => { jest.useFakeTimers(); const { getByText, queryByText } = render( <DxcToastsQueue duration={1000000}> @@ -96,7 +97,7 @@ describe("Toast component tests", () => { jest.useRealTimers(); }); - test("If duration < 3000, the toast disappears at 3000ms", async () => { + test("If duration < 3000, the toast disappears at 3000ms", () => { jest.useFakeTimers(); const { getByText, queryByText } = render( <DxcToastsQueue duration={100}> @@ -164,7 +165,7 @@ describe("Toast component tests", () => { const defaultBtn = getByText("Show toast"); userEvent.click(infoBtn); - waitFor(() => { + await waitFor(() => { expect(getByText("This is an information toast.")).toBeTruthy(); }); for (let i = 0; i < 6; i++) { @@ -175,7 +176,7 @@ describe("Toast component tests", () => { expect(getAllByText("This is a simple toast.").length).toBe(5); }); }); - test("Loading toast is never removed automatically", async () => { + test("Loading toast is never removed automatically", () => { jest.useFakeTimers(); const { getByText } = render( <DxcToastsQueue> diff --git a/packages/lib/src/toast/Toast.tsx b/packages/lib/src/toast/Toast.tsx index 760fb5abc3..4b654494c4 100644 --- a/packages/lib/src/toast/Toast.tsx +++ b/packages/lib/src/toast/Toast.tsx @@ -4,9 +4,9 @@ import styled from "@emotion/styled"; import DxcActionIcon from "../action-icon/ActionIcon"; import DxcButton from "../button/Button"; import DxcFlex from "../flex/Flex"; +import { HalstackLanguageContext } from "../HalstackContext"; import ToastPropsType from "./types"; import useTimeout from "../utils/useTimeout"; -import { HalstackLanguageContext } from "../HalstackContext"; import { responsiveSizes } from "../common/variables"; import getSemantic from "./utils"; import ToastIcon from "./ToastIcon"; @@ -188,4 +188,6 @@ const DxcToast = ({ ); }; +DxcToast.displayName = "DxcToast"; + export default memo(DxcToast); diff --git a/packages/lib/src/toast/ToastIcon.tsx b/packages/lib/src/toast/ToastIcon.tsx index 8cc77d0d8a..4e1652d51b 100644 --- a/packages/lib/src/toast/ToastIcon.tsx +++ b/packages/lib/src/toast/ToastIcon.tsx @@ -17,4 +17,6 @@ const ToastIcon = memo( } ); +ToastIcon.displayName = "ToastIcon"; + export default ToastIcon; diff --git a/packages/lib/src/toast/types.ts b/packages/lib/src/toast/types.ts index 4fe4a92bbf..c38c156070 100644 --- a/packages/lib/src/toast/types.ts +++ b/packages/lib/src/toast/types.ts @@ -28,8 +28,8 @@ type CommonProps = { }; type DefaultToast = CommonProps & { /** - * Material Symbol name or SVG element as the icon that will be placed next to the panel label. - * When using Material Symbols, replace spaces with underscores. + * Material Symbol name or SVG element as the icon that will be placed next to the panel label. + * When using Material Symbols, replace spaces with underscores. * By default they are outlined if you want it to be filled prefix the symbol name with "filled_". */ icon?: string | SVG; @@ -71,7 +71,7 @@ type ToastsQueuePropsType = { */ children: ReactNode; /** - * Duration in milliseconds before a toast automatically hides itself. + * Duration in milliseconds before a toast automatically hides itself. * The range goes from 3000ms to 5000ms, any other value will not be taken into consideration. */ duration?: number; diff --git a/packages/lib/src/toast/useToast.tsx b/packages/lib/src/toast/useToast.tsx index de5f5e1eac..bb75002b9d 100644 --- a/packages/lib/src/toast/useToast.tsx +++ b/packages/lib/src/toast/useToast.tsx @@ -11,10 +11,10 @@ export default function useToast() { success: (toast: SemanticToast) => add?.(toast, "success"), warning: (toast: SemanticToast) => add?.(toast, "warning"), info: (toast: SemanticToast) => add?.(toast, "info"), - loading: (toast: Omit<LoadingToast, "loading">) => add?.({ ...toast, loading: true } as LoadingToast, "info"), + loading: (toast: Omit<LoadingToast, "loading">) => add?.({ ...toast, loading: true }, "info"), }), [add] ); - + return toast; } diff --git a/packages/lib/src/toast/utils.ts b/packages/lib/src/toast/utils.ts index 6718ee6344..4a3a648a02 100644 --- a/packages/lib/src/toast/utils.ts +++ b/packages/lib/src/toast/utils.ts @@ -29,12 +29,12 @@ export default function getSemantic(semantic: ToastPropsType["semantic"]) { } } +const idExists = (id: string, toasts: QueuedToast[]) => toasts.some((toast) => toast.id === id); + export function generateUniqueToastId(toasts: QueuedToast[]) { let id = ""; - let exists = true; - while (exists) { + do { id = `${performance.now()}-${Math.random().toString(36).slice(2, 9)}`; - exists = toasts.some((toast) => toast.id === id); - } + } while (idExists(id, toasts)); return id; -}; +} diff --git a/packages/lib/src/toggle-group/ToggleGroup.stories.tsx b/packages/lib/src/toggle-group/ToggleGroup.stories.tsx index c0916452ee..32c1b16c8e 100644 --- a/packages/lib/src/toggle-group/ToggleGroup.stories.tsx +++ b/packages/lib/src/toggle-group/ToggleGroup.stories.tsx @@ -1,7 +1,7 @@ +import { Meta, StoryObj } from "@storybook/react"; import ExampleContainer from "../../.storybook/components/ExampleContainer"; import Title from "../../.storybook/components/Title"; import DxcToggleGroup from "./ToggleGroup"; -import { Meta, StoryObj } from "@storybook/react"; export default { title: "Toggle Group", diff --git a/packages/lib/src/toggle-group/ToggleGroup.test.tsx b/packages/lib/src/toggle-group/ToggleGroup.test.tsx index 941434b249..2aa58d02c2 100644 --- a/packages/lib/src/toggle-group/ToggleGroup.test.tsx +++ b/packages/lib/src/toggle-group/ToggleGroup.test.tsx @@ -27,7 +27,11 @@ describe("Toggle group component tests", () => { const { getByRole } = render( <DxcToggleGroup options={[ - { value: 1, icon: "https://cdn.icon-icons.com/icons2/2645/PNG/512/mic_mute_icon_159965.png", title: "Mute" }, + { + value: 1, + icon: "https://cdn.icon-icons.com/icons2/2645/PNG/512/mic_mute_icon_159965.png", + title: "Mute", + }, ]} /> ); @@ -51,10 +55,16 @@ describe("Toggle group component tests", () => { const onChange = jest.fn(); const { getAllByRole } = render(<DxcToggleGroup options={options} onChange={onChange} multiple />); const toggleOptions = getAllByRole("button"); - toggleOptions[0] && fireEvent.click(toggleOptions[0]); + if (toggleOptions[0]) { + fireEvent.click(toggleOptions[0]); + } expect(onChange).toHaveBeenCalledWith([1]); - toggleOptions[1] && fireEvent.click(toggleOptions[1]); - toggleOptions[3] && fireEvent.click(toggleOptions[3]); + if (toggleOptions[1]) { + fireEvent.click(toggleOptions[1]); + } + if (toggleOptions[3]) { + fireEvent.click(toggleOptions[3]); + } expect(onChange).toHaveBeenCalledWith([1, 2, 4]); expect(toggleOptions[0]?.getAttribute("aria-pressed")).toBe("true"); expect(toggleOptions[1]?.getAttribute("aria-pressed")).toBe("true"); diff --git a/packages/lib/src/tooltip/Tooltip.accessibility.test.tsx b/packages/lib/src/tooltip/Tooltip.accessibility.test.tsx index 14170fe013..c09eaa708c 100644 --- a/packages/lib/src/tooltip/Tooltip.accessibility.test.tsx +++ b/packages/lib/src/tooltip/Tooltip.accessibility.test.tsx @@ -3,16 +3,11 @@ import { axe } from "../../test/accessibility/axe-helper"; import DxcButton from "../button/Button"; import DxcTooltip from "./Tooltip"; -// Mocking DOMRect for Radix Primitive Popover -(global as any).globalThis = global; -(global as any).DOMRect = { - fromRect: () => ({ top: 0, left: 0, bottom: 0, right: 0, width: 0, height: 0, x: 0, y: 0 }), -}; -(global as any).ResizeObserver = class ResizeObserver { - observe() {} - unobserve() {} - disconnect() {} -}; +global.ResizeObserver = jest.fn().mockImplementation(() => ({ + observe: jest.fn(), + unobserve: jest.fn(), + disconnect: jest.fn(), +})); describe("Tooltip component accessibility tests", () => { it("Should not have basic accessibility issues for bottom position", async () => { diff --git a/packages/lib/src/tooltip/Tooltip.stories.tsx b/packages/lib/src/tooltip/Tooltip.stories.tsx index d11dbde965..c1963152ae 100644 --- a/packages/lib/src/tooltip/Tooltip.stories.tsx +++ b/packages/lib/src/tooltip/Tooltip.stories.tsx @@ -1,11 +1,11 @@ +import { Meta, StoryObj } from "@storybook/react"; import { userEvent, within } from "@storybook/test"; +import DxcTooltip from "./Tooltip"; import ExampleContainer from "../../.storybook/components/ExampleContainer"; import Title from "../../.storybook/components/Title"; import DxcButton from "../button/Button"; import DxcFlex from "../flex/Flex"; import DxcInset from "../inset/Inset"; -import DxcTooltip from "./Tooltip"; -import { Meta, StoryObj } from "@storybook/react"; export default { title: "Tooltip", diff --git a/packages/lib/src/tooltip/Tooltip.test.tsx b/packages/lib/src/tooltip/Tooltip.test.tsx index 4aa79f1b27..11227c92b7 100644 --- a/packages/lib/src/tooltip/Tooltip.test.tsx +++ b/packages/lib/src/tooltip/Tooltip.test.tsx @@ -1,19 +1,14 @@ -import "@testing-library/jest-dom"; -import { render, screen, waitFor } from "@testing-library/react"; +import { render, waitFor, screen } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; -import DxcButton from "../button/Button"; import DxcTooltip from "./Tooltip"; +import DxcButton from "../button/Button"; +import "@testing-library/jest-dom"; -// Mocking DOMRect for Radix Primitive Popover -(global as any).globalThis = global; -(global as any).DOMRect = { - fromRect: () => ({ top: 0, left: 0, bottom: 0, right: 0, width: 0, height: 0, x: 0, y: 0 }), -}; -(global as any).ResizeObserver = class ResizeObserver { - observe() {} - unobserve() {} - disconnect() {} -}; +global.ResizeObserver = jest.fn().mockImplementation(() => ({ + observe: jest.fn(), + unobserve: jest.fn(), + disconnect: jest.fn(), +})); describe("Tooltip component tests", () => { test("Tooltip does not render by default", async () => { @@ -36,7 +31,8 @@ describe("Tooltip component tests", () => { ); const triggerElement = getByText("Hoverable button"); userEvent.hover(triggerElement); - await screen.findByRole("tooltip", { name: "Tooltip Test" }); + const tooltipElement = await screen.findByRole("tooltip", { name: "Tooltip Test" }); + expect(tooltipElement).toBeInTheDocument(); }); test("Tooltip stops being rendered when hover is stopped", async () => { diff --git a/packages/lib/src/tooltip/Tooltip.tsx b/packages/lib/src/tooltip/Tooltip.tsx index 8d055a2453..9f976b2b13 100644 --- a/packages/lib/src/tooltip/Tooltip.tsx +++ b/packages/lib/src/tooltip/Tooltip.tsx @@ -1,9 +1,8 @@ import styled from "@emotion/styled"; -import TooltipPropsType, { TooltipWrapperProps } from "./types"; import { useContext } from "react"; -import { Root, Trigger, Portal, Arrow, Content } from "@radix-ui/react-tooltip"; -import { Provider } from "@radix-ui/react-tooltip"; -import { TooltipContext } from "./TooltipContext"; +import { Root, Trigger, Portal, Arrow, Content, Provider } from "@radix-ui/react-tooltip"; +import TooltipPropsType, { TooltipWrapperProps } from "./types"; +import TooltipContext from "./TooltipContext"; const TooltipTriggerContainer = styled.div` position: relative; @@ -139,6 +138,6 @@ export const Tooltip = ({ export const TooltipWrapper = ({ condition, children, label }: TooltipWrapperProps) => condition ? <Tooltip label={label}>{children}</Tooltip> : <>{children}</>; -export default function DxcTooltip(props: TooltipPropsType) { - return <Tooltip {...props} hasAdditionalContainer />; -} +const DxcTooltip = (props: TooltipPropsType) => <Tooltip {...props} hasAdditionalContainer />; + +export default DxcTooltip; diff --git a/packages/lib/src/tooltip/TooltipContext.tsx b/packages/lib/src/tooltip/TooltipContext.tsx index 04e597ce47..7452e352e1 100644 --- a/packages/lib/src/tooltip/TooltipContext.tsx +++ b/packages/lib/src/tooltip/TooltipContext.tsx @@ -1,3 +1,3 @@ import { createContext } from "react"; -export const TooltipContext = createContext<boolean>(false); +export default createContext<boolean>(false); diff --git a/packages/lib/src/tooltip/types.tsx b/packages/lib/src/tooltip/types.ts similarity index 100% rename from packages/lib/src/tooltip/types.tsx rename to packages/lib/src/tooltip/types.ts diff --git a/packages/lib/src/typography/types.ts b/packages/lib/src/typography/types.ts index d686ea1812..d88b56257d 100644 --- a/packages/lib/src/typography/types.ts +++ b/packages/lib/src/typography/types.ts @@ -35,6 +35,6 @@ export type Props = { whiteSpace?: "normal" | "nowrap" | "pre" | "pre-line" | "pre-wrap"; }; -export default Props; - export type TypographyContextProps = Required<Omit<Props, "children">>; + +export default Props; diff --git a/packages/lib/src/utils/FocusLock.tsx b/packages/lib/src/utils/FocusLock.tsx index 7a7ed2df4f..bd45bd976d 100644 --- a/packages/lib/src/utils/FocusLock.tsx +++ b/packages/lib/src/utils/FocusLock.tsx @@ -19,15 +19,15 @@ const focusableQuery = [ `[tabindex]${not.negTabIndex}${not.disabled}`, ].join(","); -const getFocusableElements = (container: HTMLElement): HTMLElement[] => - Array.prototype.slice - .call(container.querySelectorAll(focusableQuery)) - .filter( - (element: HTMLElement) => - element.getAttribute("aria-hidden") !== "true" && - window.getComputedStyle(element).display !== "none" && - window.getComputedStyle(element).visibility !== "hidden" - ); +const getFocusableElements = (container: HTMLElement): HTMLElement[] => { + const elements = Array.from(container.querySelectorAll<HTMLElement>(focusableQuery)); + return elements.filter( + (element) => + element.getAttribute("aria-hidden") !== "true" && + window.getComputedStyle(element).display !== "none" && + window.getComputedStyle(element).visibility !== "hidden" + ); +}; /** * This function will try to focus the element and return true if it was able to receive the focus. @@ -46,11 +46,12 @@ const attemptFocus = (element: HTMLElement): boolean => { * @returns boolean: true if element is contained inside a Radix Portal, false otherwise. */ const radixPortalContains = (activeElement: Node): boolean => { - const radixPortals = document.querySelectorAll("[data-radix-portal]"); - const radixPoppers = document.querySelectorAll("[data-radix-popper-content-wrapper]"); + const radixPortals = Array.from(document.querySelectorAll<HTMLElement>("[data-radix-portal]")); + const radixPoppers = Array.from(document.querySelectorAll<HTMLElement>("[data-radix-popper-content-wrapper]")); + return ( - Array.prototype.slice.call(radixPortals).some((portal) => portal.contains(activeElement)) || - Array.prototype.slice.call(radixPoppers).some((popper) => popper.contains(activeElement)) + radixPortals.some((portal) => portal.contains(activeElement)) || + radixPoppers.some((popper) => popper.contains(activeElement)) ); }; @@ -67,7 +68,9 @@ const useFocusableElements = (ref: MutableRefObject<HTMLDivElement | null>): HTM setFocusableElements(getFocusableElements(ref.current)); const observer = new MutationObserver(() => { - if (ref.current != null) setFocusableElements(getFocusableElements(ref.current)); + if (ref.current != null) { + setFocusableElements(getFocusableElements(ref.current)); + } }); observer.observe(ref.current, { childList: true, subtree: true }); return () => { @@ -94,8 +97,11 @@ const FocusLock = ({ children }: { children: ReactNode }): JSX.Element => { const focusFirst = useCallback(() => { if (focusableElements != null) { - if (focusableElements.length === 0) childrenContainerRef.current?.focus(); - else if (focusableElements.length > 0) focusableElements.some((element) => attemptFocus(element)); + if (focusableElements.length === 0) { + childrenContainerRef.current?.focus(); + } else if (focusableElements.length > 0) { + focusableElements.some((element) => attemptFocus(element)); + } } }, [focusableElements]); @@ -107,7 +113,9 @@ const FocusLock = ({ children }: { children: ReactNode }): JSX.Element => { }; const focusLock = (event: KeyboardEvent<HTMLDivElement>) => { - if (event.key === "Tab" && focusableElements?.length === 0) event.preventDefault(); + if (event.key === "Tab" && focusableElements?.length === 0) { + event.preventDefault(); + } }; useEffect(() => { @@ -130,8 +138,9 @@ const FocusLock = ({ children }: { children: ReactNode }): JSX.Element => { container?.previousElementSibling?.contains(target) || radixPortalContains(target) ) - ) + ) { focusFirst(); + } }; document.addEventListener("focusout", focusGuardHandler); diff --git a/packages/lib/src/wizard/Wizard.stories.tsx b/packages/lib/src/wizard/Wizard.stories.tsx index 5271e88c31..1cbbf1bb43 100644 --- a/packages/lib/src/wizard/Wizard.stories.tsx +++ b/packages/lib/src/wizard/Wizard.stories.tsx @@ -1,8 +1,7 @@ -import { userEvent, within } from "@storybook/test"; +import { Meta, StoryObj } from "@storybook/react"; import ExampleContainer from "../../.storybook/components/ExampleContainer"; import Title from "../../.storybook/components/Title"; import DxcWizard from "./Wizard"; -import { Meta, StoryObj } from "@storybook/react"; import DxcContainer from "../container/Container"; export default { diff --git a/packages/lib/src/wizard/Wizard.test.tsx b/packages/lib/src/wizard/Wizard.test.tsx index d64d101ef7..83d14e9777 100644 --- a/packages/lib/src/wizard/Wizard.test.tsx +++ b/packages/lib/src/wizard/Wizard.test.tsx @@ -114,7 +114,7 @@ describe("Wizard components tests", () => { }); test("Controlled wizard function is called", () => { - const onClick = jest.fn((i) => i); + const onClick = jest.fn((_i: number) => {}); const { getAllByRole } = render( <DxcWizard currentStep={0} @@ -130,8 +130,12 @@ describe("Wizard components tests", () => { /> ); const steps = getAllByRole("button"); - steps[1] && fireEvent.click(steps[1]); - steps[0] && fireEvent.click(steps[0]); + if (steps[1]) { + fireEvent.click(steps[1]); + } + if (steps[0]) { + fireEvent.click(steps[0]); + } expect(onClick).toHaveBeenCalledTimes(2); expect(onClick).toHaveBeenNthCalledWith(1, 1); expect(onClick).toHaveBeenNthCalledWith(2, 0); diff --git a/packages/lib/src/wizard/Wizard.tsx b/packages/lib/src/wizard/Wizard.tsx index 2b523492c8..aa15ebfb89 100644 --- a/packages/lib/src/wizard/Wizard.tsx +++ b/packages/lib/src/wizard/Wizard.tsx @@ -6,6 +6,7 @@ import DxcIcon from "../icon/Icon"; import WizardPropsType, { StepProps } from "./types"; import DxcFlex from "../flex/Flex"; import icons from "./Icons"; +import { css } from "@emotion/react"; const Wizard = styled.div<{ margin: WizardPropsType["margin"]; @@ -103,13 +104,14 @@ const Step = styled.button<{ } ${({ unvisited }) => unvisited && - `${IconContainer} { - border-color: var(--border-color-neutral-strongest); - } - ${IconContainer}, ${Number}, ${Label}, ${Description} { - color: var(--color-fg-neutral-stronger); - } - `} + css` + ${IconContainer} { + border-color: var(--border-color-neutral-strongest); + } + ${IconContainer}, ${Number}, ${Label}, ${Description} { + color: var(--color-fg-neutral-stronger); + } + `} &:focus:enabled { outline: var(--border-width-m) var(--border-style-default) var(--border-color-secondary-medium); } diff --git a/packages/lib/test/accessibility/axe-helper.ts b/packages/lib/test/accessibility/axe-helper.ts index 77b9611cfe..c20cb357ee 100644 --- a/packages/lib/test/accessibility/axe-helper.ts +++ b/packages/lib/test/accessibility/axe-helper.ts @@ -1,5 +1,5 @@ import { configureAxe } from "jest-axe"; -import { disabledRules } from "./rules/common/disabledRules"; +import disabledRules from "./rules/common/disabledRules"; export const formatRules = (rules: string[]) => rules.reduce( diff --git a/packages/lib/test/accessibility/rules/common/disabledRules.ts b/packages/lib/test/accessibility/rules/common/disabledRules.ts index 88e53db2be..ff2741a990 100644 --- a/packages/lib/test/accessibility/rules/common/disabledRules.ts +++ b/packages/lib/test/accessibility/rules/common/disabledRules.ts @@ -1,7 +1,7 @@ /** * Array of accessibility rule IDs to be disabled in both Jest and Storybook. */ -export const disabledRules = [ +const disabledRules = [ // Disable heading order rule to prevent errors from using h2 and h4 in the titles of the stories "heading-order", // Disable autocomplete valid rule to prevent errors from "nope" which is used on purpose as an invalid autocomplete value @@ -13,3 +13,5 @@ export const disabledRules = [ // TODO: REMOVE "color-contrast", ]; + +export default disabledRules; diff --git a/packages/lib/test/accessibility/rules/specific/breadcrumbs/disabledRules.ts b/packages/lib/test/accessibility/rules/specific/breadcrumbs/disabledRules.ts index c37ae007fa..774eb12f92 100644 --- a/packages/lib/test/accessibility/rules/specific/breadcrumbs/disabledRules.ts +++ b/packages/lib/test/accessibility/rules/specific/breadcrumbs/disabledRules.ts @@ -2,7 +2,9 @@ * Array of accessibility rule IDs to be disabled in both Jest and Storybook for the breadcrumbs component. * */ -export const disabledRules = [ +const disabledRules = [ // Disable landmark unique valid rule to prevent errors from having multiple nav in the same page (that can happen in testing environments) "landmark-unique", ]; + +export default disabledRules; diff --git a/packages/lib/test/accessibility/rules/specific/data-grid/disabledRules.ts b/packages/lib/test/accessibility/rules/specific/data-grid/disabledRules.ts index 52c467b57e..69c21fd7e2 100644 --- a/packages/lib/test/accessibility/rules/specific/data-grid/disabledRules.ts +++ b/packages/lib/test/accessibility/rules/specific/data-grid/disabledRules.ts @@ -2,7 +2,9 @@ * Array of accessibility rule IDs to be disabled in both Jest and Storybook for the data grid component. * */ -export const disabledRules = [ +const disabledRules = [ // Disable scrollable region focusable rule to prevent errors from having an empty header for the expandable data grids "empty-table-header", ]; + +export default disabledRules; diff --git a/packages/lib/test/accessibility/rules/specific/date-input/disabledRules.ts b/packages/lib/test/accessibility/rules/specific/date-input/disabledRules.ts index 4c9b3b9bbc..728ef130ba 100644 --- a/packages/lib/test/accessibility/rules/specific/date-input/disabledRules.ts +++ b/packages/lib/test/accessibility/rules/specific/date-input/disabledRules.ts @@ -2,8 +2,10 @@ * Array of accessibility rule IDs to be disabled in both Jest and Storybook for the date input component. * */ -export const disabledRules = [ +const disabledRules = [ // TODO: Remove when the false positive is fixed // Disable aria allowed rule to prevent false positive from gridcell role not being allowed in buttons "aria-allowed-role", ]; + +export default disabledRules; diff --git a/packages/lib/test/accessibility/rules/specific/footer/disabledRules.ts b/packages/lib/test/accessibility/rules/specific/footer/disabledRules.ts index 942b53bfa7..0143d0556e 100644 --- a/packages/lib/test/accessibility/rules/specific/footer/disabledRules.ts +++ b/packages/lib/test/accessibility/rules/specific/footer/disabledRules.ts @@ -2,9 +2,11 @@ * Array of accessibility rule IDs to be disabled in both Jest and Storybook for the footer component. * */ -export const disabledRules = [ +const disabledRules = [ // Disable landmark duplicate content info rule to prevent errors from having multiple footers in the same page (that can happen in testing environments) "landmark-no-duplicate-contentinfo", // Disable landmark unique valid rule to prevent errors from having multiple footers in the same page (that can happen in testing environments) "landmark-unique", ]; + +export default disabledRules; diff --git a/packages/lib/test/accessibility/rules/specific/header/disabledRules.ts b/packages/lib/test/accessibility/rules/specific/header/disabledRules.ts index c608dd0620..c864764443 100644 --- a/packages/lib/test/accessibility/rules/specific/header/disabledRules.ts +++ b/packages/lib/test/accessibility/rules/specific/header/disabledRules.ts @@ -2,9 +2,11 @@ * Array of accessibility rule IDs to be disabled in both Jest and Storybook for the header component. * */ -export const disabledRules = [ +const disabledRules = [ // Disable landmark duplicate banner rule to prevent errors from having multiple headers in the same page (that can happen in testing environments) "landmark-no-duplicate-banner", // Disable landmark unique valid rule to prevent errors from having multiple headers in the same page (that can happen in testing environments) "landmark-unique", ]; + +export default disabledRules; diff --git a/packages/lib/test/accessibility/rules/specific/resultset-table/disabledRules.ts b/packages/lib/test/accessibility/rules/specific/resultset-table/disabledRules.ts index 0c8413f962..c8b0b11468 100644 --- a/packages/lib/test/accessibility/rules/specific/resultset-table/disabledRules.ts +++ b/packages/lib/test/accessibility/rules/specific/resultset-table/disabledRules.ts @@ -2,8 +2,10 @@ * Array of accessibility rule IDs to be disabled in both Jest and Storybook for the resultset table component. * */ -export const disabledRules = [ +const disabledRules = [ // TODO: Find a better solution - // Disable scrollable region focusable rule to prevent errors from having scrollable tables with no focusable elements + // Disable scrollable region focusable rule to prevent errors from having scrollable tables with no focusable elements "scrollable-region-focusable", ]; + +export default disabledRules; diff --git a/packages/lib/test/accessibility/rules/specific/select/disabledRules.ts b/packages/lib/test/accessibility/rules/specific/select/disabledRules.ts index 25ad380084..42852ed8a3 100644 --- a/packages/lib/test/accessibility/rules/specific/select/disabledRules.ts +++ b/packages/lib/test/accessibility/rules/specific/select/disabledRules.ts @@ -2,8 +2,10 @@ * Array of accessibility rule IDs to be disabled in both Jest and Storybook for the header component. * */ -export const disabledRules = [ +const disabledRules = [ // TODO: Work on nested interaction with the DxcCheckbox component to prevent these issues "nested-interactive", - "scrollable-region-focusable" + "scrollable-region-focusable", ]; + +export default disabledRules; diff --git a/packages/lib/test/accessibility/rules/specific/switch/disabledRules.ts b/packages/lib/test/accessibility/rules/specific/switch/disabledRules.ts index c0f57cafad..49fb875b9b 100644 --- a/packages/lib/test/accessibility/rules/specific/switch/disabledRules.ts +++ b/packages/lib/test/accessibility/rules/specific/switch/disabledRules.ts @@ -2,7 +2,9 @@ * Array of accessibility rule IDs to be disabled in both Jest and Storybook for the switch component. * */ -export const disabledRules = [ +const disabledRules = [ // Disable aria toggle field name rule to prevent errors from having switches with no label on purpose "aria-toggle-field-name", ]; + +export default disabledRules; diff --git a/packages/lib/test/accessibility/rules/specific/table/disabledRules.ts b/packages/lib/test/accessibility/rules/specific/table/disabledRules.ts index e028827cf7..899019e395 100644 --- a/packages/lib/test/accessibility/rules/specific/table/disabledRules.ts +++ b/packages/lib/test/accessibility/rules/specific/table/disabledRules.ts @@ -2,8 +2,10 @@ * Array of accessibility rule IDs to be disabled in both Jest and Storybook for the table component. * */ -export const disabledRules = [ +const disabledRules = [ // TODO: Find a better solution - // Disable scrollable region focusable rule to prevent errors from having scrollable tables with no focusable elements + // Disable scrollable region focusable rule to prevent errors from having scrollable tables with no focusable elements "scrollable-region-focusable", ]; + +export default disabledRules; diff --git a/packages/lib/test/mocks/domRectMock.ts b/packages/lib/test/mocks/domRectMock.ts new file mode 100644 index 0000000000..1a8f57782e --- /dev/null +++ b/packages/lib/test/mocks/domRectMock.ts @@ -0,0 +1,31 @@ +class MockDOMRect implements DOMRect { + x = 0; + y = 0; + width = 0; + height = 0; + top = 0; + left = 0; + bottom = 0; + right = 0; + + constructor(x?: number, y?: number, width?: number, height?: number) { + this.x = x ?? 0; + this.y = y ?? 0; + this.width = width ?? 0; + this.height = height ?? 0; + this.top = this.y; + this.left = this.x; + this.bottom = this.y + this.height; + this.right = this.x + this.width; + } + + toJSON() { + return {}; + } + + static fromRect(rect?: DOMRectInit) { + return new MockDOMRect(rect?.x, rect?.y, rect?.width, rect?.height); + } +} + +export default MockDOMRect; diff --git a/packages/lib/test/mocks/pngMock.js b/packages/lib/test/mocks/pngMock.js deleted file mode 100644 index cc6e23970a..0000000000 --- a/packages/lib/test/mocks/pngMock.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = 'ImageMock'; \ No newline at end of file diff --git a/packages/lib/test/mocks/pngMock.ts b/packages/lib/test/mocks/pngMock.ts new file mode 100644 index 0000000000..6682097e5d --- /dev/null +++ b/packages/lib/test/mocks/pngMock.ts @@ -0,0 +1 @@ +export default "ImageMock"; diff --git a/packages/lib/test/mocks/svgMock.js b/packages/lib/test/mocks/svgMock.js deleted file mode 100644 index 948c11557d..0000000000 --- a/packages/lib/test/mocks/svgMock.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = 'IconMock'; \ No newline at end of file diff --git a/packages/lib/test/mocks/svgMock.ts b/packages/lib/test/mocks/svgMock.ts new file mode 100644 index 0000000000..6aaf141eba --- /dev/null +++ b/packages/lib/test/mocks/svgMock.ts @@ -0,0 +1 @@ +export default "IconMock"; diff --git a/packages/lib/tsconfig.json b/packages/lib/tsconfig.json index ad4b33f44e..4a49272b0c 100644 --- a/packages/lib/tsconfig.json +++ b/packages/lib/tsconfig.json @@ -6,5 +6,5 @@ "forceConsistentCasingInFileNames": true }, "include": ["src", "test", ".", ".storybook/**/*"], - "exclude": ["node_modules", "dist", ".turbo"] + "exclude": ["node_modules", "dist", "coverage", ".turbo"] } diff --git a/packages/lib/tsconfig.lint.json b/packages/lib/tsconfig.lint.json index 422c64dbc2..d3855d5bea 100644 --- a/packages/lib/tsconfig.lint.json +++ b/packages/lib/tsconfig.lint.json @@ -1,8 +1,4 @@ { - "extends": "@dxc-technology/typescript-config/react-library.json", - "compilerOptions": { - "outDir": "dist" - }, - "include": ["src", "turbo"], - "exclude": ["node_modules", "dist"] + "extends": "./tsconfig.json", + "exclude": ["node_modules", "dist", ".turbo", "coverage"] } diff --git a/packages/typescript-config/base.json b/packages/typescript-config/base.json index a6d77a89f5..d8161a2219 100644 --- a/packages/typescript-config/base.json +++ b/packages/typescript-config/base.json @@ -13,6 +13,6 @@ "resolveJsonModule": true, "skipLibCheck": true, "strict": true, - "target": "ES2022", + "target": "ES2022" } } diff --git a/scripts/copy-readme.js b/scripts/copy-readme.js index 06baa71f0d..dfc1f60540 100644 --- a/scripts/copy-readme.js +++ b/scripts/copy-readme.js @@ -1,3 +1,3 @@ -const fs = require('fs'); +const fs = require("fs"); -fs.createReadStream('../../README.md').pipe(fs.createWriteStream('../lib/README.md')); \ No newline at end of file +fs.createReadStream("../../README.md").pipe(fs.createWriteStream("../lib/README.md")); diff --git a/scripts/create-version.js b/scripts/create-version.js index af3736ab48..c383ab1c01 100644 --- a/scripts/create-version.js +++ b/scripts/create-version.js @@ -7,8 +7,7 @@ const setVersion = () => { }; const jsonData = JSON.stringify(object); const versionDirectory = "./catalog/version/"; - if (!fs.existsSync(versionDirectory)) - fs.mkdirSync(versionDirectory, { recursive: true }); + if (!fs.existsSync(versionDirectory)) fs.mkdirSync(versionDirectory, { recursive: true }); fs.writeFile(`${versionDirectory}version.json`, jsonData, (err) => { if (err) throw err; }); diff --git a/scripts/package-lock.json b/scripts/package-lock.json deleted file mode 100644 index ae5e700ad4..0000000000 --- a/scripts/package-lock.json +++ /dev/null @@ -1,233 +0,0 @@ -{ - "name": "@dxc-technology/halstack-react", - "version": "0.0.0", - "lockfileVersion": 1, - "requires": true, - "dependencies": { - "available-typed-arrays": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz", - "integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==" - }, - "aws-sdk": { - "version": "2.1369.0", - "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.1369.0.tgz", - "integrity": "sha512-DdCQjlhQDi9w8J4moqECrrp9ARWCay0UI38adPSS0GG43gh3bl3OoMlgKJ8aZxi4jUvzE48K9yhFHz4y/mazZw==", - "requires": { - "buffer": "4.9.2", - "events": "1.1.1", - "ieee754": "1.1.13", - "jmespath": "0.16.0", - "querystring": "0.2.0", - "sax": "1.2.1", - "url": "0.10.3", - "util": "^0.12.4", - "uuid": "8.0.0", - "xml2js": "0.5.0" - } - }, - "base64-js": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==" - }, - "buffer": { - "version": "4.9.2", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.2.tgz", - "integrity": "sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg==", - "requires": { - "base64-js": "^1.0.2", - "ieee754": "^1.1.4", - "isarray": "^1.0.0" - } - }, - "call-bind": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", - "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", - "requires": { - "function-bind": "^1.1.1", - "get-intrinsic": "^1.0.2" - } - }, - "events": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/events/-/events-1.1.1.tgz", - "integrity": "sha512-kEcvvCBByWXGnZy6JUlgAp2gBIUjfCAV6P6TgT1/aaQKcmuAEC4OZTV1I4EWQLz2gxZw76atuVyvHhTxvi0Flw==" - }, - "for-each": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", - "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", - "requires": { - "is-callable": "^1.1.3" - } - }, - "function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" - }, - "get-intrinsic": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.0.tgz", - "integrity": "sha512-L049y6nFOuom5wGyRc3/gdTLO94dySVKRACj1RmJZBQXlbTMhtNIgkWkUHq+jYmZvKf14EW1EoJnnjbmoHij0Q==", - "requires": { - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.3" - } - }, - "gopd": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", - "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", - "requires": { - "get-intrinsic": "^1.1.3" - } - }, - "has": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "requires": { - "function-bind": "^1.1.1" - } - }, - "has-symbols": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==" - }, - "has-tostringtag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", - "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", - "requires": { - "has-symbols": "^1.0.2" - } - }, - "ieee754": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz", - "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==" - }, - "inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" - }, - "is-arguments": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz", - "integrity": "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==", - "requires": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" - } - }, - "is-callable": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", - "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==" - }, - "is-generator-function": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.10.tgz", - "integrity": "sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==", - "requires": { - "has-tostringtag": "^1.0.0" - } - }, - "is-typed-array": { - "version": "1.1.10", - "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.10.tgz", - "integrity": "sha512-PJqgEHiWZvMpaFZ3uTc8kHPM4+4ADTlDniuQL7cU/UDA0Ql7F70yGfHph3cLNe+c9toaigv+DFzTJKhc2CtO6A==", - "requires": { - "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.2", - "for-each": "^0.3.3", - "gopd": "^1.0.1", - "has-tostringtag": "^1.0.0" - } - }, - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" - }, - "jmespath": { - "version": "0.16.0", - "resolved": "https://registry.npmjs.org/jmespath/-/jmespath-0.16.0.tgz", - "integrity": "sha512-9FzQjJ7MATs1tSpnco1K6ayiYE3figslrXA72G2HQ/n76RzvYlofyi5QM+iX4YRs/pu3yzxlVQSST23+dMDknw==" - }, - "punycode": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", - "integrity": "sha512-RofWgt/7fL5wP1Y7fxE7/EmTLzQVnB0ycyibJ0OOHIlJqTNzglYFxVwETOcIoJqJmpDXJ9xImDv+Fq34F/d4Dw==" - }, - "querystring": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", - "integrity": "sha512-X/xY82scca2tau62i9mDyU9K+I+djTMUsvwf7xnUX5GLvVzgJybOJf4Y6o9Zx3oJK/LSXg5tTZBjwzqVPaPO2g==" - }, - "sax": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.1.tgz", - "integrity": "sha512-8I2a3LovHTOpm7NV5yOyO8IHqgVsfK4+UuySrXU8YXkSRX7k6hCV9b3HrkKCr3nMpgj+0bmocaJJWpvp1oc7ZA==" - }, - "url": { - "version": "0.10.3", - "resolved": "https://registry.npmjs.org/url/-/url-0.10.3.tgz", - "integrity": "sha512-hzSUW2q06EqL1gKM/a+obYHLIO6ct2hwPuviqTTOcfFVc61UbfJ2Q32+uGL/HCPxKqrdGB5QUwIe7UqlDgwsOQ==", - "requires": { - "punycode": "1.3.2", - "querystring": "0.2.0" - } - }, - "util": { - "version": "0.12.5", - "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz", - "integrity": "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==", - "requires": { - "inherits": "^2.0.3", - "is-arguments": "^1.0.4", - "is-generator-function": "^1.0.7", - "is-typed-array": "^1.1.3", - "which-typed-array": "^1.1.2" - } - }, - "uuid": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.0.0.tgz", - "integrity": "sha512-jOXGuXZAWdsTH7eZLtyXMqUb9EcWMGZNbL9YcGBJl4MH4nrxHmZJhEHvyLFrkxo+28uLb/NYRcStH48fnD0Vzw==" - }, - "which-typed-array": { - "version": "1.1.9", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.9.tgz", - "integrity": "sha512-w9c4xkx6mPidwp7180ckYWfMmvxpjlZuIudNtDf4N/tTAUB8VJbX25qZoAsrtGuYNnGw3pa0AXgbGKRB8/EceA==", - "requires": { - "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.2", - "for-each": "^0.3.3", - "gopd": "^1.0.1", - "has-tostringtag": "^1.0.0", - "is-typed-array": "^1.1.10" - } - }, - "xml2js": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.5.0.tgz", - "integrity": "sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA==", - "requires": { - "sax": ">=0.6.0", - "xmlbuilder": "~11.0.0" - } - }, - "xmlbuilder": { - "version": "11.0.1", - "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", - "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==" - } - } -}