diff --git a/apps/website/screens/components/footer/code/FooterCodePage.tsx b/apps/website/screens/components/footer/code/FooterCodePage.tsx
index 0de8bdbb3..4dee76a7b 100644
--- a/apps/website/screens/components/footer/code/FooterCodePage.tsx
+++ b/apps/website/screens/components/footer/code/FooterCodePage.tsx
@@ -4,12 +4,22 @@ import DocFooter from "@/common/DocFooter";
import StatusBadge from "@/common/StatusBadge";
import Code, { ExtendedTableCode, TableCode } from "@/common/Code";
+const bottomLinksTypeString = `{
+ href: string;
+ text: string;
+}[]`;
+
const logoTypeString = `{
- href?: string;
src: string;
- title?: string;
+ alt: string;
}`;
+const socialLinkTypeString = `{
+ href: string;
+ title: string;
+ logo: string | SVG;
+}[]`;
+
const sections = [
{
title: "Props",
@@ -27,7 +37,7 @@ const sections = [
socialLinks
-
- {`{ href: string; title: string; logo: string | (React.ReactNode & React.SVGProps ); }[]`}
-
+ {socialLinkTypeString}
An array of objects representing the links that will be rendered as icons at the top-right side of the
@@ -148,7 +172,8 @@ const sections = [
number
- Value of the tabindex for all interactive elements, except those inside the custom area.
+ Value of the tabindex for all interactive elements, except those inside the leftContent and
+ rightContent.
0
@@ -158,29 +183,6 @@ const sections = [
),
},
- {
- title: "Examples",
- subSections: [
- {
- title: "Footer in application layout",
- content: (
-
- ),
- },
- ],
- },
];
const FooterCodePage = () => {
diff --git a/apps/website/screens/components/footer/overview/FooterOverviewPage.tsx b/apps/website/screens/components/footer/overview/FooterOverviewPage.tsx
index 4b901c89e..24edbe683 100644
--- a/apps/website/screens/components/footer/overview/FooterOverviewPage.tsx
+++ b/apps/website/screens/components/footer/overview/FooterOverviewPage.tsx
@@ -31,6 +31,10 @@ const sections = [
Logo: Represents the brand identity visually. Positioned on the left side, it helps
reinforce company recognition across all pages.
+
+ Border: Marks the upper boundary of the header to visually separate it from the main
+ content.
+
Social icons: A set of clickable icons linking to the company's social media platforms
(e.g., LinkedIn, Facebook). Placed on the right side for easy visibility and access.
@@ -43,6 +47,12 @@ const sections = [
Company links: A horizontal list of navigational hyperlinks such as Privacy Policy, Terms &
Conditions, etc. Offers users access to important legal or informational resources.
+
+ Left slot: Commonly used for short informational paragraphs or contact details.
+
+
+ Right slot: Commonly used for additional links, buttons, forms, or call to action.
+
>
),
@@ -52,21 +62,22 @@ const sections = [
content: (
<>
- To maintain consistency in layout flexibility and brand presentation, the footer offers three primary
- variants: Default , With Navigation , and Small .
+ To maintain consistency in layout flexibility and brand presentation, the footer offers two primary variants:{" "}
+ Default and Reduced .
Default: provides a balanced layout with branding and essential legal links. It offers a
clean, uncluttered appearance suitable for most standard applications.
+
+
+ Users can add content to the Default view using custom code such as content sections, text, links, and
+ other components for increased customization based on their specific needs.
+
+
- With Navigation: includes additional navigational sections, enabling users to quickly
- access key areas of the site. This layout is ideal for content-heavy pages or enterprise-level applications
- requiring enhanced footer functionality.
-
-
- Small: offers a compact version of the footer, typically limited to branding and minimal
+ Reduced: offers a compact version of the footer, typically limited to branding and minimal
legal text. It's best suited for lightweight experiences, login pages, or environments with constrained
vertical space.
@@ -87,13 +98,15 @@ const sections = [
<>
- Dock the footer to the bottom of the page: the footer should remain fixed at the bottom
- edge of the viewport and not scroll with the page content to maintain visibility and separation from dynamic
- areas.
+ Dock the footer at the bottom of the page: The footer should appear after the content at
+ the bottom page at all times. If the page content exceeds the current view and the user needs to scroll to
+ reach the bottom of the content and see the footer.
- Ensure full-width alignment: the footer container should always span the full width of the
- screen to create a clean, structured boundary and support responsive behavior across breakpoints.
+ Ensure full-width alignment: By default, the footer spans the entire width of the page
+ excluding the Sidenav. For cases when the left Sidenav is not used, the footer container should always span
+ the full width of the screen to create a clean, structured boundary and support responsive behavior across
+ breakpoints.
Display copyright information on the right: consistently place legal disclaimers or
diff --git a/apps/website/screens/components/footer/overview/images/footer_anatomy.png b/apps/website/screens/components/footer/overview/images/footer_anatomy.png
index ff389c855..ac635ef30 100644
Binary files a/apps/website/screens/components/footer/overview/images/footer_anatomy.png and b/apps/website/screens/components/footer/overview/images/footer_anatomy.png differ
diff --git a/apps/website/screens/components/footer/overview/images/footer_variants.png b/apps/website/screens/components/footer/overview/images/footer_variants.png
index 91dcef94b..59e8294f7 100644
Binary files a/apps/website/screens/components/footer/overview/images/footer_variants.png and b/apps/website/screens/components/footer/overview/images/footer_variants.png differ
diff --git a/packages/lib/src/footer/Footer.accessibility.test.tsx b/packages/lib/src/footer/Footer.accessibility.test.tsx
index e4fce1011..a6b04829c 100644
--- a/packages/lib/src/footer/Footer.accessibility.test.tsx
+++ b/packages/lib/src/footer/Footer.accessibility.test.tsx
@@ -2,6 +2,13 @@ import { render } from "@testing-library/react";
import { axe, formatRules } from "../../test/accessibility/axe-helper";
import DxcFooter from "./Footer";
import rules from "../../test/accessibility/rules/specific/footer/disabledRules";
+import { vi } from "vitest";
+
+global.ResizeObserver = vi.fn().mockImplementation(() => ({
+ observe: vi.fn(),
+ unobserve: vi.fn(),
+ disconnect: vi.fn(),
+}));
const disabledRules = {
rules: formatRules(rules),
@@ -90,22 +97,31 @@ const bottom = [
describe("Footer component accessibility tests", () => {
it("Should not have basic accessibility issues", async () => {
const { container } = render(
-
-
-
+
+
+ >
+ }
+ rightContent={
+
+ }
+ />
);
const results = await axe(container, disabledRules);
expect(results.violations).toHaveLength(0);
});
it("Should not have basic accessibility issues for reduced mode", async () => {
const { container } = render(
-
-
-
+
);
const results = await axe(container, disabledRules);
expect(results.violations).toHaveLength(0);
diff --git a/packages/lib/src/footer/Footer.stories.tsx b/packages/lib/src/footer/Footer.stories.tsx
index ea1e2bfb3..003a178fb 100644
--- a/packages/lib/src/footer/Footer.stories.tsx
+++ b/packages/lib/src/footer/Footer.stories.tsx
@@ -3,11 +3,16 @@ import Title from "../../.storybook/components/Title";
import preview from "../../.storybook/preview";
import disabledRules from "../../test/accessibility/rules/specific/footer/disabledRules";
import DxcFlex from "../flex/Flex";
-import DxcTypography from "../typography/Typography";
import DxcFooter from "./Footer";
import DxcLink from "../link/Link";
import { Meta, StoryObj } from "@storybook/react-vite";
import { userEvent, within } from "storybook/internal/test";
+import DxcParagraph from "../paragraph/Paragraph";
+import DxcHeading from "../heading/Heading";
+import DxcApplicationLayout from "../layout/ApplicationLayout";
+import DxcHeader from "../header/Header";
+import DxcBadge from "../badge/Badge";
+import DxcButton from "../button/Button";
export default {
title: "Footer",
@@ -52,10 +57,10 @@ const social = [
href: "https://x.com/dxctechnology",
logo: (
-
+
),
@@ -122,9 +127,59 @@ const bottom = [
},
];
-const info = [
- { label: "Example Label", text: "Example" },
- { label: "Example Label", text: "Example" },
+const bottomLong = [
+ {
+ href: "https://www.linkedin.com/company/dxctechnology",
+ text: "Linkedin",
+ },
+ {
+ href: "https://x.com/dxctechnology",
+ text: "X",
+ },
+ {
+ href: "https://www.facebook.com/DXCTechnology/",
+ text: "Facebook",
+ },
+ {
+ href: "https://www.facebook.com/DXCTechnology/",
+ text: "Facebook",
+ },
+ {
+ href: "https://www.facebook.com/DXCTechnology/",
+ text: "Facebook",
+ },
+ {
+ href: "https://www.facebook.com/DXCTechnology/",
+ text: "Facebook",
+ },
+ {
+ href: "https://www.facebook.com/DXCTechnology/",
+ text: "Facebook",
+ },
+ {
+ href: "https://www.facebook.com/DXCTechnology/",
+ text: "Facebook",
+ },
+ {
+ href: "https://www.facebook.com/DXCTechnology/",
+ text: "Facebook",
+ },
+ {
+ href: "https://www.facebook.com/DXCTechnology/",
+ text: "Facebook",
+ },
+ {
+ href: "https://www.facebook.com/DXCTechnology/",
+ text: "Facebook",
+ },
+ {
+ href: "https://www.facebook.com/DXCTechnology/",
+ text: "Facebook",
+ },
+ {
+ href: "https://www.facebook.com/DXCTechnology/",
+ text: "Facebook",
+ },
];
const Footer = () => (
@@ -133,62 +188,441 @@ const Footer = () => (
+
+
+
+
-
-
- Linkedin
-
-
+
+
+ Application description, version, notes, and contact details can go here for additional information
+
+
+ Contact Us: email@dxc.com
+
+ >
+ }
+ rightContent={
+ <>
+
+
+ Login / Sign-up
+ Subscribe
+ Unsubsicribe
+
+
+
+ Login / Sign-up
+ Subscribe
+ Unsubsicribe
+
+
+
+ Login / Sign-up
+ Subscribe
+ Unsubsicribe
+
+ >
+ }
+ />
+
+
+
+
+
+ Application description, version, notes, and contact details can go here for additional information.
+ Application description, version, notes, and contact details can go here for additional information
+ Application description, version, notes, and contact details can go here for additional information
+ Application description, version, notes, and contact details can go here for additional information
+ Application description, version, notes, and contact details can go here for additional information
+ Application description, version, notes, and contact details can go here for additional information
+
+
+ Contact Us: email@dxc.com
+
+ >
+ }
+ rightContent={
+ <>
+
+
+ Login / Sign-up
+ Subscribe
+ Unsubsicribe
+
+
+
+ Login / Sign-up
+ Subscribe
+ Unsubsicribe
+
+
+
+ Login / Sign-up
+ Subscribe
+ Unsubsicribe
+
+
+
+ Login / Sign-up
+ Subscribe
+ Unsubsicribe
+
+
+
+ Login / Sign-up
+ Subscribe
+ Unsubsicribe
+
+
+
+ Login / Sign-up
+ Subscribe
+ Unsubsicribe
+
+
+
+ Login / Sign-up
+ Subscribe
+ Unsubsicribe
+
+
+
+ Login / Sign-up
+ Subscribe
+ Unsubsicribe
+
+
+
+ Login / Sign-up
+ Subscribe
+ Unsubsicribe
+
+
+
+ Login / Sign-up
+ Subscribe
+ Unsubsicribe
+
+
+
+ Login / Sign-up
+ Subscribe
+ Unsubsicribe
+
+ >
+ }
+ />
-
-
- Linkedin
-
-
+
+
+ Application description, version, notes, and contact details can go here for additional information
+
+
+ Contact Us: email@dxc.com
+
+ >
+ }
+ rightContent={
+ <>
+
+
+ Login / Sign-up
+ Subscribe
+ Unsubsicribe
+
+
+
+ Login / Sign-up
+ Subscribe
+ Unsubsicribe
+
+
+
+ Login / Sign-up
+ Subscribe
+ Unsubsicribe
+
+ >
+ }
+ />
-
-
- Linkedin
-
-
+
+
+ Application description, version, notes, and contact details can go here for additional information
+
+
+ Contact Us: email@dxc.com
+
+ >
+ }
+ rightContent={
+ <>
+
+
+ Login / Sign-up
+ Subscribe
+ Unsubsicribe
+
+
+
+ Login / Sign-up
+ Subscribe
+ Unsubsicribe
+
+
+
+ Login / Sign-up
+ Subscribe
+ Unsubsicribe
+
+ >
+ }
+ />
-
-
- {info.map((tag, index) => (
-
- {tag.label}: {tag.text}
-
- ))}
-
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
>
);
+const dxcLogo = (
+
+ DXC Logo
+
+
+
+
+
+
+
+);
+
+const dxcBrandedLogo = {
+ src: dxcLogo,
+ alt: "DXC Logo",
+};
+
+const items = [
+ {
+ label: "Grouped Item 1",
+ icon: "favorite",
+ items: [
+ { label: "Item 1", icon: "person", selected: true },
+ {
+ label: "Grouped Item 2",
+ items: [
+ {
+ label: "Item 2",
+ icon: "bookmark",
+ badge: ,
+ },
+ { label: "Selected Item 3" },
+ ],
+ },
+ ],
+ badge: ,
+ },
+ { label: "Item 4", icon: "key" },
+ { label: "Item 5", icon: "person" },
+ { label: "Grouped Item 6", items: [{ label: "Item 7", icon: "person" }, { label: "Item 8" }] },
+ { label: "Item 9" },
+];
+
+const FooterInLayout = () => (
+
+ isResponsive ? (
+ <>
+
+ >
+ ) : (
+ <>
+
+
+
+ >
+ )
+ }
+ responsiveBottomContent={
+ <>
+
+
+ >
+ }
+ />
+ }
+ sidenav={ }
+ >
+
+
+ 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.
+
+
+
+);
+
+const ReducedFooterInLayout = () => (
+
+ isResponsive ? (
+ <>
+
+ >
+ ) : (
+ <>
+
+
+
+ >
+ )
+ }
+ responsiveBottomContent={
+ <>
+
+
+ >
+ }
+ />
+ }
+ sidenav={ }
+ footer={ }
+ >
+
+
+ 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.
+
+
+
+);
+
const Tooltip = () => {
return (
@@ -204,6 +638,24 @@ export const Chromatic: Story = {
render: Footer,
};
+export const Responsive: Story = {
+ render: Footer,
+ parameters: {
+ chromatic: { viewports: [375] },
+ },
+ globals: {
+ viewport: { value: "iphonex", isRotated: false },
+ },
+};
+
+export const InLayout: Story = {
+ render: FooterInLayout,
+};
+
+export const ReducedInLayout: Story = {
+ render: ReducedFooterInLayout,
+};
+
export const FooterTooltipFirst: Story = {
render: Tooltip,
play: async ({ canvasElement }) => {
diff --git a/packages/lib/src/footer/Footer.test.tsx b/packages/lib/src/footer/Footer.test.tsx
index e0a615d68..2cb34a346 100644
--- a/packages/lib/src/footer/Footer.test.tsx
+++ b/packages/lib/src/footer/Footer.test.tsx
@@ -1,5 +1,13 @@
import { render } from "@testing-library/react";
import DxcFooter from "./Footer";
+import { getContrastColor } from "./utils";
+
+// Mock ResizeObserver
+global.ResizeObserver = jest.fn().mockImplementation(() => ({
+ observe: jest.fn(),
+ unobserve: jest.fn(),
+ disconnect: jest.fn(),
+}));
const social = [
{
@@ -27,40 +35,52 @@ describe("Footer component tests", () => {
});
test("Footer renders with bottom links", () => {
const { getByText } = render( );
- const link = getByText("bottom-link-text");
- expect(link.getAttribute("href")).toBe("https://www.test.com/bottom");
+ const link = getByText("bottom-link-text").closest("a");
+ expect(link?.getAttribute("href")).toBe("https://www.test.com/bottom");
});
test("Footer renders with copyright text", () => {
const { getByText } = render( );
expect(getByText("test-copyright")).toBeTruthy();
});
- test("Footer renders with correct children", () => {
+ test("Footer renders LeftContent correctly", () => {
// We need to force the offsetWidth value
Object.defineProperty(HTMLElement.prototype, "offsetWidth", {
configurable: true,
value: 1024,
});
- const { getByText } = render(
-
- footer-child-text
-
- );
- expect(getByText("footer-child-text")).toBeTruthy();
+ const { getByText } = render(footer-left-text} />);
+ expect(getByText("footer-left-text")).toBeTruthy();
});
- test("Footer renders with children in mobile", () => {
+ test("Footer renders RightContent correctly", () => {
+ // We need to force the offsetWidth value
+ Object.defineProperty(HTMLElement.prototype, "offsetWidth", {
+ configurable: true,
+ value: 1024,
+ });
+ const { getByText } = render(footer-right-text} />);
+ expect(getByText("footer-right-text")).toBeTruthy();
+ });
+ test("Footer renders LeftContent in mobile", () => {
// 425 is mobile width
Object.defineProperty(HTMLElement.prototype, "offsetWidth", {
configurable: true,
value: 425,
});
- const { queryByText } = render(
-
- footer-child-text
-
- );
+ const { queryByText } = render(footer-left-text} />);
+
+ expect(queryByText("footer-left-text")).toBeTruthy();
+ });
+ test("Footer renders RightContent in mobile", () => {
+ // 425 is mobile width
+ Object.defineProperty(HTMLElement.prototype, "offsetWidth", {
+ configurable: true,
+ value: 425,
+ });
+
+ const { queryByText } = render(footer-right-text} />);
- expect(queryByText("footer-child-text")).toBeTruthy();
+ expect(queryByText("footer-right-text")).toBeTruthy();
});
test("Footer is fully rendered", () => {
Object.defineProperty(HTMLElement.prototype, "offsetWidth", {
@@ -69,16 +89,37 @@ describe("Footer component tests", () => {
});
const { getAllByRole, getByText } = render(
-
- footer-child-text
-
+ footer-left-text}
+ rightContent={footer-right-text
}
+ />
);
const socialIcon = getAllByRole("link")[0];
expect(socialIcon?.getAttribute("href")).toBe("https://www.test.com/social");
expect(socialIcon?.getAttribute("aria-label")).toBe("test");
- const bottomLink = getByText("bottom-link-text");
- expect(bottomLink.getAttribute("href")).toBe("https://www.test.com/bottom");
+ const bottomLink = getByText("bottom-link-text").closest("a");
+ expect(bottomLink?.getAttribute("href")).toBe("https://www.test.com/bottom");
expect(getByText("test-copyright")).toBeTruthy();
- expect(getByText("footer-child-text")).toBeTruthy();
+ expect(getByText("footer-left-text")).toBeTruthy();
+ expect(getByText("footer-right-text")).toBeTruthy();
+ });
+});
+
+describe("getContrastColor function", () => {
+ test("should return black color for light backgrounds", () => {
+ expect(getContrastColor("#FFFFFF")).toBe("var(--color-fg-neutral-dark)");
+ expect(getContrastColor("#F5F5F5")).toBe("var(--color-fg-neutral-dark)");
+ expect(getContrastColor("rgb(255, 255, 255)")).toBe("var(--color-fg-neutral-dark)");
+ expect(getContrastColor("rgb(245, 245, 245)")).toBe("var(--color-fg-neutral-dark)");
+ });
+
+ test("should return white color for dark backgrounds", () => {
+ expect(getContrastColor("#000000")).toBe("var(--color-fg-neutral-bright)");
+ expect(getContrastColor("#333333")).toBe("var(--color-fg-neutral-bright)");
+ expect(getContrastColor("rgb(0, 0, 0)")).toBe("var(--color-fg-neutral-bright)");
+ expect(getContrastColor("rgb(51, 51, 51)")).toBe("var(--color-fg-neutral-bright)");
});
});
diff --git a/packages/lib/src/footer/Footer.tsx b/packages/lib/src/footer/Footer.tsx
index 42019cc08..8029b93b2 100644
--- a/packages/lib/src/footer/Footer.tsx
+++ b/packages/lib/src/footer/Footer.tsx
@@ -1,208 +1,296 @@
-import { useContext } from "react";
+import { ReactNode, useContext, useEffect, useMemo, useRef, useState } from "react";
import styled from "@emotion/styled";
-import { responsiveSizes, spaces } from "../common/variables";
-import DxcFlex from "../flex/Flex";
import DxcIcon from "../icon/Icon";
import { Tooltip } from "../tooltip/Tooltip";
import { dxcLogo, dxcSmallLogo } from "./Icons";
import FooterPropsType from "./types";
import { HalstackLanguageContext } from "../HalstackContext";
+import { getContrastColor, getResponsiveStyles } from "./utils";
+import DxcLink from "../link/Link";
+import useWidth from "../utils/useWidth";
+import { css } from "@emotion/react";
const FooterContainer = styled.footer<{
- margin: FooterPropsType["margin"];
mode?: FooterPropsType["mode"];
}>`
- background-color: var(--color-bg-neutral-strongest);
box-sizing: border-box;
display: flex;
flex-direction: ${(props) => (props?.mode === "default" ? "column" : "row")};
justify-content: space-between;
- margin-top: ${(props) => (props.margin ? spaces[props.margin] : "var(--spacing-padding-none)")};
- min-height: ${(props) => (props?.mode === "default" ? "124px" : "40px")};
width: 100%;
- gap: var(--spacing-gap-m);
- padding: ${(props) =>
- props?.mode === "default"
- ? "var(--spacing-padding-m) var(--spacing-padding-xl)"
- : "var(--spacing-padding-s) var(--spacing-padding-xl)"};
- @media (max-width: ${responsiveSizes.medium}rem) {
- padding: var(--spacing-padding-l) var(--spacing-padding-ml);
- }
- @media (max-width: ${responsiveSizes.small}rem) {
- flex-direction: column;
- }
`;
-const BottomContainer = styled.div`
- display: flex;
- justify-content: space-between;
- align-items: flex-end;
-
- @media (min-width: ${responsiveSizes.small}rem) {
- flex-direction: row;
- }
- @media (max-width: ${responsiveSizes.small}rem) {
- flex-direction: column;
- align-items: center;
- }
-
- border-top: var(--border-width-s) var(--border-style-default) var(--border-color-primary-medium);
- margin-top: var(--spacing-gap-m);
-`;
+const MainContainer = styled.div<{ width: number }>`
+ display: grid;
+ grid-template-columns: 1fr 2fr;
+ min-height: 80px;
+ border-top: var(--border-width-s) var(--border-style-default) var(--border-color-neutral-lighter);
-const ChildComponents = styled.div`
- min-height: var(--height-xxs);
- color: var(--color-fg-neutral-bright);
+ ${(props) =>
+ getResponsiveStyles.isMediumScreen(props.width) &&
+ css`
+ min-height: auto;
+ grid-template-columns: 1fr;
+ grid-template-rows: auto auto;
+ gap: var(--spacing-gap-ml);
+ padding: var(--spacing-padding-l) var(--spacing-padding-m);
+ `}
`;
-const Copyright = styled.div`
- margin-top: var(--spacing-padding-xs);
- font-family: var(--typography-font-family);
- font-size: var(--typography-label-s);
- font-weight: var(--typography-label-regular);
- color: var(--color-fg-neutral-bright);
-
- @media (min-width: ${responsiveSizes.small}rem) {
- max-width: 40%;
- text-align: right;
- }
+const LeftContainer = styled.div<{ width: number }>`
+ display: flex;
+ flex-direction: column;
+ align-items: flex-start;
+ gap: var(--spacing-gap-ml);
+ color: var(--color-fg-neutral-dark);
+ box-sizing: border-box;
+ padding: var(--spacing-padding-l) var(--spacing-padding-xl);
- @media (max-width: ${responsiveSizes.small}rem) {
- max-width: 100%;
- width: 100%;
- text-align: left;
- }
+ ${(props) =>
+ getResponsiveStyles.isMediumScreen(props.width) &&
+ css`
+ padding: var(--spacing-padding-none);
+ `}
`;
const LogoContainer = styled.span<{ mode?: FooterPropsType["mode"] }>`
max-height: ${(props) => (props?.mode === "default" ? "var(--height-m)" : "var(--height-xxs)")};
- width: auto;
+ width: fit-content;
+ text-align: center;
+
+ ${(props) =>
+ props.mode === "reduced" &&
+ css`
+ display: flex;
+ justify-content: flex-start;
+ align-items: center;
+ `}
`;
-const LogoImg = styled.img<{ mode?: FooterPropsType["mode"] }>`
- max-height: ${(props) => (props?.mode === "default" ? "var(--height-m)" : "var(--height-xxs)")};
+const LogoImg = styled.img<{ mode: FooterPropsType["mode"] }>`
+ max-height: ${(props) => (props.mode === "default" ? "var(--height-m)" : "var(--height-xxs)")};
width: auto;
`;
-const SocialAnchor = styled.a<{ index: number }>`
+const RightContainer = styled.div<{ width: number }>`
+ display: flex;
+ justify-content: flex-end;
+ flex-wrap: wrap;
+ gap: var(--spacing-gap-xl);
+ box-sizing: border-box;
+ padding: var(--spacing-padding-l) var(--spacing-padding-xl);
+
+ ${(props) =>
+ getResponsiveStyles.isMediumScreen(props.width) &&
+ css`
+ justify-content: flex-start;
+ padding: var(--spacing-padding-none);
+ `}
+`;
+const SocialLinks = styled.div`
+ height: var(--height-m);
+ display: flex;
+ align-items: center;
+ gap: var(--spacing-gap-ml);
+`;
+
+const SocialAnchor = styled.a`
+ height: var(--height-s);
+ aspect-ratio: 1 / 1;
border-radius: var(--border-radius-s);
+ display: flex;
+ justify-content: center;
+ align-items: center;
- &:focus {
+ &:focus,
+ &:focus-visible {
outline: var(--border-width-m) var(--border-style-default) var(--border-color-secondary-medium);
- outline-offset: var(--border-width-m);
+ outline-offset: 0px;
}
`;
const SocialIconContainer = styled.div`
display: flex;
align-items: center;
- color: var(--color-fg-neutral-bright);
+ color: var(--color-fg-primary-strong);
overflow: hidden;
- font-size: var(--height-s);
+ font-size: var(--height-xs);
svg {
- height: var(--height-s);
- width: 24px;
+ height: var(--height-xs);
+ width: var(--height-xs);
}
`;
-const BottomLinks = styled.div`
- display: inline-flex;
- flex-wrap: wrap;
- align-self: center;
- margin-top: var(--spacing-padding-xs);
- color: var(--color-fg-neutral-bright);
+const BottomContainer = styled.div<{ textColor: string; width: number }>`
+ width: 100%;
+ min-height: var(--height-xl);
+ display: grid;
+ grid-template-columns: 60% var(--spacing-gap-ml) 1fr;
+ align-items: center;
+ background-color: var(--color-bg-primary-strong);
+ color: ${({ textColor }) => textColor};
+ padding: var(--spacing-padding-none) var(--spacing-padding-xl);
+ box-sizing: border-box;
- @media (min-width: ${responsiveSizes.small}rem) {
- max-width: 60%;
- }
- @media (max-width: ${responsiveSizes.small}rem) {
- max-width: 100%;
- width: 100%;
+ ${(props) =>
+ getResponsiveStyles.isMediumScreen(props.width) &&
+ css`
+ grid-template-columns: 1fr;
+ gap: var(--spacing-gap-ml);
+ padding: var(--spacing-padding-m);
+ `}
+`;
+
+const BottomLinks = styled.div<{ textColor: string; width: number }>`
+ height: 100%;
+ display: flex;
+ align-items: center;
+ flex-wrap: nowrap;
+ overflow: hidden;
+ box-sizing: border-box;
+ padding-left: var(--spacing-padding-xxxs);
+ font-family: var(--typography-font-family);
+ font-size: var(--typography-body-s);
+ font-weight: var(--typography-label-regular);
+
+ ${(props) =>
+ getResponsiveStyles.isMediumScreen(props.width) &&
+ css`
+ max-width: 100%;
+ width: 100%;
+ flex-wrap: wrap;
+ overflow: visible;
+ padding-left: var(--spacing-padding-none);
+ `}
+
+ & > span {
+ white-space: nowrap;
}
& > span:not(:first-child):before {
content: "ยท";
padding: var(--spacing-padding-none) var(--spacing-padding-xs);
}
+
+ & > span > a > span:hover,
+ & > span > a > span:active {
+ color: ${({ textColor }) => textColor};
+ }
`;
-const BottomLink = styled.a`
- text-decoration: none;
- border-radius: var(--border-radius-xs);
+const Copyright = styled.div<{ width: number }>`
font-family: var(--typography-font-family);
- font-size: var(--typography-label-m);
+ font-size: var(--typography-label-s);
font-weight: var(--typography-label-regular);
- color: var(--color-fg-neutral-bright);
+ white-space: nowrap;
+ display: flex;
+ justify-content: flex-end;
+ width: 100%;
+ box-sizing: border-box;
+ overflow: hidden;
+ grid-column-start: 3;
- &:focus {
- outline: var(--border-width-m) var(--border-style-default) var(--border-color-secondary-medium);
- }
+ ${(props) =>
+ getResponsiveStyles.isMediumScreen(props.width) &&
+ css`
+ justify-content: flex-start;
+ white-space: wrap;
+ grid-column-start: 1;
+ `}
`;
-const getLogoElement = (mode: FooterPropsType["mode"], logo?: FooterPropsType["logo"]) => {
- if (logo) {
- return ;
- } else {
- return mode === "default" ? dxcLogo : dxcSmallLogo;
- }
-};
-
const DxcFooter = ({
bottomLinks,
- children,
copyright,
+ leftContent,
logo,
- margin,
mode = "default",
+ rightContent,
socialLinks,
tabIndex = 0,
}: FooterPropsType): JSX.Element => {
const translatedLabels = useContext(HalstackLanguageContext);
- const footerLogo = getLogoElement(mode, logo);
+ const footerLogo = useMemo(() => {
+ if (logo) {
+ return ;
+ } else {
+ return mode === "default" ? dxcLogo : dxcSmallLogo;
+ }
+ }, [mode, logo]);
+
+ const footerRef = useRef(null);
+ const width = useWidth(footerRef);
+
+ const bottomContainerRef = useRef(null);
+ const [textColor, setTextColor] = useState("var(--color-fg-neutral-bright)");
+
+ useEffect(() => {
+ if (bottomContainerRef.current) {
+ const bg = window.getComputedStyle(bottomContainerRef.current).getPropertyValue("background-color").trim();
+ setTextColor(getContrastColor(bg));
+ }
+ }, []);
return (
-
-
- {footerLogo}
- {mode === "default" && (
-
- {socialLinks?.map((link, index) => (
-
-
-
- {typeof link.logo === "string" ? : link.logo}
-
-
-
- ))}
-
- )}
-
- {children}
+
{mode === "default" && (
-
-
+
+
+ {footerLogo}
+ {leftContent}
+
+ {(socialLinks || rightContent) && (
+
+ {rightContent}
+ {socialLinks && (
+
+ {socialLinks?.map((link, index) => (
+
+
+
+ {typeof link.logo === "string" ? : link.logo}
+
+
+
+ ))}
+
+ )}
+
+ )}
+
+ )}
+
+ {mode === "default" && bottomLinks && (
+
{bottomLinks?.map((link, index) => (
-
+
{link.text}
-
+
))}
- {copyright ?? translatedLabels.footer.copyrightText(new Date().getFullYear())}
-
- )}
+ )}
+ {mode === "reduced" && {footerLogo} }
+
+ {copyright ?? translatedLabels.footer.copyrightText(new Date().getFullYear())}
+
+
);
};
+const LeftContent = ({ children }: { children: ReactNode }) => <>{children}>;
+const RightContent = ({ children }: { children: ReactNode }) => <>{children}>;
+
+DxcFooter.LeftContent = LeftContent;
+DxcFooter.RightContent = RightContent;
export default DxcFooter;
diff --git a/packages/lib/src/footer/Icons.tsx b/packages/lib/src/footer/Icons.tsx
index 6eb3b1bf4..718c39d8b 100644
--- a/packages/lib/src/footer/Icons.tsx
+++ b/packages/lib/src/footer/Icons.tsx
@@ -5,57 +5,57 @@ export const dxcLogo = (
diff --git a/packages/lib/src/footer/types.ts b/packages/lib/src/footer/types.ts
index 881324c5d..2706fa804 100644
--- a/packages/lib/src/footer/types.ts
+++ b/packages/lib/src/footer/types.ts
@@ -1,5 +1,5 @@
import { ReactNode } from "react";
-import { SVG, Space } from "../common/utils";
+import { SVG } from "../common/utils";
type SocialLink = {
/**
@@ -28,10 +28,6 @@ type BottomLink = {
};
type Logo = {
- /**
- * URL to navigate when the logo is clicked.
- */
- href?: string;
/**
* Source of the logo image.
*/
@@ -39,7 +35,7 @@ type Logo = {
/**
* Alternative text for the logo image.
*/
- title?: string;
+ alt: string;
};
type FooterPropsType = {
@@ -48,29 +44,28 @@ type FooterPropsType = {
* the bottom part of the footer.
*/
bottomLinks?: BottomLink[];
- /**
- * The center section of the footer. Can be used to render custom
- * content in this area.
- */
- children?: ReactNode;
/**
* The text that will be displayed as copyright disclaimer.
*/
copyright?: string;
/**
- * Logo to be displayed inside the footer
+ * Content to be displayed on the left side of the footer under the logo.
*/
- logo?: Logo;
+ leftContent?: ReactNode;
/**
- * Size of the top margin to be applied to the footer.
+ * Logo to be displayed inside the footer
*/
- margin?: Space;
+ logo?: Logo;
/**
* Determines the visual style and layout
* - "default": The default mode with full content and styling.
* - "reduced": A reduced mode with minimal content and styling.
*/
mode?: "default" | "reduced";
+ /**
+ * Content to be displayed on the right side of the footer before the socialLinks if provided.
+ */
+ rightContent?: ReactNode;
/**
* An array of objects representing the links that will be rendered as
* icons at the top-right side of the footer.
@@ -78,7 +73,7 @@ type FooterPropsType = {
socialLinks?: SocialLink[];
/**
* Value of the tabindex for all interactive elements, except those
- * inside the custom area.
+ * inside the leftContent and rightContent.
*/
tabIndex?: number;
};
diff --git a/packages/lib/src/footer/utils.ts b/packages/lib/src/footer/utils.ts
new file mode 100644
index 000000000..56c9f6d6e
--- /dev/null
+++ b/packages/lib/src/footer/utils.ts
@@ -0,0 +1,40 @@
+import { responsiveSizes } from "../common/variables";
+
+const rgbToHex = (color: string): string => {
+ const rgbMatch = color.match(/\d+/g);
+ if (!rgbMatch || rgbMatch.length < 3) return "#000000";
+ const [r, g, b] = rgbMatch.map(Number);
+ return (
+ "#" +
+ [r, g, b]
+ .map((x) => x!.toString(16).padStart(2, "0"))
+ .join("")
+ .toUpperCase()
+ );
+};
+
+function getLuminance(color: string): number {
+ const hex = color.startsWith("rgb") ? rgbToHex(color) : color;
+ const match = hex.replace("#", "").match(/.{2}/g);
+ if (!match || match.length < 3) return 0;
+
+ const rgb = match
+ .map((x) => parseInt(x, 16) / 255)
+ .map((v) => (v <= 0.03928 ? v / 12.92 : ((v + 0.055) / 1.055) ** 2.4));
+
+ const [r, g, b] = rgb;
+ if (r == null || g == null || b == null) return 0;
+
+ return 0.2126 * r + 0.7152 * g + 0.0722 * b;
+}
+
+export const getContrastColor = (bgColor: string) => {
+ const luminance = getLuminance(bgColor);
+ return luminance > 0.179 ? "var(--color-fg-neutral-dark)" : "var(--color-fg-neutral-bright)";
+};
+
+export const getResponsiveStyles = {
+ isSmallScreen: (width: number) => width <= Number(responsiveSizes.small) * 16,
+ isMediumScreen: (width: number) => width <= Number(responsiveSizes.medium) * 16,
+ isLargeScreen: (width: number) => width >= Number(responsiveSizes.medium) * 16,
+};
diff --git a/packages/lib/src/layout/Icons.tsx b/packages/lib/src/layout/Icons.tsx
index 93c6c2c2a..b9ff60f7a 100644
--- a/packages/lib/src/layout/Icons.tsx
+++ b/packages/lib/src/layout/Icons.tsx
@@ -7,7 +7,7 @@ const layoutIcons = {
width="438.536px"
height="438.536px"
viewBox="0 0 438.536 438.536"
- fill="#FFFFFF"
+ fill="currentColor"
>
-
+
),
@@ -38,7 +38,7 @@ const layoutIcons = {
width="438.536px"
height="438.536px"
viewBox="0 0 438.536 438.536"
- fill="#FFFFFF"
+ fill="currentColor"
>