From 3858f10e9df27059739fe7c6c8cf5624282e56d3 Mon Sep 17 00:00:00 2001 From: Jialecl Date: Thu, 4 Dec 2025 15:14:57 +0100 Subject: [PATCH 001/275] Adding missing tags in documentation --- .../components/header/code/HeaderCodePage.tsx | 31 ++++++++++++++++--- .../sidenav/code/SidenavCodePage.tsx | 13 ++++++++ 2 files changed, 39 insertions(+), 5 deletions(-) diff --git a/apps/website/screens/components/header/code/HeaderCodePage.tsx b/apps/website/screens/components/header/code/HeaderCodePage.tsx index 6f330593be..6f45766ae9 100644 --- a/apps/website/screens/components/header/code/HeaderCodePage.tsx +++ b/apps/website/screens/components/header/code/HeaderCodePage.tsx @@ -2,6 +2,7 @@ import { DxcFlex, DxcTable } from "@dxc-technology/halstack-react"; import DocFooter from "@/common/DocFooter"; import QuickNavContainer from "@/common/QuickNavContainer"; import Code, { ExtendedTableCode, TableCode } from "@/common/Code"; +import StatusBadge from "@/common/StatusBadge"; const navItemsTypeString = `(GroupItem | Item)[]`; @@ -35,15 +36,25 @@ const sections = [ - appTitle + + + + appTitle + + string - Object used to configure the header application title. + String used to configure the header application title. - - navItems + + + + navItems + + {navItemsTypeString} @@ -65,7 +76,12 @@ const sections = [ - - responsiveBottomContent + + + + responsiveBottomContent + + React.ReactNode @@ -76,7 +92,12 @@ const sections = [ - - sideContent + + + + sideContent + + {"React.ReactNode | (isResponsive: boolean) => React.ReactNode"} diff --git a/apps/website/screens/components/sidenav/code/SidenavCodePage.tsx b/apps/website/screens/components/sidenav/code/SidenavCodePage.tsx index 8eb283ba68..d0bd0bc470 100644 --- a/apps/website/screens/components/sidenav/code/SidenavCodePage.tsx +++ b/apps/website/screens/components/sidenav/code/SidenavCodePage.tsx @@ -38,6 +38,19 @@ const sections = [ + + + + + appTitle + + + + React.ReactNode + + Object used to configure the header application title. + - + From adf064061f45da0ca6404c85f8cc7f3c0dcd9d53 Mon Sep 17 00:00:00 2001 From: PelayoFelgueroso Date: Tue, 9 Dec 2025 12:35:49 +0100 Subject: [PATCH 002/275] Fix string default values in TableCode for better formatting in documentation --- .../components/data-grid/code/DataGridCodePage.tsx | 12 +++++++++--- .../components/date-input/code/DateInputCodePage.tsx | 4 +++- .../components/select/code/SelectCodePage.tsx | 4 +++- .../components/textarea/code/TextareaCodePage.tsx | 4 +++- .../typography/code/TypographyCodePage.tsx | 12 ++++++------ 5 files changed, 24 insertions(+), 12 deletions(-) diff --git a/apps/website/screens/components/data-grid/code/DataGridCodePage.tsx b/apps/website/screens/components/data-grid/code/DataGridCodePage.tsx index ecfdd9237e..679b3bea28 100644 --- a/apps/website/screens/components/data-grid/code/DataGridCodePage.tsx +++ b/apps/website/screens/components/data-grid/code/DataGridCodePage.tsx @@ -146,7 +146,9 @@ const sections = [ number Number of items per page. - 5 + + 5 + itemsPerPageFunction @@ -270,7 +272,9 @@ const sections = [ boolean If true, a select component for navigation between pages will be displayed. - true + + true + showPaginator @@ -278,7 +282,9 @@ const sections = [ boolean If true, paginator will be displayed. - false + + false + summaryRow diff --git a/apps/website/screens/components/date-input/code/DateInputCodePage.tsx b/apps/website/screens/components/date-input/code/DateInputCodePage.tsx index 6dc0c0d3a6..766731813b 100644 --- a/apps/website/screens/components/date-input/code/DateInputCodePage.tsx +++ b/apps/website/screens/components/date-input/code/DateInputCodePage.tsx @@ -31,7 +31,9 @@ const sections = [ Specifies a string to be used as the name for the date input element when no label is provided. - 'Date input' + + 'Date input' + autocomplete diff --git a/apps/website/screens/components/select/code/SelectCodePage.tsx b/apps/website/screens/components/select/code/SelectCodePage.tsx index 82ed44dbb0..177579c903 100644 --- a/apps/website/screens/components/select/code/SelectCodePage.tsx +++ b/apps/website/screens/components/select/code/SelectCodePage.tsx @@ -269,7 +269,9 @@ const sections = [ Defines the search mode when searchable is true. If true, matches options that start with the search text. If false, matches options that contain the search text anywhere in their label. - false + + false + size diff --git a/apps/website/screens/components/textarea/code/TextareaCodePage.tsx b/apps/website/screens/components/textarea/code/TextareaCodePage.tsx index d00e56ed52..af41924602 100644 --- a/apps/website/screens/components/textarea/code/TextareaCodePage.tsx +++ b/apps/website/screens/components/textarea/code/TextareaCodePage.tsx @@ -29,7 +29,9 @@ const sections = [ Specifies a string to be used as the name for the textarea element when no label is provided. - 'Text area' + + 'Text area' + autocomplete diff --git a/apps/website/screens/components/typography/code/TypographyCodePage.tsx b/apps/website/screens/components/typography/code/TypographyCodePage.tsx index 199b916c57..340ee832c4 100644 --- a/apps/website/screens/components/typography/code/TypographyCodePage.tsx +++ b/apps/website/screens/components/typography/code/TypographyCodePage.tsx @@ -54,7 +54,7 @@ const sections = [ Color of the text. - var(--color-fg-neutral-dark) + 'var(--color-fg-neutral-dark)' @@ -78,7 +78,7 @@ const sections = [ Specifies the font-family CSS property of the component. - var(--typography-font-family) + 'var(--typography-font-family)' @@ -90,7 +90,7 @@ const sections = [ Specifies the font-size CSS property of the component. - var(--typography-body-m) + 'var(--typography-body-m)' @@ -114,7 +114,7 @@ const sections = [ Specifies the font-weight CSS property of the component. - var(--typography-body-regular) + 'var(--typography-body-regular)' @@ -126,7 +126,7 @@ const sections = [ Specifies the letter-spacing CSS property of the component. - var(--spacing-gap-none) + 'var(--spacing-gap-none)' @@ -138,7 +138,7 @@ const sections = [ Specifies the line-height CSS property of the component. - var(--height-s) + 'var(--height-s)' From e63f939652e74705fec3680ed72f214683c58757 Mon Sep 17 00:00:00 2001 From: Jialecl Date: Wed, 10 Dec 2025 09:31:36 +0100 Subject: [PATCH 003/275] Fixing build errors caused by eslint config --- apps/website/eslint.config.mjs | 26 +++++++++++++++++++++----- apps/website/package.json | 3 ++- package-lock.json | 17 +++++++++-------- packages/eslint-config/next.js | 22 ---------------------- 4 files changed, 32 insertions(+), 36 deletions(-) diff --git a/apps/website/eslint.config.mjs b/apps/website/eslint.config.mjs index afe2c241fd..d83a5e66da 100644 --- a/apps/website/eslint.config.mjs +++ b/apps/website/eslint.config.mjs @@ -1,9 +1,25 @@ +import nextPlugin from "@next/eslint-plugin-next"; import nextConfig from "@dxc-technology/eslint-config/next.js"; -import { fileURLToPath } from "url"; -import { dirname } from "path"; +import js from "@eslint/js"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; const __filename = fileURLToPath(import.meta.url); -const __dirname = dirname(__filename); +const __dirname = path.dirname(__filename); -/** @type {import("eslint").Config[]} */ -export default [{ ignores: ["out/**", ".next/**", "eslint.config.mjs"] }, ...nextConfig({ tsconfigRootDir: __dirname })]; +export default [ + js.configs.recommended, + { + plugins: { "@next/next": nextPlugin }, + rules: { + ...nextPlugin.configs.recommended.rules, + }, + }, + ...nextConfig({ + tsconfigRootDir: __dirname, + tsconfigName: "tsconfig.lint.json", + }), + { + ignores: ["out/**", ".next/**"], + }, +]; diff --git a/apps/website/package.json b/apps/website/package.json index d4b7466d15..e686a72073 100644 --- a/apps/website/package.json +++ b/apps/website/package.json @@ -29,12 +29,13 @@ }, "devDependencies": { "@dxc-technology/typescript-config": "*", + "@eslint/js": "^9.31.1", "@types/node": "^20", "@types/react": "^18", "@types/react-color": "^3.0.6", "@types/react-dom": "^18", "eslint": "^9.39.1", - "eslint-config-next": "16.0.5", + "eslint-config-next": "16.0.8", "typescript": "^5.6.3" } } diff --git a/package-lock.json b/package-lock.json index 2179947558..fdc5039fb0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -49,12 +49,13 @@ }, "devDependencies": { "@dxc-technology/typescript-config": "*", + "@eslint/js": "^9.31.1", "@types/node": "^20", "@types/react": "^18", "@types/react-color": "^3.0.6", "@types/react-dom": "^18", "eslint": "^9.39.1", - "eslint-config-next": "16.0.5", + "eslint-config-next": "16.0.8", "typescript": "^5.6.3" } }, @@ -4727,9 +4728,9 @@ "integrity": "sha512-4h6Y2NyEkIEN7Z8YxkA27pq6zTkS09bUSYC0xjd0NpwFxjnIKeZEeH591o5WECSmjpUhLn3H2QLJcDye3Uzcvg==" }, "node_modules/@next/eslint-plugin-next": { - "version": "16.0.5", - "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-16.0.5.tgz", - "integrity": "sha512-m1zPz6hsBvQt1CMRz7rTga8OXpRE9rVW4JHCSjW+tswTxiEU+6ev+GTlgm7ZzcCiMEVQAHTNhpEGFzDtVha9qg==", + "version": "16.0.8", + "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-16.0.8.tgz", + "integrity": "sha512-1miV0qXDcLUaOdHridVPCh4i39ElRIAraseVIbb3BEqyZ5ol9sPyjTP/GNTPV5rBxqxjF6/vv5zQTVbhiNaLqA==", "dev": true, "license": "MIT", "dependencies": { @@ -9920,13 +9921,13 @@ } }, "node_modules/eslint-config-next": { - "version": "16.0.5", - "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-16.0.5.tgz", - "integrity": "sha512-9rBjZ/biSpolkIUiqvx/iwJJaz8sxJ6pKWSPptJenpj01HlWbCDeaA1v0yG3a71IIPMplxVCSXhmtP27SXqMdg==", + "version": "16.0.8", + "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-16.0.8.tgz", + "integrity": "sha512-8J5cOAboXIV3f8OD6BOyj7Fik6n/as7J4MboiUSExWruf/lCu1OPR3ZVSdnta6WhzebrmAATEmNSBZsLWA6kbg==", "dev": true, "license": "MIT", "dependencies": { - "@next/eslint-plugin-next": "16.0.5", + "@next/eslint-plugin-next": "16.0.8", "eslint-import-resolver-node": "^0.3.6", "eslint-import-resolver-typescript": "^3.5.2", "eslint-plugin-import": "^2.32.0", diff --git a/packages/eslint-config/next.js b/packages/eslint-config/next.js index 8e3fcb15d3..8f0868364e 100644 --- a/packages/eslint-config/next.js +++ b/packages/eslint-config/next.js @@ -33,27 +33,5 @@ export default function nextConfig({ tsconfigRootDir, tsconfigName = "tsconfig.l "@typescript-eslint/triple-slash-reference": "off", }, }, - { - files: ["**/*.{ts,tsx}"], - plugins: { "@typescript-eslint": tsPlugin }, - languageOptions: { - parser: tsParser, - parserOptions: { - project: join(tsconfigRootDir, tsconfigName), - tsconfigRootDir, - }, - globals: { - ...globals.browser, - ...globals.node, - }, - }, - rules: { - ...tsPlugin.configs.recommended.rules, - ...tsPlugin.configs["recommended-requiring-type-checking"].rules, - "@typescript-eslint/no-unused-vars": ["warn", { argsIgnorePattern: "^_" }], - "no-unused-vars": "off", - "@typescript-eslint/triple-slash-reference": "off", - }, - }, ]; } From 13b5ada964fe8078bc8b4ed9d235c8d43be3f95b Mon Sep 17 00:00:00 2001 From: PelayoFelgueroso Date: Wed, 10 Dec 2025 08:06:47 +0100 Subject: [PATCH 004/275] Fix appLayout Footer prop Description --- .../application-layout/code/ApplicationLayoutCodePage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/website/screens/components/application-layout/code/ApplicationLayoutCodePage.tsx b/apps/website/screens/components/application-layout/code/ApplicationLayoutCodePage.tsx index 01aeedb0a7..8a8942209b 100644 --- a/apps/website/screens/components/application-layout/code/ApplicationLayoutCodePage.tsx +++ b/apps/website/screens/components/application-layout/code/ApplicationLayoutCodePage.tsx @@ -45,7 +45,7 @@ const ApplicationLayoutPropsTable = () => ( Footer of the application layout shown at the bottom of the screen. It is optional and if it is not specified, - the default header will be shown. Please check the Footer documentation{" "} + the default footer will be shown. Please check the Footer documentation{" "} here From 94f62316dcaee2fae6bc165c055dfc462a8b75a9 Mon Sep 17 00:00:00 2001 From: PelayoFelgueroso Date: Wed, 10 Dec 2025 08:10:05 +0100 Subject: [PATCH 005/275] Fix header prop description --- .../application-layout/code/ApplicationLayoutCodePage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/website/screens/components/application-layout/code/ApplicationLayoutCodePage.tsx b/apps/website/screens/components/application-layout/code/ApplicationLayoutCodePage.tsx index 8a8942209b..f688b33c1c 100644 --- a/apps/website/screens/components/application-layout/code/ApplicationLayoutCodePage.tsx +++ b/apps/website/screens/components/application-layout/code/ApplicationLayoutCodePage.tsx @@ -60,7 +60,7 @@ const ApplicationLayoutPropsTable = () => ( Header of the application layout shown at the top of the screen. It is optional and if it is not specified, - the default header will be shown. Please check the Header documentation{" "} + the header will not be shown. Please check the Header documentation{" "} here From 68d1737682994d084c8f7cfa8869226939aa738c Mon Sep 17 00:00:00 2001 From: PelayoFelgueroso Date: Wed, 10 Dec 2025 08:48:14 +0100 Subject: [PATCH 006/275] Add default value to logo prop in footer component --- .../screens/components/footer/code/FooterCodePage.tsx | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/apps/website/screens/components/footer/code/FooterCodePage.tsx b/apps/website/screens/components/footer/code/FooterCodePage.tsx index a4666a0f16..124e5ab5c4 100644 --- a/apps/website/screens/components/footer/code/FooterCodePage.tsx +++ b/apps/website/screens/components/footer/code/FooterCodePage.tsx @@ -20,6 +20,11 @@ const socialLinkTypeString = `{ logo: string | SVG; }[]`; +const defaultFooterLogo = `{ + src: DxcLogo, + alt: "DXC Technology Logo" +}`; + const sections = [ { title: "Props", @@ -85,7 +90,9 @@ const sections = [ {logoTypeString} Logo to be displayed inside the footer. - - + + {defaultFooterLogo} + mode From 4ebc4376475ec790d9cb8964f092962a8a8bc211 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 12 Dec 2025 01:54:12 +0000 Subject: [PATCH 007/275] Bump next from 15.5.7 to 15.5.9 Bumps [next](https://github.com/vercel/next.js) from 15.5.7 to 15.5.9. - [Release notes](https://github.com/vercel/next.js/releases) - [Changelog](https://github.com/vercel/next.js/blob/canary/release.js) - [Commits](https://github.com/vercel/next.js/compare/v15.5.7...v15.5.9) --- updated-dependencies: - dependency-name: next dependency-version: 15.5.9 dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- apps/website/package.json | 2 +- package-lock.json | 16 ++++++++-------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/apps/website/package.json b/apps/website/package.json index e686a72073..22fa36cff8 100644 --- a/apps/website/package.json +++ b/apps/website/package.json @@ -16,7 +16,7 @@ "@emotion/styled": "^11.14.1", "@radix-ui/react-popover": "^1.0.7", "cross-env": "^7.0.3", - "next": "^15.5.7", + "next": "^15.5.9", "raw-loader": "^4.0.2", "react": "^18", "react-color": "^2.19.3", diff --git a/package-lock.json b/package-lock.json index fdc5039fb0..5ae0e24abb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -36,7 +36,7 @@ "@emotion/styled": "^11.14.1", "@radix-ui/react-popover": "^1.0.7", "cross-env": "^7.0.3", - "next": "^15.5.7", + "next": "^15.5.9", "raw-loader": "^4.0.2", "react": "^18", "react-color": "^2.19.3", @@ -4723,9 +4723,9 @@ "license": "MIT" }, "node_modules/@next/env": { - "version": "15.5.7", - "resolved": "https://registry.npmjs.org/@next/env/-/env-15.5.7.tgz", - "integrity": "sha512-4h6Y2NyEkIEN7Z8YxkA27pq6zTkS09bUSYC0xjd0NpwFxjnIKeZEeH591o5WECSmjpUhLn3H2QLJcDye3Uzcvg==" + "version": "15.5.9", + "resolved": "https://registry.npmjs.org/@next/env/-/env-15.5.9.tgz", + "integrity": "sha512-4GlTZ+EJM7WaW2HEZcyU317tIQDjkQIyENDLxYJfSWlfqguN+dHkZgyQTV/7ykvobU7yEH5gKvreNrH4B6QgIg==" }, "node_modules/@next/eslint-plugin-next": { "version": "16.0.8", @@ -15541,11 +15541,11 @@ "peer": true }, "node_modules/next": { - "version": "15.5.7", - "resolved": "https://registry.npmjs.org/next/-/next-15.5.7.tgz", - "integrity": "sha512-+t2/0jIJ48kUpGKkdlhgkv+zPTEOoXyr60qXe68eB/pl3CMJaLeIGjzp5D6Oqt25hCBiBTt8wEeeAzfJvUKnPQ==", + "version": "15.5.9", + "resolved": "https://registry.npmjs.org/next/-/next-15.5.9.tgz", + "integrity": "sha512-agNLK89seZEtC5zUHwtut0+tNrc0Xw4FT/Dg+B/VLEo9pAcS9rtTKpek3V6kVcVwsB2YlqMaHdfZL4eLEVYuCg==", "dependencies": { - "@next/env": "15.5.7", + "@next/env": "15.5.9", "@swc/helpers": "0.5.15", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", From 49d0292a4b1009c4ca7dec526eb9066eaf777b04 Mon Sep 17 00:00:00 2001 From: PelayoFelgueroso Date: Tue, 16 Dec 2025 12:07:16 +0100 Subject: [PATCH 008/275] Fix footer logo documentation --- .../screens/components/footer/code/FooterCodePage.tsx | 10 +++++++--- .../screens/migration/Components16MigrationPage.tsx | 1 - 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/apps/website/screens/components/footer/code/FooterCodePage.tsx b/apps/website/screens/components/footer/code/FooterCodePage.tsx index 124e5ab5c4..943aee2080 100644 --- a/apps/website/screens/components/footer/code/FooterCodePage.tsx +++ b/apps/website/screens/components/footer/code/FooterCodePage.tsx @@ -20,11 +20,13 @@ const socialLinkTypeString = `{ logo: string | SVG; }[]`; -const defaultFooterLogo = `{ +const defaultFooterLogoString = `{ src: DxcLogo, alt: "DXC Technology Logo" }`; +const defaultCopyrightString = `"© DXC Technology \${year}. All rights reserved."`; + const sections = [ { title: "Props", @@ -64,7 +66,9 @@ const sections = [ string The text that will be displayed as copyright disclaimer. - - + + {defaultCopyrightString} + @@ -91,7 +95,7 @@ const sections = [ Logo to be displayed inside the footer. - {defaultFooterLogo} + {defaultFooterLogoString} diff --git a/apps/website/screens/migration/Components16MigrationPage.tsx b/apps/website/screens/migration/Components16MigrationPage.tsx index 0328f88f3a..cda5514bd9 100644 --- a/apps/website/screens/migration/Components16MigrationPage.tsx +++ b/apps/website/screens/migration/Components16MigrationPage.tsx @@ -200,7 +200,6 @@ const sections = [ ), }, - // TODO: CHECK PELAYO PR FOR THE NEW FOOTER API { title: "ApplicationLayout.Footer", content: ( From 3841ee13f552070a51c96cd30202ac219b872a51 Mon Sep 17 00:00:00 2001 From: PelayoFelgueroso Date: Wed, 10 Dec 2025 08:48:14 +0100 Subject: [PATCH 009/275] Add default value to logo prop in footer component --- .../website/screens/components/footer/code/FooterCodePage.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/website/screens/components/footer/code/FooterCodePage.tsx b/apps/website/screens/components/footer/code/FooterCodePage.tsx index 943aee2080..657385d029 100644 --- a/apps/website/screens/components/footer/code/FooterCodePage.tsx +++ b/apps/website/screens/components/footer/code/FooterCodePage.tsx @@ -20,7 +20,7 @@ const socialLinkTypeString = `{ logo: string | SVG; }[]`; -const defaultFooterLogoString = `{ +const defaultFooterLogo = `{ src: DxcLogo, alt: "DXC Technology Logo" }`; @@ -95,7 +95,7 @@ const sections = [ Logo to be displayed inside the footer. - {defaultFooterLogoString} + {defaultFooterLogo} From 16574db0012b982b6d0a9b9af10b9881aea3f208 Mon Sep 17 00:00:00 2001 From: PelayoFelgueroso Date: Tue, 16 Dec 2025 12:07:16 +0100 Subject: [PATCH 010/275] Fix footer logo documentation --- .../website/screens/components/footer/code/FooterCodePage.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/website/screens/components/footer/code/FooterCodePage.tsx b/apps/website/screens/components/footer/code/FooterCodePage.tsx index 657385d029..943aee2080 100644 --- a/apps/website/screens/components/footer/code/FooterCodePage.tsx +++ b/apps/website/screens/components/footer/code/FooterCodePage.tsx @@ -20,7 +20,7 @@ const socialLinkTypeString = `{ logo: string | SVG; }[]`; -const defaultFooterLogo = `{ +const defaultFooterLogoString = `{ src: DxcLogo, alt: "DXC Technology Logo" }`; @@ -95,7 +95,7 @@ const sections = [ Logo to be displayed inside the footer. - {defaultFooterLogo} + {defaultFooterLogoString} From 94eed768be54c36ea3eb85be3011f304ccd80a4c Mon Sep 17 00:00:00 2001 From: PelayoFelgueroso Date: Thu, 11 Dec 2025 16:39:34 +0100 Subject: [PATCH 011/275] Fix: set max-width to 100% for Input component in TextInput --- packages/lib/src/text-input/TextInput.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/lib/src/text-input/TextInput.tsx b/packages/lib/src/text-input/TextInput.tsx index ba7c26be01..03c59e4887 100644 --- a/packages/lib/src/text-input/TextInput.tsx +++ b/packages/lib/src/text-input/TextInput.tsx @@ -70,6 +70,7 @@ const TextInput = styled.div<{ const Input = styled.input<{ alignment: TextInputPropsType["alignment"]; }>` + max-width: 100%; background: none; border: none; outline: none; From 7fa7ef9c80f47f84d1cc8608d1807893af7a01de Mon Sep 17 00:00:00 2001 From: PelayoFelgueroso Date: Thu, 11 Dec 2025 12:15:32 +0100 Subject: [PATCH 012/275] Improve migration documentation for version 16, related to using alias tokens in the props of the components instead of hardcoded values --- .../pages/migration/16/component-updates.tsx | 2 +- .../screens/migration/TokensMigrationPage.tsx | 4 ++- .../Components16MigrationPage.tsx | 24 ++++++++++++++ .../migration/components/examples/new.tsx | 33 +++++++++++++++++++ .../components/examples/previous.tsx | 33 +++++++++++++++++++ 5 files changed, 94 insertions(+), 2 deletions(-) rename apps/website/screens/migration/{ => components}/Components16MigrationPage.tsx (94%) create mode 100644 apps/website/screens/migration/components/examples/new.tsx create mode 100644 apps/website/screens/migration/components/examples/previous.tsx diff --git a/apps/website/pages/migration/16/component-updates.tsx b/apps/website/pages/migration/16/component-updates.tsx index 0292cb1a78..0d5d94fa36 100644 --- a/apps/website/pages/migration/16/component-updates.tsx +++ b/apps/website/pages/migration/16/component-updates.tsx @@ -1,5 +1,5 @@ import Head from "next/head"; -import Components16MigrationPage from "screens/migration/Components16MigrationPage"; +import Components16MigrationPage from "screens/migration/components/Components16MigrationPage"; const Components16Migration = () => ( <> diff --git a/apps/website/screens/migration/TokensMigrationPage.tsx b/apps/website/screens/migration/TokensMigrationPage.tsx index a4c80f11c7..6c8f834d25 100644 --- a/apps/website/screens/migration/TokensMigrationPage.tsx +++ b/apps/website/screens/migration/TokensMigrationPage.tsx @@ -66,6 +66,7 @@ const sections = [
  • Migrating color, spacing, and typography overrides to CSS tokens.
  • Replacing any custom component overrides that referenced theme object values.
  • Updating global styles to rely on CSS variables instead of hardcoded values.
  • +
  • Refactoring prop values to rely on alias tokens instead of hardcoded values.
  • ), @@ -153,7 +154,8 @@ return ( This can be applied to colors, fonts, spacings and borders. However, keep in mind that, for now, only core tokens can be overwritten and they affect all the components which are wrapped within the{" "} - HalstackProvider. + HalstackProvider. Note that the former theme prop has been renamed to{" "} + opinionatedTheme. ), diff --git a/apps/website/screens/migration/Components16MigrationPage.tsx b/apps/website/screens/migration/components/Components16MigrationPage.tsx similarity index 94% rename from apps/website/screens/migration/Components16MigrationPage.tsx rename to apps/website/screens/migration/components/Components16MigrationPage.tsx index cda5514bd9..2a2d81203c 100644 --- a/apps/website/screens/migration/Components16MigrationPage.tsx +++ b/apps/website/screens/migration/components/Components16MigrationPage.tsx @@ -4,6 +4,9 @@ import DocFooter from "@/common/DocFooter"; import PageHeading from "@/common/PageHeading"; import Code, { ExtendedTableCode } from "@/common/Code"; import Link from "next/link"; +import Example from "@/common/example/Example"; +import previousExample from "./examples/previous"; +import newExample from "./examples/new"; const sections = [ { @@ -16,6 +19,27 @@ const sections = [ ), }, + { + title: "Usage of components", + content: ( + <> + + In our component props, instead of passing hardcoded values such as 2rem, we should always use an + alias token whenever possible. Only if no suitable alias token exists, a core token or a hardcoded value may + be used. + + Previous version: + + + + For more information about tokens refer to{" "} + + its documentation + + + + ), + }, { title: "Added components", content: ( diff --git a/apps/website/screens/migration/components/examples/new.tsx b/apps/website/screens/migration/components/examples/new.tsx new file mode 100644 index 0000000000..1b22888b6d --- /dev/null +++ b/apps/website/screens/migration/components/examples/new.tsx @@ -0,0 +1,33 @@ +import { DxcContainer, DxcFlex, DxcInset } from "@dxc-technology/halstack-react"; + +const code = `() => { + return ( + + + + + + + + ); +}`; + +const scope = { + DxcFlex, + DxcInset, + DxcContainer, +}; + +export default { code, scope }; diff --git a/apps/website/screens/migration/components/examples/previous.tsx b/apps/website/screens/migration/components/examples/previous.tsx new file mode 100644 index 0000000000..aaf7d28e72 --- /dev/null +++ b/apps/website/screens/migration/components/examples/previous.tsx @@ -0,0 +1,33 @@ +import { DxcContainer, DxcFlex, DxcInset } from "@dxc-technology/halstack-react"; + +const code = `() => { + return ( + + + + + + + + ); +}`; + +const scope = { + DxcFlex, + DxcInset, + DxcContainer, +}; + +export default { code, scope }; From 087026a3ab7e8999120753a268b2301232281bc7 Mon Sep 17 00:00:00 2001 From: Jialecl Date: Tue, 23 Dec 2025 14:39:44 +0100 Subject: [PATCH 013/275] Correct url added to DocFooter --- .../components/Components16MigrationPage.tsx | 32 +++++++++++-------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/apps/website/screens/migration/components/Components16MigrationPage.tsx b/apps/website/screens/migration/components/Components16MigrationPage.tsx index 2a2d81203c..56eab849c7 100644 --- a/apps/website/screens/migration/components/Components16MigrationPage.tsx +++ b/apps/website/screens/migration/components/Components16MigrationPage.tsx @@ -8,6 +8,21 @@ import Example from "@/common/example/Example"; import previousExample from "./examples/previous"; import newExample from "./examples/new"; +const groupItemType = `{ + badge?: ReactElement; + icon?: string | SVG; + label: string; + items: (Item)[]; +}`; + +const itemType = `{ + badge?: ReactElement; + icon?: string | SVG; + label: string; + onSelect?: () => void; + selected?: boolean; +}`; + const sections = [ { title: "Introduction", @@ -179,22 +194,11 @@ const sections = [ The navItems prop accepts an array of Item and GroupItem objects. Each GroupItem has the following structure:

    - {`{ - badge?: ReactElement; - icon?: string | SVG; - label: string; - items: (Item)[]; -}`} + {groupItemType}

    Each Item has the following structure:

    - {`{ - badge?: ReactElement; - icon?: string | SVG; - label: string; - onSelect?: () => void; - selected?: boolean; -}`} + {itemType} @@ -390,7 +394,7 @@ const Components16MigrationPage = () => (
    - + ); From c63f4fe049d5d2b23432191055f3e12ebe1f67a1 Mon Sep 17 00:00:00 2001 From: Pelayo Felgueroso Date: Wed, 7 Jan 2026 11:14:11 +0100 Subject: [PATCH 014/275] Add ewstatus to fullHeight prop --- apps/website/screens/components/flex/code/FlexCodePage.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/apps/website/screens/components/flex/code/FlexCodePage.tsx b/apps/website/screens/components/flex/code/FlexCodePage.tsx index 175a58535f..01af8bfe48 100644 --- a/apps/website/screens/components/flex/code/FlexCodePage.tsx +++ b/apps/website/screens/components/flex/code/FlexCodePage.tsx @@ -136,7 +136,12 @@ const sections = [ - fullHeight + + + + fullHeight + + boolean From e94e3c7b0f7d65b3f43736ffc650d2e42dc08fa4 Mon Sep 17 00:00:00 2001 From: Jialecl Date: Fri, 2 Jan 2026 08:25:03 +0100 Subject: [PATCH 015/275] Internal searchBar component added --- packages/lib/src/layout/ApplicationLayout.tsx | 37 +++++--------- .../lib/src/search-bar/SearchBar.stories.tsx | 48 +++++++++++++++++++ packages/lib/src/search-bar/SearchBar.tsx | 41 ++++++++++++++++ .../lib/src/search-bar/SearchBarTrigger.tsx | 15 ++++++ packages/lib/src/search-bar/types.ts | 32 +++++++++++++ 5 files changed, 149 insertions(+), 24 deletions(-) create mode 100644 packages/lib/src/search-bar/SearchBar.stories.tsx create mode 100644 packages/lib/src/search-bar/SearchBar.tsx create mode 100644 packages/lib/src/search-bar/SearchBarTrigger.tsx create mode 100644 packages/lib/src/search-bar/types.ts diff --git a/packages/lib/src/layout/ApplicationLayout.tsx b/packages/lib/src/layout/ApplicationLayout.tsx index 16b55ab42c..905e7f497f 100644 --- a/packages/lib/src/layout/ApplicationLayout.tsx +++ b/packages/lib/src/layout/ApplicationLayout.tsx @@ -40,22 +40,13 @@ const SidenavContainer = styled.div` overflow: auto; `; -const MainContainer = styled.div` - display: flex; - flex-grow: 1; - flex-direction: column; - width: 100%; - height: 100%; - position: relative; - overflow: auto; -`; - const FooterContainer = styled.div` height: fit-content; width: 100%; `; const MainContentContainer = styled.main` + width: 100%; height: 100%; display: grid; grid-template-rows: 1fr auto; @@ -78,20 +69,18 @@ const DxcApplicationLayout = ({ logo, header, sidenav, footer, children }: Appli {header && {header}} {sidenav && {sidenav}} - - - {findChildType(children, Main)} - - {footer ?? ( - - )} - - - + + {findChildType(children, Main)} + + {footer ?? ( + + )} + + diff --git a/packages/lib/src/search-bar/SearchBar.stories.tsx b/packages/lib/src/search-bar/SearchBar.stories.tsx new file mode 100644 index 0000000000..3649abe23b --- /dev/null +++ b/packages/lib/src/search-bar/SearchBar.stories.tsx @@ -0,0 +1,48 @@ +import { Meta, StoryObj } from "@storybook/react-vite"; +import ExampleContainer from "../../.storybook/components/ExampleContainer"; +import Title from "../../.storybook/components/Title"; +import DxcLink from "./Link"; +import DxcSearchBarTrigger from "./SearchBarTrigger"; +import { useState } from "react"; +import DxcSearchBar from "./SearchBar"; +import DxcFlex from "../flex/Flex"; + +export default { + title: "SearchBar", + component: DxcSearchBar, +} satisfies Meta; + +const Link = () => { + const [showSearch, setShowSearch] = useState(false); + return ( + <> + + <ExampleContainer> + <DxcFlex alignItems="center"> + {!showSearch ? ( + <DxcSearchBarTrigger onTriggerClick={() => setShowSearch(!showSearch)} /> + ) : ( + <DxcSearchBar + placeholder="Search..." + onBlur={(value) => { + console.log("onBlur", value); + }} + onChange={(value) => console.log("onChange", value)} + onEnter={(value) => { + console.log("onEnter", value); + setShowSearch(false); + }} + onCancel={() => setShowSearch(false)} + /> + )} + </DxcFlex> + </ExampleContainer> + </> + ); +}; + +type Story = StoryObj<typeof DxcLink>; + +export const Chromatic: Story = { + render: Link, +}; diff --git a/packages/lib/src/search-bar/SearchBar.tsx b/packages/lib/src/search-bar/SearchBar.tsx new file mode 100644 index 0000000000..b3a4bfae6d --- /dev/null +++ b/packages/lib/src/search-bar/SearchBar.tsx @@ -0,0 +1,41 @@ +import styled from "@emotion/styled"; +import DxcButton from "../button/Button"; +import DxcFlex from "../flex/Flex"; +import { SearchBarProps } from "./types"; + +const SearchBarStyles = styled.input` + width: 100%; + min-width: 200px; + max-width: 720px; + height: var(--height-m); + border-radius: var(--border-radius-xl); + border: var(--border-width-s) var(--border-style-default) var(--border-color-neutral-dark); + box-sizing: border-box; + padding: 0 var(--spacing-padding-s); + color: var(--color-fg-neutral-dark); + font-family: var(--typography-font-family); + font-size: var(--typography-label-m); + font-weight: var(--typography-label-regular); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +`; + +const DxcSearchBar = ({ autoFocus, onBlur, onCancel, onChange, onEnter, placeholder }: SearchBarProps) => ( + <DxcFlex gap="var(--spacing-gap-m)" alignItems="center" grow={1}> + <SearchBarStyles + placeholder={placeholder} + onBlur={(e) => typeof onBlur === "function" && onBlur(e.target.value)} + onChange={(e) => typeof onChange === "function" && onChange(e.target.value)} + onKeyDown={(e) => + e.key === "Enter" && typeof onEnter === "function" && onEnter((e.target as HTMLInputElement).value) + } + autoFocus={autoFocus} + /> + {typeof onCancel === "function" && ( + <DxcButton label="Cancel" onClick={onCancel} mode="tertiary" size={{ height: "medium" }} /> + )} + </DxcFlex> +); + +export default DxcSearchBar; diff --git a/packages/lib/src/search-bar/SearchBarTrigger.tsx b/packages/lib/src/search-bar/SearchBarTrigger.tsx new file mode 100644 index 0000000000..26b6477477 --- /dev/null +++ b/packages/lib/src/search-bar/SearchBarTrigger.tsx @@ -0,0 +1,15 @@ +import DxcButton from "../button/Button"; +import { SearchBarTriggerProps } from "./types"; + +const DxcSearchBarTrigger = ({ onTriggerClick }: SearchBarTriggerProps) => ( + <DxcButton + onClick={onTriggerClick} + icon="Search" + mode="tertiary" + title="Search" + semantic="default" + size={{ height: "medium" }} + /> +); + +export default DxcSearchBarTrigger; diff --git a/packages/lib/src/search-bar/types.ts b/packages/lib/src/search-bar/types.ts new file mode 100644 index 0000000000..6e89ba7d36 --- /dev/null +++ b/packages/lib/src/search-bar/types.ts @@ -0,0 +1,32 @@ +export type SearchBarTriggerProps = { + /** + * Function invoked when the trigger button is clicked. + */ + onTriggerClick?: () => void; +}; +export type SearchBarProps = { + /** + * If true, the search bar input will be focused when rendered. + */ + autoFocus?: boolean; + /** + * Function invoked when the search bar loses focus. + */ + onBlur?: (value: string) => void; + /** + * Function invoked when the user cancels the search. + */ + onCancel?: () => void; + /** + * Function invoked when the user changes the input value. + */ + onChange?: (value: string) => void; + /** + * Function invoked when the Enter key is pressed. + */ + onEnter?: (value: string) => void; + /** + * Placeholder text displayed in the search bar input field. + */ + placeholder?: string; +}; From 73f3c6469897640f6a8e3e352a4fc7401ffb7475 Mon Sep 17 00:00:00 2001 From: Jialecl <jialestrabajos@gmail.com> Date: Fri, 2 Jan 2026 08:33:08 +0100 Subject: [PATCH 016/275] Corrected some errors in storybook --- packages/lib/src/search-bar/SearchBar.stories.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/lib/src/search-bar/SearchBar.stories.tsx b/packages/lib/src/search-bar/SearchBar.stories.tsx index 3649abe23b..c67efef07c 100644 --- a/packages/lib/src/search-bar/SearchBar.stories.tsx +++ b/packages/lib/src/search-bar/SearchBar.stories.tsx @@ -1,7 +1,6 @@ import { Meta, StoryObj } from "@storybook/react-vite"; import ExampleContainer from "../../.storybook/components/ExampleContainer"; import Title from "../../.storybook/components/Title"; -import DxcLink from "./Link"; import DxcSearchBarTrigger from "./SearchBarTrigger"; import { useState } from "react"; import DxcSearchBar from "./SearchBar"; @@ -16,7 +15,7 @@ const Link = () => { const [showSearch, setShowSearch] = useState(false); return ( <> - <Title title="With anchor" theme="light" level={2} /> + <Title title="SearchBar component" theme="light" level={2} /> <ExampleContainer> <DxcFlex alignItems="center"> {!showSearch ? ( @@ -41,7 +40,7 @@ const Link = () => { ); }; -type Story = StoryObj<typeof DxcLink>; +type Story = StoryObj<typeof DxcSearchBar>; export const Chromatic: Story = { render: Link, From afa086ab6b3bf71d46cfa346994a2fe20ea5b5b5 Mon Sep 17 00:00:00 2001 From: Pelayo Felgueroso <pfelguerosogalguera@gmail.com> Date: Fri, 2 Jan 2026 10:19:48 +0100 Subject: [PATCH 017/275] Revert AppLayout changes --- packages/lib/src/layout/ApplicationLayout.tsx | 37 ++++++++++++------- 1 file changed, 24 insertions(+), 13 deletions(-) diff --git a/packages/lib/src/layout/ApplicationLayout.tsx b/packages/lib/src/layout/ApplicationLayout.tsx index 905e7f497f..16b55ab42c 100644 --- a/packages/lib/src/layout/ApplicationLayout.tsx +++ b/packages/lib/src/layout/ApplicationLayout.tsx @@ -40,13 +40,22 @@ const SidenavContainer = styled.div` overflow: auto; `; +const MainContainer = styled.div` + display: flex; + flex-grow: 1; + flex-direction: column; + width: 100%; + height: 100%; + position: relative; + overflow: auto; +`; + const FooterContainer = styled.div` height: fit-content; width: 100%; `; const MainContentContainer = styled.main` - width: 100%; height: 100%; display: grid; grid-template-rows: 1fr auto; @@ -69,18 +78,20 @@ const DxcApplicationLayout = ({ logo, header, sidenav, footer, children }: Appli {header && <HeaderContainer>{header}</HeaderContainer>} <BodyContainer hasSidenav={!!sidenav}> {sidenav && <SidenavContainer>{sidenav}</SidenavContainer>} - <MainContentContainer> - {findChildType(children, Main)} - <FooterContainer> - {footer ?? ( - <DxcFooter - copyright={`© DXC Technology ${year}. All rights reserved.`} - bottomLinks={bottomLinks} - socialLinks={socialLinks} - /> - )} - </FooterContainer> - </MainContentContainer> + <MainContainer> + <MainContentContainer> + {findChildType(children, Main)} + <FooterContainer> + {footer ?? ( + <DxcFooter + copyright={`© DXC Technology ${year}. All rights reserved.`} + bottomLinks={bottomLinks} + socialLinks={socialLinks} + /> + )} + </FooterContainer> + </MainContentContainer> + </MainContainer> </BodyContainer> </ApplicationLayoutContext.Provider> </ApplicationLayoutContainer> From 0538912e1ef7fd7971b2f2c5e07968fb337e9fce Mon Sep 17 00:00:00 2001 From: Pelayo Felgueroso <pfelguerosogalguera@gmail.com> Date: Fri, 2 Jan 2026 14:11:22 +0100 Subject: [PATCH 018/275] Add tests and enhance SearchBar component functionality - Implement accessibility tests for DxcSearchBar and DxcSearchBarTrigger components. - Refactor SearchBar component to manage internal state and handle clear action. - Update SearchBarProps type to include a disabled property. - Improve SearchBar stories to demonstrate various states and interactions. --- .../SearchBar.accessibility.test.tsx | 58 ++++++++ .../lib/src/search-bar/SearchBar.stories.tsx | 110 ++++++++++++--- .../lib/src/search-bar/SearchBar.test.tsx | 99 +++++++++++++ packages/lib/src/search-bar/SearchBar.tsx | 130 +++++++++++++++--- packages/lib/src/search-bar/types.ts | 4 + 5 files changed, 364 insertions(+), 37 deletions(-) create mode 100644 packages/lib/src/search-bar/SearchBar.accessibility.test.tsx create mode 100644 packages/lib/src/search-bar/SearchBar.test.tsx diff --git a/packages/lib/src/search-bar/SearchBar.accessibility.test.tsx b/packages/lib/src/search-bar/SearchBar.accessibility.test.tsx new file mode 100644 index 0000000000..36671272f5 --- /dev/null +++ b/packages/lib/src/search-bar/SearchBar.accessibility.test.tsx @@ -0,0 +1,58 @@ +import { render } from "@testing-library/react"; +import DxcSearchBar from "./SearchBar"; +import DxcSearchBarTrigger from "./SearchBarTrigger"; +import { axe } from "../../test/accessibility/axe-helper"; + +describe("SearchBar component accessibility tests", () => { + it("Should not have basic accessibility issues", async () => { + const { container } = render(<DxcSearchBar />); + const results = await axe(container); + expect(results.violations).toHaveLength(0); + }); + + it("Should not have basic accessibility issues with placeholder", async () => { + const { container } = render(<DxcSearchBar placeholder="Search here..." />); + const results = await axe(container); + expect(results.violations).toHaveLength(0); + }); + + it("Should not have basic accessibility issues with disabled state", async () => { + const { container } = render(<DxcSearchBar disabled />); + const results = await axe(container); + expect(results.violations).toHaveLength(0); + }); + + it("Should not have basic accessibility issues with cancel button", async () => { + const { container } = render(<DxcSearchBar onCancel={() => {}} />); + const results = await axe(container); + expect(results.violations).toHaveLength(0); + }); + + it("Should not have basic accessibility issues with all props", async () => { + const { container } = render( + <DxcSearchBar + placeholder="Search items..." + onChange={() => {}} + onEnter={() => {}} + onBlur={() => {}} + onCancel={() => {}} + /> + ); + const results = await axe(container); + expect(results.violations).toHaveLength(0); + }); +}); + +describe("SearchBarTrigger component accessibility tests", () => { + it("Should not have basic accessibility issues", async () => { + const { container } = render(<DxcSearchBarTrigger />); + const results = await axe(container); + expect(results.violations).toHaveLength(0); + }); + + it("Should not have basic accessibility issues with onTriggerClick", async () => { + const { container } = render(<DxcSearchBarTrigger onTriggerClick={() => {}} />); + const results = await axe(container); + expect(results.violations).toHaveLength(0); + }); +}); diff --git a/packages/lib/src/search-bar/SearchBar.stories.tsx b/packages/lib/src/search-bar/SearchBar.stories.tsx index c67efef07c..c287ff6d5a 100644 --- a/packages/lib/src/search-bar/SearchBar.stories.tsx +++ b/packages/lib/src/search-bar/SearchBar.stories.tsx @@ -5,36 +5,106 @@ import DxcSearchBarTrigger from "./SearchBarTrigger"; import { useState } from "react"; import DxcSearchBar from "./SearchBar"; import DxcFlex from "../flex/Flex"; +import DxcContainer from "../container/Container"; export default { title: "SearchBar", component: DxcSearchBar, } satisfies Meta<typeof DxcSearchBar>; -const Link = () => { +const SearchBarComponent = () => { const [showSearch, setShowSearch] = useState(false); + + return ( + <DxcFlex alignItems="center"> + {!showSearch ? ( + <DxcSearchBarTrigger onTriggerClick={() => setShowSearch(!showSearch)} /> + ) : ( + <DxcSearchBar + placeholder="Search..." + onBlur={(value) => { + console.log("onBlur", value); + }} + onChange={(value) => console.log("onChange", value)} + onEnter={(value) => { + console.log("onEnter", value); + setShowSearch(false); + }} + onCancel={() => setShowSearch(false)} + /> + )} + </DxcFlex> + ); +}; + +const SearchBar = () => { return ( <> <Title title="SearchBar component" theme="light" level={2} /> <ExampleContainer> - <DxcFlex alignItems="center"> - {!showSearch ? ( - <DxcSearchBarTrigger onTriggerClick={() => setShowSearch(!showSearch)} /> - ) : ( - <DxcSearchBar - placeholder="Search..." - onBlur={(value) => { - console.log("onBlur", value); - }} - onChange={(value) => console.log("onChange", value)} - onEnter={(value) => { - console.log("onEnter", value); - setShowSearch(false); - }} - onCancel={() => setShowSearch(false)} - /> - )} - </DxcFlex> + <SearchBarComponent /> + </ExampleContainer> + + <Title title="States" theme="light" level={2} /> + <ExampleContainer> + <Title title="Default" theme="light" level={4} /> + <DxcSearchBar + placeholder="Search..." + onBlur={(value) => { + console.log("onBlur", value); + }} + onChange={(value) => console.log("onChange", value)} + onEnter={(value) => { + console.log("onEnter", value); + }} + /> + </ExampleContainer> + <ExampleContainer pseudoState="pseudo-hover"> + <Title title="Hover" theme="light" level={4} /> + <DxcSearchBar + placeholder="Search..." + onBlur={(value) => { + console.log("onBlur", value); + }} + onChange={(value) => console.log("onChange", value)} + onEnter={(value) => { + console.log("onEnter", value); + }} + /> + </ExampleContainer> + <ExampleContainer pseudoState="pseudo-focus-within"> + <Title title="Focus" theme="light" level={4} /> + <DxcSearchBar + placeholder="Search..." + onBlur={(value) => { + console.log("onBlur", value); + }} + onChange={(value) => console.log("onChange", value)} + onEnter={(value) => { + console.log("onEnter", value); + }} + /> + </ExampleContainer> + <ExampleContainer> + <Title title="Disabled" theme="light" level={4} /> + <DxcSearchBar + placeholder="Search..." + onBlur={(value) => { + console.log("onBlur", value); + }} + onChange={(value) => console.log("onChange", value)} + onEnter={(value) => { + console.log("onEnter", value); + }} + disabled + /> + </ExampleContainer> + + <Title title="Small SearchBar component" theme="light" level={2} /> + <ExampleContainer> + <DxcContainer width="220px"> + <SearchBarComponent /> + </DxcContainer> </ExampleContainer> </> ); @@ -43,5 +113,5 @@ const Link = () => { type Story = StoryObj<typeof DxcSearchBar>; export const Chromatic: Story = { - render: Link, + render: SearchBar, }; diff --git a/packages/lib/src/search-bar/SearchBar.test.tsx b/packages/lib/src/search-bar/SearchBar.test.tsx new file mode 100644 index 0000000000..2a59203c01 --- /dev/null +++ b/packages/lib/src/search-bar/SearchBar.test.tsx @@ -0,0 +1,99 @@ +import { render, fireEvent } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import DxcSearchBar from "./SearchBar"; +import DxcSearchBarTrigger from "./SearchBarTrigger"; + +describe("SearchBarTrigger component tests", () => { + test("Renders correctly", () => { + const { getByRole } = render(<DxcSearchBarTrigger />); + + const button = getByRole("button"); + expect(button).toBeTruthy(); + }); + + test("Calls onTriggerClick when button is clicked", () => { + const onTriggerClick = jest.fn(); + const { getByRole } = render(<DxcSearchBarTrigger onTriggerClick={onTriggerClick} />); + + const button = getByRole("button"); + userEvent.click(button); + + expect(onTriggerClick).toHaveBeenCalledTimes(1); + }); +}); + +describe("SearchBar component tests", () => { + test("Renders correctly", () => { + const { getByPlaceholderText } = render(<DxcSearchBar placeholder="Search..." />); + + const text = getByPlaceholderText("Search..."); + expect(text).toBeTruthy(); + }); + + test("Calls onChange when typing", () => { + const onChange = jest.fn(); + const { getByRole } = render(<DxcSearchBar onChange={onChange} />); + + const input = getByRole("textbox") as HTMLInputElement; + userEvent.type(input, "hello"); + + expect(onChange).toHaveBeenCalled(); + expect(onChange).toHaveBeenLastCalledWith("hello"); + }); + + test("Calls onEnter with value when pressing Enter", () => { + const onEnter = jest.fn(); + const { getByRole } = render(<DxcSearchBar onEnter={onEnter} />); + + const input = getByRole("textbox") as HTMLInputElement; + userEvent.type(input, "search text"); + fireEvent.keyDown(input, { key: "Enter" }); + + expect(onEnter).toHaveBeenCalledTimes(1); + expect(onEnter).toHaveBeenCalledWith("search text"); + }); + + test("Clears value when clicking clear icon", () => { + const { getByRole } = render(<DxcSearchBar />); + + const input = getByRole("textbox") as HTMLInputElement; + userEvent.type(input, "abc"); + + const clearButton = getByRole("button"); + expect(clearButton).toBeTruthy(); + + userEvent.click(clearButton); + expect(input.value).toBe(""); + }); + + test("Clears value when pressing Escape", () => { + const { getByRole } = render(<DxcSearchBar />); + + const input = getByRole("textbox") as HTMLInputElement; + userEvent.type(input, "xyz"); + fireEvent.keyDown(input, { key: "Escape" }); + + expect(input.value).toBe(""); + }); + + test("Calls onBlur with current value when blurred", () => { + const onBlur = jest.fn(); + const { getByRole } = render(<DxcSearchBar onBlur={onBlur} />); + + const input = getByRole("textbox") as HTMLInputElement; + userEvent.type(input, "blur me"); + fireEvent.blur(input); + + expect(onBlur).toHaveBeenCalledWith("blur me"); + }); + + test("Calls onCancel when Cancel button is clicked", () => { + const onCancel = jest.fn(); + const { getByRole } = render(<DxcSearchBar onCancel={onCancel} />); + + const cancelButton = getByRole("button", { name: /Cancel/i }); + userEvent.click(cancelButton); + + expect(onCancel).toHaveBeenCalledTimes(1); + }); +}); diff --git a/packages/lib/src/search-bar/SearchBar.tsx b/packages/lib/src/search-bar/SearchBar.tsx index b3a4bfae6d..e4079c24cb 100644 --- a/packages/lib/src/search-bar/SearchBar.tsx +++ b/packages/lib/src/search-bar/SearchBar.tsx @@ -2,40 +2,136 @@ import styled from "@emotion/styled"; import DxcButton from "../button/Button"; import DxcFlex from "../flex/Flex"; import { SearchBarProps } from "./types"; +import DxcActionIcon from "../action-icon/ActionIcon"; +import { KeyboardEvent, useContext, useRef, useState } from "react"; +import { HalstackLanguageContext } from "../HalstackContext"; +import { css } from "@emotion/react"; +import DxcIcon from "../icon/Icon"; -const SearchBarStyles = styled.input` +const SearchBarContainer = styled.div<{ disabled: Required<SearchBarProps>["disabled"] }>` width: 100%; min-width: 200px; max-width: 720px; height: var(--height-m); + display: flex; + align-items: center; + gap: var(--spacing-gap-s); border-radius: var(--border-radius-xl); border: var(--border-width-s) var(--border-style-default) var(--border-color-neutral-dark); box-sizing: border-box; padding: 0 var(--spacing-padding-s); color: var(--color-fg-neutral-dark); + + ${({ disabled }) => + !disabled + ? css` + &:hover { + border-color: var(--border-color-primary-strong); + } + &:focus, + &:focus-within { + border-color: transparent; + outline-offset: -2px; + outline: var(--border-width-m) var(--border-style-default) var(--border-color-secondary-medium); + } + ` + : css` + color: var(--color-fg-neutral-medium); + border-color: var(--border-color-neutral-strong); + cursor: not-allowed; + `} +`; + +const SearchBarInput = styled.input<{ disabled: Required<SearchBarProps>["disabled"] }>` + width: 100%; + max-width: 100%; + background: none; + border: none; + outline: none; + padding: 0; font-family: var(--typography-font-family); font-size: var(--typography-label-m); font-weight: var(--typography-label-regular); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; + cursor: ${({ disabled }) => (disabled ? "not-allowed" : "text")}; `; -const DxcSearchBar = ({ autoFocus, onBlur, onCancel, onChange, onEnter, placeholder }: SearchBarProps) => ( - <DxcFlex gap="var(--spacing-gap-m)" alignItems="center" grow={1}> - <SearchBarStyles - placeholder={placeholder} - onBlur={(e) => typeof onBlur === "function" && onBlur(e.target.value)} - onChange={(e) => typeof onChange === "function" && onChange(e.target.value)} - onKeyDown={(e) => - e.key === "Enter" && typeof onEnter === "function" && onEnter((e.target as HTMLInputElement).value) - } - autoFocus={autoFocus} - /> - {typeof onCancel === "function" && ( - <DxcButton label="Cancel" onClick={onCancel} mode="tertiary" size={{ height: "medium" }} /> - )} - </DxcFlex> -); +const DxcSearchBar = ({ + autoFocus, + disabled = false, + onBlur, + onCancel, + onChange, + onEnter, + placeholder, +}: SearchBarProps) => { + const translatedLabels = useContext(HalstackLanguageContext); + const inputRef = useRef<HTMLInputElement>(null); + const [innerValue, setInnerValue] = useState(""); + + const handleClearActionOnClick = () => { + setInnerValue(""); + inputRef.current?.focus(); + }; + + const handleSearchChangeValue = (value: string) => { + setInnerValue(value); + if (typeof onChange === "function") { + onChange(value); + } + }; + + const handleInputOnKeyDown = (e: KeyboardEvent<HTMLInputElement>) => { + switch (e.key) { + case "Esc": + case "Escape": + e.preventDefault(); + if (innerValue.length > 0) { + handleClearActionOnClick(); + } + break; + case "Enter": + if (typeof onEnter === "function") { + onEnter(innerValue); + } + break; + default: + break; + } + }; + + return ( + <DxcFlex gap="var(--spacing-gap-m)" alignItems="center" justifyContent="center" grow={1}> + <SearchBarContainer disabled={disabled} autoFocus={autoFocus}> + <DxcIcon icon="search" /> + <SearchBarInput + ref={inputRef} + value={innerValue} + placeholder={placeholder} + onBlur={(e) => typeof onBlur === "function" && onBlur(e.target.value)} + onChange={(e) => handleSearchChangeValue(e.target.value)} + onKeyDown={handleInputOnKeyDown} + disabled={disabled} + /> + {!disabled && innerValue.length > 0 && ( + <DxcActionIcon + size="xsmall" + shape="circle" + icon="cancel" + onClick={handleClearActionOnClick} + tabIndex={0} + title={!disabled ? translatedLabels.textInput.clearFieldActionTitle : undefined} + /> + )} + </SearchBarContainer> + + {typeof onCancel === "function" && ( + <DxcButton label="Cancel" onClick={onCancel} mode="tertiary" size={{ height: "medium" }} /> + )} + </DxcFlex> + ); +}; export default DxcSearchBar; diff --git a/packages/lib/src/search-bar/types.ts b/packages/lib/src/search-bar/types.ts index 6e89ba7d36..5d67a8f39f 100644 --- a/packages/lib/src/search-bar/types.ts +++ b/packages/lib/src/search-bar/types.ts @@ -9,6 +9,10 @@ export type SearchBarProps = { * If true, the search bar input will be focused when rendered. */ autoFocus?: boolean; + /** + * If true, the component will be disabled. + */ + disabled?: boolean; /** * Function invoked when the search bar loses focus. */ From a1e0fcb54077e3113cd4eb36fe507c0c904a9d55 Mon Sep 17 00:00:00 2001 From: Pelayo Felgueroso <pfelguerosogalguera@gmail.com> Date: Mon, 5 Jan 2026 08:39:55 +0100 Subject: [PATCH 019/275] Enhance SearchBar focus styles to include focus-visible state --- packages/lib/src/search-bar/SearchBar.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/lib/src/search-bar/SearchBar.tsx b/packages/lib/src/search-bar/SearchBar.tsx index e4079c24cb..f8d4b537b7 100644 --- a/packages/lib/src/search-bar/SearchBar.tsx +++ b/packages/lib/src/search-bar/SearchBar.tsx @@ -29,7 +29,8 @@ const SearchBarContainer = styled.div<{ disabled: Required<SearchBarProps>["disa border-color: var(--border-color-primary-strong); } &:focus, - &:focus-within { + &:focus-within, + &:focus-visible { border-color: transparent; outline-offset: -2px; outline: var(--border-width-m) var(--border-style-default) var(--border-color-secondary-medium); From 7300fce0a16816cf8f0a33edf6fa4c66b0d5aafb Mon Sep 17 00:00:00 2001 From: Pelayo Felgueroso <pfelguerosogalguera@gmail.com> Date: Mon, 5 Jan 2026 08:52:48 +0100 Subject: [PATCH 020/275] Retype stories titles --- packages/lib/src/search-bar/SearchBar.stories.tsx | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/packages/lib/src/search-bar/SearchBar.stories.tsx b/packages/lib/src/search-bar/SearchBar.stories.tsx index c287ff6d5a..47e660975f 100644 --- a/packages/lib/src/search-bar/SearchBar.stories.tsx +++ b/packages/lib/src/search-bar/SearchBar.stories.tsx @@ -40,7 +40,7 @@ const SearchBarComponent = () => { const SearchBar = () => { return ( <> - <Title title="SearchBar component" theme="light" level={2} /> + <Title title="Searchbar component" theme="light" level={2} /> <ExampleContainer> <SearchBarComponent /> </ExampleContainer> @@ -100,10 +100,19 @@ const SearchBar = () => { /> </ExampleContainer> - <Title title="Small SearchBar component" theme="light" level={2} /> + <Title title="Small Searchbar" theme="light" level={2} /> <ExampleContainer> <DxcContainer width="220px"> - <SearchBarComponent /> + <DxcSearchBar + placeholder="Search..." + onBlur={(value) => { + console.log("onBlur", value); + }} + onChange={(value) => console.log("onChange", value)} + onEnter={(value) => { + console.log("onEnter", value); + }} + /> </DxcContainer> </ExampleContainer> </> From c9049a1bd64aae56461e06cd7be0b2dec5ccf99a Mon Sep 17 00:00:00 2001 From: Pelayo Felgueroso <pfelguerosogalguera@gmail.com> Date: Mon, 5 Jan 2026 09:26:04 +0100 Subject: [PATCH 021/275] Retype searchbar stories title --- packages/lib/src/search-bar/SearchBar.stories.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/lib/src/search-bar/SearchBar.stories.tsx b/packages/lib/src/search-bar/SearchBar.stories.tsx index 47e660975f..048145b971 100644 --- a/packages/lib/src/search-bar/SearchBar.stories.tsx +++ b/packages/lib/src/search-bar/SearchBar.stories.tsx @@ -8,7 +8,7 @@ import DxcFlex from "../flex/Flex"; import DxcContainer from "../container/Container"; export default { - title: "SearchBar", + title: "Searchbar", component: DxcSearchBar, } satisfies Meta<typeof DxcSearchBar>; From a44550f8c46d6a79b94598dd537ee626d3fb03c9 Mon Sep 17 00:00:00 2001 From: Pelayo Felgueroso <pfelguerosogalguera@gmail.com> Date: Thu, 8 Jan 2026 10:29:54 +0100 Subject: [PATCH 022/275] Add tooltip to Cancel button --- packages/lib/src/search-bar/SearchBar.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/lib/src/search-bar/SearchBar.tsx b/packages/lib/src/search-bar/SearchBar.tsx index f8d4b537b7..fe3b8e0584 100644 --- a/packages/lib/src/search-bar/SearchBar.tsx +++ b/packages/lib/src/search-bar/SearchBar.tsx @@ -129,7 +129,7 @@ const DxcSearchBar = ({ </SearchBarContainer> {typeof onCancel === "function" && ( - <DxcButton label="Cancel" onClick={onCancel} mode="tertiary" size={{ height: "medium" }} /> + <DxcButton label="Cancel" title="Cancel" onClick={onCancel} mode="tertiary" size={{ height: "medium" }} /> )} </DxcFlex> ); From de72dae8ffc74088cf62ad499610eca7462a746f Mon Sep 17 00:00:00 2001 From: Pelayo Felgueroso <pfelguerosogalguera@gmail.com> Date: Tue, 13 Jan 2026 09:31:50 +0100 Subject: [PATCH 023/275] Change autoFocus to correct container --- packages/lib/src/search-bar/SearchBar.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/lib/src/search-bar/SearchBar.tsx b/packages/lib/src/search-bar/SearchBar.tsx index fe3b8e0584..b846274796 100644 --- a/packages/lib/src/search-bar/SearchBar.tsx +++ b/packages/lib/src/search-bar/SearchBar.tsx @@ -105,7 +105,7 @@ const DxcSearchBar = ({ return ( <DxcFlex gap="var(--spacing-gap-m)" alignItems="center" justifyContent="center" grow={1}> - <SearchBarContainer disabled={disabled} autoFocus={autoFocus}> + <SearchBarContainer disabled={disabled}> <DxcIcon icon="search" /> <SearchBarInput ref={inputRef} @@ -115,6 +115,7 @@ const DxcSearchBar = ({ onChange={(e) => handleSearchChangeValue(e.target.value)} onKeyDown={handleInputOnKeyDown} disabled={disabled} + autoFocus={autoFocus} /> {!disabled && innerValue.length > 0 && ( <DxcActionIcon From 0a94474a0276fe86fd126c5f6730ef9d1e297dc5 Mon Sep 17 00:00:00 2001 From: Jialecl <jialestrabajos@gmail.com> Date: Tue, 13 Jan 2026 11:11:59 +0100 Subject: [PATCH 024/275] Added localization support and accessibility improvements - Search Bar has its own section in the localization variables. - Updated documentation to reflect new localization keys. - Improved visual test coverage for Search Bar component. --- .../localization/LocalizationPage.tsx | 33 ++++++++++++++++ packages/lib/src/common/variables.ts | 5 +++ .../lib/src/search-bar/SearchBar.stories.tsx | 39 +++++++++++++++++-- packages/lib/src/search-bar/SearchBar.tsx | 5 ++- .../lib/src/search-bar/SearchBarTrigger.tsx | 25 +++++++----- 5 files changed, 92 insertions(+), 15 deletions(-) diff --git a/apps/website/screens/principles/localization/LocalizationPage.tsx b/apps/website/screens/principles/localization/LocalizationPage.tsx index db5baaa667..befa34b3e6 100644 --- a/apps/website/screens/principles/localization/LocalizationPage.tsx +++ b/apps/website/screens/principles/localization/LocalizationPage.tsx @@ -551,6 +551,39 @@ const sections = [ </DxcTable> ), }, + { + title: "searchBar", + content: ( + <DxcTable> + <thead> + <tr> + <th>Label Name</th> + <th>Default value</th> + </tr> + </thead> + <tbody> + <tr> + <td> + <Code>clearFieldActionTitle</Code> + </td> + <td>Clear field</td> + </tr> + <tr> + <td> + <Code>inputAriaLabel</Code> + </td> + <td>Search input</td> + </tr> + <tr> + <td> + <Code>triggerTitle</Code> + </td> + <td>Search</td> + </tr> + </tbody> + </DxcTable> + ), + }, { title: "select", content: ( diff --git a/packages/lib/src/common/variables.ts b/packages/lib/src/common/variables.ts index 3f382b3af0..5242bb1336 100644 --- a/packages/lib/src/common/variables.ts +++ b/packages/lib/src/common/variables.ts @@ -105,6 +105,11 @@ export const defaultTranslatedComponentLabels = { radioGroup: { optionalItemLabelDefault: "N/A", }, + searchBar: { + clearFieldActionTitle: "Clear field", + inputAriaLabel: "Search input", + triggerTitle: "Search", + }, select: { actionClearSelectionTitle: "Clear selection", actionClearSearchTitle: "Clear search", diff --git a/packages/lib/src/search-bar/SearchBar.stories.tsx b/packages/lib/src/search-bar/SearchBar.stories.tsx index 048145b971..c005007345 100644 --- a/packages/lib/src/search-bar/SearchBar.stories.tsx +++ b/packages/lib/src/search-bar/SearchBar.stories.tsx @@ -6,9 +6,10 @@ import { useState } from "react"; import DxcSearchBar from "./SearchBar"; import DxcFlex from "../flex/Flex"; import DxcContainer from "../container/Container"; +import { userEvent, within } from "storybook/internal/test"; export default { - title: "Searchbar", + title: "Search Bar", component: DxcSearchBar, } satisfies Meta<typeof DxcSearchBar>; @@ -40,7 +41,7 @@ const SearchBarComponent = () => { const SearchBar = () => { return ( <> - <Title title="Searchbar component" theme="light" level={2} /> + <Title title="Search Bar component" theme="light" level={2} /> <ExampleContainer> <SearchBarComponent /> </ExampleContainer> @@ -48,6 +49,7 @@ const SearchBar = () => { <Title title="States" theme="light" level={2} /> <ExampleContainer> <Title title="Default" theme="light" level={4} /> + <DxcSearchBarTrigger onTriggerClick={() => {}} /> <DxcSearchBar placeholder="Search..." onBlur={(value) => { @@ -57,10 +59,12 @@ const SearchBar = () => { onEnter={(value) => { console.log("onEnter", value); }} + onCancel={() => {}} /> </ExampleContainer> <ExampleContainer pseudoState="pseudo-hover"> <Title title="Hover" theme="light" level={4} /> + <DxcSearchBarTrigger onTriggerClick={() => {}} /> <DxcSearchBar placeholder="Search..." onBlur={(value) => { @@ -70,10 +74,12 @@ const SearchBar = () => { onEnter={(value) => { console.log("onEnter", value); }} + onCancel={() => {}} /> </ExampleContainer> - <ExampleContainer pseudoState="pseudo-focus-within"> + <ExampleContainer pseudoState={["pseudo-focus", "pseudo-focus-within"]}> <Title title="Focus" theme="light" level={4} /> + <DxcSearchBarTrigger onTriggerClick={() => {}} /> <DxcSearchBar placeholder="Search..." onBlur={(value) => { @@ -83,6 +89,22 @@ const SearchBar = () => { onEnter={(value) => { console.log("onEnter", value); }} + onCancel={() => {}} + /> + </ExampleContainer> + <ExampleContainer pseudoState={["pseudo-focus", "pseudo-active"]}> + <Title title="Focus and active" theme="light" level={4} /> + <DxcSearchBarTrigger onTriggerClick={() => {}} /> + <DxcSearchBar + placeholder="Search..." + onBlur={(value) => { + console.log("onBlur", value); + }} + onChange={(value) => console.log("onChange", value)} + onEnter={(value) => { + console.log("onEnter", value); + }} + onCancel={() => {}} /> </ExampleContainer> <ExampleContainer> @@ -96,6 +118,7 @@ const SearchBar = () => { onEnter={(value) => { console.log("onEnter", value); }} + onCancel={() => {}} disabled /> </ExampleContainer> @@ -123,4 +146,14 @@ type Story = StoryObj<typeof DxcSearchBar>; export const Chromatic: Story = { render: SearchBar, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + const user = userEvent.setup(); + + const enabledInputs = (await canvas.findAllByRole("textbox")).filter((input) => !input.hasAttribute("disabled")); + + for (let i = 0; i < enabledInputs.length - 1; i++) { + await user.type(enabledInputs[i]!, "Lorem ipsum"); + } + }, }; diff --git a/packages/lib/src/search-bar/SearchBar.tsx b/packages/lib/src/search-bar/SearchBar.tsx index b846274796..9c8386c9fc 100644 --- a/packages/lib/src/search-bar/SearchBar.tsx +++ b/packages/lib/src/search-bar/SearchBar.tsx @@ -106,7 +106,7 @@ const DxcSearchBar = ({ return ( <DxcFlex gap="var(--spacing-gap-m)" alignItems="center" justifyContent="center" grow={1}> <SearchBarContainer disabled={disabled}> - <DxcIcon icon="search" /> + <DxcIcon icon="search" aria-hidden="true" /> <SearchBarInput ref={inputRef} value={innerValue} @@ -115,6 +115,7 @@ const DxcSearchBar = ({ onChange={(e) => handleSearchChangeValue(e.target.value)} onKeyDown={handleInputOnKeyDown} disabled={disabled} + aria-label={translatedLabels.searchBar.inputAriaLabel} autoFocus={autoFocus} /> {!disabled && innerValue.length > 0 && ( @@ -124,7 +125,7 @@ const DxcSearchBar = ({ icon="cancel" onClick={handleClearActionOnClick} tabIndex={0} - title={!disabled ? translatedLabels.textInput.clearFieldActionTitle : undefined} + title={!disabled ? translatedLabels.searchBar.clearFieldActionTitle : undefined} /> )} </SearchBarContainer> diff --git a/packages/lib/src/search-bar/SearchBarTrigger.tsx b/packages/lib/src/search-bar/SearchBarTrigger.tsx index 26b6477477..e0b49d0028 100644 --- a/packages/lib/src/search-bar/SearchBarTrigger.tsx +++ b/packages/lib/src/search-bar/SearchBarTrigger.tsx @@ -1,15 +1,20 @@ +import { useContext } from "react"; import DxcButton from "../button/Button"; +import { HalstackLanguageContext } from "../HalstackContext"; import { SearchBarTriggerProps } from "./types"; -const DxcSearchBarTrigger = ({ onTriggerClick }: SearchBarTriggerProps) => ( - <DxcButton - onClick={onTriggerClick} - icon="Search" - mode="tertiary" - title="Search" - semantic="default" - size={{ height: "medium" }} - /> -); +const DxcSearchBarTrigger = ({ onTriggerClick }: SearchBarTriggerProps) => { + const translatedLabels = useContext(HalstackLanguageContext); + return ( + <DxcButton + onClick={onTriggerClick} + icon="Search" + mode="tertiary" + title={translatedLabels.searchBar.triggerTitle} + semantic="default" + size={{ height: "medium" }} + /> + ); +}; export default DxcSearchBarTrigger; From fd72e553fcf913eed636a1369769f87085373683 Mon Sep 17 00:00:00 2001 From: Jialecl <jialestrabajos@gmail.com> Date: Tue, 13 Jan 2026 11:27:05 +0100 Subject: [PATCH 025/275] Added default value to placeholder --- packages/lib/src/search-bar/SearchBar.stories.tsx | 1 - packages/lib/src/search-bar/SearchBar.tsx | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/lib/src/search-bar/SearchBar.stories.tsx b/packages/lib/src/search-bar/SearchBar.stories.tsx index c005007345..bb25bd911e 100644 --- a/packages/lib/src/search-bar/SearchBar.stories.tsx +++ b/packages/lib/src/search-bar/SearchBar.stories.tsx @@ -22,7 +22,6 @@ const SearchBarComponent = () => { <DxcSearchBarTrigger onTriggerClick={() => setShowSearch(!showSearch)} /> ) : ( <DxcSearchBar - placeholder="Search..." onBlur={(value) => { console.log("onBlur", value); }} diff --git a/packages/lib/src/search-bar/SearchBar.tsx b/packages/lib/src/search-bar/SearchBar.tsx index 9c8386c9fc..d7b67f76cf 100644 --- a/packages/lib/src/search-bar/SearchBar.tsx +++ b/packages/lib/src/search-bar/SearchBar.tsx @@ -66,7 +66,7 @@ const DxcSearchBar = ({ onCancel, onChange, onEnter, - placeholder, + placeholder = "Search...", }: SearchBarProps) => { const translatedLabels = useContext(HalstackLanguageContext); const inputRef = useRef<HTMLInputElement>(null); From e737d7f86d8348c8b81ab46cf8a97c276d7b53c7 Mon Sep 17 00:00:00 2001 From: Pelayo Felgueroso <pfelguerosogalguera@gmail.com> Date: Tue, 13 Jan 2026 12:09:16 +0100 Subject: [PATCH 026/275] Resolve comments --- .../search-bar/SearchBar.accessibility.test.tsx | 15 --------------- packages/lib/src/search-bar/SearchBar.tsx | 2 +- 2 files changed, 1 insertion(+), 16 deletions(-) diff --git a/packages/lib/src/search-bar/SearchBar.accessibility.test.tsx b/packages/lib/src/search-bar/SearchBar.accessibility.test.tsx index 36671272f5..976c7c4a6c 100644 --- a/packages/lib/src/search-bar/SearchBar.accessibility.test.tsx +++ b/packages/lib/src/search-bar/SearchBar.accessibility.test.tsx @@ -1,6 +1,5 @@ import { render } from "@testing-library/react"; import DxcSearchBar from "./SearchBar"; -import DxcSearchBarTrigger from "./SearchBarTrigger"; import { axe } from "../../test/accessibility/axe-helper"; describe("SearchBar component accessibility tests", () => { @@ -42,17 +41,3 @@ describe("SearchBar component accessibility tests", () => { expect(results.violations).toHaveLength(0); }); }); - -describe("SearchBarTrigger component accessibility tests", () => { - it("Should not have basic accessibility issues", async () => { - const { container } = render(<DxcSearchBarTrigger />); - const results = await axe(container); - expect(results.violations).toHaveLength(0); - }); - - it("Should not have basic accessibility issues with onTriggerClick", async () => { - const { container } = render(<DxcSearchBarTrigger onTriggerClick={() => {}} />); - const results = await axe(container); - expect(results.violations).toHaveLength(0); - }); -}); diff --git a/packages/lib/src/search-bar/SearchBar.tsx b/packages/lib/src/search-bar/SearchBar.tsx index d7b67f76cf..8915af251b 100644 --- a/packages/lib/src/search-bar/SearchBar.tsx +++ b/packages/lib/src/search-bar/SearchBar.tsx @@ -60,7 +60,7 @@ const SearchBarInput = styled.input<{ disabled: Required<SearchBarProps>["disabl `; const DxcSearchBar = ({ - autoFocus, + autoFocus = false, disabled = false, onBlur, onCancel, From 1a018b32546a6548fcf0582d9d403e46b759ae8a Mon Sep 17 00:00:00 2001 From: Pelayo Felgueroso <pfelguerosogalguera@gmail.com> Date: Tue, 13 Jan 2026 12:24:04 +0100 Subject: [PATCH 027/275] Add translatedLabel to "Cancel"button --- .../screens/principles/localization/LocalizationPage.tsx | 6 ++++++ packages/lib/src/common/variables.ts | 1 + packages/lib/src/search-bar/SearchBar.tsx | 8 +++++++- 3 files changed, 14 insertions(+), 1 deletion(-) diff --git a/apps/website/screens/principles/localization/LocalizationPage.tsx b/apps/website/screens/principles/localization/LocalizationPage.tsx index befa34b3e6..c27d010f92 100644 --- a/apps/website/screens/principles/localization/LocalizationPage.tsx +++ b/apps/website/screens/principles/localization/LocalizationPage.tsx @@ -580,6 +580,12 @@ const sections = [ </td> <td>Search</td> </tr> + <tr> + <td> + <Code>cancelButtonLabel</Code> + </td> + <td>Cancel</td> + </tr> </tbody> </DxcTable> ), diff --git a/packages/lib/src/common/variables.ts b/packages/lib/src/common/variables.ts index 5242bb1336..5b06093ecd 100644 --- a/packages/lib/src/common/variables.ts +++ b/packages/lib/src/common/variables.ts @@ -109,6 +109,7 @@ export const defaultTranslatedComponentLabels = { clearFieldActionTitle: "Clear field", inputAriaLabel: "Search input", triggerTitle: "Search", + cancelButtonLabel: "Cancel", }, select: { actionClearSelectionTitle: "Clear selection", diff --git a/packages/lib/src/search-bar/SearchBar.tsx b/packages/lib/src/search-bar/SearchBar.tsx index 8915af251b..a160ee95e9 100644 --- a/packages/lib/src/search-bar/SearchBar.tsx +++ b/packages/lib/src/search-bar/SearchBar.tsx @@ -131,7 +131,13 @@ const DxcSearchBar = ({ </SearchBarContainer> {typeof onCancel === "function" && ( - <DxcButton label="Cancel" title="Cancel" onClick={onCancel} mode="tertiary" size={{ height: "medium" }} /> + <DxcButton + label={translatedLabels.searchBar.cancelButtonLabel} + title={translatedLabels.searchBar.cancelButtonLabel} + onClick={onCancel} + mode="tertiary" + size={{ height: "medium" }} + /> )} </DxcFlex> ); From f9c0a0b64250036c6807e97eac69ff32c1d97deb Mon Sep 17 00:00:00 2001 From: Pelayo Felgueroso <pfelguerosogalguera@gmail.com> Date: Mon, 5 Jan 2026 08:50:50 +0100 Subject: [PATCH 028/275] Implement new Searchbar component into header --- packages/lib/src/header/Header.tsx | 84 +++++++++++++++++------------- 1 file changed, 49 insertions(+), 35 deletions(-) diff --git a/packages/lib/src/header/Header.tsx b/packages/lib/src/header/Header.tsx index d175649c1a..28485eed25 100644 --- a/packages/lib/src/header/Header.tsx +++ b/packages/lib/src/header/Header.tsx @@ -12,6 +12,8 @@ import { responsiveSizes } from "../common/variables"; import DxcButton from "../button/Button"; import scrollbarStyles from "../styles/scroll"; import ApplicationLayoutContext from "../layout/ApplicationLayoutContext"; +import DxcSearchBarTrigger from "../search-bar/SearchBarTrigger"; +import DxcSearchBar from "../search-bar/SearchBar"; const MAX_MAIN_NAV_SIZE = "60%"; const LEVEL_LIMIT = 1; @@ -135,6 +137,7 @@ const sanitizeNavItems = (navItems: HeaderProps["navItems"], level?: number): (G const DxcHeader = ({ appTitle, navItems, sideContent, responsiveBottomContent }: HeaderProps): JSX.Element => { const [isResponsive, setIsResponsive] = useState(false); const [isMenuVisible, setIsMenuVisible] = useState(false); + const [showSearch, setShowSearch] = useState(false); const logo = useContext(ApplicationLayoutContext).logo || undefined; useEffect(() => { @@ -162,51 +165,62 @@ const DxcHeader = ({ appTitle, navItems, sideContent, responsiveBottomContent }: <HeaderContainer> <DxcGrid templateColumns={ - !isResponsive && sanitizedNavItems && sanitizedNavItems.length > 0 - ? [`auto`, `minmax(auto, ${MAX_MAIN_NAV_SIZE})`, `auto`] - : ["auto", "auto"] + showSearch + ? ["auto"] + : !isResponsive && sanitizedNavItems && sanitizedNavItems.length > 0 + ? [`auto`, `minmax(auto, ${MAX_MAIN_NAV_SIZE})`, `auto`] + : ["auto", "auto"] } templateRows={["var(--height-xxxl)"]} gap="var(--spacing-gap-ml)" placeItems="center" > - <BrandingContainer> - {logo && ( - <LogoContainer - role={logo.onClick ? "button" : undefined} - onClick={typeof logo.onClick === "function" ? logo.onClick : undefined} - as={logo.href ? "a" : undefined} - href={logo.href} - hasAction={!!(logo.onClick || logo.href)} - > - {typeof logo.src === "string" ? ( - <DxcImage src={logo.src} alt={logo.alt} height="var(--height-m)" objectFit="contain" /> - ) : ( - logo.src - )} - </LogoContainer> - )} - {appTitle && !isResponsive && ( - <> - {logo && <DxcDivider orientation="vertical" />} - <DxcHeading text={appTitle} as="h1" level={5} /> - </> - )} - </BrandingContainer> + {!showSearch && ( + <BrandingContainer> + {logo && ( + <LogoContainer + role={logo.onClick ? "button" : undefined} + onClick={typeof logo.onClick === "function" ? logo.onClick : undefined} + as={logo.href ? "a" : undefined} + href={logo.href} + hasAction={!!(logo.onClick || logo.href)} + > + {typeof logo.src === "string" ? ( + <DxcImage src={logo.src} alt={logo.alt} height="var(--height-m)" objectFit="contain" /> + ) : ( + logo.src + )} + </LogoContainer> + )} + {appTitle && !isResponsive && ( + <> + {logo && <DxcDivider orientation="vertical" />} + <DxcHeading text={appTitle} as="h1" level={5} /> + </> + )} + </BrandingContainer> + )} + {!isResponsive && sanitizedNavItems && sanitizedNavItems.length > 0 && ( <MainNavContainer> - <DxcNavigationTree - items={sanitizedNavItems} - displayGroupLines={false} - displayBorder={false} - displayControlsAfter - hasPopOver - isHorizontal - /> + {showSearch ? ( + <DxcSearchBar onCancel={() => setShowSearch(false)} /> + ) : ( + <DxcNavigationTree + items={sanitizedNavItems} + displayGroupLines={false} + displayBorder={false} + displayControlsAfter + hasPopOver + isHorizontal + /> + )} </MainNavContainer> )} - {(sideContent || isResponsive) && ( + + {!showSearch && (sideContent || isResponsive) && ( <RightSideContainer> + <DxcSearchBarTrigger onTriggerClick={() => setShowSearch(!showSearch)} /> {typeof sideContent === "function" ? sideContent(isResponsive) : sideContent} {isResponsive && ((navItems && navItems.length) || responsiveBottomContent) && ( <HamburguerButton onClick={toggleMenu} /> From bd15399ccc3ab2f24ce4ce057bb7b2ac1b836e49 Mon Sep 17 00:00:00 2001 From: Pelayo Felgueroso <pfelguerosogalguera@gmail.com> Date: Mon, 5 Jan 2026 09:52:45 +0100 Subject: [PATCH 029/275] Add searchbar prop to header and add story --- packages/lib/src/header/Header.stories.tsx | 2 ++ packages/lib/src/header/Header.tsx | 29 +++++++++++++++++++--- packages/lib/src/header/types.ts | 2 ++ 3 files changed, 29 insertions(+), 4 deletions(-) diff --git a/packages/lib/src/header/Header.stories.tsx b/packages/lib/src/header/Header.stories.tsx index 35d43c5a4c..ee33cde9a6 100644 --- a/packages/lib/src/header/Header.stories.tsx +++ b/packages/lib/src/header/Header.stories.tsx @@ -114,6 +114,8 @@ const Header = () => ( <DxcHeader /> <DxcHeader sideContent={<div>Side Content</div>} /> <DxcHeader navItems={items} sideContent={<div>Side Content</div>} /> + <Title title="Header with searchbar" theme="light" level={3} /> + <DxcHeader navItems={items} sideContent={<div>Side Content</div>} searchBar={{ placeholder: "Search..." }} /> <Title title="Header with long content" theme="light" level={3} /> <DxcHeader navItems={items} sideContent={<div>{longSideContent}</div>} /> <DxcHeader appTitle={longAppTitle} navItems={items} /> diff --git a/packages/lib/src/header/Header.tsx b/packages/lib/src/header/Header.tsx index 28485eed25..3507e90b95 100644 --- a/packages/lib/src/header/Header.tsx +++ b/packages/lib/src/header/Header.tsx @@ -134,7 +134,13 @@ const sanitizeNavItems = (navItems: HeaderProps["navItems"], level?: number): (G return sanitizedItems; }; -const DxcHeader = ({ appTitle, navItems, sideContent, responsiveBottomContent }: HeaderProps): JSX.Element => { +const DxcHeader = ({ + appTitle, + navItems, + responsiveBottomContent, + searchBar, + sideContent, +}: HeaderProps): JSX.Element => { const [isResponsive, setIsResponsive] = useState(false); const [isMenuVisible, setIsMenuVisible] = useState(false); const [showSearch, setShowSearch] = useState(false); @@ -160,6 +166,13 @@ const DxcHeader = ({ appTitle, navItems, sideContent, responsiveBottomContent }: }; const sanitizedNavItems = useMemo(() => (navItems ? sanitizeNavItems(navItems) : []), [navItems]); + const handleCancelSearch = () => { + if (typeof searchBar?.onCancel === "function") { + searchBar.onCancel(); + } + setShowSearch(false); + }; + return ( <MainContainer isResponsive={isResponsive} isMenuVisible={isMenuVisible}> <HeaderContainer> @@ -203,8 +216,16 @@ const DxcHeader = ({ appTitle, navItems, sideContent, responsiveBottomContent }: {!isResponsive && sanitizedNavItems && sanitizedNavItems.length > 0 && ( <MainNavContainer> - {showSearch ? ( - <DxcSearchBar onCancel={() => setShowSearch(false)} /> + {!!searchBar && showSearch ? ( + <DxcSearchBar + autoFocus={searchBar.autoFocus} + disabled={searchBar.disabled} + onBlur={searchBar.onBlur} + onCancel={handleCancelSearch} + onChange={searchBar.onChange} + onEnter={searchBar.onEnter} + placeholder={searchBar.placeholder} + /> ) : ( <DxcNavigationTree items={sanitizedNavItems} @@ -220,7 +241,7 @@ const DxcHeader = ({ appTitle, navItems, sideContent, responsiveBottomContent }: {!showSearch && (sideContent || isResponsive) && ( <RightSideContainer> - <DxcSearchBarTrigger onTriggerClick={() => setShowSearch(!showSearch)} /> + {!!searchBar && <DxcSearchBarTrigger onTriggerClick={() => setShowSearch(!showSearch)} />} {typeof sideContent === "function" ? sideContent(isResponsive) : sideContent} {isResponsive && ((navItems && navItems.length) || responsiveBottomContent) && ( <HamburguerButton onClick={toggleMenu} /> diff --git a/packages/lib/src/header/types.ts b/packages/lib/src/header/types.ts index 472004b613..dcbb33034a 100644 --- a/packages/lib/src/header/types.ts +++ b/packages/lib/src/header/types.ts @@ -1,5 +1,6 @@ import { ReactNode } from "react"; import { CommonItemProps, Item } from "../base-menu/types"; +import { SearchBarProps } from "../search-bar/types"; type GroupItem = CommonItemProps & { items: Item[]; @@ -11,6 +12,7 @@ type Props = { appTitle?: string; navItems?: MainNavPropsType; responsiveBottomContent?: ReactNode; + searchBar?: SearchBarProps; sideContent?: ReactNode | ((isResponsive: boolean) => ReactNode); }; From 1d124038e5a962ecfcb9f993ee7498def4776dc8 Mon Sep 17 00:00:00 2001 From: Pelayo Felgueroso <pfelguerosogalguera@gmail.com> Date: Mon, 5 Jan 2026 10:15:54 +0100 Subject: [PATCH 030/275] Add searchBar prop to HeaderCodePage --- .../components/header/code/HeaderCodePage.tsx | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/apps/website/screens/components/header/code/HeaderCodePage.tsx b/apps/website/screens/components/header/code/HeaderCodePage.tsx index 6f45766ae9..62af77c945 100644 --- a/apps/website/screens/components/header/code/HeaderCodePage.tsx +++ b/apps/website/screens/components/header/code/HeaderCodePage.tsx @@ -21,6 +21,16 @@ const groupItemTypeString = `{ items: (Item)[]; }`; +const searchBarTypeString = `{ + autoFocus?: boolean; + disabled?: boolean; + onBlur: (value: string) => void; + onCancel: () => void; + onChange: (value: string) => void; + onEnter: (value: string) => void; + placeholder?: string; +}`; + const sections = [ { title: "Props", @@ -91,6 +101,22 @@ const sections = [ </td> <td>-</td> </tr> + <tr> + <td> + <DxcFlex direction="column" gap="var(--spacing-gap-xs)" alignItems="baseline"> + <StatusBadge status="new" /> + searchBar + </DxcFlex> + </td> + <td> + <ExtendedTableCode>{searchBarTypeString}</ExtendedTableCode> + </td> + <td> + When provided, displays a searchbar trigger at the beginning of the side content. Clicking the trigger + expands the searchbar, allowing users to perform search operations. + </td> + <td>-</td> + </tr> <tr> <td> <DxcFlex direction="column" gap="var(--spacing-gap-xs)" alignItems="baseline"> @@ -105,6 +131,7 @@ const sections = [ Content to be displayed on the right side of the header. It can be a ReactNode or a function that receives a boolean indicating if the header is in responsive mode and returns a ReactNode. </td> + <td>-</td> </tr> </tbody> </DxcTable> From d7ce4f9b250b4b499b7f0f37b258921393f51121 Mon Sep 17 00:00:00 2001 From: Pelayo Felgueroso <pfelguerosogalguera@gmail.com> Date: Mon, 5 Jan 2026 11:29:43 +0100 Subject: [PATCH 031/275] Update tests as needed --- .../src/header/Header.accessibility.test.tsx | 1 + packages/lib/src/header/Header.stories.tsx | 1 + packages/lib/src/header/Header.test.tsx | 17 +++++++++++++++++ packages/lib/src/header/Header.tsx | 4 ++-- 4 files changed, 21 insertions(+), 2 deletions(-) diff --git a/packages/lib/src/header/Header.accessibility.test.tsx b/packages/lib/src/header/Header.accessibility.test.tsx index e337488f2a..e4ccced8bd 100644 --- a/packages/lib/src/header/Header.accessibility.test.tsx +++ b/packages/lib/src/header/Header.accessibility.test.tsx @@ -59,6 +59,7 @@ describe("Header component accessibility tests", () => { <DxcHeader appTitle={appTitle} navItems={items} + searchBar={{ placeholder: "Search" }} sideContent={ <DxcButton title="Settings" icon="settings" mode="tertiary" size={{ height: "medium" }} onClick={() => {}} /> } diff --git a/packages/lib/src/header/Header.stories.tsx b/packages/lib/src/header/Header.stories.tsx index ee33cde9a6..4eeed6ddb1 100644 --- a/packages/lib/src/header/Header.stories.tsx +++ b/packages/lib/src/header/Header.stories.tsx @@ -133,6 +133,7 @@ const HeaderInLayout = () => ( <DxcApplicationLayout.Header appTitle="Application Layout with Header" navItems={items} + searchBar={{ placeholder: "Search..." }} sideContent={(isResponsive) => isResponsive ? ( <> diff --git a/packages/lib/src/header/Header.test.tsx b/packages/lib/src/header/Header.test.tsx index a1546a6d46..3e9fa3bf63 100644 --- a/packages/lib/src/header/Header.test.tsx +++ b/packages/lib/src/header/Header.test.tsx @@ -59,4 +59,21 @@ describe("Header component tests", () => { expect(screen.queryByText("Frontend")).not.toBeInTheDocument(); expect(screen.queryByText("Backend")).not.toBeInTheDocument(); }); + + test("search bar appears and functions correctly", () => { + const onEnterMock = jest.fn(); + const onCancelMock = jest.fn(); + render(<DxcHeader searchBar={{ placeholder: "Search...", onEnter: onEnterMock, onCancel: onCancelMock }} />); + const searchIcon = screen.getByRole("button", { name: /search/i }); + fireEvent.click(searchIcon); + const searchInput = screen.getByPlaceholderText("Search..."); + expect(searchInput).toBeInTheDocument(); + fireEvent.change(searchInput, { target: { value: "test query" } }); + fireEvent.keyDown(searchInput, { key: "Enter", code: "Enter" }); + expect(onEnterMock).toHaveBeenCalledWith("test query"); + const cancelButton = screen.getByRole("button", { name: /cancel/i }); + fireEvent.click(cancelButton); + expect(onCancelMock).toHaveBeenCalled(); + expect(searchInput).not.toBeInTheDocument(); + }); }); diff --git a/packages/lib/src/header/Header.tsx b/packages/lib/src/header/Header.tsx index 3507e90b95..dd43266bab 100644 --- a/packages/lib/src/header/Header.tsx +++ b/packages/lib/src/header/Header.tsx @@ -214,7 +214,7 @@ const DxcHeader = ({ </BrandingContainer> )} - {!isResponsive && sanitizedNavItems && sanitizedNavItems.length > 0 && ( + {((!isResponsive && sanitizedNavItems && sanitizedNavItems.length > 0) || (!!searchBar && showSearch)) && ( <MainNavContainer> {!!searchBar && showSearch ? ( <DxcSearchBar @@ -239,7 +239,7 @@ const DxcHeader = ({ </MainNavContainer> )} - {!showSearch && (sideContent || isResponsive) && ( + {!showSearch && (sideContent || isResponsive || !!searchBar) && ( <RightSideContainer> {!!searchBar && <DxcSearchBarTrigger onTriggerClick={() => setShowSearch(!showSearch)} />} {typeof sideContent === "function" ? sideContent(isResponsive) : sideContent} From b5fc9864a3bf57a30fa7e7daac045c1873dab492 Mon Sep 17 00:00:00 2001 From: Pelayo Felgueroso <pfelguerosogalguera@gmail.com> Date: Mon, 5 Jan 2026 11:42:29 +0100 Subject: [PATCH 032/275] Update storybook test --- packages/lib/src/header/Header.stories.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/lib/src/header/Header.stories.tsx b/packages/lib/src/header/Header.stories.tsx index 4eeed6ddb1..91828754cd 100644 --- a/packages/lib/src/header/Header.stories.tsx +++ b/packages/lib/src/header/Header.stories.tsx @@ -229,6 +229,8 @@ export const Responsive: Story = { await userEvent.tab(); await new Promise<void>((resolve) => setTimeout(resolve, 100)); await userEvent.tab(); + await new Promise<void>((resolve) => setTimeout(resolve, 100)); + await userEvent.tab(); await userEvent.keyboard("{Enter}"); await canvas.findByText("Bottom content button"); }, From a0b04148683a6d43e4f34727efd5cede732ebe10 Mon Sep 17 00:00:00 2001 From: Pelayo Felgueroso <pfelguerosogalguera@gmail.com> Date: Wed, 7 Jan 2026 11:05:25 +0100 Subject: [PATCH 033/275] Enhance search bar functionality and responsiveness in Header component - Updated search bar trigger behavior and description in HeaderCodePage. - Refined test cases for search bar appearance and functionality in responsive mode. - Adjusted Header component to ensure search bar displays correctly based on responsiveness. --- .../components/header/code/HeaderCodePage.tsx | 8 +++++-- packages/lib/src/header/Header.stories.tsx | 2 -- packages/lib/src/header/Header.test.tsx | 19 +++++++++++++++ packages/lib/src/header/Header.tsx | 24 ++++++++++++++----- 4 files changed, 43 insertions(+), 10 deletions(-) diff --git a/apps/website/screens/components/header/code/HeaderCodePage.tsx b/apps/website/screens/components/header/code/HeaderCodePage.tsx index 62af77c945..054d0a8c07 100644 --- a/apps/website/screens/components/header/code/HeaderCodePage.tsx +++ b/apps/website/screens/components/header/code/HeaderCodePage.tsx @@ -112,8 +112,12 @@ const sections = [ <ExtendedTableCode>{searchBarTypeString}</ExtendedTableCode> </td> <td> - When provided, displays a searchbar trigger at the beginning of the side content. Clicking the trigger - expands the searchbar, allowing users to perform search operations. + When provided, a search bar trigger is shown at the start of the side content. Activating the trigger + expands the search bar, enabling search interactions. + <p> + In responsive mode, the search bar is displayed directly (without a trigger), and the{" "} + <Code>onCancel</Code> callback is not called. + </p> </td> <td>-</td> </tr> diff --git a/packages/lib/src/header/Header.stories.tsx b/packages/lib/src/header/Header.stories.tsx index 91828754cd..4eeed6ddb1 100644 --- a/packages/lib/src/header/Header.stories.tsx +++ b/packages/lib/src/header/Header.stories.tsx @@ -229,8 +229,6 @@ export const Responsive: Story = { await userEvent.tab(); await new Promise<void>((resolve) => setTimeout(resolve, 100)); await userEvent.tab(); - await new Promise<void>((resolve) => setTimeout(resolve, 100)); - await userEvent.tab(); await userEvent.keyboard("{Enter}"); await canvas.findByText("Bottom content button"); }, diff --git a/packages/lib/src/header/Header.test.tsx b/packages/lib/src/header/Header.test.tsx index 3e9fa3bf63..0b4367cbf6 100644 --- a/packages/lib/src/header/Header.test.tsx +++ b/packages/lib/src/header/Header.test.tsx @@ -63,7 +63,9 @@ describe("Header component tests", () => { test("search bar appears and functions correctly", () => { const onEnterMock = jest.fn(); const onCancelMock = jest.fn(); + render(<DxcHeader searchBar={{ placeholder: "Search...", onEnter: onEnterMock, onCancel: onCancelMock }} />); + const searchIcon = screen.getByRole("button", { name: /search/i }); fireEvent.click(searchIcon); const searchInput = screen.getByPlaceholderText("Search..."); @@ -76,4 +78,21 @@ describe("Header component tests", () => { expect(onCancelMock).toHaveBeenCalled(); expect(searchInput).not.toBeInTheDocument(); }); + + test("search bar appears correctly in responsive mode", () => { + mockMatchMedia.mockImplementation(() => ({ + matches: true, + addEventListener: jest.fn(), + removeEventListener: jest.fn(), + })); + + render(<DxcHeader searchBar={{ placeholder: "Search..." }} />); + + const menuButton = screen.getByRole("button", { name: /toggle menu/i }); + fireEvent.click(menuButton); + const searchInput = screen.getByPlaceholderText("Search..."); + expect(searchInput).toBeInTheDocument(); + const cancelButton = screen.queryByRole("button", { name: /cancel/i }); + expect(cancelButton).not.toBeInTheDocument(); + }); }); diff --git a/packages/lib/src/header/Header.tsx b/packages/lib/src/header/Header.tsx index dd43266bab..7fb78d348c 100644 --- a/packages/lib/src/header/Header.tsx +++ b/packages/lib/src/header/Header.tsx @@ -178,7 +178,7 @@ const DxcHeader = ({ <HeaderContainer> <DxcGrid templateColumns={ - showSearch + showSearch && !isResponsive ? ["auto"] : !isResponsive && sanitizedNavItems && sanitizedNavItems.length > 0 ? [`auto`, `minmax(auto, ${MAX_MAIN_NAV_SIZE})`, `auto`] @@ -188,7 +188,7 @@ const DxcHeader = ({ gap="var(--spacing-gap-ml)" placeItems="center" > - {!showSearch && ( + {(!showSearch || isResponsive) && ( <BrandingContainer> {logo && ( <LogoContainer @@ -214,7 +214,7 @@ const DxcHeader = ({ </BrandingContainer> )} - {((!isResponsive && sanitizedNavItems && sanitizedNavItems.length > 0) || (!!searchBar && showSearch)) && ( + {!isResponsive && ((sanitizedNavItems && sanitizedNavItems.length > 0) || (!!searchBar && showSearch)) && ( <MainNavContainer> {!!searchBar && showSearch ? ( <DxcSearchBar @@ -239,11 +239,13 @@ const DxcHeader = ({ </MainNavContainer> )} - {!showSearch && (sideContent || isResponsive || !!searchBar) && ( + {(!showSearch || isResponsive) && (sideContent || isResponsive || !!searchBar) && ( <RightSideContainer> - {!!searchBar && <DxcSearchBarTrigger onTriggerClick={() => setShowSearch(!showSearch)} />} + {!!searchBar && !isResponsive && ( + <DxcSearchBarTrigger onTriggerClick={() => setShowSearch(!showSearch)} /> + )} {typeof sideContent === "function" ? sideContent(isResponsive) : sideContent} - {isResponsive && ((navItems && navItems.length) || responsiveBottomContent) && ( + {isResponsive && ((navItems && navItems.length) || responsiveBottomContent || !!searchBar) && ( <HamburguerButton onClick={toggleMenu} /> )} </RightSideContainer> @@ -254,6 +256,16 @@ const DxcHeader = ({ <ResponsiveMenuContainer> <ResponsiveMenu> {appTitle && <DxcHeading text={appTitle} as="h1" level={5} />} + {!!searchBar && ( + <DxcSearchBar + autoFocus={searchBar.autoFocus} + disabled={searchBar.disabled} + onBlur={searchBar.onBlur} + onChange={searchBar.onChange} + onEnter={searchBar.onEnter} + placeholder={searchBar.placeholder} + /> + )} <DxcNavigationTree items={sanitizedNavItems} displayGroupLines={false} From 32c3ab9c2e4530769b800a9394fa5bc98a5ed5be Mon Sep 17 00:00:00 2001 From: Pelayo Felgueroso <pfelguerosogalguera@gmail.com> Date: Tue, 13 Jan 2026 16:57:14 +0100 Subject: [PATCH 034/275] Fix based on comments --- .../screens/components/header/code/HeaderCodePage.tsx | 2 -- packages/lib/src/header/Header.tsx | 9 +++++---- packages/lib/src/header/types.ts | 4 +++- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/apps/website/screens/components/header/code/HeaderCodePage.tsx b/apps/website/screens/components/header/code/HeaderCodePage.tsx index 054d0a8c07..3b59f354d3 100644 --- a/apps/website/screens/components/header/code/HeaderCodePage.tsx +++ b/apps/website/screens/components/header/code/HeaderCodePage.tsx @@ -22,8 +22,6 @@ const groupItemTypeString = `{ }`; const searchBarTypeString = `{ - autoFocus?: boolean; - disabled?: boolean; onBlur: (value: string) => void; onCancel: () => void; onChange: (value: string) => void; diff --git a/packages/lib/src/header/Header.tsx b/packages/lib/src/header/Header.tsx index 7fb78d348c..cdd4089c60 100644 --- a/packages/lib/src/header/Header.tsx +++ b/packages/lib/src/header/Header.tsx @@ -173,6 +173,10 @@ const DxcHeader = ({ setShowSearch(false); }; + useEffect(() => { + setShowSearch(false); + }, [isResponsive]); + return ( <MainContainer isResponsive={isResponsive} isMenuVisible={isMenuVisible}> <HeaderContainer> @@ -218,8 +222,7 @@ const DxcHeader = ({ <MainNavContainer> {!!searchBar && showSearch ? ( <DxcSearchBar - autoFocus={searchBar.autoFocus} - disabled={searchBar.disabled} + autoFocus={true} onBlur={searchBar.onBlur} onCancel={handleCancelSearch} onChange={searchBar.onChange} @@ -258,8 +261,6 @@ const DxcHeader = ({ {appTitle && <DxcHeading text={appTitle} as="h1" level={5} />} {!!searchBar && ( <DxcSearchBar - autoFocus={searchBar.autoFocus} - disabled={searchBar.disabled} onBlur={searchBar.onBlur} onChange={searchBar.onChange} onEnter={searchBar.onEnter} diff --git a/packages/lib/src/header/types.ts b/packages/lib/src/header/types.ts index dcbb33034a..b8ba097537 100644 --- a/packages/lib/src/header/types.ts +++ b/packages/lib/src/header/types.ts @@ -8,11 +8,13 @@ type GroupItem = CommonItemProps & { type MainNavPropsType = (GroupItem | Item)[]; +type SearchBarHeaderProps = Omit<SearchBarProps, "autoFocus" | "disabled">; + type Props = { appTitle?: string; navItems?: MainNavPropsType; responsiveBottomContent?: ReactNode; - searchBar?: SearchBarProps; + searchBar?: SearchBarHeaderProps; sideContent?: ReactNode | ((isResponsive: boolean) => ReactNode); }; From f3022dbcc95647e3f945d7a3f92ed9c517f697ed Mon Sep 17 00:00:00 2001 From: Pelayo Felgueroso <pfelguerosogalguera@gmail.com> Date: Wed, 14 Jan 2026 12:25:44 +0100 Subject: [PATCH 035/275] Add open search bar story to header stories --- packages/lib/src/header/Header.stories.tsx | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/packages/lib/src/header/Header.stories.tsx b/packages/lib/src/header/Header.stories.tsx index 4eeed6ddb1..5b11bd2d7d 100644 --- a/packages/lib/src/header/Header.stories.tsx +++ b/packages/lib/src/header/Header.stories.tsx @@ -233,3 +233,13 @@ export const Responsive: Story = { await canvas.findByText("Bottom content button"); }, }; + +export const OpenSearchBar: Story = { + render: HeaderInLayout, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + const searchButton = canvas.getByRole("button", { name: "Search" }); + await userEvent.click(searchButton); + await canvas.findByPlaceholderText("Search..."); + }, +}; From 9029a3dea243d0ce54c60405a2247b2d3a2f98ba Mon Sep 17 00:00:00 2001 From: Pelayo Felgueroso <pfelguerosogalguera@gmail.com> Date: Thu, 8 Jan 2026 12:54:38 +0100 Subject: [PATCH 036/275] Implement search bar in Sidenav component and update related types and tests --- apps/website/pages/_app.tsx | 16 ++------------ .../sidenav/code/SidenavCodePage.tsx | 22 +++++++++++++++++++ packages/lib/src/search-bar/types.ts | 1 + .../sidenav/Sidenav.accessibility.test.tsx | 4 +++- packages/lib/src/sidenav/Sidenav.stories.tsx | 7 ++++++ packages/lib/src/sidenav/Sidenav.test.tsx | 19 +++++++++++++++- packages/lib/src/sidenav/Sidenav.tsx | 21 ++++++++++++++++-- packages/lib/src/sidenav/types.ts | 5 +++++ 8 files changed, 77 insertions(+), 18 deletions(-) diff --git a/apps/website/pages/_app.tsx b/apps/website/pages/_app.tsx index 2603ad3498..ed382f285e 100644 --- a/apps/website/pages/_app.tsx +++ b/apps/website/pages/_app.tsx @@ -2,7 +2,7 @@ import { ReactElement, ReactNode, useMemo, useState } from "react"; import type { NextPage } from "next"; import type { AppProps } from "next/app"; import Head from "next/head"; -import { DxcApplicationLayout, DxcTextInput, DxcToastsQueue } from "@dxc-technology/halstack-react"; +import { DxcApplicationLayout, DxcToastsQueue } from "@dxc-technology/halstack-react"; import MainContent from "@/common/MainContent"; import { useRouter } from "next/router"; import { LinkDetails, LinksSectionDetails, LinksSections } from "@/common/pagesList"; @@ -108,19 +108,7 @@ export default function App({ Component, pageProps, emotionCache = clientSideEmo <DxcApplicationLayout.Sidenav navItems={navItems} appTitle={isExpanded && <SidenavLogo />} - topContent={ - isExpanded && ( - <DxcTextInput - placeholder="Search docs" - value={filter} - onChange={({ value }) => { - setFilter(value); - }} - size="fillParent" - clearable - /> - ) - } + searchBar={{ placeholder: "Search docs", onChange: (value) => setFilter(value) }} expanded={isExpanded} onExpandedChange={() => { setIsExpanded((currentlyExpanded) => !currentlyExpanded); diff --git a/apps/website/screens/components/sidenav/code/SidenavCodePage.tsx b/apps/website/screens/components/sidenav/code/SidenavCodePage.tsx index d0bd0bc470..b21fb54001 100644 --- a/apps/website/screens/components/sidenav/code/SidenavCodePage.tsx +++ b/apps/website/screens/components/sidenav/code/SidenavCodePage.tsx @@ -24,6 +24,15 @@ const sectionTypeString = `{ title?: string }; }`; +const searchBarTypeString = `{ + autoFocus?: boolean; + disabled?: boolean; + onBlur: (value: string) => void; + onChange: (value: string) => void; + onEnter: (value: string) => void; + placeholder?: string; +}`; + const sections = [ { title: "Props", @@ -147,6 +156,19 @@ const sections = [ <td>Function called when the expansion state of the sidenav changes.</td> <td>-</td> </tr> + <tr> + <td> + <DxcFlex direction="column" gap="var(--spacing-gap-xs)" alignItems="baseline"> + <StatusBadge status="new" /> + searchBar + </DxcFlex> + </td> + <td> + <ExtendedTableCode>{searchBarTypeString}</ExtendedTableCode> + </td> + <td>When provided, a search bar will be rendered at the top of the sidenav.</td> + <td>-</td> + </tr> <tr> <td> <DxcFlex direction="column" gap="var(--spacing-gap-xs)" alignItems="baseline"> diff --git a/packages/lib/src/search-bar/types.ts b/packages/lib/src/search-bar/types.ts index 5d67a8f39f..869f84be2f 100644 --- a/packages/lib/src/search-bar/types.ts +++ b/packages/lib/src/search-bar/types.ts @@ -4,6 +4,7 @@ export type SearchBarTriggerProps = { */ onTriggerClick?: () => void; }; +export type RefType = HTMLDivElement; export type SearchBarProps = { /** * If true, the search bar input will be focused when rendered. diff --git a/packages/lib/src/sidenav/Sidenav.accessibility.test.tsx b/packages/lib/src/sidenav/Sidenav.accessibility.test.tsx index 157d20d52c..7b2ac21e15 100644 --- a/packages/lib/src/sidenav/Sidenav.accessibility.test.tsx +++ b/packages/lib/src/sidenav/Sidenav.accessibility.test.tsx @@ -47,7 +47,9 @@ describe("Sidenav component accessibility tests", () => { ], }, ]; - const { container } = render(<DxcSidenav navItems={groupItems} appTitle="Application Name" />); + const { container } = render( + <DxcSidenav navItems={groupItems} appTitle="Application Name" searchBar={{ placeholder: "Search..." }} /> + ); const results = await axe(container); expect(results.violations).toHaveLength(0); }); diff --git a/packages/lib/src/sidenav/Sidenav.stories.tsx b/packages/lib/src/sidenav/Sidenav.stories.tsx index 8c47a08455..e0a68423d2 100644 --- a/packages/lib/src/sidenav/Sidenav.stories.tsx +++ b/packages/lib/src/sidenav/Sidenav.stories.tsx @@ -140,6 +140,7 @@ const Sidenav = () => ( <DxcSidenav navItems={groupItems} appTitle="Application Name" + searchBar={{ placeholder: "Search..." }} bottomContent={ <> <DetailedAvatar /> @@ -169,6 +170,7 @@ const Sidenav = () => ( <DxcSidenav navItems={groupItems} appTitle="Application Name" + searchBar={{ placeholder: "Search..." }} bottomContent={ <> <DetailedAvatar /> @@ -208,6 +210,7 @@ const Collapsed = () => { <DxcSidenav navItems={groupItems} appTitle="App Name" + searchBar={{ placeholder: "Search..." }} bottomContent={ isExpanded ? ( <> @@ -261,6 +264,7 @@ const Collapsed = () => { <DxcSidenav navItems={groupItems} appTitle="App Name" + searchBar={{ placeholder: "Search..." }} bottomContent={ isExpandedGroupsNoLines ? ( <> @@ -314,6 +318,7 @@ const Collapsed = () => { <DxcSidenav navItems={groupItems} appTitle="App Name" + searchBar={{ placeholder: "Search..." }} bottomContent={ isExpandedGroups ? ( <> @@ -373,6 +378,7 @@ const Hovered = () => ( <DxcSidenav navItems={groupItems} appTitle="Application Name" + searchBar={{ placeholder: "Search..." }} bottomContent={ <> <DetailedAvatar /> @@ -405,6 +411,7 @@ const SelectedGroup = () => ( <DxcSidenav navItems={selectedGroupItems} appTitle="Application name" + searchBar={{ placeholder: "Search..." }} bottomContent={ <> <DetailedAvatar /> diff --git a/packages/lib/src/sidenav/Sidenav.test.tsx b/packages/lib/src/sidenav/Sidenav.test.tsx index eb0edfc1c8..cbbc8c7178 100644 --- a/packages/lib/src/sidenav/Sidenav.test.tsx +++ b/packages/lib/src/sidenav/Sidenav.test.tsx @@ -1,5 +1,5 @@ import "@testing-library/jest-dom"; -import { render, fireEvent } from "@testing-library/react"; +import { render, fireEvent, waitFor } from "@testing-library/react"; import DxcSidenav from "./Sidenav"; import { ReactNode } from "react"; @@ -103,4 +103,21 @@ describe("DxcSidenav component", () => { const collapseButton = getByRole("button", { name: "Collapse" }); expect(collapseButton).toBeTruthy(); }); + + test("Sidenav renders search bar when searchBar prop is provided", () => { + const { getByPlaceholderText } = render(<DxcSidenav searchBar={{ placeholder: "Search..." }} />); + expect(getByPlaceholderText("Search...")).toBeTruthy(); + }); + + test("Sidenav expands and focuses search input when handleExpandSearch is called", async () => { + const { getByRole, getByPlaceholderText } = render( + <DxcSidenav searchBar={{ placeholder: "Search..." }} defaultExpanded={false} /> + ); + const expandButton = getByRole("button", { name: "Search" }); + fireEvent.click(expandButton); + const searchInput = getByPlaceholderText("Search...") as HTMLInputElement; + await waitFor(() => { + expect(document.activeElement).toBe(searchInput); + }); + }); }); diff --git a/packages/lib/src/sidenav/Sidenav.tsx b/packages/lib/src/sidenav/Sidenav.tsx index 8d2292058b..8f7c885c2c 100644 --- a/packages/lib/src/sidenav/Sidenav.tsx +++ b/packages/lib/src/sidenav/Sidenav.tsx @@ -5,10 +5,12 @@ import SidenavPropsType from "./types"; import DxcDivider from "../divider/Divider"; import DxcButton from "../button/Button"; import DxcImage from "../image/Image"; -import { useContext, useState } from "react"; +import { useContext, useRef, useState } from "react"; import DxcNavigationTree from "../navigation-tree/NavigationTree"; import DxcInset from "../inset/Inset"; import ApplicationLayoutContext from "../layout/ApplicationLayoutContext"; +import DxcSearchBar from "../search-bar/SearchBar"; +import DxcSearchBarTrigger from "../search-bar/SearchBarTrigger"; const SidenavContainer = styled.div<{ expanded: boolean }>` box-sizing: border-box; @@ -67,11 +69,13 @@ const DxcSidenav = ({ expanded, defaultExpanded = true, onExpandedChange, + searchBar, }: SidenavPropsType): JSX.Element => { const [internalExpanded, setInternalExpanded] = useState(defaultExpanded); const { logo, headerExists } = useContext(ApplicationLayoutContext); const isControlled = expanded !== undefined; const isExpanded = isControlled ? !!expanded : internalExpanded; + const searchBarRef = useRef<HTMLDivElement>(null); const handleToggle = () => { const nextState = !isExpanded; @@ -79,6 +83,13 @@ const DxcSidenav = ({ onExpandedChange?.(nextState); }; + const handleExpandSearch = () => { + handleToggle(); + setTimeout(() => { + searchBarRef.current?.querySelector("input")?.focus(); + }, 1); + }; + return ( <SidenavContainer expanded={isExpanded}> <DxcFlex @@ -114,8 +125,14 @@ const DxcSidenav = ({ <SidenavTitle>{appTitle}</SidenavTitle> </DxcFlex> </DxcFlex> - {topContent && ( + {(topContent || searchBar) && ( <DxcFlex direction="column" gap={"var(--spacing-gap-l)"}> + {searchBar && + (isExpanded ? ( + <DxcSearchBar ref={searchBarRef} {...searchBar} /> + ) : ( + <DxcSearchBarTrigger onTriggerClick={handleExpandSearch} /> + ))} {topContent} </DxcFlex> )} diff --git a/packages/lib/src/sidenav/types.ts b/packages/lib/src/sidenav/types.ts index 378fd196f1..5a315b75a7 100644 --- a/packages/lib/src/sidenav/types.ts +++ b/packages/lib/src/sidenav/types.ts @@ -1,5 +1,6 @@ import { ReactElement, ReactNode } from "react"; import { SVG } from "../common/utils"; +import { SearchBarProps } from "../search-bar/types"; type Section = { items: (Item | GroupItem)[]; title?: string }; @@ -34,6 +35,10 @@ type Props = { * Function called when the expansion state of the sidenav changes. */ onExpandedChange?: (value: boolean) => void; + /** + * When provided, a search bar will be rendered at the top of the sidenav. + */ + searchBar?: Omit<SearchBarProps, "onCancel">; /** * The additional content rendered in the upper part of the sidenav, under the branding. */ From 787bce0077f9216c735ca6e825563398067418b6 Mon Sep 17 00:00:00 2001 From: Pelayo Felgueroso <pfelguerosogalguera@gmail.com> Date: Tue, 13 Jan 2026 09:18:53 +0100 Subject: [PATCH 037/275] Refactor searchBar autofocus on Sidenav component --- packages/lib/src/sidenav/Sidenav.tsx | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/lib/src/sidenav/Sidenav.tsx b/packages/lib/src/sidenav/Sidenav.tsx index 8f7c885c2c..e2f7e5f204 100644 --- a/packages/lib/src/sidenav/Sidenav.tsx +++ b/packages/lib/src/sidenav/Sidenav.tsx @@ -76,18 +76,20 @@ const DxcSidenav = ({ const isControlled = expanded !== undefined; const isExpanded = isControlled ? !!expanded : internalExpanded; const searchBarRef = useRef<HTMLDivElement>(null); + const shouldFocusSearchBar = useRef(false); const handleToggle = () => { const nextState = !isExpanded; if (!isControlled) setInternalExpanded(nextState); onExpandedChange?.(nextState); + if (searchBar && nextState === false) { + shouldFocusSearchBar.current = false; + } }; const handleExpandSearch = () => { + shouldFocusSearchBar.current = true; handleToggle(); - setTimeout(() => { - searchBarRef.current?.querySelector("input")?.focus(); - }, 1); }; return ( @@ -129,7 +131,7 @@ const DxcSidenav = ({ <DxcFlex direction="column" gap={"var(--spacing-gap-l)"}> {searchBar && (isExpanded ? ( - <DxcSearchBar ref={searchBarRef} {...searchBar} /> + <DxcSearchBar ref={searchBarRef} {...searchBar} autoFocus={shouldFocusSearchBar.current} /> ) : ( <DxcSearchBarTrigger onTriggerClick={handleExpandSearch} /> ))} From c93a46ddd8dc42a4b40381c00fa4116afd1e8c3e Mon Sep 17 00:00:00 2001 From: Pelayo Felgueroso <pfelguerosogalguera@gmail.com> Date: Tue, 13 Jan 2026 09:23:38 +0100 Subject: [PATCH 038/275] Remove unnecesary changes --- packages/lib/src/search-bar/types.ts | 1 - packages/lib/src/sidenav/Sidenav.tsx | 3 +-- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/lib/src/search-bar/types.ts b/packages/lib/src/search-bar/types.ts index 869f84be2f..5d67a8f39f 100644 --- a/packages/lib/src/search-bar/types.ts +++ b/packages/lib/src/search-bar/types.ts @@ -4,7 +4,6 @@ export type SearchBarTriggerProps = { */ onTriggerClick?: () => void; }; -export type RefType = HTMLDivElement; export type SearchBarProps = { /** * If true, the search bar input will be focused when rendered. diff --git a/packages/lib/src/sidenav/Sidenav.tsx b/packages/lib/src/sidenav/Sidenav.tsx index e2f7e5f204..7481e199e7 100644 --- a/packages/lib/src/sidenav/Sidenav.tsx +++ b/packages/lib/src/sidenav/Sidenav.tsx @@ -75,7 +75,6 @@ const DxcSidenav = ({ const { logo, headerExists } = useContext(ApplicationLayoutContext); const isControlled = expanded !== undefined; const isExpanded = isControlled ? !!expanded : internalExpanded; - const searchBarRef = useRef<HTMLDivElement>(null); const shouldFocusSearchBar = useRef(false); const handleToggle = () => { @@ -131,7 +130,7 @@ const DxcSidenav = ({ <DxcFlex direction="column" gap={"var(--spacing-gap-l)"}> {searchBar && (isExpanded ? ( - <DxcSearchBar ref={searchBarRef} {...searchBar} autoFocus={shouldFocusSearchBar.current} /> + <DxcSearchBar {...searchBar} autoFocus={shouldFocusSearchBar.current} /> ) : ( <DxcSearchBarTrigger onTriggerClick={handleExpandSearch} /> ))} From 5560e484939ce54ea56ecbd05c7454e1eef802e9 Mon Sep 17 00:00:00 2001 From: Pelayo Felgueroso <pfelguerosogalguera@gmail.com> Date: Tue, 13 Jan 2026 17:07:19 +0100 Subject: [PATCH 039/275] Remove unnecesary searchbar props from sidenav searchBar prop --- .../screens/components/sidenav/code/SidenavCodePage.tsx | 2 -- packages/lib/src/sidenav/types.ts | 4 +++- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/website/screens/components/sidenav/code/SidenavCodePage.tsx b/apps/website/screens/components/sidenav/code/SidenavCodePage.tsx index b21fb54001..137e967e32 100644 --- a/apps/website/screens/components/sidenav/code/SidenavCodePage.tsx +++ b/apps/website/screens/components/sidenav/code/SidenavCodePage.tsx @@ -25,8 +25,6 @@ const sectionTypeString = `{ }`; const searchBarTypeString = `{ - autoFocus?: boolean; - disabled?: boolean; onBlur: (value: string) => void; onChange: (value: string) => void; onEnter: (value: string) => void; diff --git a/packages/lib/src/sidenav/types.ts b/packages/lib/src/sidenav/types.ts index 5a315b75a7..34e511b4e3 100644 --- a/packages/lib/src/sidenav/types.ts +++ b/packages/lib/src/sidenav/types.ts @@ -4,6 +4,8 @@ import { SearchBarProps } from "../search-bar/types"; type Section = { items: (Item | GroupItem)[]; title?: string }; +type SearchbarSidenavProps = Omit<SearchBarProps, "autoFocus" | "disabled" | "onCancel">; + type Props = { /** * The content rendered in the bottom part of the sidenav, under the navigation menu. @@ -38,7 +40,7 @@ type Props = { /** * When provided, a search bar will be rendered at the top of the sidenav. */ - searchBar?: Omit<SearchBarProps, "onCancel">; + searchBar?: SearchbarSidenavProps; /** * The additional content rendered in the upper part of the sidenav, under the branding. */ From c1be1479f7578527368c4b2e2446121ca07a0605 Mon Sep 17 00:00:00 2001 From: Pelayo Felgueroso <pfelguerosogalguera@gmail.com> Date: Wed, 21 Jan 2026 13:17:40 +0100 Subject: [PATCH 040/275] Refactor Avatar and ActionIcon to move Overlay and Status from ActionIcon to Avatar --- .../src/action-icon/ActionIcon.stories.tsx | 22 +--- packages/lib/src/action-icon/ActionIcon.tsx | 112 +++++++----------- packages/lib/src/action-icon/types.ts | 5 - packages/lib/src/action-icon/utils.ts | 23 ---- packages/lib/src/avatar/Avatar.stories.tsx | 73 ++++++++++++ packages/lib/src/avatar/Avatar.tsx | 67 ++++++++++- packages/lib/src/avatar/utils.ts | 23 ++++ 7 files changed, 204 insertions(+), 121 deletions(-) diff --git a/packages/lib/src/action-icon/ActionIcon.stories.tsx b/packages/lib/src/action-icon/ActionIcon.stories.tsx index 503b11f00e..0059e36d83 100644 --- a/packages/lib/src/action-icon/ActionIcon.stories.tsx +++ b/packages/lib/src/action-icon/ActionIcon.stories.tsx @@ -99,7 +99,6 @@ const ActionIconRow = ({ size={size} shape={shape} color={color} - status={position && mode ? { position, mode: mode } : undefined} onClick={pseudoStatesEnabled ? () => console.log("") : undefined} disabled={isDisabled} /> @@ -163,29 +162,16 @@ export const Colors: Story = { ), }; -export const Statuses: Story = { - render: () => ( - <> - <Title title="Statuses" theme="light" level={2} /> - <ActionIconRow - sizes={["xsmall", "small", "medium", "large", "xlarge", "xxlarge"]} - colors={["neutral", "primary", "secondary", "tertiary", "success", "warning", "error", "info", "transparent"]} - shapes={["circle"]} - statusModes={["default", "info", "success", "warning", "error"]} - statusPositions={["top", "bottom"]} - groupBy={["statusPosition", "statusMode", "color"]} - /> - </> - ), -}; - export const PseudoStates: Story = { render: () => ( <> <Title title="Pseudo states" theme="light" level={2} /> + <DxcActionIcon color="transparent" icon="settings" onClick={() => console.log("hello")} /> + <ActionIconRow + colors={["transparent"]} sizes={["xsmall", "small", "medium", "large", "xlarge", "xxlarge"]} - shapes={["circle"]} + shapes={["square"]} statusModes={["success"]} statusPositions={[undefined, "top", "bottom"]} pseudoStates={[undefined, "pseudo-hover", "pseudo-focus", "pseudo-active", "disabled"]} diff --git a/packages/lib/src/action-icon/ActionIcon.tsx b/packages/lib/src/action-icon/ActionIcon.tsx index b4b521dcee..60bfaa6847 100644 --- a/packages/lib/src/action-icon/ActionIcon.tsx +++ b/packages/lib/src/action-icon/ActionIcon.tsx @@ -2,55 +2,47 @@ import { forwardRef } from "react"; import styled from "@emotion/styled"; import { css } from "@emotion/react"; import { ActionIconPropTypes, RefType } from "./types"; -import { - getBackgroundColor, - getBorderRadius, - getBorderWidth, - getColor, - getIconSize, - getModeColor, - getOutlineWidth, - getSize, -} from "./utils"; +import { getBackgroundColor, getBorderRadius, getColor, getIconSize, getOutlineWidth, getSize } from "./utils"; import DxcIcon from "../icon/Icon"; import { Tooltip } from "../tooltip/Tooltip"; const ActionIconContainer = styled.div< { hasAction?: boolean; + shape: ActionIconPropTypes["shape"]; size: ActionIconPropTypes["size"]; disabled?: ActionIconPropTypes["disabled"]; + color: ActionIconPropTypes["color"]; } & React.AnchorHTMLAttributes<HTMLAnchorElement> >` - position: relative; - display: flex; - justify-content: center; - align-items: center; - height: ${({ size }) => getSize(size)}; - aspect-ratio: 1 / 1; - text-decoration: none; - /* Reset button default styles when rendered as button */ &[type="button"] { - background: none; border: none; padding: 0; margin: 0; font: inherit; - color: inherit; outline: none; } + + position: relative; + display: flex; + justify-content: center; + align-items: center; + height: ${({ size }) => getSize(size)}; + aspect-ratio: 1 / 1; + text-decoration: none; + border-radius: ${({ shape, size }) => getBorderRadius(shape, size)}; + background-color: ${({ color }) => getBackgroundColor(color)}; + color: ${({ color }) => getColor(color)}; + ${({ hasAction, disabled, size }) => !disabled && hasAction && css` cursor: pointer; - &:hover > div:first-child > div:first-child, - &:active > div:first-child > div:first-child { - display: block; - } - &:focus:enabled > div:first-child, - &:active:enabled > div:first-child { + + &:focus:enabled, + &:active:enabled { outline-style: solid; outline-width: ${getOutlineWidth(size)}; outline-color: var(--border-color-secondary-medium); @@ -65,36 +57,31 @@ const ActionIconContainer = styled.div< css` cursor: not-allowed; & > div:first-child > div:first-child { - display: block; - background-color: rgba(255, 255, 255, 0.5); + color: var(--color-fg-neutral-medium); } `} `; -const ActionIconWrapper = styled.div<{ - shape: ActionIconPropTypes["shape"]; - color: ActionIconPropTypes["color"]; - size: ActionIconPropTypes["size"]; +const IconContainerWrapper = styled.div<{ + hasAction?: boolean; + disabled: ActionIconPropTypes["disabled"]; }>` - position: relative; height: 100%; - aspect-ratio: 1 / 1; + width: 100%; display: flex; justify-content: center; align-items: center; - overflow: hidden; - background-color: ${({ color }) => getBackgroundColor(color)}; - color: ${({ color }) => getColor(color)}; - border-radius: ${({ shape, size }) => getBorderRadius(shape, size)}; -`; + border-radius: inherit; -const Overlay = styled.div` - display: none; - position: absolute; - inset: 0; - height: 100%; - width: 100%; - background-color: var(--color-alpha-400-a); + ${({ hasAction, disabled }) => + !disabled && + hasAction && + css` + &:hover, + &:active { + background-color: var(--color-bg-alpha-light); + } + `} `; const IconContainer = styled.div<{ size: ActionIconPropTypes["size"] }>` @@ -107,22 +94,6 @@ const IconContainer = styled.div<{ size: ActionIconPropTypes["size"] }>` width: ${({ size }) => getIconSize(size)}; `; -const StatusContainer = styled.div<{ - status: ActionIconPropTypes["status"]; - size: ActionIconPropTypes["size"]; -}>` - position: absolute; - right: 0px; - ${({ status }) => (status?.position === "top" ? "top: 0px;" : "bottom: 0px;")} - width: 25%; - height: 25%; - border-width: ${({ size }) => getBorderWidth(size)}; - border-style: solid; - border-color: var(--border-color-neutral-brighter); - border-radius: 100%; - background-color: ${({ status }) => getModeColor(status!.mode)}; -`; - const ForwardedActionIcon = forwardRef<RefType, ActionIconPropTypes>( ( { @@ -135,7 +106,6 @@ const ForwardedActionIcon = forwardRef<RefType, ActionIconPropTypes>( onClick, shape = "square", size = "medium", - status, tabIndex = 0, title, }, @@ -155,18 +125,18 @@ const ForwardedActionIcon = forwardRef<RefType, ActionIconPropTypes>( aria-label={(onClick || linkHref) && (ariaLabel || title || "Action Icon")} disabled={disabled} ref={ref} + shape={shape} + color={color} > - <ActionIconWrapper shape={shape} color={color} size={size}> - {(!!onClick || !!linkHref) && <Overlay aria-hidden="true" />} - {content ? ( - content - ) : ( + {content ? ( + content + ) : ( + <IconContainerWrapper disabled={disabled} hasAction={!!onClick || !!linkHref}> <IconContainer size={size} color={color}> {icon && (typeof icon === "string" ? <DxcIcon icon={icon} /> : icon)} </IconContainer> - )} - </ActionIconWrapper> - {status && <StatusContainer role="status" size={size} status={status} />} + </IconContainerWrapper> + )} </ActionIconContainer> </Tooltip> ); diff --git a/packages/lib/src/action-icon/types.ts b/packages/lib/src/action-icon/types.ts index f323bac03b..afbf189fca 100644 --- a/packages/lib/src/action-icon/types.ts +++ b/packages/lib/src/action-icon/types.ts @@ -53,11 +53,6 @@ type CommonProps = { * Size of the component. */ size?: Size; - /** - * Defines the color of the status indicator displayed on the Action Icon and where it will be placed. - * If not provided, no indicator will be rendered. - */ - status?: Status; /** * Value of the tabindex attribute. It will only apply when the onClick property is passed. */ diff --git a/packages/lib/src/action-icon/utils.ts b/packages/lib/src/action-icon/utils.ts index 4fc932bfe9..8daf642ca7 100644 --- a/packages/lib/src/action-icon/utils.ts +++ b/packages/lib/src/action-icon/utils.ts @@ -75,23 +75,6 @@ const outlineWidthMap = { xxlarge: "var(--border-width-l)", }; -const borderWidthMap = { - xsmall: "var(--border-width-s)", - small: "var(--border-width-s)", - medium: "var(--border-width-s)", - large: "var(--border-width-m)", - xlarge: "var(--border-width-m)", - xxlarge: "var(--border-width-m)", -}; - -const modeColorMap = { - default: "var(--color-fg-neutral-strong)", - info: "var(--color-fg-secondary-medium)", - success: "var(--color-fg-success-medium)", - warning: "var(--color-fg-warning-strong)", - error: "var(--color-fg-error-medium)", -}; - export const getColor = (color: ActionIconPropTypes["color"]) => color && contextualColorMap[color] ? contextualColorMap[color].text : contextualColorMap.transparent.text; @@ -114,11 +97,5 @@ export const getSize = (size: ActionIconPropTypes["size"]) => export const getIconSize = (size: ActionIconPropTypes["size"]) => size && iconSizeMap[size] ? iconSizeMap[size] : "var(--height-s)"; -export const getBorderWidth = (size: ActionIconPropTypes["size"]) => - size && borderWidthMap[size] ? borderWidthMap[size] : "var(--border-width-s)"; - export const getOutlineWidth = (size: ActionIconPropTypes["size"]) => size && outlineWidthMap[size] ? outlineWidthMap[size] : "var(--border-width-m)"; - -export const getModeColor = (mode: Required<ActionIconPropTypes>["status"]["mode"]) => - mode && modeColorMap[mode] ? modeColorMap[mode] : "var(--color-fg-neutral-strong)"; diff --git a/packages/lib/src/avatar/Avatar.stories.tsx b/packages/lib/src/avatar/Avatar.stories.tsx index 754486786b..8b05ed4ab3 100644 --- a/packages/lib/src/avatar/Avatar.stories.tsx +++ b/packages/lib/src/avatar/Avatar.stories.tsx @@ -2,6 +2,7 @@ import { Meta, StoryObj } from "@storybook/react-vite"; import DxcAvatar from "./Avatar"; import Title from "../../.storybook/components/Title"; import ExampleContainer from "../../.storybook/components/ExampleContainer"; +import DxcFlex from "../flex/Flex"; export default { title: "Avatar", @@ -248,6 +249,78 @@ export const Chromatic: Story = { ), }; +export const Statuses: Story = { + render: () => ( + <> + <Title title="Statuses" theme="light" level={2} /> + {(["top", "bottom"] as const).map((position) => ( + <div key={position}> + <Title title={`${position}`} theme="light" level={3} /> + {(["default", "info", "success", "warning", "error"] as const).map((mode) => ( + <div key={`${position}-${mode}`}> + <Title title={`${mode}`} theme="light" level={4} /> + {(["xsmall", "small", "medium", "large", "xlarge", "xxlarge"] as const).map((size) => ( + <div key={`${position}-${mode}-${size}`}> + <Title title={`${size}`} theme="light" level={5} /> + <ExampleContainer> + <DxcFlex gap="var(--spacing-gap-l)" wrap="wrap"> + {( + ["primary", "secondary", "tertiary", "success", "info", "neutral", "warning", "error"] as const + ).map((color) => ( + <DxcAvatar + key={`${position}-${mode}-${size}-${color}`} + label="Michael Ramirez" + color={color} + size={size} + status={{ mode, position }} + /> + ))} + </DxcFlex> + </ExampleContainer> + </div> + ))} + </div> + ))} + </div> + ))} + </> + ), +}; + +export const PseudoStates: Story = { + render: () => ( + <> + <Title title="PseudoStates" theme="light" level={2} /> + {(["pseudo-hover", "pseudo-focus", "pseudo-active", "disabled"] as const).map((state) => ( + <div key={state}> + <Title title={`${state}`} theme="light" level={3} /> + {(["primary", "secondary", "tertiary", "success", "info", "neutral", "warning", "error"] as const).map( + (color) => ( + <div key={`${state}-${color}`}> + <Title title={`${color}`} theme="light" level={4} /> + <ExampleContainer pseudoState={state !== "disabled" ? state : undefined}> + <DxcFlex gap="var(--spacing-gap-l)"> + {(["xsmall", "small", "medium", "large", "xlarge", "xxlarge"] as const).map((size) => ( + <DxcAvatar + key={`${state}-${color}-${size}`} + label="Michael Ramirez" + color={color} + size={size} + onClick={() => {}} + disabled={state === "disabled"} + /> + ))} + </DxcFlex> + </ExampleContainer> + </div> + ) + )} + </div> + ))} + </> + ), +}; + export const Labels: Story = { render: () => ( <> diff --git a/packages/lib/src/avatar/Avatar.tsx b/packages/lib/src/avatar/Avatar.tsx index bc4ef62f30..461717482d 100644 --- a/packages/lib/src/avatar/Avatar.tsx +++ b/packages/lib/src/avatar/Avatar.tsx @@ -1,10 +1,65 @@ import { memo, useCallback, useMemo, useState } from "react"; import AvatarPropsType from "./types"; -import { getFontSize, getInitials } from "./utils"; +import { getBorderWidth, getFontSize, getInitials, getModeColor } from "./utils"; import DxcTypography from "../typography/Typography"; import DxcImage from "../image/Image"; import DxcActionIcon from "../action-icon/ActionIcon"; import DxcFlex from "../flex/Flex"; +import styled from "@emotion/styled"; +import { css } from "@emotion/react"; + +const ContentWrapper = styled.div<{ hasAction: boolean }>` + position: relative; + width: 100%; + height: 100%; + display: flex; + align-items: center; + justify-content: center; + border-radius: inherit; + + ${({ hasAction }) => + hasAction && + css` + &:hover > div:first-of-type, + &:active > div:first-of-type { + display: block; + } + `} +`; + +const Overlay = styled.div<{ disabled: AvatarPropsType["disabled"] }>` + display: none; + position: absolute; + inset: 0; + height: 100%; + width: 100%; + border-radius: inherit; + background-color: var(--color-alpha-400-a); + pointer-events: none; + + ${({ disabled }) => + disabled && + css` + display: block; + background-color: rgba(255, 255, 255, 0.5); + `} +`; + +const StatusContainer = styled.div<{ + status: AvatarPropsType["status"]; + size: AvatarPropsType["size"]; +}>` + position: absolute; + right: 0px; + ${({ status }) => (status?.position === "top" ? "top: 0px;" : "bottom: 0px;")} + width: 25%; + height: 25%; + border-width: ${({ size }) => getBorderWidth(size)}; + border-style: solid; + border-color: var(--border-color-neutral-brighter); + border-radius: 100%; + background-color: ${({ status }) => getModeColor(status!.mode)}; +`; const DxcAvatar = memo( ({ @@ -26,9 +81,12 @@ const DxcAvatar = memo( const [error, setError] = useState<boolean>(false); const initials = useMemo(() => getInitials(label), [label]); const handleError = useCallback(() => setError(true), []); + const hasAction = !disabled && (!!onClick || !!linkHref); const content = ( - <> + <ContentWrapper hasAction={hasAction}> + {(!!onClick || !!linkHref || disabled) && <Overlay disabled={disabled} aria-hidden="true" />} + {imageSrc && !error ? ( <DxcImage src={imageSrc} @@ -52,7 +110,9 @@ const DxcAvatar = memo( {initials} </DxcTypography> )} - </> + + {status && <StatusContainer status={status} size={size} aria-hidden="true" />} + </ContentWrapper> ); const LabelWrapper = ({ condition, children }: { condition: boolean; children: React.ReactNode }) => @@ -108,7 +168,6 @@ const DxcAvatar = memo( onClick={onClick} shape={shape} size={size} - status={status} tabIndex={tabIndex} title={title} /> diff --git a/packages/lib/src/avatar/utils.ts b/packages/lib/src/avatar/utils.ts index 68f7cf2926..4bcea462fa 100644 --- a/packages/lib/src/avatar/utils.ts +++ b/packages/lib/src/avatar/utils.ts @@ -9,6 +9,23 @@ const fontSizeMap = { xxlarge: "36px", }; +const borderWidthMap = { + xsmall: "var(--border-width-s)", + small: "var(--border-width-s)", + medium: "var(--border-width-s)", + large: "var(--border-width-m)", + xlarge: "var(--border-width-m)", + xxlarge: "var(--border-width-m)", +}; + +const modeColorMap = { + default: "var(--color-fg-neutral-strong)", + info: "var(--color-fg-secondary-medium)", + success: "var(--color-fg-success-medium)", + warning: "var(--color-fg-warning-strong)", + error: "var(--color-fg-error-medium)", +}; + export const getFontSize = (size: AvatarPropsType["size"]) => (size ? fontSizeMap[size] : "var(--typography-label-l)"); export const getInitials = (label?: string): string => { @@ -22,3 +39,9 @@ export const getInitials = (label?: string): string => { const firstWord = words[0] ?? ""; return firstWord.slice(0, 2).toUpperCase(); }; + +export const getBorderWidth = (size: AvatarPropsType["size"]) => + size && borderWidthMap[size] ? borderWidthMap[size] : "var(--border-width-s)"; + +export const getModeColor = (mode: Required<AvatarPropsType>["status"]["mode"]) => + mode && modeColorMap[mode] ? modeColorMap[mode] : "var(--color-fg-neutral-strong)"; From 0bbca5e2b2751c7abe63c863c6dc125a69a00011 Mon Sep 17 00:00:00 2001 From: Pelayo Felgueroso <pfelguerosogalguera@gmail.com> Date: Wed, 21 Jan 2026 15:49:00 +0100 Subject: [PATCH 041/275] Fix failing tests --- .../ActionIcon.accessibility.test.tsx | 5 ---- .../lib/src/action-icon/ActionIcon.test.tsx | 24 ------------------- packages/lib/src/avatar/Avatar.test.tsx | 16 ++++++------- packages/lib/src/avatar/Avatar.tsx | 2 +- 4 files changed, 9 insertions(+), 38 deletions(-) diff --git a/packages/lib/src/action-icon/ActionIcon.accessibility.test.tsx b/packages/lib/src/action-icon/ActionIcon.accessibility.test.tsx index fe6f70ae3a..ecd8b1c41e 100644 --- a/packages/lib/src/action-icon/ActionIcon.accessibility.test.tsx +++ b/packages/lib/src/action-icon/ActionIcon.accessibility.test.tsx @@ -23,9 +23,4 @@ describe("ActionIcon component accessibility tests", () => { const results = await axe(container); expect(results.violations).toHaveLength(0); }); - it("Should not have basic accessibility issues when status is passed", async () => { - const { container } = render(<DxcActionIcon icon="house" status={{ mode: "success", position: "top" }} />); - const results = await axe(container); - expect(results.violations).toHaveLength(0); - }); }); diff --git a/packages/lib/src/action-icon/ActionIcon.test.tsx b/packages/lib/src/action-icon/ActionIcon.test.tsx index be6cdbe355..88754a21ea 100644 --- a/packages/lib/src/action-icon/ActionIcon.test.tsx +++ b/packages/lib/src/action-icon/ActionIcon.test.tsx @@ -28,30 +28,6 @@ describe("ActionIcon component tests", () => { fireEvent.click(buttonDiv); expect(handleClick).toHaveBeenCalledTimes(1); }); - test("ActionIcon renders status indicator correctly", () => { - const { rerender, queryByRole, getByRole } = render( - <DxcActionIcon icon="house" status={{ mode: "default", position: "top" }} /> - ); - expect(getByRole("status")).toHaveStyle("background-color: var(--color-fg-neutral-strong)"); - rerender(<DxcActionIcon icon="house" status={{ mode: "info", position: "top" }} />); - expect(getByRole("status")).toHaveStyle("background-color: var(--color-fg-secondary-medium)"); - rerender(<DxcActionIcon icon="house" status={{ mode: "success", position: "top" }} />); - expect(getByRole("status")).toHaveStyle("background-color: var(--color-fg-success-medium)"); - rerender(<DxcActionIcon icon="house" status={{ mode: "warning", position: "top" }} />); - expect(getByRole("status")).toHaveStyle("background-color: var(--color-fg-warning-strong)"); - rerender(<DxcActionIcon icon="house" status={{ mode: "error", position: "top" }} />); - expect(getByRole("status")).toHaveStyle("background-color: var(--color-fg-error-medium)"); - rerender(<DxcActionIcon icon="house" />); - expect(queryByRole("status")).toBeNull(); - }); - test("ActionIcon renders status indicator in correct position", () => { - const { rerender, getByRole } = render( - <DxcActionIcon icon="house" status={{ mode: "default", position: "top" }} /> - ); - expect(getByRole("status")).toHaveStyle("top: 0px;"); - rerender(<DxcActionIcon icon="house" status={{ mode: "info", position: "bottom" }} />); - expect(getByRole("status")).toHaveStyle("bottom: 0px"); - }); test("ActionIcon is focusable when onClick is passed", () => { const handleClick = jest.fn(); const { getByRole } = render(<DxcActionIcon icon="house" onClick={handleClick} />); diff --git a/packages/lib/src/avatar/Avatar.test.tsx b/packages/lib/src/avatar/Avatar.test.tsx index 50a67ab5a4..74aee35054 100644 --- a/packages/lib/src/avatar/Avatar.test.tsx +++ b/packages/lib/src/avatar/Avatar.test.tsx @@ -98,25 +98,25 @@ describe("Avatar component tests", () => { const { rerender, queryByRole, getByRole } = render( <DxcAvatar label="John Doe" status={{ mode: "default", position: "top" }} /> ); - expect(getByRole("status")).toHaveStyle("background-color: var(--color-fg-neutral-strong)"); + expect(getByRole("status", { hidden: true })).toHaveStyle("background-color: var(--color-fg-neutral-strong)"); rerender(<DxcAvatar label="John Doe" status={{ mode: "info", position: "top" }} />); - expect(getByRole("status")).toHaveStyle("background-color: var(--color-fg-secondary-medium)"); + expect(getByRole("status", { hidden: true })).toHaveStyle("background-color: var(--color-fg-secondary-medium)"); rerender(<DxcAvatar label="John Doe" status={{ mode: "success", position: "top" }} />); - expect(getByRole("status")).toHaveStyle("background-color: var(--color-fg-success-medium)"); + expect(getByRole("status", { hidden: true })).toHaveStyle("background-color: var(--color-fg-success-medium)"); rerender(<DxcAvatar label="John Doe" status={{ mode: "warning", position: "top" }} />); - expect(getByRole("status")).toHaveStyle("background-color: var(--color-fg-warning-strong)"); + expect(getByRole("status", { hidden: true })).toHaveStyle("background-color: var(--color-fg-warning-strong)"); rerender(<DxcAvatar label="John Doe" status={{ mode: "error", position: "top" }} />); - expect(getByRole("status")).toHaveStyle("background-color: var(--color-fg-error-medium)"); + expect(getByRole("status", { hidden: true })).toHaveStyle("background-color: var(--color-fg-error-medium)"); rerender(<DxcAvatar label="John Doe" />); - expect(queryByRole("status")).toBeNull(); + expect(queryByRole("status", { hidden: true })).toBeNull(); }); test("Avatar renders status indicator in correct position", () => { const { rerender, getByRole } = render( <DxcAvatar label="John Doe" status={{ mode: "default", position: "top" }} /> ); - expect(getByRole("status")).toHaveStyle("top: 0px;"); + expect(getByRole("status", { hidden: true })).toHaveStyle("top: 0px;"); rerender(<DxcAvatar label="John Doe" status={{ mode: "info", position: "bottom" }} />); - expect(getByRole("status")).toHaveStyle("bottom: 0px"); + expect(getByRole("status", { hidden: true })).toHaveStyle("bottom: 0px"); }); test("Avatar renders primaryText and secondaryText correctly", () => { const { rerender, getByText } = render(<DxcAvatar primaryText="Primary Text" secondaryText="Secondary Text" />); diff --git a/packages/lib/src/avatar/Avatar.tsx b/packages/lib/src/avatar/Avatar.tsx index 461717482d..6e41fd551f 100644 --- a/packages/lib/src/avatar/Avatar.tsx +++ b/packages/lib/src/avatar/Avatar.tsx @@ -111,7 +111,7 @@ const DxcAvatar = memo( </DxcTypography> )} - {status && <StatusContainer status={status} size={size} aria-hidden="true" />} + {status && <StatusContainer role="status" status={status} size={size} />} </ContentWrapper> ); From c54005b6569fd67eebbc09c43f2686c815588878 Mon Sep 17 00:00:00 2001 From: Pelayo Felgueroso <pfelguerosogalguera@gmail.com> Date: Wed, 21 Jan 2026 16:12:20 +0100 Subject: [PATCH 042/275] Fix icon rendering --- packages/lib/src/action-icon/ActionIcon.tsx | 2 +- packages/lib/src/avatar/Avatar.tsx | 12 ++++++++---- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/packages/lib/src/action-icon/ActionIcon.tsx b/packages/lib/src/action-icon/ActionIcon.tsx index 60bfaa6847..8ba2fad9c0 100644 --- a/packages/lib/src/action-icon/ActionIcon.tsx +++ b/packages/lib/src/action-icon/ActionIcon.tsx @@ -84,7 +84,7 @@ const IconContainerWrapper = styled.div<{ `} `; -const IconContainer = styled.div<{ size: ActionIconPropTypes["size"] }>` +export const IconContainer = styled.div<{ size: ActionIconPropTypes["size"] }>` display: flex; justify-content: center; align-items: center; diff --git a/packages/lib/src/avatar/Avatar.tsx b/packages/lib/src/avatar/Avatar.tsx index 6e41fd551f..0beaa591e1 100644 --- a/packages/lib/src/avatar/Avatar.tsx +++ b/packages/lib/src/avatar/Avatar.tsx @@ -3,10 +3,11 @@ import AvatarPropsType from "./types"; import { getBorderWidth, getFontSize, getInitials, getModeColor } from "./utils"; import DxcTypography from "../typography/Typography"; import DxcImage from "../image/Image"; -import DxcActionIcon from "../action-icon/ActionIcon"; +import DxcActionIcon, { IconContainer } from "../action-icon/ActionIcon"; import DxcFlex from "../flex/Flex"; import styled from "@emotion/styled"; import { css } from "@emotion/react"; +import DxcIcon from "../icon/Icon"; const ContentWrapper = styled.div<{ hasAction: boolean }>` position: relative; @@ -97,7 +98,7 @@ const DxcAvatar = memo( objectFit="cover" objectPosition="center" /> - ) : ( + ) : label ? ( <DxcTypography as="span" fontFamily="var(--typography-font-family)" @@ -109,6 +110,10 @@ const DxcAvatar = memo( > {initials} </DxcTypography> + ) : ( + <IconContainer size={size} color={color}> + {icon && (typeof icon === "string" ? <DxcIcon icon={icon} /> : icon)} + </IconContainer> )} {status && <StatusContainer role="status" status={status} size={size} />} @@ -156,14 +161,13 @@ const DxcAvatar = memo( <LabelWrapper condition={!!(primaryText || secondaryText)}> <DxcActionIcon ariaLabel={label} - content={(imageSrc && !error) || initials ? content : undefined} + content={content} color={ ["primary", "secondary", "tertiary", "success", "info", "neutral", "warning", "error"].includes(color) ? color : "neutral" } disabled={disabled} - icon={icon} linkHref={linkHref} onClick={onClick} shape={shape} From 5a3e02fa650f39acdba401fa53866e4359a27e7c Mon Sep 17 00:00:00 2001 From: Pelayo Felgueroso <pfelguerosogalguera@gmail.com> Date: Wed, 21 Jan 2026 16:39:50 +0100 Subject: [PATCH 043/275] Fix image overflow --- packages/lib/src/avatar/Avatar.tsx | 64 +++++++++++++++--------------- 1 file changed, 33 insertions(+), 31 deletions(-) diff --git a/packages/lib/src/avatar/Avatar.tsx b/packages/lib/src/avatar/Avatar.tsx index 0beaa591e1..01d3dffe66 100644 --- a/packages/lib/src/avatar/Avatar.tsx +++ b/packages/lib/src/avatar/Avatar.tsx @@ -17,6 +17,7 @@ const ContentWrapper = styled.div<{ hasAction: boolean }>` align-items: center; justify-content: center; border-radius: inherit; + overflow: hidden; ${({ hasAction }) => hasAction && @@ -85,39 +86,40 @@ const DxcAvatar = memo( const hasAction = !disabled && (!!onClick || !!linkHref); const content = ( - <ContentWrapper hasAction={hasAction}> - {(!!onClick || !!linkHref || disabled) && <Overlay disabled={disabled} aria-hidden="true" />} - - {imageSrc && !error ? ( - <DxcImage - src={imageSrc} - alt={label || title || "Avatar"} - onError={handleError} - width="100%" - height="100%" - objectFit="cover" - objectPosition="center" - /> - ) : label ? ( - <DxcTypography - as="span" - fontFamily="var(--typography-font-family)" - fontSize={getFontSize(size)} - fontWeight="var(--typography-label-semibold)" - fontStyle="normal" - lineHeight="normal" - color="inherit" - > - {initials} - </DxcTypography> - ) : ( - <IconContainer size={size} color={color}> - {icon && (typeof icon === "string" ? <DxcIcon icon={icon} /> : icon)} - </IconContainer> - )} + <> + <ContentWrapper hasAction={hasAction}> + {(!!onClick || !!linkHref || disabled) && <Overlay disabled={disabled} aria-hidden="true" />} + {imageSrc && !error ? ( + <DxcImage + src={imageSrc} + alt={label || title || "Avatar"} + onError={handleError} + width="100%" + height="100%" + objectFit="cover" + objectPosition="center" + /> + ) : label ? ( + <DxcTypography + as="span" + fontFamily="var(--typography-font-family)" + fontSize={getFontSize(size)} + fontWeight="var(--typography-label-semibold)" + fontStyle="normal" + lineHeight="normal" + color="inherit" + > + {initials} + </DxcTypography> + ) : ( + <IconContainer size={size} color={color}> + {icon && (typeof icon === "string" ? <DxcIcon icon={icon} /> : icon)} + </IconContainer> + )} + </ContentWrapper> {status && <StatusContainer role="status" status={status} size={size} />} - </ContentWrapper> + </> ); const LabelWrapper = ({ condition, children }: { condition: boolean; children: React.ReactNode }) => From 858cf5c62aa6d1817a02ed17de4be0cffcdc7b20 Mon Sep 17 00:00:00 2001 From: Pelayo Felgueroso <pfelguerosogalguera@gmail.com> Date: Wed, 21 Jan 2026 17:05:40 +0100 Subject: [PATCH 044/275] Remove focus:enabled --- packages/lib/src/action-icon/ActionIcon.tsx | 3 --- 1 file changed, 3 deletions(-) diff --git a/packages/lib/src/action-icon/ActionIcon.tsx b/packages/lib/src/action-icon/ActionIcon.tsx index 8ba2fad9c0..f6402921d3 100644 --- a/packages/lib/src/action-icon/ActionIcon.tsx +++ b/packages/lib/src/action-icon/ActionIcon.tsx @@ -48,9 +48,6 @@ const ActionIconContainer = styled.div< outline-color: var(--border-color-secondary-medium); outline-offset: -2px; } - &:focus-visible:enabled { - outline: none; - } `} ${({ disabled }) => disabled && From 324d89574bb2f05b80ca5607c4d6c459a342a198 Mon Sep 17 00:00:00 2001 From: PelayoFelgueroso <pfelguerosogalguera@gmail.com> Date: Wed, 28 Jan 2026 10:09:21 +0100 Subject: [PATCH 045/275] -Change Footer width on ApplicationLayout to span the full width of the page -Add SVG to footer `logo` prop -Fix handleClearAction function on the Searchbar component(it was not showing results after clicking on it) --- .../ApplicationLayoutOverviewPage.tsx | 2 +- .../components/footer/code/FooterCodePage.tsx | 2 +- packages/lib/src/footer/Footer.tsx | 6 ++- packages/lib/src/footer/types.ts | 2 +- packages/lib/src/layout/ApplicationLayout.tsx | 45 ++++++++----------- packages/lib/src/search-bar/SearchBar.tsx | 8 ++-- 6 files changed, 29 insertions(+), 36 deletions(-) diff --git a/apps/website/screens/components/application-layout/overview/ApplicationLayoutOverviewPage.tsx b/apps/website/screens/components/application-layout/overview/ApplicationLayoutOverviewPage.tsx index efb1c5da37..cac27581e9 100644 --- a/apps/website/screens/components/application-layout/overview/ApplicationLayoutOverviewPage.tsx +++ b/apps/website/screens/components/application-layout/overview/ApplicationLayoutOverviewPage.tsx @@ -248,7 +248,7 @@ const sections = [ multiples of 8px for visual harmony. </DxcBulletedList.Item> <DxcBulletedList.Item> - <strong>imit 4px adjustments:</strong> Reserve finer increments only for edge cases like icon alignment or + <strong>Imit 4px adjustments:</strong> Reserve finer increments only for edge cases like icon alignment or typography. </DxcBulletedList.Item> <DxcBulletedList.Item> diff --git a/apps/website/screens/components/footer/code/FooterCodePage.tsx b/apps/website/screens/components/footer/code/FooterCodePage.tsx index 943aee2080..7bffbaa62a 100644 --- a/apps/website/screens/components/footer/code/FooterCodePage.tsx +++ b/apps/website/screens/components/footer/code/FooterCodePage.tsx @@ -10,7 +10,7 @@ const bottomLinksTypeString = `{ }[]`; const logoTypeString = `{ - src: string; + src: string | SVG; alt: string; }`; diff --git a/packages/lib/src/footer/Footer.tsx b/packages/lib/src/footer/Footer.tsx index 577dfe1c52..91d1278be8 100644 --- a/packages/lib/src/footer/Footer.tsx +++ b/packages/lib/src/footer/Footer.tsx @@ -1,4 +1,4 @@ -import { useContext, useEffect, useMemo, useRef, useState } from "react"; +import { isValidElement, useContext, useEffect, useMemo, useRef, useState } from "react"; import styled from "@emotion/styled"; import DxcIcon from "../icon/Icon"; import { Tooltip } from "../tooltip/Tooltip"; @@ -213,8 +213,10 @@ const DxcFooter = ({ const translatedLabels = useContext(HalstackLanguageContext); const footerLogo = useMemo(() => { - if (logo) { + if (logo && typeof logo.src === "string") { return <LogoImg mode={mode} alt={logo.alt} src={logo.src} title={logo.alt} />; + } else if (isValidElement(logo?.src)) { + return logo.src; } else { return mode === "default" ? dxcLogo : dxcSmallLogo; } diff --git a/packages/lib/src/footer/types.ts b/packages/lib/src/footer/types.ts index 2706fa8047..8574256078 100644 --- a/packages/lib/src/footer/types.ts +++ b/packages/lib/src/footer/types.ts @@ -31,7 +31,7 @@ type Logo = { /** * Source of the logo image. */ - src: string; + src: string | SVG; /** * Alternative text for the logo image. */ diff --git a/packages/lib/src/layout/ApplicationLayout.tsx b/packages/lib/src/layout/ApplicationLayout.tsx index 16b55ab42c..bc64960505 100644 --- a/packages/lib/src/layout/ApplicationLayout.tsx +++ b/packages/lib/src/layout/ApplicationLayout.tsx @@ -15,10 +15,12 @@ const ApplicationLayoutContainer = styled.div<{ header?: React.ReactNode }>` height: 100vh; width: 100vw; position: absolute; - overflow: hidden; + overflow: auto; `; const HeaderContainer = styled.div` + position: sticky; + top: 0; width: 100%; min-height: var(--height-xxxl); height: fit-content; @@ -26,10 +28,10 @@ const HeaderContainer = styled.div` `; const BodyContainer = styled.div<{ hasSidenav?: boolean }>` + position: relative; display: grid; grid-template-columns: ${({ hasSidenav }) => (hasSidenav ? "auto 1fr" : "1fr")}; grid-template-rows: 1fr; - overflow: hidden; `; const SidenavContainer = styled.div` @@ -37,17 +39,16 @@ const SidenavContainer = styled.div` height: 100%; z-index: var(--z-app-layout-sidenav); position: sticky; + top: var(--height-xxxl); overflow: auto; + max-height: calc(100vh - var(--height-xxxl)); `; const MainContainer = styled.div` - display: flex; - flex-grow: 1; - flex-direction: column; + position: relative; + display: grid; width: 100%; height: 100%; - position: relative; - overflow: auto; `; const FooterContainer = styled.div` @@ -55,12 +56,6 @@ const FooterContainer = styled.div` width: 100%; `; -const MainContentContainer = styled.main` - height: 100%; - display: grid; - grid-template-rows: 1fr auto; -`; - const Main = ({ children }: AppLayoutMainPropsType): JSX.Element => <div>{children}</div>; const DxcApplicationLayout = ({ logo, header, sidenav, footer, children }: ApplicationLayoutPropsType): JSX.Element => { @@ -78,21 +73,17 @@ const DxcApplicationLayout = ({ logo, header, sidenav, footer, children }: Appli {header && <HeaderContainer>{header}</HeaderContainer>} <BodyContainer hasSidenav={!!sidenav}> {sidenav && <SidenavContainer>{sidenav}</SidenavContainer>} - <MainContainer> - <MainContentContainer> - {findChildType(children, Main)} - <FooterContainer> - {footer ?? ( - <DxcFooter - copyright={`© DXC Technology ${year}. All rights reserved.`} - bottomLinks={bottomLinks} - socialLinks={socialLinks} - /> - )} - </FooterContainer> - </MainContentContainer> - </MainContainer> + <MainContainer>{findChildType(children, Main)}</MainContainer> </BodyContainer> + <FooterContainer> + {footer ?? ( + <DxcFooter + copyright={`© DXC Technology ${year}. All rights reserved.`} + bottomLinks={bottomLinks} + socialLinks={socialLinks} + /> + )} + </FooterContainer> </ApplicationLayoutContext.Provider> </ApplicationLayoutContainer> ); diff --git a/packages/lib/src/search-bar/SearchBar.tsx b/packages/lib/src/search-bar/SearchBar.tsx index a160ee95e9..42440567c4 100644 --- a/packages/lib/src/search-bar/SearchBar.tsx +++ b/packages/lib/src/search-bar/SearchBar.tsx @@ -72,8 +72,8 @@ const DxcSearchBar = ({ const inputRef = useRef<HTMLInputElement>(null); const [innerValue, setInnerValue] = useState(""); - const handleClearActionOnClick = () => { - setInnerValue(""); + const handleClearAction = () => { + handleSearchChangeValue(""); inputRef.current?.focus(); }; @@ -90,7 +90,7 @@ const DxcSearchBar = ({ case "Escape": e.preventDefault(); if (innerValue.length > 0) { - handleClearActionOnClick(); + handleClearAction(); } break; case "Enter": @@ -123,7 +123,7 @@ const DxcSearchBar = ({ size="xsmall" shape="circle" icon="cancel" - onClick={handleClearActionOnClick} + onClick={handleClearAction} tabIndex={0} title={!disabled ? translatedLabels.searchBar.clearFieldActionTitle : undefined} /> From d63a9ea159602f332a6d7b74c1d41790d89e51d5 Mon Sep 17 00:00:00 2001 From: PelayoFelgueroso <pfelguerosogalguera@gmail.com> Date: Thu, 29 Jan 2026 13:44:17 +0100 Subject: [PATCH 046/275] Fix based on comment --- packages/lib/src/layout/ApplicationLayout.tsx | 21 +++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/packages/lib/src/layout/ApplicationLayout.tsx b/packages/lib/src/layout/ApplicationLayout.tsx index bc64960505..0cdf7e48b3 100644 --- a/packages/lib/src/layout/ApplicationLayout.tsx +++ b/packages/lib/src/layout/ApplicationLayout.tsx @@ -1,4 +1,4 @@ -import { useMemo, useRef } from "react"; +import { useMemo, useRef, useState, useCallback } from "react"; import styled from "@emotion/styled"; import DxcFooter from "../footer/Footer"; import DxcHeader from "../header/Header"; @@ -34,14 +34,14 @@ const BodyContainer = styled.div<{ hasSidenav?: boolean }>` grid-template-rows: 1fr; `; -const SidenavContainer = styled.div` +const SidenavContainer = styled.div<{ headerHeight: string }>` width: fit-content; height: 100%; z-index: var(--z-app-layout-sidenav); position: sticky; - top: var(--height-xxxl); + top: ${({ headerHeight }) => headerHeight || "0"}; overflow: auto; - max-height: calc(100vh - var(--height-xxxl)); + max-height: ${({ headerHeight }) => `calc(100vh - ${headerHeight || "0"})`}; `; const MainContainer = styled.div` @@ -59,6 +59,15 @@ const FooterContainer = styled.div` const Main = ({ children }: AppLayoutMainPropsType): JSX.Element => <div>{children}</div>; const DxcApplicationLayout = ({ logo, header, sidenav, footer, children }: ApplicationLayoutPropsType): JSX.Element => { + const [headerHeight, setHeaderHeight] = useState("0px"); + + const handleHeaderHeight = useCallback((headerElement: HTMLDivElement | null) => { + if (headerElement) { + const height = headerElement.offsetHeight; + setHeaderHeight(`${height}px`); + } + }, []); + const contextValue = useMemo(() => { return { logo, @@ -70,9 +79,9 @@ const DxcApplicationLayout = ({ logo, header, sidenav, footer, children }: Appli return ( <ApplicationLayoutContainer ref={ref} header={header}> <ApplicationLayoutContext.Provider value={contextValue}> - {header && <HeaderContainer>{header}</HeaderContainer>} + {header && <HeaderContainer ref={handleHeaderHeight}>{header}</HeaderContainer>} <BodyContainer hasSidenav={!!sidenav}> - {sidenav && <SidenavContainer>{sidenav}</SidenavContainer>} + {sidenav && <SidenavContainer headerHeight={headerHeight}>{sidenav}</SidenavContainer>} <MainContainer>{findChildType(children, Main)}</MainContainer> </BodyContainer> <FooterContainer> From 394feff0e9f4293717960e0866ac180d3b417ebd Mon Sep 17 00:00:00 2001 From: Pelayo Felgueroso <pfelguerosogalguera@gmail.com> Date: Fri, 30 Jan 2026 08:38:13 +0100 Subject: [PATCH 047/275] -Fix scroll issues in ApplicationLayout -Remove padding from stories as we remove position `absolute` from AplicationLayout --- packages/lib/src/footer/Footer.stories.tsx | 10 ++++++++++ .../lib/src/layout/ApplicationLayout.stories.tsx | 10 ++++++++++ packages/lib/src/layout/ApplicationLayout.tsx | 15 +++++---------- packages/lib/src/sidenav/Sidenav.stories.tsx | 15 ++++++++++++--- 4 files changed, 37 insertions(+), 13 deletions(-) diff --git a/packages/lib/src/footer/Footer.stories.tsx b/packages/lib/src/footer/Footer.stories.tsx index 73b52fb5a3..6470583001 100644 --- a/packages/lib/src/footer/Footer.stories.tsx +++ b/packages/lib/src/footer/Footer.stories.tsx @@ -7,6 +7,7 @@ import DxcFooter from "./Footer"; import DxcLink from "../link/Link"; import { Meta, StoryObj } from "@storybook/react-vite"; import { userEvent, within } from "storybook/internal/test"; +import { useEffect } from "react"; import DxcParagraph from "../paragraph/Paragraph"; import DxcHeading from "../heading/Heading"; import DxcApplicationLayout from "../layout/ApplicationLayout"; @@ -17,6 +18,15 @@ import DxcButton from "../button/Button"; export default { title: "Footer", component: DxcFooter, + decorators: [ + (Story) => { + useEffect(() => { + document.body.style.padding = "0"; + }, []); + + return <Story />; + }, + ], parameters: { a11y: { config: { diff --git a/packages/lib/src/layout/ApplicationLayout.stories.tsx b/packages/lib/src/layout/ApplicationLayout.stories.tsx index 4f1fa5b291..9d468b65d2 100644 --- a/packages/lib/src/layout/ApplicationLayout.stories.tsx +++ b/packages/lib/src/layout/ApplicationLayout.stories.tsx @@ -2,10 +2,20 @@ import { Meta, StoryObj } from "@storybook/react-vite"; import Title from "../../.storybook/components/Title"; import DxcApplicationLayout from "./ApplicationLayout"; import { userEvent, within } from "storybook/internal/test"; +import { useEffect } from "react"; export default { title: "Application Layout", component: DxcApplicationLayout, + decorators: [ + (Story) => { + useEffect(() => { + document.body.style.padding = "0"; + }, []); + + return <Story />; + }, + ], } satisfies Meta<typeof DxcApplicationLayout>; const dxcLogo = ( diff --git a/packages/lib/src/layout/ApplicationLayout.tsx b/packages/lib/src/layout/ApplicationLayout.tsx index 0cdf7e48b3..aa1b5f1a77 100644 --- a/packages/lib/src/layout/ApplicationLayout.tsx +++ b/packages/lib/src/layout/ApplicationLayout.tsx @@ -1,4 +1,4 @@ -import { useMemo, useRef, useState, useCallback } from "react"; +import React, { useMemo, useRef, useState, useCallback } from "react"; import styled from "@emotion/styled"; import DxcFooter from "../footer/Footer"; import DxcHeader from "../header/Header"; @@ -8,21 +8,15 @@ import { bottomLinks, findChildType, socialLinks, year } from "./utils"; import ApplicationLayoutContext from "./ApplicationLayoutContext"; const ApplicationLayoutContainer = styled.div<{ header?: React.ReactNode }>` - top: 0; - left: 0; display: grid; - grid-template-rows: ${({ header }) => (header ? "auto 1fr" : "1fr")}; - height: 100vh; - width: 100vw; - position: absolute; - overflow: auto; + grid-template-rows: ${({ header }) => (header ? "auto 1fr auto" : "1fr auto")}; + min-height: 100vh; `; const HeaderContainer = styled.div` position: sticky; top: 0; width: 100%; - min-height: var(--height-xxxl); height: fit-content; z-index: var(--z-app-layout-header); `; @@ -32,6 +26,7 @@ const BodyContainer = styled.div<{ hasSidenav?: boolean }>` display: grid; grid-template-columns: ${({ hasSidenav }) => (hasSidenav ? "auto 1fr" : "1fr")}; grid-template-rows: 1fr; + min-height: 100%; `; const SidenavContainer = styled.div<{ headerHeight: string }>` @@ -44,7 +39,7 @@ const SidenavContainer = styled.div<{ headerHeight: string }>` max-height: ${({ headerHeight }) => `calc(100vh - ${headerHeight || "0"})`}; `; -const MainContainer = styled.div` +const MainContainer = styled.main` position: relative; display: grid; width: 100%; diff --git a/packages/lib/src/sidenav/Sidenav.stories.tsx b/packages/lib/src/sidenav/Sidenav.stories.tsx index e0a68423d2..27adbfa100 100644 --- a/packages/lib/src/sidenav/Sidenav.stories.tsx +++ b/packages/lib/src/sidenav/Sidenav.stories.tsx @@ -10,11 +10,20 @@ import DxcAvatar from "../avatar/Avatar"; import { userEvent, within } from "storybook/internal/test"; import disabledRules from "../../test/accessibility/rules/specific/sidenav/disabledRules"; import preview from "../../.storybook/preview"; -import { useState } from "react"; +import { useEffect, useState } from "react"; export default { title: "Sidenav", component: DxcSidenav, + decorators: [ + (Story) => { + useEffect(() => { + document.body.style.padding = "0"; + }, []); + + return <Story />; + }, + ], parameters: { a11y: { config: { @@ -34,7 +43,7 @@ const DetailedAvatar = () => { <DxcAvatar color="primary" status={{ mode: "error", position: "bottom" }} title="Michael Ramirez" /> <DxcFlex direction="column"> <DxcTypography - color="var(--color-fg-neutral-dark" + color="var(--color-fg-neutral-dark)" fontFamily="var(--typography-font-family)" fontSize="var(--typography-label-l)" fontWeight="var(--typography-label-regular)" @@ -42,7 +51,7 @@ const DetailedAvatar = () => { Michael Ramirez </DxcTypography> <DxcTypography - color="var(--color-fg-neutral-stronger" + color="var(--color-fg-neutral-stronger)" fontFamily="var(--typography-font-family)" fontSize="var(--typography-label-s)" fontWeight="var(--typography-label-regular)" From d7e34a8d9d780172ec8eea2ec79c5d92ad8c8947 Mon Sep 17 00:00:00 2001 From: PelayoFelgueroso <pfelguerosogalguera@gmail.com> Date: Fri, 30 Jan 2026 11:26:33 +0100 Subject: [PATCH 048/275] Fix based con comments --- packages/lib/src/footer/Footer.tsx | 5 +++++ packages/lib/src/layout/ApplicationLayout.tsx | 15 +++++++++------ 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/packages/lib/src/footer/Footer.tsx b/packages/lib/src/footer/Footer.tsx index 91d1278be8..b5f3b3971d 100644 --- a/packages/lib/src/footer/Footer.tsx +++ b/packages/lib/src/footer/Footer.tsx @@ -65,6 +65,11 @@ const LogoContainer = styled.span<{ mode?: FooterPropsType["mode"] }>` justify-content: flex-start; align-items: center; `} + + & > svg { + max-height: ${(props) => (props.mode === "default" ? "var(--height-m)" : "var(--height-xxs)")}; + width: auto; + } `; const LogoImg = styled.img<{ mode: FooterPropsType["mode"] }>` diff --git a/packages/lib/src/layout/ApplicationLayout.tsx b/packages/lib/src/layout/ApplicationLayout.tsx index aa1b5f1a77..1b760aa71e 100644 --- a/packages/lib/src/layout/ApplicationLayout.tsx +++ b/packages/lib/src/layout/ApplicationLayout.tsx @@ -56,12 +56,15 @@ const Main = ({ children }: AppLayoutMainPropsType): JSX.Element => <div>{childr const DxcApplicationLayout = ({ logo, header, sidenav, footer, children }: ApplicationLayoutPropsType): JSX.Element => { const [headerHeight, setHeaderHeight] = useState("0px"); - const handleHeaderHeight = useCallback((headerElement: HTMLDivElement | null) => { - if (headerElement) { - const height = headerElement.offsetHeight; - setHeaderHeight(`${height}px`); - } - }, []); + const handleHeaderHeight = useCallback( + (headerElement: HTMLDivElement | null) => { + if (headerElement) { + const height = headerElement.offsetHeight; + setHeaderHeight(`${height}px`); + } + }, + [header] + ); const contextValue = useMemo(() => { return { From 96418b6dad9813c3254a8fab67618437beb75178 Mon Sep 17 00:00:00 2001 From: PelayoFelgueroso <pfelguerosogalguera@gmail.com> Date: Fri, 30 Jan 2026 12:36:48 +0100 Subject: [PATCH 049/275] Fix based on comment --- packages/lib/src/footer/Footer.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/lib/src/footer/Footer.tsx b/packages/lib/src/footer/Footer.tsx index b5f3b3971d..2d56ad6811 100644 --- a/packages/lib/src/footer/Footer.tsx +++ b/packages/lib/src/footer/Footer.tsx @@ -66,8 +66,8 @@ const LogoContainer = styled.span<{ mode?: FooterPropsType["mode"] }>` align-items: center; `} - & > svg { - max-height: ${(props) => (props.mode === "default" ? "var(--height-m)" : "var(--height-xxs)")}; + svg { + height: ${(props) => (props.mode === "default" ? "var(--height-m)" : "var(--height-xxs)")}; width: auto; } `; From 6a53877c975218eb0e821dc3140332ec51a5a07b Mon Sep 17 00:00:00 2001 From: Mil4n0r <morenocarmonaenrique@gmail.com> Date: Mon, 19 Jan 2026 17:27:13 +0100 Subject: [PATCH 050/275] Added resizing functionality for sidenav --- packages/lib/src/sidenav/Sidenav.tsx | 71 ++++++++++++++++++++++++---- 1 file changed, 63 insertions(+), 8 deletions(-) diff --git a/packages/lib/src/sidenav/Sidenav.tsx b/packages/lib/src/sidenav/Sidenav.tsx index 7481e199e7..a19b0dfe14 100644 --- a/packages/lib/src/sidenav/Sidenav.tsx +++ b/packages/lib/src/sidenav/Sidenav.tsx @@ -5,20 +5,29 @@ import SidenavPropsType from "./types"; import DxcDivider from "../divider/Divider"; import DxcButton from "../button/Button"; import DxcImage from "../image/Image"; -import { useContext, useRef, useState } from "react"; +import { useContext, useEffect, useRef, useState } from "react"; import DxcNavigationTree from "../navigation-tree/NavigationTree"; import DxcInset from "../inset/Inset"; import ApplicationLayoutContext from "../layout/ApplicationLayoutContext"; import DxcSearchBar from "../search-bar/SearchBar"; import DxcSearchBarTrigger from "../search-bar/SearchBarTrigger"; -const SidenavContainer = styled.div<{ expanded: boolean }>` +const COLLAPSED_WIDTH = 56; +const MIN_WIDTH = 240; +const MAX_WIDTH = 320; +const DEFAULT_WIDTH = 280; + +const SidenavContainer = styled.div<{ + expanded: boolean; + width: number; +}>` + position: relative; box-sizing: border-box; display: flex; flex-direction: column; - /* TODO: IMPLEMENT RESIZABLE SIDENAV */ - min-width: ${({ expanded }) => (expanded ? "240px" : "56px")}; - max-width: ${({ expanded }) => (expanded ? "320px" : "56px")}; + + width: ${({ expanded, width }) => (expanded ? `${width}px` : `${COLLAPSED_WIDTH}px`)}; + min-width: 56px; height: 100%; @media (max-width: ${responsiveSizes.large}rem) { width: 100vw; @@ -35,6 +44,18 @@ const SidenavContainer = styled.div<{ expanded: boolean }>` } `; +// TODO: REFACTOR IN CASE SIDEPANEL CAN GO RIGHT +const ResizeHandle = styled.span<{ active: boolean }>` + position: absolute; + top: 0; + right: calc(-1 * var(--spacing-padding-xs)); + padding: var(--spacing-padding-xxs); + height: 100%; + z-index: 10; + cursor: ew-resize; + background-color: ${({ active }) => (active ? "var(--color-bg-neutral-medium)" : "var(--color-bg-neutral-light)")}; +`; + const SidenavTitle = styled.div` display: flex; align-items: center; @@ -76,6 +97,39 @@ const DxcSidenav = ({ const isControlled = expanded !== undefined; const isExpanded = isControlled ? !!expanded : internalExpanded; const shouldFocusSearchBar = useRef(false); + const sidenavRef = useRef<HTMLDivElement>(null); + const [width, setWidth] = useState(DEFAULT_WIDTH); + const resizing = useRef(false); + useEffect(() => { + const onMove = (e: MouseEvent) => { + if (!resizing.current || !sidenavRef.current) return; + + const rect = sidenavRef?.current?.getBoundingClientRect(); + const nextWidth = e.clientX - rect.left; + + setWidth(() => Math.min(Math.max(nextWidth, MIN_WIDTH), MAX_WIDTH)); + }; + + const stop = () => { + resizing.current = false; + document.body.style.cursor = ""; + document.body.style.userSelect = ""; + }; + + window.addEventListener("mousemove", onMove); + window.addEventListener("mouseup", stop); + + return () => { + window.removeEventListener("mousemove", onMove); + window.removeEventListener("mouseup", stop); + }; + }, []); + + const startResize = () => { + resizing.current = true; + document.body.style.cursor = "ew-resize"; + document.body.style.userSelect = "none"; + }; const handleToggle = () => { const nextState = !isExpanded; @@ -92,7 +146,8 @@ const DxcSidenav = ({ }; return ( - <SidenavContainer expanded={isExpanded}> + <SidenavContainer expanded={isExpanded} width={width} ref={sidenavRef}> + {isExpanded && <ResizeHandle active={resizing.current} onMouseDown={startResize} />} <DxcFlex justifyContent={isExpanded ? "normal" : "center"} gap={isExpanded ? "var(--spacing-gap-xs)" : "var(--spacing-gap-s)"} @@ -127,7 +182,7 @@ const DxcSidenav = ({ </DxcFlex> </DxcFlex> {(topContent || searchBar) && ( - <DxcFlex direction="column" gap={"var(--spacing-gap-l)"}> + <DxcFlex direction="column" gap="var(--spacing-gap-l)"> {searchBar && (isExpanded ? ( <DxcSearchBar {...searchBar} autoFocus={shouldFocusSearchBar.current} /> @@ -151,7 +206,7 @@ const DxcSidenav = ({ <DxcInset horizontal="var(--spacing-padding-xs)"> <DxcDivider color="lightGrey" /> </DxcInset> - <DxcFlex direction="column" gap={"var(--spacing-gap-l)"}> + <DxcFlex direction="column" gap="var(--spacing-gap-l)"> {bottomContent} </DxcFlex> </> From 2c38c5a12e06bc2707c3074b6e10e8b2e189b0ec Mon Sep 17 00:00:00 2001 From: Mil4n0r <morenocarmonaenrique@gmail.com> Date: Tue, 20 Jan 2026 17:11:35 +0100 Subject: [PATCH 051/275] Added resizing function and started to work on responsive view --- packages/lib/src/base-menu/GroupItem.tsx | 12 ++++++ packages/lib/src/base-menu/SubMenu.tsx | 16 +++---- packages/lib/src/sidenav/Sidenav.tsx | 42 ++++--------------- packages/lib/src/utils/useResize.ts | 53 ++++++++++++++++++++++++ 4 files changed, 81 insertions(+), 42 deletions(-) create mode 100644 packages/lib/src/utils/useResize.ts diff --git a/packages/lib/src/base-menu/GroupItem.tsx b/packages/lib/src/base-menu/GroupItem.tsx index d5a7422884..4d09a9d664 100644 --- a/packages/lib/src/base-menu/GroupItem.tsx +++ b/packages/lib/src/base-menu/GroupItem.tsx @@ -51,6 +51,18 @@ const GroupItem = ({ items, ...props }: GroupItemProps) => { sideOffset={isHorizontal ? 16 : 0} onInteractOutside={isHorizontal ? () => toggleOpen() : undefined} > + {!isHorizontal && props.depthLevel === 0 && ( + <ItemAction + aria-controls={isOpen ? groupMenuId : undefined} + aria-expanded={isOpen ? true : undefined} + aria-pressed={groupSelected && !isOpen} + collapseIcon={isOpen ? <DxcIcon icon="filled_expand_less" /> : <DxcIcon icon="filled_expand_more" />} + onClick={() => toggleOpen()} + selected={groupSelected && !isOpen} + {...props} + icon={undefined} + /> + )} <SubMenu id={groupMenuId} depthLevel={props.depthLevel} isPopOver={true}> {items.map((item, index) => ( <MenuItem diff --git a/packages/lib/src/base-menu/SubMenu.tsx b/packages/lib/src/base-menu/SubMenu.tsx index f7114b96c0..88d0a9e175 100644 --- a/packages/lib/src/base-menu/SubMenu.tsx +++ b/packages/lib/src/base-menu/SubMenu.tsx @@ -15,23 +15,23 @@ const SubMenuContainer = styled.ul<{ flex-direction: ${({ isHorizontal }) => (isHorizontal ? "row" : "column")}; gap: ${({ isHorizontal }) => (isHorizontal ? "var(--spacing-gap-s)" : "var(--spacing-gap-xs)")}; list-style: none; - ${({ isPopOver }) => - isPopOver && - ` + // TODO: CHECK PADDING, CAUSING OFFSET IN SIDENAV (ASK JIALE) + ${({ depthLevel, displayGroupLines, isPopOver }) => + isPopOver + ? ` min-width: 200px; max-width: 320px; padding: var(--spacing-padding-xs); background-color: var(--color-bg-neutral-lightest); border-radius: var(--border-radius-m); box-shadow: var(--shadow-100); - `} - ${({ depthLevel, displayGroupLines }) => - displayGroupLines && - depthLevel >= 0 && ` + : displayGroupLines && + depthLevel >= 0 && + ` margin-left: calc(var(--spacing-padding-m) + ${depthLevel} * var(--spacing-padding-xs)); border-left: var(--border-width-s) solid var(--border-color-neutral-lighter); - `}; + `} `; export default function SubMenu({ diff --git a/packages/lib/src/sidenav/Sidenav.tsx b/packages/lib/src/sidenav/Sidenav.tsx index a19b0dfe14..ef2a532c60 100644 --- a/packages/lib/src/sidenav/Sidenav.tsx +++ b/packages/lib/src/sidenav/Sidenav.tsx @@ -5,12 +5,13 @@ import SidenavPropsType from "./types"; import DxcDivider from "../divider/Divider"; import DxcButton from "../button/Button"; import DxcImage from "../image/Image"; -import { useContext, useEffect, useRef, useState } from "react"; +import { useContext, useRef, useState } from "react"; import DxcNavigationTree from "../navigation-tree/NavigationTree"; import DxcInset from "../inset/Inset"; import ApplicationLayoutContext from "../layout/ApplicationLayoutContext"; import DxcSearchBar from "../search-bar/SearchBar"; import DxcSearchBarTrigger from "../search-bar/SearchBarTrigger"; +import useResize from "../utils/useResize"; const COLLAPSED_WIDTH = 56; const MIN_WIDTH = 240; @@ -97,39 +98,12 @@ const DxcSidenav = ({ const isControlled = expanded !== undefined; const isExpanded = isControlled ? !!expanded : internalExpanded; const shouldFocusSearchBar = useRef(false); - const sidenavRef = useRef<HTMLDivElement>(null); - const [width, setWidth] = useState(DEFAULT_WIDTH); - const resizing = useRef(false); - useEffect(() => { - const onMove = (e: MouseEvent) => { - if (!resizing.current || !sidenavRef.current) return; - const rect = sidenavRef?.current?.getBoundingClientRect(); - const nextWidth = e.clientX - rect.left; - - setWidth(() => Math.min(Math.max(nextWidth, MIN_WIDTH), MAX_WIDTH)); - }; - - const stop = () => { - resizing.current = false; - document.body.style.cursor = ""; - document.body.style.userSelect = ""; - }; - - window.addEventListener("mousemove", onMove); - window.addEventListener("mouseup", stop); - - return () => { - window.removeEventListener("mousemove", onMove); - window.removeEventListener("mouseup", stop); - }; - }, []); - - const startResize = () => { - resizing.current = true; - document.body.style.cursor = "ew-resize"; - document.body.style.userSelect = "none"; - }; + const { width, sidenavRef, resizing, startResize } = useResize({ + minWidth: MIN_WIDTH, + maxWidth: MAX_WIDTH, + defaultWidth: DEFAULT_WIDTH, + }); const handleToggle = () => { const nextState = !isExpanded; @@ -178,7 +152,7 @@ const DxcSidenav = ({ )} </LogoContainer> )} - <SidenavTitle>{appTitle}</SidenavTitle> + {isExpanded && <SidenavTitle>{appTitle}</SidenavTitle>} </DxcFlex> </DxcFlex> {(topContent || searchBar) && ( diff --git a/packages/lib/src/utils/useResize.ts b/packages/lib/src/utils/useResize.ts new file mode 100644 index 0000000000..b05ee1230b --- /dev/null +++ b/packages/lib/src/utils/useResize.ts @@ -0,0 +1,53 @@ +import { useEffect, useRef, useState } from "react"; + +type UseResizeProps = { + minWidth: number; + maxWidth: number; + defaultWidth: number; +}; + +const useResize = ({ minWidth, maxWidth, defaultWidth }: UseResizeProps) => { + const [width, setWidth] = useState(defaultWidth); + const sidenavRef = useRef<HTMLDivElement>(null); + const resizing = useRef(false); + + useEffect(() => { + const onMove = (e: MouseEvent) => { + if (!resizing.current || !sidenavRef.current) return; + + const rect = sidenavRef.current.getBoundingClientRect(); + const nextWidth = e.clientX - rect.left; + + setWidth(Math.min(Math.max(nextWidth, minWidth), maxWidth)); + }; + + const stop = () => { + resizing.current = false; + document.body.style.cursor = ""; + document.body.style.userSelect = ""; + }; + + window.addEventListener("mousemove", onMove); + window.addEventListener("mouseup", stop); + + return () => { + window.removeEventListener("mousemove", onMove); + window.removeEventListener("mouseup", stop); + }; + }, [minWidth, maxWidth]); + + const startResize = () => { + resizing.current = true; + document.body.style.cursor = "ew-resize"; + document.body.style.userSelect = "none"; + }; + + return { + width, + sidenavRef, + resizing, + startResize, + }; +}; + +export default useResize; From 41dfd24c7446632dd776ed6badba0dc6e9350733 Mon Sep 17 00:00:00 2001 From: Pelayo Felgueroso <pfelguerosogalguera@gmail.com> Date: Wed, 8 Apr 2026 09:35:16 +0200 Subject: [PATCH 052/275] Refactored breakpoints handling, changed layout for mobile and fixed minor bugs --- packages/lib/src/base-menu/GroupItem.tsx | 18 +- packages/lib/src/base-menu/SubMenu.tsx | 5 - packages/lib/src/layout/ApplicationLayout.tsx | 2 +- packages/lib/src/sidenav/Sidenav.tsx | 176 ++++++++++-------- packages/lib/src/utils/useBreakpoint.ts | 17 ++ packages/lib/src/utils/useResize.ts | 12 +- 6 files changed, 139 insertions(+), 91 deletions(-) create mode 100644 packages/lib/src/utils/useBreakpoint.ts diff --git a/packages/lib/src/base-menu/GroupItem.tsx b/packages/lib/src/base-menu/GroupItem.tsx index 4d09a9d664..ea328b4397 100644 --- a/packages/lib/src/base-menu/GroupItem.tsx +++ b/packages/lib/src/base-menu/GroupItem.tsx @@ -15,7 +15,6 @@ const GroupItem = ({ items, ...props }: GroupItemProps) => { const contextValue = useContext(BaseMenuContext) ?? {}; const { groupSelected, isOpen, toggleOpen, hasPopOver, isHorizontal } = useGroupItem(items, contextValue); - // TODO: SET A FIXED WIDTH TO PREVENT MOVING CONTENT WHEN EXPANDING/COLLAPSING IN RESPONSIVEVIEW return hasPopOver ? ( <> <Popover.Root open={isOpen}> @@ -47,8 +46,21 @@ const GroupItem = ({ items, ...props }: GroupItemProps) => { }} align="start" side={isHorizontal ? "bottom" : "right"} - style={{ zIndex: "var(--z-contextualmenu)" }} - sideOffset={isHorizontal ? 16 : 0} + style={{ + zIndex: "var(--z-contextualmenu)", + padding: "var(--spacing-padding-xs)", + boxShadow: "var(--shadow-100)", + backgroundColor: "var(--color-bg-neutral-lightest)", + borderRadius: "var(--border-radius-m)", + ...(isHorizontal + ? {} + : { + display: "flex", + flexDirection: "column", + gap: "var(--spacing-gap-xxs)", + }), + }} + sideOffset={16} onInteractOutside={isHorizontal ? () => toggleOpen() : undefined} > {!isHorizontal && props.depthLevel === 0 && ( diff --git a/packages/lib/src/base-menu/SubMenu.tsx b/packages/lib/src/base-menu/SubMenu.tsx index 88d0a9e175..c4396c4055 100644 --- a/packages/lib/src/base-menu/SubMenu.tsx +++ b/packages/lib/src/base-menu/SubMenu.tsx @@ -15,16 +15,11 @@ const SubMenuContainer = styled.ul<{ flex-direction: ${({ isHorizontal }) => (isHorizontal ? "row" : "column")}; gap: ${({ isHorizontal }) => (isHorizontal ? "var(--spacing-gap-s)" : "var(--spacing-gap-xs)")}; list-style: none; - // TODO: CHECK PADDING, CAUSING OFFSET IN SIDENAV (ASK JIALE) ${({ depthLevel, displayGroupLines, isPopOver }) => isPopOver ? ` min-width: 200px; max-width: 320px; - padding: var(--spacing-padding-xs); - background-color: var(--color-bg-neutral-lightest); - border-radius: var(--border-radius-m); - box-shadow: var(--shadow-100); ` : displayGroupLines && depthLevel >= 0 && diff --git a/packages/lib/src/layout/ApplicationLayout.tsx b/packages/lib/src/layout/ApplicationLayout.tsx index 1b760aa71e..22b11761f0 100644 --- a/packages/lib/src/layout/ApplicationLayout.tsx +++ b/packages/lib/src/layout/ApplicationLayout.tsx @@ -32,10 +32,10 @@ const BodyContainer = styled.div<{ hasSidenav?: boolean }>` const SidenavContainer = styled.div<{ headerHeight: string }>` width: fit-content; height: 100%; - z-index: var(--z-app-layout-sidenav); position: sticky; top: ${({ headerHeight }) => headerHeight || "0"}; overflow: auto; + z-index: var(--z-app-layout-sidenav); max-height: ${({ headerHeight }) => `calc(100vh - ${headerHeight || "0"})`}; `; diff --git a/packages/lib/src/sidenav/Sidenav.tsx b/packages/lib/src/sidenav/Sidenav.tsx index ef2a532c60..e4567920df 100644 --- a/packages/lib/src/sidenav/Sidenav.tsx +++ b/packages/lib/src/sidenav/Sidenav.tsx @@ -1,5 +1,4 @@ import styled from "@emotion/styled"; -import { responsiveSizes } from "../common/variables"; import DxcFlex from "../flex/Flex"; import SidenavPropsType from "./types"; import DxcDivider from "../divider/Divider"; @@ -12,6 +11,7 @@ import ApplicationLayoutContext from "../layout/ApplicationLayoutContext"; import DxcSearchBar from "../search-bar/SearchBar"; import DxcSearchBarTrigger from "../search-bar/SearchBarTrigger"; import useResize from "../utils/useResize"; +import { useBreakpoint } from "../utils/useBreakpoint"; const COLLAPSED_WIDTH = 56; const MIN_WIDTH = 240; @@ -21,23 +21,34 @@ const DEFAULT_WIDTH = 280; const SidenavContainer = styled.div<{ expanded: boolean; width: number; + showBorder?: boolean; + side?: "left" | "right"; }>` position: relative; box-sizing: border-box; display: flex; - flex-direction: column; - + flex-direction: ${({ side }) => (side === "right" ? "row-reverse" : "row")}; + ${({ showBorder }) => + showBorder && + `border-right: var(--border-width-s) var(--border-style-default) var(--border-color-neutral-lighter);`} width: ${({ expanded, width }) => (expanded ? `${width}px` : `${COLLAPSED_WIDTH}px`)}; min-width: 56px; height: 100%; - @media (max-width: ${responsiveSizes.large}rem) { - width: 100vw; - } + background-color: var(--color-bg-neutral-lightest); +`; + +const SidenavContent = styled.div` + position: relative; + box-sizing: border-box; + display: flex; + flex-direction: column; + height: 100%; + width: 100%; padding-top: var(--spacing-padding-m); padding-bottom: var(--spacing-padding-m); gap: var(--spacing-gap-l); background-color: var(--color-bg-neutral-lightest); - border-right: var(--border-width-s) var(--border-style-default) var(--border-color-neutral-lighter); + & > div { box-sizing: border-box; padding-left: var(--spacing-padding-xs); @@ -45,12 +56,8 @@ const SidenavContainer = styled.div<{ } `; -// TODO: REFACTOR IN CASE SIDEPANEL CAN GO RIGHT const ResizeHandle = styled.span<{ active: boolean }>` - position: absolute; - top: 0; - right: calc(-1 * var(--spacing-padding-xs)); - padding: var(--spacing-padding-xxs); + padding: var(--spacing-padding-none) var(--spacing-padding-xxs); height: 100%; z-index: 10; cursor: ew-resize; @@ -89,17 +96,20 @@ const DxcSidenav = ({ appTitle, displayGroupLines = false, expanded, - defaultExpanded = true, + defaultExpanded, onExpandedChange, searchBar, }: SidenavPropsType): JSX.Element => { - const [internalExpanded, setInternalExpanded] = useState(defaultExpanded); + const isBelowLarge = useBreakpoint("large"); + const isBelowMedium = useBreakpoint("medium"); + + const [internalExpanded, setInternalExpanded] = useState(defaultExpanded ?? !isBelowLarge); const { logo, headerExists } = useContext(ApplicationLayoutContext); const isControlled = expanded !== undefined; const isExpanded = isControlled ? !!expanded : internalExpanded; const shouldFocusSearchBar = useRef(false); - const { width, sidenavRef, resizing, startResize } = useResize({ + const { width, sidenavRef, isResizing, startResize } = useResize({ minWidth: MIN_WIDTH, maxWidth: MAX_WIDTH, defaultWidth: DEFAULT_WIDTH, @@ -120,71 +130,85 @@ const DxcSidenav = ({ }; return ( - <SidenavContainer expanded={isExpanded} width={width} ref={sidenavRef}> - {isExpanded && <ResizeHandle active={resizing.current} onMouseDown={startResize} />} - <DxcFlex - justifyContent={isExpanded ? "normal" : "center"} - gap={isExpanded ? "var(--spacing-gap-xs)" : "var(--spacing-gap-s)"} - direction={isExpanded ? "row" : "column-reverse"} - alignItems={isExpanded ? "normal" : "center"} - > - <DxcButton - icon={`left_panel_${isExpanded ? "close" : "open"}`} - size={{ height: "medium" }} - mode="tertiary" - title={isExpanded ? "Collapse" : "Expand"} - onClick={handleToggle} - /> + <SidenavContainer + expanded={isExpanded} + width={width} + showBorder={!(isBelowMedium && !isExpanded)} + ref={sidenavRef} + side="left" + > + <SidenavContent> + <DxcFlex + justifyContent={isExpanded ? "normal" : "center"} + gap={isExpanded ? "var(--spacing-gap-xs)" : "var(--spacing-gap-s)"} + direction={isExpanded ? "row" : "column-reverse"} + alignItems="flex-start" + > + <DxcButton + icon={`left_panel_${isExpanded ? "close" : "open"}`} + size={{ height: "medium" }} + mode="tertiary" + title={isExpanded ? "Collapse" : "Expand"} + onClick={handleToggle} + /> - <DxcFlex direction="column" gap="var(--spacing-gap-m)" justifyContent="center" alignItems="flex-start"> - {logo && !headerExists && ( - <LogoContainer - onClick={logo.onClick} - hasAction={!!logo.onClick || !!logo.href} - role={logo.onClick ? "button" : logo.href ? "link" : "presentation"} - as={logo.href ? "a" : undefined} - href={logo.href} - > - {typeof logo.src === "string" ? ( - <DxcImage alt={logo.alt ?? ""} src={logo.src} height="100%" width="100%" /> - ) : ( - logo.src + {!(isBelowMedium && !isExpanded) && ( + <DxcFlex direction="column" gap="var(--spacing-gap-m)" justifyContent="center" alignItems="flex-start"> + {logo && !headerExists && ( + <LogoContainer + onClick={logo.onClick} + hasAction={!!logo.onClick || !!logo.href} + role={logo.onClick ? "button" : logo.href ? "link" : "presentation"} + as={logo.href ? "a" : undefined} + href={logo.href} + > + {typeof logo.src === "string" ? ( + <DxcImage alt={logo.alt ?? ""} src={logo.src} height="100%" width="100%" /> + ) : ( + logo.src + )} + </LogoContainer> )} - </LogoContainer> + {isExpanded && <SidenavTitle>{appTitle}</SidenavTitle>} + </DxcFlex> )} - {isExpanded && <SidenavTitle>{appTitle}</SidenavTitle>} - </DxcFlex> - </DxcFlex> - {(topContent || searchBar) && ( - <DxcFlex direction="column" gap="var(--spacing-gap-l)"> - {searchBar && - (isExpanded ? ( - <DxcSearchBar {...searchBar} autoFocus={shouldFocusSearchBar.current} /> - ) : ( - <DxcSearchBarTrigger onTriggerClick={handleExpandSearch} /> - ))} - {topContent} </DxcFlex> - )} - {navItems && ( - <DxcNavigationTree - items={navItems} - displayGroupLines={displayGroupLines} - displayBorder={false} - hasPopOver={!isExpanded} - displayControlsAfter - /> - )} - {bottomContent && ( - <> - <DxcInset horizontal="var(--spacing-padding-xs)"> - <DxcDivider color="lightGrey" /> - </DxcInset> - <DxcFlex direction="column" gap="var(--spacing-gap-l)"> - {bottomContent} - </DxcFlex> - </> - )} + {!(isBelowMedium && !isExpanded) && ( + <> + {(topContent || searchBar) && ( + <DxcFlex direction="column" gap="var(--spacing-gap-l)"> + {searchBar && + (isExpanded ? ( + <DxcSearchBar {...searchBar} autoFocus={shouldFocusSearchBar.current} /> + ) : ( + <DxcSearchBarTrigger onTriggerClick={handleExpandSearch} /> + ))} + {topContent} + </DxcFlex> + )} + {navItems && ( + <DxcNavigationTree + items={navItems} + displayGroupLines={displayGroupLines} + displayBorder={false} + hasPopOver={!isExpanded} + displayControlsAfter + /> + )} + {bottomContent && ( + <> + <DxcInset horizontal="var(--spacing-padding-xs)"> + <DxcDivider color="lightGrey" /> + </DxcInset> + <DxcFlex direction="column" gap="var(--spacing-gap-l)"> + {bottomContent} + </DxcFlex> + </> + )} + </> + )} + </SidenavContent> + {isExpanded && <ResizeHandle active={isResizing} onMouseDown={startResize} />} </SidenavContainer> ); }; diff --git a/packages/lib/src/utils/useBreakpoint.ts b/packages/lib/src/utils/useBreakpoint.ts new file mode 100644 index 0000000000..10f14aeb0f --- /dev/null +++ b/packages/lib/src/utils/useBreakpoint.ts @@ -0,0 +1,17 @@ +import { useEffect, useState } from "react"; +import { responsiveSizes } from "../common/variables"; + +export const useBreakpoint = (breakpoint: keyof typeof responsiveSizes) => { + const query = `(max-width: ${responsiveSizes[breakpoint]}rem)`; + const [matches, setMatches] = useState(() => window.matchMedia(query).matches); + + useEffect(() => { + const media = window.matchMedia(query); + const handler = () => setMatches(media.matches); + + media.addEventListener("change", handler); + return () => media.removeEventListener("change", handler); + }, [query]); + + return matches; +}; diff --git a/packages/lib/src/utils/useResize.ts b/packages/lib/src/utils/useResize.ts index b05ee1230b..3f8a6ccb03 100644 --- a/packages/lib/src/utils/useResize.ts +++ b/packages/lib/src/utils/useResize.ts @@ -9,11 +9,11 @@ type UseResizeProps = { const useResize = ({ minWidth, maxWidth, defaultWidth }: UseResizeProps) => { const [width, setWidth] = useState(defaultWidth); const sidenavRef = useRef<HTMLDivElement>(null); - const resizing = useRef(false); + const [isResizing, setIsResizing] = useState(false); // <-- new state useEffect(() => { const onMove = (e: MouseEvent) => { - if (!resizing.current || !sidenavRef.current) return; + if (!isResizing || !sidenavRef.current) return; const rect = sidenavRef.current.getBoundingClientRect(); const nextWidth = e.clientX - rect.left; @@ -22,7 +22,7 @@ const useResize = ({ minWidth, maxWidth, defaultWidth }: UseResizeProps) => { }; const stop = () => { - resizing.current = false; + setIsResizing(false); document.body.style.cursor = ""; document.body.style.userSelect = ""; }; @@ -34,10 +34,10 @@ const useResize = ({ minWidth, maxWidth, defaultWidth }: UseResizeProps) => { window.removeEventListener("mousemove", onMove); window.removeEventListener("mouseup", stop); }; - }, [minWidth, maxWidth]); + }, [minWidth, maxWidth, isResizing]); const startResize = () => { - resizing.current = true; + setIsResizing(true); document.body.style.cursor = "ew-resize"; document.body.style.userSelect = "none"; }; @@ -45,7 +45,7 @@ const useResize = ({ minWidth, maxWidth, defaultWidth }: UseResizeProps) => { return { width, sidenavRef, - resizing, + isResizing, startResize, }; }; From 12c7f64bbc47191e25064bd1bad79305f7f1ff84 Mon Sep 17 00:00:00 2001 From: Mil4n0r <morenocarmonaenrique@gmail.com> Date: Thu, 22 Jan 2026 15:12:52 +0100 Subject: [PATCH 053/275] Fixed problem with window not being initialized --- packages/lib/src/utils/useBreakpoint.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/lib/src/utils/useBreakpoint.ts b/packages/lib/src/utils/useBreakpoint.ts index 10f14aeb0f..91b83e0e4e 100644 --- a/packages/lib/src/utils/useBreakpoint.ts +++ b/packages/lib/src/utils/useBreakpoint.ts @@ -3,9 +3,17 @@ import { responsiveSizes } from "../common/variables"; export const useBreakpoint = (breakpoint: keyof typeof responsiveSizes) => { const query = `(max-width: ${responsiveSizes[breakpoint]}rem)`; - const [matches, setMatches] = useState(() => window.matchMedia(query).matches); + + const [matches, setMatches] = useState(() => { + if (typeof window !== "undefined") { + return window.matchMedia(query).matches; + } + return false; + }); useEffect(() => { + if (typeof window === "undefined") return; + const media = window.matchMedia(query); const handler = () => setMatches(media.matches); From 74b98979a9e4143197967ae7bf6b06a5777ec810 Mon Sep 17 00:00:00 2001 From: Mil4n0r <morenocarmonaenrique@gmail.com> Date: Thu, 22 Jan 2026 15:47:03 +0100 Subject: [PATCH 054/275] Fixed styles issues related to scrollbar in Sidenav --- packages/lib/src/base-menu/MenuItem.tsx | 2 +- packages/lib/src/sidenav/Sidenav.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/lib/src/base-menu/MenuItem.tsx b/packages/lib/src/base-menu/MenuItem.tsx index b70663a489..19c87dd9ff 100644 --- a/packages/lib/src/base-menu/MenuItem.tsx +++ b/packages/lib/src/base-menu/MenuItem.tsx @@ -6,7 +6,7 @@ import { isGroupItem } from "./utils"; const MenuItemContainer = styled.li` display: grid; - gap: var(--spacing-gap-xs); + margin-right: var(--spacing-padding-xxs); `; export default function MenuItem({ item, depthLevel = 0 }: MenuItemProps) { diff --git a/packages/lib/src/sidenav/Sidenav.tsx b/packages/lib/src/sidenav/Sidenav.tsx index e4567920df..95e6aa24d7 100644 --- a/packages/lib/src/sidenav/Sidenav.tsx +++ b/packages/lib/src/sidenav/Sidenav.tsx @@ -52,7 +52,7 @@ const SidenavContent = styled.div` & > div { box-sizing: border-box; padding-left: var(--spacing-padding-xs); - padding-right: var(--spacing-padding-xs); + margin-right: var(--spacing-padding-xs); } `; From 39022a109d4872b9b87b2f3abb8574a71962ac2d Mon Sep 17 00:00:00 2001 From: Mil4n0r <morenocarmonaenrique@gmail.com> Date: Mon, 26 Jan 2026 16:28:03 +0100 Subject: [PATCH 055/275] Fixed problem with tests in sidenav --- packages/lib/src/sidenav/Sidenav.accessibility.test.tsx | 8 ++++++++ packages/lib/src/sidenav/Sidenav.test.tsx | 9 +++++++++ 2 files changed, 17 insertions(+) diff --git a/packages/lib/src/sidenav/Sidenav.accessibility.test.tsx b/packages/lib/src/sidenav/Sidenav.accessibility.test.tsx index 7b2ac21e15..0f58a6a2bf 100644 --- a/packages/lib/src/sidenav/Sidenav.accessibility.test.tsx +++ b/packages/lib/src/sidenav/Sidenav.accessibility.test.tsx @@ -11,6 +11,14 @@ global.ResizeObserver = vi.fn().mockImplementation(() => ({ })); describe("Sidenav component accessibility tests", () => { + beforeAll(() => { + Object.defineProperty(window, "matchMedia", { + writable: true, + value: vi.fn().mockImplementation(() => ({ + matches: false, + })), + }); + }); it("Should not have basic accessibility issues", async () => { const groupItems = [ { diff --git a/packages/lib/src/sidenav/Sidenav.test.tsx b/packages/lib/src/sidenav/Sidenav.test.tsx index cbbc8c7178..a88602dc91 100644 --- a/packages/lib/src/sidenav/Sidenav.test.tsx +++ b/packages/lib/src/sidenav/Sidenav.test.tsx @@ -10,6 +10,15 @@ global.ResizeObserver = jest.fn().mockImplementation(() => ({ })); describe("DxcSidenav component", () => { + const mockMatchMedia = jest.fn(); + + beforeAll(() => { + Object.defineProperty(window, "matchMedia", { + writable: true, + value: mockMatchMedia, + }); + }); + beforeEach(() => { jest.clearAllMocks(); }); From 3515eaa662470884420a48e8b8105c44a544bbd8 Mon Sep 17 00:00:00 2001 From: Mil4n0r <morenocarmonaenrique@gmail.com> Date: Mon, 26 Jan 2026 16:38:01 +0100 Subject: [PATCH 056/275] Mocked matchMedia and listener actions for jest test --- packages/lib/src/sidenav/Sidenav.test.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/lib/src/sidenav/Sidenav.test.tsx b/packages/lib/src/sidenav/Sidenav.test.tsx index a88602dc91..0808a30163 100644 --- a/packages/lib/src/sidenav/Sidenav.test.tsx +++ b/packages/lib/src/sidenav/Sidenav.test.tsx @@ -20,7 +20,11 @@ describe("DxcSidenav component", () => { }); beforeEach(() => { - jest.clearAllMocks(); + mockMatchMedia.mockImplementation(() => ({ + matches: false, + addEventListener: jest.fn(), + removeEventListener: jest.fn(), + })); }); test("Sidenav renders title and children correctly", () => { From 3f10df1a5c113ad412ff30c0e2c37f6de8a3d3fb Mon Sep 17 00:00:00 2001 From: Mil4n0r <morenocarmonaenrique@gmail.com> Date: Mon, 26 Jan 2026 16:47:32 +0100 Subject: [PATCH 057/275] Added eventListeners to accessibility test --- packages/lib/src/sidenav/Sidenav.accessibility.test.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/lib/src/sidenav/Sidenav.accessibility.test.tsx b/packages/lib/src/sidenav/Sidenav.accessibility.test.tsx index 0f58a6a2bf..963bec30b3 100644 --- a/packages/lib/src/sidenav/Sidenav.accessibility.test.tsx +++ b/packages/lib/src/sidenav/Sidenav.accessibility.test.tsx @@ -16,6 +16,8 @@ describe("Sidenav component accessibility tests", () => { writable: true, value: vi.fn().mockImplementation(() => ({ matches: false, + addEventListener: vi.fn(), + removeEventListener: vi.fn(), })), }); }); From eabeaa8a2bda2b7ad174d562706145b17245b345 Mon Sep 17 00:00:00 2001 From: Mil4n0r <morenocarmonaenrique@gmail.com> Date: Tue, 27 Jan 2026 17:19:59 +0100 Subject: [PATCH 058/275] Fixed minor problems based on feedback --- packages/lib/src/sidenav/Sidenav.tsx | 4 ++-- packages/lib/src/utils/useResize.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/lib/src/sidenav/Sidenav.tsx b/packages/lib/src/sidenav/Sidenav.tsx index 95e6aa24d7..4bbdf849f6 100644 --- a/packages/lib/src/sidenav/Sidenav.tsx +++ b/packages/lib/src/sidenav/Sidenav.tsx @@ -139,10 +139,10 @@ const DxcSidenav = ({ > <SidenavContent> <DxcFlex - justifyContent={isExpanded ? "normal" : "center"} + justifyContent={isExpanded ? "flex-start" : "center"} gap={isExpanded ? "var(--spacing-gap-xs)" : "var(--spacing-gap-s)"} direction={isExpanded ? "row" : "column-reverse"} - alignItems="flex-start" + alignItems="center" > <DxcButton icon={`left_panel_${isExpanded ? "close" : "open"}`} diff --git a/packages/lib/src/utils/useResize.ts b/packages/lib/src/utils/useResize.ts index 3f8a6ccb03..0746822f39 100644 --- a/packages/lib/src/utils/useResize.ts +++ b/packages/lib/src/utils/useResize.ts @@ -9,7 +9,7 @@ type UseResizeProps = { const useResize = ({ minWidth, maxWidth, defaultWidth }: UseResizeProps) => { const [width, setWidth] = useState(defaultWidth); const sidenavRef = useRef<HTMLDivElement>(null); - const [isResizing, setIsResizing] = useState(false); // <-- new state + const [isResizing, setIsResizing] = useState(false); useEffect(() => { const onMove = (e: MouseEvent) => { From 47f4ff7e9b5ff528fea0116c2c01c49b81c20e9a Mon Sep 17 00:00:00 2001 From: Mil4n0r <morenocarmonaenrique@gmail.com> Date: Wed, 28 Jan 2026 12:55:59 +0100 Subject: [PATCH 059/275] Improved user experience for resize handle --- packages/lib/src/navigation-tree/NavigationTree.tsx | 6 +++++- packages/lib/src/sidenav/Sidenav.tsx | 7 +++---- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/packages/lib/src/navigation-tree/NavigationTree.tsx b/packages/lib/src/navigation-tree/NavigationTree.tsx index a139d82254..dd04065495 100644 --- a/packages/lib/src/navigation-tree/NavigationTree.tsx +++ b/packages/lib/src/navigation-tree/NavigationTree.tsx @@ -16,8 +16,9 @@ const NavigationTreeContainer = styled.div<{ displayBorder: boolean }>` /* min-width: 248px; */ max-height: 100%; background-color: var(--color-bg-neutral-lightest); - overflow-y: auto; + overflow-y: hidden; overflow-x: hidden; + scrollbar-gutter: stable; ${scrollbarStyles}; ${({ displayBorder }) => displayBorder && @@ -26,6 +27,9 @@ const NavigationTreeContainer = styled.div<{ displayBorder: boolean }>` border-radius: var(--border-radius-s); padding: var(--spacing-padding-m) var(--spacing-padding-xs); `} + &:hover { + overflow-y: auto; + } `; export default function DxcNavigationTree({ diff --git a/packages/lib/src/sidenav/Sidenav.tsx b/packages/lib/src/sidenav/Sidenav.tsx index 4bbdf849f6..9fd6f311a6 100644 --- a/packages/lib/src/sidenav/Sidenav.tsx +++ b/packages/lib/src/sidenav/Sidenav.tsx @@ -52,16 +52,15 @@ const SidenavContent = styled.div` & > div { box-sizing: border-box; padding-left: var(--spacing-padding-xs); - margin-right: var(--spacing-padding-xs); + padding-right: var(--spacing-padding-xs); } `; const ResizeHandle = styled.span<{ active: boolean }>` - padding: var(--spacing-padding-none) var(--spacing-padding-xxs); + padding: var(--spacing-padding-none) var(--spacing-padding-xxxs); height: 100%; - z-index: 10; cursor: ew-resize; - background-color: ${({ active }) => (active ? "var(--color-bg-neutral-medium)" : "var(--color-bg-neutral-light)")}; + background-color: transparent; `; const SidenavTitle = styled.div` From ce857502d36cf2dc795acce62aaa84fd977ffbad Mon Sep 17 00:00:00 2001 From: Mil4n0r <morenocarmonaenrique@gmail.com> Date: Thu, 29 Jan 2026 17:08:03 +0100 Subject: [PATCH 060/275] Improved testing for sidenav and added additional functionalities for responsiveness --- apps/website/pages/_app.tsx | 2 +- packages/lib/src/layout/ApplicationLayout.tsx | 12 ++ .../src/navigation-tree/NavigationTree.tsx | 7 +- packages/lib/src/sidenav/Sidenav.stories.tsx | 110 ++++++++++++++++++ packages/lib/src/sidenav/Sidenav.tsx | 45 +++++-- packages/lib/src/utils/useBreakpoint.ts | 8 +- packages/lib/src/utils/useResize.ts | 2 +- 7 files changed, 163 insertions(+), 23 deletions(-) diff --git a/apps/website/pages/_app.tsx b/apps/website/pages/_app.tsx index ed382f285e..8bdffef335 100644 --- a/apps/website/pages/_app.tsx +++ b/apps/website/pages/_app.tsx @@ -107,7 +107,7 @@ export default function App({ Component, pageProps, emotionCache = clientSideEmo sidenav={ <DxcApplicationLayout.Sidenav navItems={navItems} - appTitle={isExpanded && <SidenavLogo />} + appTitle={<SidenavLogo />} searchBar={{ placeholder: "Search docs", onChange: (value) => setFilter(value) }} expanded={isExpanded} onExpandedChange={() => { diff --git a/packages/lib/src/layout/ApplicationLayout.tsx b/packages/lib/src/layout/ApplicationLayout.tsx index 22b11761f0..07fa66f6e2 100644 --- a/packages/lib/src/layout/ApplicationLayout.tsx +++ b/packages/lib/src/layout/ApplicationLayout.tsx @@ -6,6 +6,7 @@ import DxcSidenav from "../sidenav/Sidenav"; import ApplicationLayoutPropsType, { AppLayoutMainPropsType } from "./types"; import { bottomLinks, findChildType, socialLinks, year } from "./utils"; import ApplicationLayoutContext from "./ApplicationLayoutContext"; +import { responsiveSizes } from "../common/variables"; const ApplicationLayoutContainer = styled.div<{ header?: React.ReactNode }>` display: grid; @@ -27,6 +28,13 @@ const BodyContainer = styled.div<{ hasSidenav?: boolean }>` grid-template-columns: ${({ hasSidenav }) => (hasSidenav ? "auto 1fr" : "1fr")}; grid-template-rows: 1fr; min-height: 100%; + overflow: hidden; + + @media (max-width: ${responsiveSizes.medium}rem) { + grid-template-columns: 1fr; + grid-template-rows: auto 1fr; + overflow-y: auto; + } `; const SidenavContainer = styled.div<{ headerHeight: string }>` @@ -37,6 +45,10 @@ const SidenavContainer = styled.div<{ headerHeight: string }>` overflow: auto; z-index: var(--z-app-layout-sidenav); max-height: ${({ headerHeight }) => `calc(100vh - ${headerHeight || "0"})`}; + + @media (max-width: ${responsiveSizes.medium}rem) { + width: 100%; + } `; const MainContainer = styled.main` diff --git a/packages/lib/src/navigation-tree/NavigationTree.tsx b/packages/lib/src/navigation-tree/NavigationTree.tsx index dd04065495..f47568d38a 100644 --- a/packages/lib/src/navigation-tree/NavigationTree.tsx +++ b/packages/lib/src/navigation-tree/NavigationTree.tsx @@ -13,12 +13,10 @@ const NavigationTreeContainer = styled.div<{ displayBorder: boolean }>` margin: 0; display: grid; gap: var(--spacing-gap-xs); - /* min-width: 248px; */ max-height: 100%; background-color: var(--color-bg-neutral-lightest); - overflow-y: hidden; + overflow-y: auto; overflow-x: hidden; - scrollbar-gutter: stable; ${scrollbarStyles}; ${({ displayBorder }) => displayBorder && @@ -27,9 +25,6 @@ const NavigationTreeContainer = styled.div<{ displayBorder: boolean }>` border-radius: var(--border-radius-s); padding: var(--spacing-padding-m) var(--spacing-padding-xs); `} - &:hover { - overflow-y: auto; - } `; export default function DxcNavigationTree({ diff --git a/packages/lib/src/sidenav/Sidenav.stories.tsx b/packages/lib/src/sidenav/Sidenav.stories.tsx index 27adbfa100..448c0f8dee 100644 --- a/packages/lib/src/sidenav/Sidenav.stories.tsx +++ b/packages/lib/src/sidenav/Sidenav.stories.tsx @@ -11,6 +11,8 @@ import { userEvent, within } from "storybook/internal/test"; import disabledRules from "../../test/accessibility/rules/specific/sidenav/disabledRules"; import preview from "../../.storybook/preview"; import { useEffect, useState } from "react"; +import DxcApplicationLayout from "../layout/ApplicationLayout"; +import DxcParagraph from "../paragraph/Paragraph"; export default { title: "Sidenav", @@ -142,6 +144,31 @@ const selectedGroupItems = [ }, ]; +const dxcLogo = ( + <svg xmlns="http://www.w3.org/2000/svg" width="73" height="40" viewBox="0 0 73 40"> + <title>DXC Logo + + + + + + + +); + +const dxcBrandedLogo = { + src: dxcLogo, + alt: "DXC Logo", +}; + const Sidenav = () => ( <> @@ -446,6 +473,89 @@ const SelectedGroup = () => ( /> ); + +const SidenavInLayout = () => ( + + } + > + + + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec ultrices fermentum ante et pharetra. Integer + ullamcorper ante non laoreet suscipit. Integer pharetra viverra nunc, quis fermentum urna eleifend eget. + Maecenas dolor justo, ullamcorper ac posuere tincidunt, dictum id urna. Suspendisse est metus, euismod et felis + eget, condimentum elementum eros. Curabitur ut lorem ut odio volutpat lacinia. Interdum et malesuada fames ac + ante ipsum primis in faucibus. Sed leo quam, lobortis in ultricies ac, interdum in sem. Suspendisse magna enim, + rhoncus eget lectus vitae, rutrum interdum ligula. Nunc efficitur neque ac orci pretium lacinia. Proin sagittis + condimentum mi, eu dapibus quam faucibus eget. Aenean fermentum nisl ut mauris convallis, in imperdiet neque + porttitor. Aliquam erat volutpat. Fusce tincidunt arcu id arcu dignissim viverra. Sed imperdiet vitae odio eget + consequat. Vivamus eu dictum orci. + + + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec ultrices fermentum ante et pharetra. Integer + ullamcorper ante non laoreet suscipit. Integer pharetra viverra nunc, quis fermentum urna eleifend eget. + Maecenas dolor justo, ullamcorper ac posuere tincidunt, dictum id urna. Suspendisse est metus, euismod et felis + eget, condimentum elementum eros. Curabitur ut lorem ut odio volutpat lacinia. Interdum et malesuada fames ac + ante ipsum primis in faucibus. Sed leo quam, lobortis in ultricies ac, interdum in sem. Suspendisse magna enim, + rhoncus eget lectus vitae, rutrum interdum ligula. Nunc efficitur neque ac orci pretium lacinia. Proin sagittis + condimentum mi, eu dapibus quam faucibus eget. Aenean fermentum nisl ut mauris convallis, in imperdiet neque + porttitor. Aliquam erat volutpat. Fusce tincidunt arcu id arcu dignissim viverra. Sed imperdiet vitae odio eget + consequat. Vivamus eu dictum orci. + + + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec ultrices fermentum ante et pharetra. Integer + ullamcorper ante non laoreet suscipit. Integer pharetra viverra nunc, quis fermentum urna eleifend eget. + Maecenas dolor justo, ullamcorper ac posuere tincidunt, dictum id urna. Suspendisse est metus, euismod et felis + eget, condimentum elementum eros. Curabitur ut lorem ut odio volutpat lacinia. Interdum et malesuada fames ac + ante ipsum primis in faucibus. Sed leo quam, lobortis in ultricies ac, interdum in sem. Suspendisse magna enim, + rhoncus eget lectus vitae, rutrum interdum ligula. Nunc efficitur neque ac orci pretium lacinia. Proin sagittis + condimentum mi, eu dapibus quam faucibus eget. Aenean fermentum nisl ut mauris convallis, in imperdiet neque + porttitor. Aliquam erat volutpat. Fusce tincidunt arcu id arcu dignissim viverra. Sed imperdiet vitae odio eget + consequat. Vivamus eu dictum orci. + + + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec ultrices fermentum ante et pharetra. Integer + ullamcorper ante non laoreet suscipit. Integer pharetra viverra nunc, quis fermentum urna eleifend eget. + Maecenas dolor justo, ullamcorper ac posuere tincidunt, dictum id urna. Suspendisse est metus, euismod et felis + eget, condimentum elementum eros. Curabitur ut lorem ut odio volutpat lacinia. Interdum et malesuada fames ac + ante ipsum primis in faucibus. Sed leo quam, lobortis in ultricies ac, interdum in sem. Suspendisse magna enim, + rhoncus eget lectus vitae, rutrum interdum ligula. Nunc efficitur neque ac orci pretium lacinia. Proin sagittis + condimentum mi, eu dapibus quam faucibus eget. Aenean fermentum nisl ut mauris convallis, in imperdiet neque + porttitor. Aliquam erat volutpat. Fusce tincidunt arcu id arcu dignissim viverra. Sed imperdiet vitae odio eget + consequat. Vivamus eu dictum orci. + + + +); + +export const InLayout: Story = { + render: SidenavInLayout, +}; + +export const Responsive: Story = { + render: SidenavInLayout, + parameters: { + chromatic: { viewports: [375] }, + }, + globals: { + viewport: { value: "iphonex", isRotated: false }, + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + await new Promise((resolve) => setTimeout(resolve, 100)); + await canvas.findByLabelText("Expand"); + await userEvent.tab(); + await userEvent.keyboard("{Enter}"); + await canvas.findByLabelText("Collapse"); + }, +}; + type Story = StoryObj; export const Chromatic: Story = { diff --git a/packages/lib/src/sidenav/Sidenav.tsx b/packages/lib/src/sidenav/Sidenav.tsx index 9fd6f311a6..b9f2daf013 100644 --- a/packages/lib/src/sidenav/Sidenav.tsx +++ b/packages/lib/src/sidenav/Sidenav.tsx @@ -4,7 +4,7 @@ import SidenavPropsType from "./types"; import DxcDivider from "../divider/Divider"; import DxcButton from "../button/Button"; import DxcImage from "../image/Image"; -import { useContext, useRef, useState } from "react"; +import { useContext, useEffect, useRef, useState } from "react"; import DxcNavigationTree from "../navigation-tree/NavigationTree"; import DxcInset from "../inset/Inset"; import ApplicationLayoutContext from "../layout/ApplicationLayoutContext"; @@ -12,9 +12,10 @@ import DxcSearchBar from "../search-bar/SearchBar"; import DxcSearchBarTrigger from "../search-bar/SearchBarTrigger"; import useResize from "../utils/useResize"; import { useBreakpoint } from "../utils/useBreakpoint"; +import { responsiveSizes } from "../common/variables"; const COLLAPSED_WIDTH = 56; -const MIN_WIDTH = 240; +const MIN_WIDTH = 0; const MAX_WIDTH = 320; const DEFAULT_WIDTH = 280; @@ -32,9 +33,17 @@ const SidenavContainer = styled.div<{ showBorder && `border-right: var(--border-width-s) var(--border-style-default) var(--border-color-neutral-lighter);`} width: ${({ expanded, width }) => (expanded ? `${width}px` : `${COLLAPSED_WIDTH}px`)}; - min-width: 56px; + min-width: ${({ expanded }) => (expanded ? "240px" : "56px")}; height: 100%; background-color: var(--color-bg-neutral-lightest); + @media (max-width: ${responsiveSizes.medium}rem) { + ${({ expanded }) => + expanded && + ` + width: 100%; + height: 100vh; + `} + } `; const SidenavContent = styled.div` @@ -59,7 +68,7 @@ const SidenavContent = styled.div` const ResizeHandle = styled.span<{ active: boolean }>` padding: var(--spacing-padding-none) var(--spacing-padding-xxxs); height: 100%; - cursor: ew-resize; + cursor: col-resize; background-color: transparent; `; @@ -101,19 +110,25 @@ const DxcSidenav = ({ }: SidenavPropsType): JSX.Element => { const isBelowLarge = useBreakpoint("large"); const isBelowMedium = useBreakpoint("medium"); - - const [internalExpanded, setInternalExpanded] = useState(defaultExpanded ?? !isBelowLarge); const { logo, headerExists } = useContext(ApplicationLayoutContext); const isControlled = expanded !== undefined; - const isExpanded = isControlled ? !!expanded : internalExpanded; - const shouldFocusSearchBar = useRef(false); - const { width, sidenavRef, isResizing, startResize } = useResize({ minWidth: MIN_WIDTH, maxWidth: MAX_WIDTH, defaultWidth: DEFAULT_WIDTH, }); + const [internalExpanded, setInternalExpanded] = useState(defaultExpanded ?? !isBelowLarge); + + useEffect(() => { + if (!isControlled) { + setInternalExpanded(!isBelowLarge && width > COLLAPSED_WIDTH); + } + }, [isBelowLarge, width]); + + const isExpanded = isControlled ? !!expanded : internalExpanded; + const shouldFocusSearchBar = useRef(false); + const handleToggle = () => { const nextState = !isExpanded; if (!isControlled) setInternalExpanded(nextState); @@ -138,10 +153,10 @@ const DxcSidenav = ({ > {!(isBelowMedium && !isExpanded) && ( - + {logo && !headerExists && ( { +export const useBreakpoint = (breakpoint: keyof typeof responsiveSizes): boolean | undefined => { const query = `(max-width: ${responsiveSizes[breakpoint]}rem)`; - const [matches, setMatches] = useState(() => { + const [matches, setMatches] = useState(() => { if (typeof window !== "undefined") { return window.matchMedia(query).matches; } - return false; + return undefined; }); useEffect(() => { @@ -17,6 +17,8 @@ export const useBreakpoint = (breakpoint: keyof typeof responsiveSizes) => { const media = window.matchMedia(query); const handler = () => setMatches(media.matches); + setMatches(media.matches); + media.addEventListener("change", handler); return () => media.removeEventListener("change", handler); }, [query]); diff --git a/packages/lib/src/utils/useResize.ts b/packages/lib/src/utils/useResize.ts index 0746822f39..c33895cbed 100644 --- a/packages/lib/src/utils/useResize.ts +++ b/packages/lib/src/utils/useResize.ts @@ -38,7 +38,7 @@ const useResize = ({ minWidth, maxWidth, defaultWidth }: UseResizeProps) => { const startResize = () => { setIsResizing(true); - document.body.style.cursor = "ew-resize"; + document.body.style.cursor = "col-resize"; document.body.style.userSelect = "none"; }; From fc10e452d5ac4c861d841262f892a492218d5f99 Mon Sep 17 00:00:00 2001 From: Mil4n0r Date: Thu, 29 Jan 2026 17:17:17 +0100 Subject: [PATCH 061/275] Fixed problem with defaultExpanded from test --- packages/lib/src/sidenav/Sidenav.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/lib/src/sidenav/Sidenav.tsx b/packages/lib/src/sidenav/Sidenav.tsx index b9f2daf013..12c43df834 100644 --- a/packages/lib/src/sidenav/Sidenav.tsx +++ b/packages/lib/src/sidenav/Sidenav.tsx @@ -121,7 +121,7 @@ const DxcSidenav = ({ const [internalExpanded, setInternalExpanded] = useState(defaultExpanded ?? !isBelowLarge); useEffect(() => { - if (!isControlled) { + if (defaultExpanded === undefined) { setInternalExpanded(!isBelowLarge && width > COLLAPSED_WIDTH); } }, [isBelowLarge, width]); From b7740b6f73f6a98314724e70434dccee3aab732f Mon Sep 17 00:00:00 2001 From: Mil4n0r Date: Fri, 30 Jan 2026 10:28:23 +0100 Subject: [PATCH 062/275] Fixed styles for responsive sidenav collapsed --- packages/lib/src/sidenav/Sidenav.tsx | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/packages/lib/src/sidenav/Sidenav.tsx b/packages/lib/src/sidenav/Sidenav.tsx index 12c43df834..e5a833d276 100644 --- a/packages/lib/src/sidenav/Sidenav.tsx +++ b/packages/lib/src/sidenav/Sidenav.tsx @@ -37,12 +37,8 @@ const SidenavContainer = styled.div<{ height: 100%; background-color: var(--color-bg-neutral-lightest); @media (max-width: ${responsiveSizes.medium}rem) { - ${({ expanded }) => - expanded && - ` - width: 100%; - height: 100vh; - `} + width: 100%; + ${({ expanded }) => expanded && "height: 100vh;"} } `; @@ -156,7 +152,7 @@ const DxcSidenav = ({ justifyContent="flex-start" gap={isExpanded ? "var(--spacing-gap-xs)" : "var(--spacing-gap-s)"} direction={isExpanded ? "row" : "column-reverse"} - alignItems={isExpanded ? "flex-start" : "center"} + alignItems={isExpanded || isBelowMedium ? "flex-start" : "center"} > Date: Fri, 30 Jan 2026 10:59:14 +0100 Subject: [PATCH 063/275] Fixed logo not displaying in responsive --- packages/lib/src/sidenav/Sidenav.tsx | 57 ++++++++++++++-------------- 1 file changed, 29 insertions(+), 28 deletions(-) diff --git a/packages/lib/src/sidenav/Sidenav.tsx b/packages/lib/src/sidenav/Sidenav.tsx index e5a833d276..c667e7d4a2 100644 --- a/packages/lib/src/sidenav/Sidenav.tsx +++ b/packages/lib/src/sidenav/Sidenav.tsx @@ -24,6 +24,7 @@ const SidenavContainer = styled.div<{ width: number; showBorder?: boolean; side?: "left" | "right"; + hasHeader: boolean; }>` position: relative; box-sizing: border-box; @@ -38,7 +39,8 @@ const SidenavContainer = styled.div<{ background-color: var(--color-bg-neutral-lightest); @media (max-width: ${responsiveSizes.medium}rem) { width: 100%; - ${({ expanded }) => expanded && "height: 100vh;"} + ${({ expanded, hasHeader }) => + expanded && (hasHeader ? "height: calc(100vh - var(--height-xxxl));" : "height: 100vh;")} } `; @@ -146,12 +148,13 @@ const DxcSidenav = ({ showBorder={!(isBelowMedium && !isExpanded)} ref={sidenavRef} side="left" + hasHeader={headerExists} > - {!(isBelowMedium && !isExpanded) && ( - - {logo && !headerExists && ( - - {typeof logo.src === "string" ? ( - - ) : ( - logo.src - )} - - )} - {isExpanded && {appTitle}} - - )} + + {logo && !headerExists && ( + + {typeof logo.src === "string" ? ( + + ) : ( + logo.src + )} + + )} + {isExpanded && !isBelowMedium && {appTitle}} + {!(isBelowMedium && !isExpanded) && ( <> From 0688e9ea62dd6c93537ee214836bec00e675fe4d Mon Sep 17 00:00:00 2001 From: Mil4n0r Date: Fri, 30 Jan 2026 11:00:25 +0100 Subject: [PATCH 064/275] Fixed logo not displaying in responsive --- packages/lib/src/sidenav/Sidenav.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/lib/src/sidenav/Sidenav.tsx b/packages/lib/src/sidenav/Sidenav.tsx index c667e7d4a2..b85b62be45 100644 --- a/packages/lib/src/sidenav/Sidenav.tsx +++ b/packages/lib/src/sidenav/Sidenav.tsx @@ -24,7 +24,7 @@ const SidenavContainer = styled.div<{ width: number; showBorder?: boolean; side?: "left" | "right"; - hasHeader: boolean; + hasHeader?: boolean; }>` position: relative; box-sizing: border-box; From 2bbdefa0a242b47135b46290bbcb83608ed51c1e Mon Sep 17 00:00:00 2001 From: Mil4n0r Date: Fri, 30 Jan 2026 11:17:51 +0100 Subject: [PATCH 065/275] Added default value to documentation --- .../screens/components/sidenav/code/SidenavCodePage.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/website/screens/components/sidenav/code/SidenavCodePage.tsx b/apps/website/screens/components/sidenav/code/SidenavCodePage.tsx index 137e967e32..551981c7e5 100644 --- a/apps/website/screens/components/sidenav/code/SidenavCodePage.tsx +++ b/apps/website/screens/components/sidenav/code/SidenavCodePage.tsx @@ -95,7 +95,9 @@ const sections = [ boolean If true the nav menu will have lines marking the groups. - - + + false + From d753096bf21de650a8aaed53c548982bee094410 Mon Sep 17 00:00:00 2001 From: Mil4n0r Date: Fri, 30 Jan 2026 12:24:04 +0100 Subject: [PATCH 066/275] Display title when expanded --- packages/lib/src/sidenav/Sidenav.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/lib/src/sidenav/Sidenav.tsx b/packages/lib/src/sidenav/Sidenav.tsx index b85b62be45..30755426da 100644 --- a/packages/lib/src/sidenav/Sidenav.tsx +++ b/packages/lib/src/sidenav/Sidenav.tsx @@ -187,7 +187,7 @@ const DxcSidenav = ({ )} )} - {isExpanded && !isBelowMedium && {appTitle}} + {isExpanded && {appTitle}} {!(isBelowMedium && !isExpanded) && ( From b6d45a0d64a5d09a749e982dfad06b8d9294a3d2 Mon Sep 17 00:00:00 2001 From: Mil4n0r Date: Fri, 30 Jan 2026 12:25:56 +0100 Subject: [PATCH 067/275] ApplicationLayout now has defaultExpanded --- packages/lib/src/layout/ApplicationLayout.stories.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/lib/src/layout/ApplicationLayout.stories.tsx b/packages/lib/src/layout/ApplicationLayout.stories.tsx index 9d468b65d2..6e03eba3ee 100644 --- a/packages/lib/src/layout/ApplicationLayout.stories.tsx +++ b/packages/lib/src/layout/ApplicationLayout.stories.tsx @@ -154,7 +154,9 @@ const ApplicationLayoutCustomFooter = () => ( const Tooltip = () => ( } + sidenav={ + + } >

    Main Content

    From 634f8433fce04b8cffd4ce5f590506c9477cdb24 Mon Sep 17 00:00:00 2001 From: Mil4n0r Date: Fri, 30 Jan 2026 12:39:16 +0100 Subject: [PATCH 068/275] Updated sidenav width --- packages/lib/src/sidenav/Sidenav.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/lib/src/sidenav/Sidenav.tsx b/packages/lib/src/sidenav/Sidenav.tsx index 30755426da..e73189adba 100644 --- a/packages/lib/src/sidenav/Sidenav.tsx +++ b/packages/lib/src/sidenav/Sidenav.tsx @@ -17,7 +17,6 @@ import { responsiveSizes } from "../common/variables"; const COLLAPSED_WIDTH = 56; const MIN_WIDTH = 0; const MAX_WIDTH = 320; -const DEFAULT_WIDTH = 280; const SidenavContainer = styled.div<{ expanded: boolean; @@ -113,7 +112,7 @@ const DxcSidenav = ({ const { width, sidenavRef, isResizing, startResize } = useResize({ minWidth: MIN_WIDTH, maxWidth: MAX_WIDTH, - defaultWidth: DEFAULT_WIDTH, + defaultWidth: MIN_WIDTH, }); const [internalExpanded, setInternalExpanded] = useState(defaultExpanded ?? !isBelowLarge); From ea681cc52d4fe7971bfd1896ce14daa148f69f19 Mon Sep 17 00:00:00 2001 From: Mil4n0r Date: Fri, 30 Jan 2026 12:49:30 +0100 Subject: [PATCH 069/275] Updated sidenav width --- packages/lib/src/sidenav/Sidenav.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/lib/src/sidenav/Sidenav.tsx b/packages/lib/src/sidenav/Sidenav.tsx index e73189adba..22b3fbad96 100644 --- a/packages/lib/src/sidenav/Sidenav.tsx +++ b/packages/lib/src/sidenav/Sidenav.tsx @@ -17,6 +17,7 @@ import { responsiveSizes } from "../common/variables"; const COLLAPSED_WIDTH = 56; const MIN_WIDTH = 0; const MAX_WIDTH = 320; +const DEFAULT_WIDTH = 240; const SidenavContainer = styled.div<{ expanded: boolean; @@ -112,7 +113,7 @@ const DxcSidenav = ({ const { width, sidenavRef, isResizing, startResize } = useResize({ minWidth: MIN_WIDTH, maxWidth: MAX_WIDTH, - defaultWidth: MIN_WIDTH, + defaultWidth: DEFAULT_WIDTH, }); const [internalExpanded, setInternalExpanded] = useState(defaultExpanded ?? !isBelowLarge); From 81c2a332b4cb063dcb164e51d72cc35f856ae7da Mon Sep 17 00:00:00 2001 From: Mil4n0r Date: Fri, 30 Jan 2026 13:50:00 +0100 Subject: [PATCH 070/275] Restored gap for MenuItemContainer --- packages/lib/src/base-menu/MenuItem.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/lib/src/base-menu/MenuItem.tsx b/packages/lib/src/base-menu/MenuItem.tsx index 19c87dd9ff..b70663a489 100644 --- a/packages/lib/src/base-menu/MenuItem.tsx +++ b/packages/lib/src/base-menu/MenuItem.tsx @@ -6,7 +6,7 @@ import { isGroupItem } from "./utils"; const MenuItemContainer = styled.li` display: grid; - margin-right: var(--spacing-padding-xxs); + gap: var(--spacing-gap-xs); `; export default function MenuItem({ item, depthLevel = 0 }: MenuItemProps) { From 404bc4b719e64aec65e8c75c6372949db67aef0b Mon Sep 17 00:00:00 2001 From: Mil4n0r Date: Fri, 30 Jan 2026 15:04:02 +0100 Subject: [PATCH 071/275] Fixed layout problems after conflicts --- packages/lib/src/layout/ApplicationLayout.tsx | 27 +++++++++++++++---- .../src/layout/ApplicationLayoutContext.tsx | 1 + packages/lib/src/layout/types.ts | 4 +++ packages/lib/src/sidenav/Sidenav.tsx | 14 ++++++---- 4 files changed, 36 insertions(+), 10 deletions(-) diff --git a/packages/lib/src/layout/ApplicationLayout.tsx b/packages/lib/src/layout/ApplicationLayout.tsx index 07fa66f6e2..aa4332e7a5 100644 --- a/packages/lib/src/layout/ApplicationLayout.tsx +++ b/packages/lib/src/layout/ApplicationLayout.tsx @@ -37,7 +37,7 @@ const BodyContainer = styled.div<{ hasSidenav?: boolean }>` } `; -const SidenavContainer = styled.div<{ headerHeight: string }>` +const SidenavContainer = styled.div<{ headerHeight: string; footerHeight: string }>` width: fit-content; height: 100%; position: sticky; @@ -48,6 +48,7 @@ const SidenavContainer = styled.div<{ headerHeight: string }>` @media (max-width: ${responsiveSizes.medium}rem) { width: 100%; + max-height: ${({ headerHeight, footerHeight }) => `calc(100vh - ${headerHeight || "0"} - ${footerHeight || "0"})`}; } `; @@ -67,7 +68,8 @@ const Main = ({ children }: AppLayoutMainPropsType): JSX.Element =>
    {childr const DxcApplicationLayout = ({ logo, header, sidenav, footer, children }: ApplicationLayoutPropsType): JSX.Element => { const [headerHeight, setHeaderHeight] = useState("0px"); - + const [footerHeight, setFooterHeight] = useState("0px"); + const [hideMainContent, setHideMainContent] = useState(false); const handleHeaderHeight = useCallback( (headerElement: HTMLDivElement | null) => { if (headerElement) { @@ -78,10 +80,21 @@ const DxcApplicationLayout = ({ logo, header, sidenav, footer, children }: Appli [header] ); + const handleFooterHeight = useCallback( + (footerElement: HTMLDivElement | null) => { + if (footerElement) { + const height = footerElement.offsetHeight; + setFooterHeight(`${height}px`); + } + }, + [footer] + ); + const contextValue = useMemo(() => { return { logo, headerExists: !!header, + setHideMainContent, }; }, [header, logo]); const ref = useRef(null); @@ -91,10 +104,14 @@ const DxcApplicationLayout = ({ logo, header, sidenav, footer, children }: Appli {header && {header}} - {sidenav && {sidenav}} - {findChildType(children, Main)} + {sidenav && ( + + {sidenav} + + )} + {!hideMainContent && {findChildType(children, Main)}} - + {footer ?? ( ({ logo: undefined, headerExists: false, + setHideMainContent: () => {}, }); diff --git a/packages/lib/src/layout/types.ts b/packages/lib/src/layout/types.ts index fed8502e0d..f8b1e667ac 100644 --- a/packages/lib/src/layout/types.ts +++ b/packages/lib/src/layout/types.ts @@ -47,6 +47,10 @@ export type ApplicationLayoutContextType = { * Indicates if the header exists. */ headerExists: boolean; + /** + * Allows the sidenav to hide the main content when needed + */ + setHideMainContent: (hide: boolean) => void; }; type ApplicationLayoutPropsType = { diff --git a/packages/lib/src/sidenav/Sidenav.tsx b/packages/lib/src/sidenav/Sidenav.tsx index 22b3fbad96..cd1f421d14 100644 --- a/packages/lib/src/sidenav/Sidenav.tsx +++ b/packages/lib/src/sidenav/Sidenav.tsx @@ -24,7 +24,6 @@ const SidenavContainer = styled.div<{ width: number; showBorder?: boolean; side?: "left" | "right"; - hasHeader?: boolean; }>` position: relative; box-sizing: border-box; @@ -39,8 +38,6 @@ const SidenavContainer = styled.div<{ background-color: var(--color-bg-neutral-lightest); @media (max-width: ${responsiveSizes.medium}rem) { width: 100%; - ${({ expanded, hasHeader }) => - expanded && (hasHeader ? "height: calc(100vh - var(--height-xxxl));" : "height: 100vh;")} } `; @@ -108,7 +105,7 @@ const DxcSidenav = ({ }: SidenavPropsType): JSX.Element => { const isBelowLarge = useBreakpoint("large"); const isBelowMedium = useBreakpoint("medium"); - const { logo, headerExists } = useContext(ApplicationLayoutContext); + const { logo, headerExists, setHideMainContent } = useContext(ApplicationLayoutContext); const isControlled = expanded !== undefined; const { width, sidenavRef, isResizing, startResize } = useResize({ minWidth: MIN_WIDTH, @@ -127,6 +124,14 @@ const DxcSidenav = ({ const isExpanded = isControlled ? !!expanded : internalExpanded; const shouldFocusSearchBar = useRef(false); + useEffect(() => { + if (isBelowMedium && isExpanded) { + setHideMainContent(true); + } else { + setHideMainContent(false); + } + }, [isBelowMedium, isExpanded]); + const handleToggle = () => { const nextState = !isExpanded; if (!isControlled) setInternalExpanded(nextState); @@ -148,7 +153,6 @@ const DxcSidenav = ({ showBorder={!(isBelowMedium && !isExpanded)} ref={sidenavRef} side="left" - hasHeader={headerExists} > Date: Mon, 2 Feb 2026 08:52:03 +0100 Subject: [PATCH 072/275] Fixed small problem with rows layout --- packages/lib/src/layout/ApplicationLayout.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/lib/src/layout/ApplicationLayout.tsx b/packages/lib/src/layout/ApplicationLayout.tsx index aa4332e7a5..00c32fedb9 100644 --- a/packages/lib/src/layout/ApplicationLayout.tsx +++ b/packages/lib/src/layout/ApplicationLayout.tsx @@ -32,8 +32,7 @@ const BodyContainer = styled.div<{ hasSidenav?: boolean }>` @media (max-width: ${responsiveSizes.medium}rem) { grid-template-columns: 1fr; - grid-template-rows: auto 1fr; - overflow-y: auto; + grid-template-rows: ${({ hasSidenav }) => (hasSidenav ? "auto 1fr" : "1fr")}; } `; From 0414a560ed1ddc116e4993be28a78a5f7a39ba79 Mon Sep 17 00:00:00 2001 From: Mil4n0r Date: Mon, 2 Feb 2026 10:56:53 +0100 Subject: [PATCH 073/275] Made sidenav uncontrolled for _app and fixed minor styling issues --- apps/website/pages/_app.tsx | 5 ----- packages/lib/src/sidenav/Sidenav.tsx | 8 +------- 2 files changed, 1 insertion(+), 12 deletions(-) diff --git a/apps/website/pages/_app.tsx b/apps/website/pages/_app.tsx index 8bdffef335..7fa01ce302 100644 --- a/apps/website/pages/_app.tsx +++ b/apps/website/pages/_app.tsx @@ -30,7 +30,6 @@ export default function App({ Component, pageProps, emotionCache = clientSideEmo const getLayout = Component.getLayout || ((page) => page); const componentWithLayout = getLayout(); const [filter, setFilter] = useState(""); - const [isExpanded, setIsExpanded] = useState(true); const { asPath: currentPath } = useRouter(); const matchPaths = (linkPath: string) => { @@ -109,10 +108,6 @@ export default function App({ Component, pageProps, emotionCache = clientSideEmo navItems={navItems} appTitle={} searchBar={{ placeholder: "Search docs", onChange: (value) => setFilter(value) }} - expanded={isExpanded} - onExpandedChange={() => { - setIsExpanded((currentlyExpanded) => !currentlyExpanded); - }} /> } > diff --git a/packages/lib/src/sidenav/Sidenav.tsx b/packages/lib/src/sidenav/Sidenav.tsx index cd1f421d14..867f147daa 100644 --- a/packages/lib/src/sidenav/Sidenav.tsx +++ b/packages/lib/src/sidenav/Sidenav.tsx @@ -147,13 +147,7 @@ const DxcSidenav = ({ }; return ( - + Date: Wed, 28 Jan 2026 14:07:52 +0100 Subject: [PATCH 074/275] -Autoclose submenus in sidenav when collapsed -Close submenus when clicking outside or pressing "esc" --- packages/lib/src/base-menu/GroupItem.tsx | 11 +++++------ packages/lib/src/sidenav/Sidenav.tsx | 1 + 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/lib/src/base-menu/GroupItem.tsx b/packages/lib/src/base-menu/GroupItem.tsx index ea328b4397..d96e2163dc 100644 --- a/packages/lib/src/base-menu/GroupItem.tsx +++ b/packages/lib/src/base-menu/GroupItem.tsx @@ -38,11 +38,10 @@ const GroupItem = ({ items, ...props }: GroupItemProps) => { { - event.preventDefault(); - }} - onOpenAutoFocus={(event) => { - event.preventDefault(); + onKeyDown={(event) => { + if (event.key === "Escape") { + toggleOpen(); + } }} align="start" side={isHorizontal ? "bottom" : "right"} @@ -60,7 +59,7 @@ const GroupItem = ({ items, ...props }: GroupItemProps) => { gap: "var(--spacing-gap-xxs)", }), }} - sideOffset={16} + sideOffset={isHorizontal ? 16 : 0} onInteractOutside={isHorizontal ? () => toggleOpen() : undefined} > {!isHorizontal && props.depthLevel === 0 && ( diff --git a/packages/lib/src/sidenav/Sidenav.tsx b/packages/lib/src/sidenav/Sidenav.tsx index 867f147daa..e77ada18b9 100644 --- a/packages/lib/src/sidenav/Sidenav.tsx +++ b/packages/lib/src/sidenav/Sidenav.tsx @@ -203,6 +203,7 @@ const DxcSidenav = ({ )} {navItems && ( Date: Mon, 2 Feb 2026 11:24:44 +0100 Subject: [PATCH 075/275] Handle conflicts --- packages/lib/src/base-menu/GroupItem.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/lib/src/base-menu/GroupItem.tsx b/packages/lib/src/base-menu/GroupItem.tsx index d96e2163dc..44af314288 100644 --- a/packages/lib/src/base-menu/GroupItem.tsx +++ b/packages/lib/src/base-menu/GroupItem.tsx @@ -60,7 +60,7 @@ const GroupItem = ({ items, ...props }: GroupItemProps) => { }), }} sideOffset={isHorizontal ? 16 : 0} - onInteractOutside={isHorizontal ? () => toggleOpen() : undefined} + onInteractOutside={() => toggleOpen()} > {!isHorizontal && props.depthLevel === 0 && ( Date: Mon, 2 Feb 2026 13:22:56 +0100 Subject: [PATCH 076/275] Remove footer from expanded sidenav on small devices --- packages/lib/src/layout/ApplicationLayout.tsx | 41 +++++++------------ 1 file changed, 14 insertions(+), 27 deletions(-) diff --git a/packages/lib/src/layout/ApplicationLayout.tsx b/packages/lib/src/layout/ApplicationLayout.tsx index 00c32fedb9..bbf450b6a7 100644 --- a/packages/lib/src/layout/ApplicationLayout.tsx +++ b/packages/lib/src/layout/ApplicationLayout.tsx @@ -36,7 +36,7 @@ const BodyContainer = styled.div<{ hasSidenav?: boolean }>` } `; -const SidenavContainer = styled.div<{ headerHeight: string; footerHeight: string }>` +const SidenavContainer = styled.div<{ headerHeight: string }>` width: fit-content; height: 100%; position: sticky; @@ -47,7 +47,7 @@ const SidenavContainer = styled.div<{ headerHeight: string; footerHeight: string @media (max-width: ${responsiveSizes.medium}rem) { width: 100%; - max-height: ${({ headerHeight, footerHeight }) => `calc(100vh - ${headerHeight || "0"} - ${footerHeight || "0"})`}; + max-height: ${({ headerHeight }) => `calc(100vh - ${headerHeight || "0"})`}; } `; @@ -67,7 +67,6 @@ const Main = ({ children }: AppLayoutMainPropsType): JSX.Element =>
    {childr const DxcApplicationLayout = ({ logo, header, sidenav, footer, children }: ApplicationLayoutPropsType): JSX.Element => { const [headerHeight, setHeaderHeight] = useState("0px"); - const [footerHeight, setFooterHeight] = useState("0px"); const [hideMainContent, setHideMainContent] = useState(false); const handleHeaderHeight = useCallback( (headerElement: HTMLDivElement | null) => { @@ -79,16 +78,6 @@ const DxcApplicationLayout = ({ logo, header, sidenav, footer, children }: Appli [header] ); - const handleFooterHeight = useCallback( - (footerElement: HTMLDivElement | null) => { - if (footerElement) { - const height = footerElement.offsetHeight; - setFooterHeight(`${height}px`); - } - }, - [footer] - ); - const contextValue = useMemo(() => { return { logo, @@ -103,22 +92,20 @@ const DxcApplicationLayout = ({ logo, header, sidenav, footer, children }: Appli {header && {header}} - {sidenav && ( - - {sidenav} - - )} + {sidenav && {sidenav}} {!hideMainContent && {findChildType(children, Main)}} - - {footer ?? ( - - )} - + {!hideMainContent && ( + + {footer ?? ( + + )} + + )} ); From f26a9711ca05f2166d6ad88d1579f1ec4751fcbc Mon Sep 17 00:00:00 2001 From: Mil4n0r Date: Mon, 2 Feb 2026 13:26:16 +0100 Subject: [PATCH 077/275] Fixed tokens list and doc --- .../foundations/tokens/CoreTokenList.tsx | 51 ------------------- packages/lib/src/styles/tokens.ts | 39 +++++++------- packages/lib/src/styles/variables.css | 2 +- 3 files changed, 19 insertions(+), 73 deletions(-) diff --git a/apps/website/screens/foundations/tokens/CoreTokenList.tsx b/apps/website/screens/foundations/tokens/CoreTokenList.tsx index 54c94f3c82..7076a5c521 100644 --- a/apps/website/screens/foundations/tokens/CoreTokenList.tsx +++ b/apps/website/screens/foundations/tokens/CoreTokenList.tsx @@ -7,56 +7,6 @@ import TokensTable from "./tables/TokensTable"; const sections = [ { title: "Colors", - content: ( - <> - <> - {/* Color Tokens */} - - - - - - - - - - - - - - - - - - - - - - - <> - {/* Dimensions Tokens */} - - - - <> - {/* Font Tokens */} - - - - - - - - - - - <> - {/* Border Tokens */} - - - - - ), subSections: [ { title: "Absolute", content: }, { title: "Primary", content: }, @@ -73,7 +23,6 @@ const sections = [ { title: "Dimensions", content: }, { title: "Font", - content: , subSections: [ { title: "Font size", diff --git a/packages/lib/src/styles/tokens.ts b/packages/lib/src/styles/tokens.ts index a1a657dec5..f634b6c097 100644 --- a/packages/lib/src/styles/tokens.ts +++ b/packages/lib/src/styles/tokens.ts @@ -19,9 +19,9 @@ export const coreTokens: Record = { "--z-contextualmenu": 340, /* Modals and overlays */ - "--z-spinner-overlay": 400, - "--z-progressbar-overlay": 410, - "--z-dialog": 420, + "--z-dialog": 400, + "--z-spinner-overlay": 410, + "--z-progressbar-overlay": 420, "--z-alert": 430, /* Notifications */ @@ -33,6 +33,8 @@ export const coreTokens: Record = { /************/ /** TOKENS **/ /************/ + + /* Core tokens */ "--color-absolutes-black": "#000000", "--color-absolutes-white": "#ffffff", "--color-alpha-100-a": "#ebebeb1a", @@ -167,6 +169,11 @@ export const coreTokens: Record = { }; export const aliasTokens: Record = { + /************/ + /** TOKENS **/ + /************/ + + /* Alias tokens */ "--border-color-info-lightest": "var(--color-semantic01-50)", "--border-color-info-lighter": "var(--color-semantic01-100)", "--border-color-info-light": "var(--color-semantic01-200)", @@ -299,8 +306,6 @@ export const aliasTokens: Record = { "--color-fg-secondary-strong": "var(--color-secondary-700)", "--color-fg-secondary-stronger": "var(--color-secondary-800)", "--color-fg-secondary-strongest": "var(--color-secondary-900)", - "--shadow-dark": "var(--color-alpha-400-a)", - "--shadow-light": "var(--color-alpha-300-a)", "--border-radius-none": "var(--dimensions-0)", "--border-radius-xs": "var(--dimensions-2)", "--border-radius-s": "var(--dimensions-4)", @@ -320,22 +325,14 @@ export const aliasTokens: Record = { "--height-xl": "var(--dimensions-40)", "--height-xxl": "var(--dimensions-48)", "--height-xxxl": "var(--dimensions-56)", - "--shadow-high-spread": "var(--dimensions-0)", - "--shadow-high-x-position": "var(--dimensions-0)", - "--shadow-high-blur": "var(--dimensions-24)", - "--shadow-high-y-position": "var(--dimensions-24)", - "--shadow-higher-spread": "var(--dimensions-0)", - "--shadow-higher-x-position": "var(--dimensions-0)", - "--shadow-higher-blur": "var(--dimensions-48)", - "--shadow-higher-y-position": "var(--dimensions-48)", - "--shadow-low-spread": "var(--dimensions-0)", - "--shadow-low-x-position": "var(--dimensions-0)", - "--shadow-low-blur": "var(--dimensions-2)", - "--shadow-low-y-position": "var(--dimensions-2)", - "--shadow-mid-spread": "var(--dimensions-0)", - "--shadow-mid-x-position": "var(--dimensions-0)", - "--shadow-mid-blur": "var(--dimensions-12)", - "--shadow-mid-y-position": "var(--dimensions-12)", + "--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)", "--spacing-gap-none": "var(--dimensions-0)", "--spacing-gap-xxs": "var(--dimensions-2)", "--spacing-gap-xs": "var(--dimensions-4)", diff --git a/packages/lib/src/styles/variables.css b/packages/lib/src/styles/variables.css index cf4919b5d4..d668cd7971 100644 --- a/packages/lib/src/styles/variables.css +++ b/packages/lib/src/styles/variables.css @@ -367,6 +367,7 @@ --typography-helper-text-light: var(--font-weight-light); --typography-helper-text-regular: var(--font-weight-regular); --typography-helper-text-semibold: var(--font-weight-semibold); + --typography-helper-text-italic: var(--font-style-lightitalic); --typography-label-s: var(--font-size-12); --typography-label-m: var(--font-size-14); --typography-label-l: var(--font-size-16); @@ -383,5 +384,4 @@ --border-style-default: var(--line-style-solid); --border-style-outline: var(--line-style-dashed); --typography-font-family: var(--font-family-sans); - --typography-helper-text-italic: var(--font-style-lightitalic); } From 84d10be3ddf659bab25ae69349290a1a661acf17 Mon Sep 17 00:00:00 2001 From: Mil4n0r Date: Mon, 2 Feb 2026 16:25:46 +0100 Subject: [PATCH 078/275] Added shadow to doc --- .../screens/foundations/tokens/AliasTokenList.tsx | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/apps/website/screens/foundations/tokens/AliasTokenList.tsx b/apps/website/screens/foundations/tokens/AliasTokenList.tsx index f7c16a6bde..0958801236 100644 --- a/apps/website/screens/foundations/tokens/AliasTokenList.tsx +++ b/apps/website/screens/foundations/tokens/AliasTokenList.tsx @@ -18,7 +18,6 @@ const sections = [ subSections: [ { title: "Border", content: }, { title: "Background", content: }, - { title: "Shadow", content: }, { title: "Text", content: }, ], }, @@ -26,13 +25,13 @@ const sections = [ title: "Dimensions", subSections: [ { title: "Height", content: }, - { - title: "Shadow", - content: , - }, { title: "Spacing", content: }, ], }, + { + title: "Shadow", + content: , + }, { title: "Typography", subSections: [ From 5dc8a56cff5036dc99bfefae34f1b4dfbd5bbdcf Mon Sep 17 00:00:00 2001 From: Jialecl Date: Mon, 2 Feb 2026 16:55:27 +0100 Subject: [PATCH 079/275] [doc] Updated codesandbox links to point to a specific next version --- .../application-layout/code/ApplicationLayoutCodePage.tsx | 2 +- apps/website/screens/components/footer/code/FooterCodePage.tsx | 2 +- apps/website/screens/components/header/code/HeaderCodePage.tsx | 2 +- .../website/screens/components/sidenav/code/SidenavCodePage.tsx | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/website/screens/components/application-layout/code/ApplicationLayoutCodePage.tsx b/apps/website/screens/components/application-layout/code/ApplicationLayoutCodePage.tsx index f688b33c1c..2187c25a99 100644 --- a/apps/website/screens/components/application-layout/code/ApplicationLayoutCodePage.tsx +++ b/apps/website/screens/components/application-layout/code/ApplicationLayoutCodePage.tsx @@ -122,7 +122,7 @@ const sections = [ title: "Application layout with components", content: (