Skip to content
Closed
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
16 changes: 2 additions & 14 deletions apps/website/pages/_app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { ReactElement, ReactNode, useMemo, useState } from "react";
import type { NextPage } from "next";
import type { AppProps } from "next/app";
import Head from "next/head";
import { DxcApplicationLayout, DxcTextInput, DxcToastsQueue } from "@dxc-technology/halstack-react";
import { DxcApplicationLayout, DxcToastsQueue } from "@dxc-technology/halstack-react";
import MainContent from "@/common/MainContent";
import { useRouter } from "next/router";
import { LinkDetails, LinksSectionDetails, LinksSections } from "@/common/pagesList";
Expand Down Expand Up @@ -108,19 +108,7 @@ export default function App({ Component, pageProps, emotionCache = clientSideEmo
<DxcApplicationLayout.Sidenav
navItems={navItems}
appTitle={isExpanded && <SidenavLogo />}
topContent={
isExpanded && (
<DxcTextInput
placeholder="Search docs"
value={filter}
onChange={({ value }) => {
setFilter(value);
}}
size="fillParent"
clearable
/>
)
}
searchBar={{ placeholder: "Search docs", onChange: (value) => setFilter(value) }}
expanded={isExpanded}
onExpandedChange={() => {
setIsExpanded((currentlyExpanded) => !currentlyExpanded);
Expand Down
22 changes: 22 additions & 0 deletions apps/website/screens/components/sidenav/code/SidenavCodePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,15 @@ const sectionTypeString = `{
title?: string };
}`;

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

const sections = [
{
title: "Props",
Expand Down Expand Up @@ -147,6 +156,19 @@ const sections = [
<td>Function called when the expansion state of the sidenav changes.</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 will be rendered at the top of the sidenav.</td>
<td>-</td>
</tr>
<tr>
<td>
<DxcFlex direction="column" gap="var(--spacing-gap-xs)" alignItems="baseline">
Expand Down
136 changes: 65 additions & 71 deletions packages/lib/src/search-bar/SearchBar.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import styled from "@emotion/styled";
import DxcButton from "../button/Button";
import DxcFlex from "../flex/Flex";
import { SearchBarProps } from "./types";
import { RefType, SearchBarProps } from "./types";
import DxcActionIcon from "../action-icon/ActionIcon";
import { KeyboardEvent, useContext, useRef, useState } from "react";
import { forwardRef, KeyboardEvent, useContext, useRef, useState } from "react";
import { HalstackLanguageContext } from "../HalstackContext";
import { css } from "@emotion/react";
import DxcIcon from "../icon/Icon";
Expand Down Expand Up @@ -59,80 +59,74 @@ const SearchBarInput = styled.input<{ disabled: Required<SearchBarProps>["disabl
cursor: ${({ disabled }) => (disabled ? "not-allowed" : "text")};
`;

const DxcSearchBar = ({
autoFocus,
disabled = false,
onBlur,
onCancel,
onChange,
onEnter,
placeholder,
}: SearchBarProps) => {
const translatedLabels = useContext(HalstackLanguageContext);
const inputRef = useRef<HTMLInputElement>(null);
const [innerValue, setInnerValue] = useState("");
const DxcSearchBar = forwardRef<RefType, SearchBarProps>(
({ autoFocus, disabled = false, onBlur, onCancel, onChange, onEnter, placeholder }, ref) => {
const translatedLabels = useContext(HalstackLanguageContext);
const inputRef = useRef<HTMLInputElement>(null);
const [innerValue, setInnerValue] = useState("");

const handleClearActionOnClick = () => {
setInnerValue("");
inputRef.current?.focus();
};
const handleClearActionOnClick = () => {
setInnerValue("");
inputRef.current?.focus();
};

const handleSearchChangeValue = (value: string) => {
setInnerValue(value);
if (typeof onChange === "function") {
onChange(value);
}
};
const handleSearchChangeValue = (value: string) => {
setInnerValue(value);
if (typeof onChange === "function") {
onChange(value);
}
};

const handleInputOnKeyDown = (e: KeyboardEvent<HTMLInputElement>) => {
switch (e.key) {
case "Esc":
case "Escape":
e.preventDefault();
if (innerValue.length > 0) {
handleClearActionOnClick();
}
break;
case "Enter":
if (typeof onEnter === "function") {
onEnter(innerValue);
}
break;
default:
break;
}
};
const handleInputOnKeyDown = (e: KeyboardEvent<HTMLInputElement>) => {
switch (e.key) {
case "Esc":
case "Escape":
e.preventDefault();
if (innerValue.length > 0) {
handleClearActionOnClick();
}
break;
case "Enter":
if (typeof onEnter === "function") {
onEnter(innerValue);
}
break;
default:
break;
}
};

return (
<DxcFlex gap="var(--spacing-gap-m)" alignItems="center" justifyContent="center" grow={1}>
<SearchBarContainer disabled={disabled} autoFocus={autoFocus}>
<DxcIcon icon="search" />
<SearchBarInput
ref={inputRef}
value={innerValue}
placeholder={placeholder}
onBlur={(e) => typeof onBlur === "function" && onBlur(e.target.value)}
onChange={(e) => handleSearchChangeValue(e.target.value)}
onKeyDown={handleInputOnKeyDown}
disabled={disabled}
/>
{!disabled && innerValue.length > 0 && (
<DxcActionIcon
size="xsmall"
shape="circle"
icon="cancel"
onClick={handleClearActionOnClick}
tabIndex={0}
title={!disabled ? translatedLabels.textInput.clearFieldActionTitle : undefined}
return (
<DxcFlex gap="var(--spacing-gap-m)" alignItems="center" justifyContent="center" grow={1}>
<SearchBarContainer ref={ref} disabled={disabled} autoFocus={autoFocus}>
<DxcIcon icon="search" />
<SearchBarInput
ref={inputRef}
value={innerValue}
placeholder={placeholder}
onBlur={(e) => typeof onBlur === "function" && onBlur(e.target.value)}
onChange={(e) => handleSearchChangeValue(e.target.value)}
onKeyDown={handleInputOnKeyDown}
disabled={disabled}
/>
)}
</SearchBarContainer>
{!disabled && innerValue.length > 0 && (
<DxcActionIcon
size="xsmall"
shape="circle"
icon="cancel"
onClick={handleClearActionOnClick}
tabIndex={0}
title={!disabled ? translatedLabels.textInput.clearFieldActionTitle : undefined}
/>
)}
</SearchBarContainer>

{typeof onCancel === "function" && (
<DxcButton label="Cancel" title="Cancel" onClick={onCancel} mode="tertiary" size={{ height: "medium" }} />
)}
</DxcFlex>
);
};
{typeof onCancel === "function" && (
<DxcButton label="Cancel" title="Cancel" onClick={onCancel} mode="tertiary" size={{ height: "medium" }} />
)}
</DxcFlex>
);
}
);

export default DxcSearchBar;
1 change: 1 addition & 0 deletions packages/lib/src/search-bar/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export type SearchBarTriggerProps = {
*/
onTriggerClick?: () => void;
};
export type RefType = HTMLDivElement;
export type SearchBarProps = {
/**
* If true, the search bar input will be focused when rendered.
Expand Down
4 changes: 3 additions & 1 deletion packages/lib/src/sidenav/Sidenav.accessibility.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,9 @@ describe("Sidenav component accessibility tests", () => {
],
},
];
const { container } = render(<DxcSidenav navItems={groupItems} appTitle="Application Name" />);
const { container } = render(
<DxcSidenav navItems={groupItems} appTitle="Application Name" searchBar={{ placeholder: "Search..." }} />
);
const results = await axe(container);
expect(results.violations).toHaveLength(0);
});
Expand Down
7 changes: 7 additions & 0 deletions packages/lib/src/sidenav/Sidenav.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@ const Sidenav = () => (
<DxcSidenav
navItems={groupItems}
appTitle="Application Name"
searchBar={{ placeholder: "Search..." }}
bottomContent={
<>
<DetailedAvatar />
Expand Down Expand Up @@ -169,6 +170,7 @@ const Sidenav = () => (
<DxcSidenav
navItems={groupItems}
appTitle="Application Name"
searchBar={{ placeholder: "Search..." }}
bottomContent={
<>
<DetailedAvatar />
Expand Down Expand Up @@ -208,6 +210,7 @@ const Collapsed = () => {
<DxcSidenav
navItems={groupItems}
appTitle="App Name"
searchBar={{ placeholder: "Search..." }}
bottomContent={
isExpanded ? (
<>
Expand Down Expand Up @@ -261,6 +264,7 @@ const Collapsed = () => {
<DxcSidenav
navItems={groupItems}
appTitle="App Name"
searchBar={{ placeholder: "Search..." }}
bottomContent={
isExpandedGroupsNoLines ? (
<>
Expand Down Expand Up @@ -314,6 +318,7 @@ const Collapsed = () => {
<DxcSidenav
navItems={groupItems}
appTitle="App Name"
searchBar={{ placeholder: "Search..." }}
bottomContent={
isExpandedGroups ? (
<>
Expand Down Expand Up @@ -373,6 +378,7 @@ const Hovered = () => (
<DxcSidenav
navItems={groupItems}
appTitle="Application Name"
searchBar={{ placeholder: "Search..." }}
bottomContent={
<>
<DetailedAvatar />
Expand Down Expand Up @@ -405,6 +411,7 @@ const SelectedGroup = () => (
<DxcSidenav
navItems={selectedGroupItems}
appTitle="Application name"
searchBar={{ placeholder: "Search..." }}
bottomContent={
<>
<DetailedAvatar />
Expand Down
19 changes: 18 additions & 1 deletion packages/lib/src/sidenav/Sidenav.test.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import "@testing-library/jest-dom";
import { render, fireEvent } from "@testing-library/react";
import { render, fireEvent, waitFor } from "@testing-library/react";
import DxcSidenav from "./Sidenav";
import { ReactNode } from "react";

Expand Down Expand Up @@ -103,4 +103,21 @@ describe("DxcSidenav component", () => {
const collapseButton = getByRole("button", { name: "Collapse" });
expect(collapseButton).toBeTruthy();
});

test("Sidenav renders search bar when searchBar prop is provided", () => {
const { getByPlaceholderText } = render(<DxcSidenav searchBar={{ placeholder: "Search..." }} />);
expect(getByPlaceholderText("Search...")).toBeTruthy();
});

test("Sidenav expands and focuses search input when handleExpandSearch is called", async () => {
const { getByRole, getByPlaceholderText } = render(
<DxcSidenav searchBar={{ placeholder: "Search..." }} defaultExpanded={false} />
);
const expandButton = getByRole("button", { name: "Search" });
fireEvent.click(expandButton);
const searchInput = getByPlaceholderText("Search...") as HTMLInputElement;
await waitFor(() => {
expect(document.activeElement).toBe(searchInput);
});
});
});
21 changes: 19 additions & 2 deletions packages/lib/src/sidenav/Sidenav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,12 @@ import SidenavPropsType from "./types";
import DxcDivider from "../divider/Divider";
import DxcButton from "../button/Button";
import DxcImage from "../image/Image";
import { useContext, useState } from "react";
import { useContext, useRef, useState } from "react";
import DxcNavigationTree from "../navigation-tree/NavigationTree";
import DxcInset from "../inset/Inset";
import ApplicationLayoutContext from "../layout/ApplicationLayoutContext";
import DxcSearchBar from "../search-bar/SearchBar";
import DxcSearchBarTrigger from "../search-bar/SearchBarTrigger";

const SidenavContainer = styled.div<{ expanded: boolean }>`
box-sizing: border-box;
Expand Down Expand Up @@ -67,18 +69,27 @@ const DxcSidenav = ({
expanded,
defaultExpanded = true,
onExpandedChange,
searchBar,
}: SidenavPropsType): JSX.Element => {
const [internalExpanded, setInternalExpanded] = useState(defaultExpanded);
const { logo, headerExists } = useContext(ApplicationLayoutContext);
const isControlled = expanded !== undefined;
const isExpanded = isControlled ? !!expanded : internalExpanded;
const searchBarRef = useRef<HTMLDivElement>(null);

const handleToggle = () => {
const nextState = !isExpanded;
if (!isControlled) setInternalExpanded(nextState);
onExpandedChange?.(nextState);
};

const handleExpandSearch = () => {
handleToggle();
setTimeout(() => {
searchBarRef.current?.querySelector("input")?.focus();
}, 1);
};

return (
<SidenavContainer expanded={isExpanded}>
<DxcFlex
Expand Down Expand Up @@ -114,8 +125,14 @@ const DxcSidenav = ({
<SidenavTitle>{appTitle}</SidenavTitle>
</DxcFlex>
</DxcFlex>
{topContent && (
{(topContent || searchBar) && (
<DxcFlex direction="column" gap={"var(--spacing-gap-l)"}>
{searchBar &&
(isExpanded ? (
<DxcSearchBar ref={searchBarRef} {...searchBar} />
) : (
<DxcSearchBarTrigger onTriggerClick={handleExpandSearch} />
))}
{topContent}
</DxcFlex>
)}
Expand Down
Loading