Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
31 changes: 31 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,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",
Expand Down Expand Up @@ -91,6 +101,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 +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.
</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
3 changes: 3 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
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();
});
});
123 changes: 85 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,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>
Expand All @@ -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}
Expand Down
2 changes: 2 additions & 0 deletions packages/lib/src/header/types.ts
Original file line number Diff line number Diff line change
@@ -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[];
Expand All @@ -11,6 +12,7 @@ type Props = {
appTitle?: string;
navItems?: MainNavPropsType;
responsiveBottomContent?: ReactNode;
searchBar?: SearchBarProps;
sideContent?: ReactNode | ((isResponsive: boolean) => ReactNode);
};

Expand Down