Skip to content

Commit fc73d3b

Browse files
authored
Merge pull request #2375 from dxc-technology/PelayoFelgueroso/searchbar-header
[minor] Implement new Searchbar component into Header
2 parents 0c59a10 + 5a830c7 commit fc73d3b

File tree

6 files changed

+169
-38
lines changed

6 files changed

+169
-38
lines changed

apps/website/screens/components/header/code/HeaderCodePage.tsx

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,14 @@ const groupItemTypeString = `{
2121
items: (Item)[];
2222
}`;
2323

24+
const searchBarTypeString = `{
25+
onBlur: (value: string) => void;
26+
onCancel: () => void;
27+
onChange: (value: string) => void;
28+
onEnter: (value: string) => void;
29+
placeholder?: string;
30+
}`;
31+
2432
const sections = [
2533
{
2634
title: "Props",
@@ -91,6 +99,26 @@ const sections = [
9199
</td>
92100
<td>-</td>
93101
</tr>
102+
<tr>
103+
<td>
104+
<DxcFlex direction="column" gap="var(--spacing-gap-xs)" alignItems="baseline">
105+
<StatusBadge status="new" />
106+
searchBar
107+
</DxcFlex>
108+
</td>
109+
<td>
110+
<ExtendedTableCode>{searchBarTypeString}</ExtendedTableCode>
111+
</td>
112+
<td>
113+
When provided, a search bar trigger is shown at the start of the side content. Activating the trigger
114+
expands the search bar, enabling search interactions.
115+
<p>
116+
In responsive mode, the search bar is displayed directly (without a trigger), and the{" "}
117+
<Code>onCancel</Code> callback is not called.
118+
</p>
119+
</td>
120+
<td>-</td>
121+
</tr>
94122
<tr>
95123
<td>
96124
<DxcFlex direction="column" gap="var(--spacing-gap-xs)" alignItems="baseline">
@@ -105,6 +133,7 @@ const sections = [
105133
Content to be displayed on the right side of the header. It can be a ReactNode or a function that receives
106134
a boolean indicating if the header is in responsive mode and returns a ReactNode.
107135
</td>
136+
<td>-</td>
108137
</tr>
109138
</tbody>
110139
</DxcTable>

packages/lib/src/header/Header.accessibility.test.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ describe("Header component accessibility tests", () => {
5959
<DxcHeader
6060
appTitle={appTitle}
6161
navItems={items}
62+
searchBar={{ placeholder: "Search" }}
6263
sideContent={
6364
<DxcButton title="Settings" icon="settings" mode="tertiary" size={{ height: "medium" }} onClick={() => {}} />
6465
}

packages/lib/src/header/Header.stories.tsx

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,8 @@ const Header = () => (
114114
<DxcHeader />
115115
<DxcHeader sideContent={<div>Side Content</div>} />
116116
<DxcHeader navItems={items} sideContent={<div>Side Content</div>} />
117+
<Title title="Header with searchbar" theme="light" level={3} />
118+
<DxcHeader navItems={items} sideContent={<div>Side Content</div>} searchBar={{ placeholder: "Search..." }} />
117119
<Title title="Header with long content" theme="light" level={3} />
118120
<DxcHeader navItems={items} sideContent={<div>{longSideContent}</div>} />
119121
<DxcHeader appTitle={longAppTitle} navItems={items} />
@@ -131,6 +133,7 @@ const HeaderInLayout = () => (
131133
<DxcApplicationLayout.Header
132134
appTitle="Application Layout with Header"
133135
navItems={items}
136+
searchBar={{ placeholder: "Search..." }}
134137
sideContent={(isResponsive) =>
135138
isResponsive ? (
136139
<>
@@ -230,3 +233,13 @@ export const Responsive: Story = {
230233
await canvas.findByText("Bottom content button");
231234
},
232235
};
236+
237+
export const OpenSearchBar: Story = {
238+
render: HeaderInLayout,
239+
play: async ({ canvasElement }) => {
240+
const canvas = within(canvasElement);
241+
const searchButton = canvas.getByRole("button", { name: "Search" });
242+
await userEvent.click(searchButton);
243+
await canvas.findByPlaceholderText("Search...");
244+
},
245+
};

packages/lib/src/header/Header.test.tsx

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,4 +59,40 @@ describe("Header component tests", () => {
5959
expect(screen.queryByText("Frontend")).not.toBeInTheDocument();
6060
expect(screen.queryByText("Backend")).not.toBeInTheDocument();
6161
});
62+
63+
test("search bar appears and functions correctly", () => {
64+
const onEnterMock = jest.fn();
65+
const onCancelMock = jest.fn();
66+
67+
render(<DxcHeader searchBar={{ placeholder: "Search...", onEnter: onEnterMock, onCancel: onCancelMock }} />);
68+
69+
const searchIcon = screen.getByRole("button", { name: /search/i });
70+
fireEvent.click(searchIcon);
71+
const searchInput = screen.getByPlaceholderText("Search...");
72+
expect(searchInput).toBeInTheDocument();
73+
fireEvent.change(searchInput, { target: { value: "test query" } });
74+
fireEvent.keyDown(searchInput, { key: "Enter", code: "Enter" });
75+
expect(onEnterMock).toHaveBeenCalledWith("test query");
76+
const cancelButton = screen.getByRole("button", { name: /cancel/i });
77+
fireEvent.click(cancelButton);
78+
expect(onCancelMock).toHaveBeenCalled();
79+
expect(searchInput).not.toBeInTheDocument();
80+
});
81+
82+
test("search bar appears correctly in responsive mode", () => {
83+
mockMatchMedia.mockImplementation(() => ({
84+
matches: true,
85+
addEventListener: jest.fn(),
86+
removeEventListener: jest.fn(),
87+
}));
88+
89+
render(<DxcHeader searchBar={{ placeholder: "Search..." }} />);
90+
91+
const menuButton = screen.getByRole("button", { name: /toggle menu/i });
92+
fireEvent.click(menuButton);
93+
const searchInput = screen.getByPlaceholderText("Search...");
94+
expect(searchInput).toBeInTheDocument();
95+
const cancelButton = screen.queryByRole("button", { name: /cancel/i });
96+
expect(cancelButton).not.toBeInTheDocument();
97+
});
6298
});

packages/lib/src/header/Header.tsx

Lines changed: 86 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ import { responsiveSizes } from "../common/variables";
1212
import DxcButton from "../button/Button";
1313
import scrollbarStyles from "../styles/scroll";
1414
import ApplicationLayoutContext from "../layout/ApplicationLayoutContext";
15+
import DxcSearchBarTrigger from "../search-bar/SearchBarTrigger";
16+
import DxcSearchBar from "../search-bar/SearchBar";
1517

1618
const MAX_MAIN_NAV_SIZE = "60%";
1719
const LEVEL_LIMIT = 1;
@@ -132,9 +134,16 @@ const sanitizeNavItems = (navItems: HeaderProps["navItems"], level?: number): (G
132134
return sanitizedItems;
133135
};
134136

135-
const DxcHeader = ({ appTitle, navItems, sideContent, responsiveBottomContent }: HeaderProps): JSX.Element => {
137+
const DxcHeader = ({
138+
appTitle,
139+
navItems,
140+
responsiveBottomContent,
141+
searchBar,
142+
sideContent,
143+
}: HeaderProps): JSX.Element => {
136144
const [isResponsive, setIsResponsive] = useState(false);
137145
const [isMenuVisible, setIsMenuVisible] = useState(false);
146+
const [showSearch, setShowSearch] = useState(false);
138147
const logo = useContext(ApplicationLayoutContext).logo || undefined;
139148

140149
useEffect(() => {
@@ -157,58 +166,89 @@ const DxcHeader = ({ appTitle, navItems, sideContent, responsiveBottomContent }:
157166
};
158167
const sanitizedNavItems = useMemo(() => (navItems ? sanitizeNavItems(navItems) : []), [navItems]);
159168

169+
const handleCancelSearch = () => {
170+
if (typeof searchBar?.onCancel === "function") {
171+
searchBar.onCancel();
172+
}
173+
setShowSearch(false);
174+
};
175+
176+
useEffect(() => {
177+
setShowSearch(false);
178+
}, [isResponsive]);
179+
160180
return (
161181
<MainContainer isResponsive={isResponsive} isMenuVisible={isMenuVisible}>
162182
<HeaderContainer>
163183
<DxcGrid
164184
templateColumns={
165-
!isResponsive && sanitizedNavItems && sanitizedNavItems.length > 0
166-
? [`auto`, `minmax(auto, ${MAX_MAIN_NAV_SIZE})`, `auto`]
167-
: ["auto", "auto"]
185+
showSearch && !isResponsive
186+
? ["auto"]
187+
: !isResponsive && sanitizedNavItems && sanitizedNavItems.length > 0
188+
? [`auto`, `minmax(auto, ${MAX_MAIN_NAV_SIZE})`, `auto`]
189+
: ["auto", "auto"]
168190
}
169191
templateRows={["var(--height-xxxl)"]}
170192
gap="var(--spacing-gap-ml)"
171193
placeItems="center"
172194
>
173-
<BrandingContainer>
174-
{logo && (
175-
<LogoContainer
176-
role={logo.onClick ? "button" : undefined}
177-
onClick={typeof logo.onClick === "function" ? logo.onClick : undefined}
178-
as={logo.href ? "a" : undefined}
179-
href={logo.href}
180-
hasAction={!!(logo.onClick || logo.href)}
181-
>
182-
{typeof logo.src === "string" ? (
183-
<DxcImage src={logo.src} alt={logo.alt} height="var(--height-m)" objectFit="contain" />
184-
) : (
185-
logo.src
186-
)}
187-
</LogoContainer>
188-
)}
189-
{appTitle && !isResponsive && (
190-
<>
191-
{logo && <DxcDivider orientation="vertical" />}
192-
<DxcHeading text={appTitle} as="h1" level={5} />
193-
</>
194-
)}
195-
</BrandingContainer>
196-
{!isResponsive && sanitizedNavItems && sanitizedNavItems.length > 0 && (
195+
{(!showSearch || isResponsive) && (
196+
<BrandingContainer>
197+
{logo && (
198+
<LogoContainer
199+
role={logo.onClick ? "button" : undefined}
200+
onClick={typeof logo.onClick === "function" ? logo.onClick : undefined}
201+
as={logo.href ? "a" : undefined}
202+
href={logo.href}
203+
hasAction={!!(logo.onClick || logo.href)}
204+
>
205+
{typeof logo.src === "string" ? (
206+
<DxcImage src={logo.src} alt={logo.alt} height="var(--height-m)" objectFit="contain" />
207+
) : (
208+
logo.src
209+
)}
210+
</LogoContainer>
211+
)}
212+
{appTitle && !isResponsive && (
213+
<>
214+
{logo && <DxcDivider orientation="vertical" />}
215+
<DxcHeading text={appTitle} as="h1" level={5} />
216+
</>
217+
)}
218+
</BrandingContainer>
219+
)}
220+
221+
{!isResponsive && ((sanitizedNavItems && sanitizedNavItems.length > 0) || (!!searchBar && showSearch)) && (
197222
<MainNavContainer>
198-
<DxcNavigationTree
199-
items={sanitizedNavItems}
200-
displayGroupLines={false}
201-
displayBorder={false}
202-
displayControlsAfter
203-
hasPopOver
204-
isHorizontal
205-
/>
223+
{!!searchBar && showSearch ? (
224+
<DxcSearchBar
225+
autoFocus={true}
226+
onBlur={searchBar.onBlur}
227+
onCancel={handleCancelSearch}
228+
onChange={searchBar.onChange}
229+
onEnter={searchBar.onEnter}
230+
placeholder={searchBar.placeholder}
231+
/>
232+
) : (
233+
<DxcNavigationTree
234+
items={sanitizedNavItems}
235+
displayGroupLines={false}
236+
displayBorder={false}
237+
displayControlsAfter
238+
hasPopOver
239+
isHorizontal
240+
/>
241+
)}
206242
</MainNavContainer>
207243
)}
208-
{(sideContent || isResponsive) && (
244+
245+
{(!showSearch || isResponsive) && (sideContent || isResponsive || !!searchBar) && (
209246
<RightSideContainer>
247+
{!!searchBar && !isResponsive && (
248+
<DxcSearchBarTrigger onTriggerClick={() => setShowSearch(!showSearch)} />
249+
)}
210250
{typeof sideContent === "function" ? sideContent(isResponsive) : sideContent}
211-
{isResponsive && ((navItems && navItems.length) || responsiveBottomContent) && (
251+
{isResponsive && ((navItems && navItems.length) || responsiveBottomContent || !!searchBar) && (
212252
<HamburguerButton onClick={toggleMenu} />
213253
)}
214254
</RightSideContainer>
@@ -219,6 +259,14 @@ const DxcHeader = ({ appTitle, navItems, sideContent, responsiveBottomContent }:
219259
<ResponsiveMenuContainer>
220260
<ResponsiveMenu>
221261
{appTitle && <DxcHeading text={appTitle} as="h1" level={5} />}
262+
{!!searchBar && (
263+
<DxcSearchBar
264+
onBlur={searchBar.onBlur}
265+
onChange={searchBar.onChange}
266+
onEnter={searchBar.onEnter}
267+
placeholder={searchBar.placeholder}
268+
/>
269+
)}
222270
<DxcNavigationTree
223271
items={sanitizedNavItems}
224272
displayGroupLines={false}

packages/lib/src/header/types.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,20 @@
11
import { ReactNode } from "react";
22
import { CommonItemProps, Item } from "../base-menu/types";
3+
import { SearchBarProps } from "../search-bar/types";
34

45
type GroupItem = CommonItemProps & {
56
items: Item[];
67
};
78

89
type MainNavPropsType = (GroupItem | Item)[];
910

11+
type SearchBarHeaderProps = Omit<SearchBarProps, "autoFocus" | "disabled">;
12+
1013
type Props = {
1114
appTitle?: string;
1215
navItems?: MainNavPropsType;
1316
responsiveBottomContent?: ReactNode;
17+
searchBar?: SearchBarHeaderProps;
1418
sideContent?: ReactNode | ((isResponsive: boolean) => ReactNode);
1519
};
1620

0 commit comments

Comments
 (0)