diff --git a/apps/website/screens/components/header/code/HeaderCodePage.tsx b/apps/website/screens/components/header/code/HeaderCodePage.tsx index 6f45766ae..054d0a8c0 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,26 @@ const sections = [ - + + + + + searchBar + + + + {searchBarTypeString} + + + 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. +

+ In responsive mode, the search bar is displayed directly (without a trigger), and the{" "} + onCancel callback is not called. +

+ + - + @@ -105,6 +135,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. + - diff --git a/packages/lib/src/header/Header.accessibility.test.tsx b/packages/lib/src/header/Header.accessibility.test.tsx index e337488f2..e4ccced8b 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", () => { {}} /> } diff --git a/packages/lib/src/header/Header.stories.tsx b/packages/lib/src/header/Header.stories.tsx index 35d43c5a4..4eeed6ddb 100644 --- a/packages/lib/src/header/Header.stories.tsx +++ b/packages/lib/src/header/Header.stories.tsx @@ -114,6 +114,8 @@ const Header = () => ( Side Content} /> Side Content} /> + + <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} /> @@ -131,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 a1546a6d4..0b4367cbf 100644 --- a/packages/lib/src/header/Header.test.tsx +++ b/packages/lib/src/header/Header.test.tsx @@ -59,4 +59,40 @@ 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(); + }); + + 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 d175649c1..7fb78d348 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; @@ -132,9 +134,16 @@ 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); const logo = useContext(ApplicationLayoutContext).logo || undefined; useEffect(() => { @@ -157,58 +166,86 @@ 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> <DxcGrid templateColumns={ - !isResponsive && sanitizedNavItems && sanitizedNavItems.length > 0 - ? [`auto`, `minmax(auto, ${MAX_MAIN_NAV_SIZE})`, `auto`] - : ["auto", "auto"] + showSearch && !isResponsive + ? ["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> - {!isResponsive && sanitizedNavItems && sanitizedNavItems.length > 0 && ( + {(!showSearch || isResponsive) && ( + <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) || (!!searchBar && showSearch)) && ( <MainNavContainer> - <DxcNavigationTree - items={sanitizedNavItems} - displayGroupLines={false} - displayBorder={false} - displayControlsAfter - hasPopOver - isHorizontal - /> + {!!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} + displayGroupLines={false} + displayBorder={false} + displayControlsAfter + hasPopOver + isHorizontal + /> + )} </MainNavContainer> )} - {(sideContent || isResponsive) && ( + + {(!showSearch || isResponsive) && (sideContent || isResponsive || !!searchBar) && ( <RightSideContainer> + {!!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> @@ -219,6 +256,16 @@ const DxcHeader = ({ appTitle, navItems, sideContent, responsiveBottomContent }: <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} diff --git a/packages/lib/src/header/types.ts b/packages/lib/src/header/types.ts index 472004b61..dcbb33034 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); };