Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
a87b8cf
Implement new Searchbar component into header
PelayoFelgueroso Jan 5, 2026
45473e9
Merge branch 'jialecl/searchBar' into PelayoFelgueroso/searchbar-header
PelayoFelgueroso Jan 5, 2026
2b39963
Add searchbar prop to header and add story
PelayoFelgueroso Jan 5, 2026
eb091e6
Merge branch 'PelayoFelgueroso/searchbar-header' of https://github.co…
PelayoFelgueroso Jan 5, 2026
c329168
Merge branch 'jialecl/searchBar' into PelayoFelgueroso/searchbar-header
PelayoFelgueroso Jan 5, 2026
8888e5d
Add searchBar prop to HeaderCodePage
PelayoFelgueroso Jan 5, 2026
b29c892
Merge branch 'PelayoFelgueroso/searchbar-header' of https://github.co…
PelayoFelgueroso Jan 5, 2026
2ac2a94
Update tests as needed
PelayoFelgueroso Jan 5, 2026
4ac15e7
Update storybook test
PelayoFelgueroso Jan 5, 2026
e16b37a
Enhance search bar functionality and responsiveness in Header component
PelayoFelgueroso Jan 7, 2026
f700ece
Merge branch 'jialecl/searchBar' into PelayoFelgueroso/searchbar-header
PelayoFelgueroso Jan 8, 2026
d017ba1
Remove redundant prop
PelayoFelgueroso Jan 8, 2026
da0d765
Merge branch 'PelayoFelgueroso/searchbar-header' of https://github.co…
PelayoFelgueroso Jan 8, 2026
e8940b3
Merge branch 'jialecl/searchBar' into PelayoFelgueroso/searchbar-header
PelayoFelgueroso Jan 13, 2026
ce8308c
Merge branch 'master' into PelayoFelgueroso/searchbar-header
PelayoFelgueroso Jan 13, 2026
012a7e3
Fix based on comments
PelayoFelgueroso Jan 13, 2026
5a830c7
Add open search bar story to header stories
PelayoFelgueroso Jan 14, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions apps/website/screens/components/header/code/HeaderCodePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,14 @@ const groupItemTypeString = `{
items: (Item)[];
}`;

const searchBarTypeString = `{
onBlur: (value: string) => void;
onCancel: () => void;
onChange: (value: string) => void;
onEnter: (value: string) => void;
placeholder?: string;
}`;

const sections = [
{
title: "Props",
Expand Down Expand Up @@ -91,6 +99,26 @@ 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, 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>
<tr>
<td>
<DxcFlex direction="column" gap="var(--spacing-gap-xs)" alignItems="baseline">
Expand All @@ -105,6 +133,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>
Expand Down
1 change: 1 addition & 0 deletions packages/lib/src/header/Header.accessibility.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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={() => {}} />
}
Expand Down
13 changes: 13 additions & 0 deletions packages/lib/src/header/Header.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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} />
Expand All @@ -131,6 +133,7 @@ const HeaderInLayout = () => (
<DxcApplicationLayout.Header
appTitle="Application Layout with Header"
navItems={items}
searchBar={{ placeholder: "Search..." }}
sideContent={(isResponsive) =>
isResponsive ? (
<>
Expand Down Expand Up @@ -230,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...");
},
};
36 changes: 36 additions & 0 deletions packages/lib/src/header/Header.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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();
});
});
124 changes: 86 additions & 38 deletions packages/lib/src/header/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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(() => {
Expand All @@ -157,58 +166,89 @@ const DxcHeader = ({ appTitle, navItems, sideContent, responsiveBottomContent }:
};
const sanitizedNavItems = useMemo(() => (navItems ? sanitizeNavItems(navItems) : []), [navItems]);

const handleCancelSearch = () => {
if (typeof searchBar?.onCancel === "function") {
searchBar.onCancel();
}
setShowSearch(false);
};

useEffect(() => {
setShowSearch(false);
}, [isResponsive]);

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={true}
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>
Expand All @@ -219,6 +259,14 @@ const DxcHeader = ({ appTitle, navItems, sideContent, responsiveBottomContent }:
<ResponsiveMenuContainer>
<ResponsiveMenu>
{appTitle && <DxcHeading text={appTitle} as="h1" level={5} />}
{!!searchBar && (
<DxcSearchBar
onBlur={searchBar.onBlur}
onChange={searchBar.onChange}
onEnter={searchBar.onEnter}
placeholder={searchBar.placeholder}
/>
)}
<DxcNavigationTree
items={sanitizedNavItems}
displayGroupLines={false}
Expand Down
4 changes: 4 additions & 0 deletions packages/lib/src/header/types.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,20 @@
import { ReactNode } from "react";
import { CommonItemProps, Item } from "../base-menu/types";
import { SearchBarProps } from "../search-bar/types";

type GroupItem = CommonItemProps & {
items: Item[];
};

type MainNavPropsType = (GroupItem | Item)[];

type SearchBarHeaderProps = Omit<SearchBarProps, "autoFocus" | "disabled">;

type Props = {
appTitle?: string;
navItems?: MainNavPropsType;
responsiveBottomContent?: ReactNode;
searchBar?: SearchBarHeaderProps;
sideContent?: ReactNode | ((isResponsive: boolean) => ReactNode);
};

Expand Down