From fc515855685cf2fca2ec8924c17371f80cc5053f Mon Sep 17 00:00:00 2001 From: Jialecl Date: Fri, 2 Jan 2026 08:25:03 +0100 Subject: [PATCH 1/8] Internal searchBar component added --- packages/lib/src/layout/ApplicationLayout.tsx | 37 +++++--------- .../lib/src/search-bar/SearchBar.stories.tsx | 48 +++++++++++++++++++ packages/lib/src/search-bar/SearchBar.tsx | 41 ++++++++++++++++ .../lib/src/search-bar/SearchBarTrigger.tsx | 15 ++++++ packages/lib/src/search-bar/types.ts | 32 +++++++++++++ 5 files changed, 149 insertions(+), 24 deletions(-) create mode 100644 packages/lib/src/search-bar/SearchBar.stories.tsx create mode 100644 packages/lib/src/search-bar/SearchBar.tsx create mode 100644 packages/lib/src/search-bar/SearchBarTrigger.tsx create mode 100644 packages/lib/src/search-bar/types.ts diff --git a/packages/lib/src/layout/ApplicationLayout.tsx b/packages/lib/src/layout/ApplicationLayout.tsx index 16b55ab42..905e7f497 100644 --- a/packages/lib/src/layout/ApplicationLayout.tsx +++ b/packages/lib/src/layout/ApplicationLayout.tsx @@ -40,22 +40,13 @@ const SidenavContainer = styled.div` overflow: auto; `; -const MainContainer = styled.div` - display: flex; - flex-grow: 1; - flex-direction: column; - width: 100%; - height: 100%; - position: relative; - overflow: auto; -`; - const FooterContainer = styled.div` height: fit-content; width: 100%; `; const MainContentContainer = styled.main` + width: 100%; height: 100%; display: grid; grid-template-rows: 1fr auto; @@ -78,20 +69,18 @@ const DxcApplicationLayout = ({ logo, header, sidenav, footer, children }: Appli {header && {header}} {sidenav && {sidenav}} - - - {findChildType(children, Main)} - - {footer ?? ( - - )} - - - + + {findChildType(children, Main)} + + {footer ?? ( + + )} + + diff --git a/packages/lib/src/search-bar/SearchBar.stories.tsx b/packages/lib/src/search-bar/SearchBar.stories.tsx new file mode 100644 index 000000000..3649abe23 --- /dev/null +++ b/packages/lib/src/search-bar/SearchBar.stories.tsx @@ -0,0 +1,48 @@ +import { Meta, StoryObj } from "@storybook/react-vite"; +import ExampleContainer from "../../.storybook/components/ExampleContainer"; +import Title from "../../.storybook/components/Title"; +import DxcLink from "./Link"; +import DxcSearchBarTrigger from "./SearchBarTrigger"; +import { useState } from "react"; +import DxcSearchBar from "./SearchBar"; +import DxcFlex from "../flex/Flex"; + +export default { + title: "SearchBar", + component: DxcSearchBar, +} satisfies Meta; + +const Link = () => { + const [showSearch, setShowSearch] = useState(false); + return ( + <> + + <ExampleContainer> + <DxcFlex alignItems="center"> + {!showSearch ? ( + <DxcSearchBarTrigger onTriggerClick={() => setShowSearch(!showSearch)} /> + ) : ( + <DxcSearchBar + placeholder="Search..." + onBlur={(value) => { + console.log("onBlur", value); + }} + onChange={(value) => console.log("onChange", value)} + onEnter={(value) => { + console.log("onEnter", value); + setShowSearch(false); + }} + onCancel={() => setShowSearch(false)} + /> + )} + </DxcFlex> + </ExampleContainer> + </> + ); +}; + +type Story = StoryObj<typeof DxcLink>; + +export const Chromatic: Story = { + render: Link, +}; diff --git a/packages/lib/src/search-bar/SearchBar.tsx b/packages/lib/src/search-bar/SearchBar.tsx new file mode 100644 index 000000000..b3a4bfae6 --- /dev/null +++ b/packages/lib/src/search-bar/SearchBar.tsx @@ -0,0 +1,41 @@ +import styled from "@emotion/styled"; +import DxcButton from "../button/Button"; +import DxcFlex from "../flex/Flex"; +import { SearchBarProps } from "./types"; + +const SearchBarStyles = styled.input` + width: 100%; + min-width: 200px; + max-width: 720px; + height: var(--height-m); + border-radius: var(--border-radius-xl); + border: var(--border-width-s) var(--border-style-default) var(--border-color-neutral-dark); + box-sizing: border-box; + padding: 0 var(--spacing-padding-s); + color: var(--color-fg-neutral-dark); + font-family: var(--typography-font-family); + font-size: var(--typography-label-m); + font-weight: var(--typography-label-regular); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +`; + +const DxcSearchBar = ({ autoFocus, onBlur, onCancel, onChange, onEnter, placeholder }: SearchBarProps) => ( + <DxcFlex gap="var(--spacing-gap-m)" alignItems="center" grow={1}> + <SearchBarStyles + placeholder={placeholder} + onBlur={(e) => typeof onBlur === "function" && onBlur(e.target.value)} + onChange={(e) => typeof onChange === "function" && onChange(e.target.value)} + onKeyDown={(e) => + e.key === "Enter" && typeof onEnter === "function" && onEnter((e.target as HTMLInputElement).value) + } + autoFocus={autoFocus} + /> + {typeof onCancel === "function" && ( + <DxcButton label="Cancel" onClick={onCancel} mode="tertiary" size={{ height: "medium" }} /> + )} + </DxcFlex> +); + +export default DxcSearchBar; diff --git a/packages/lib/src/search-bar/SearchBarTrigger.tsx b/packages/lib/src/search-bar/SearchBarTrigger.tsx new file mode 100644 index 000000000..26b647747 --- /dev/null +++ b/packages/lib/src/search-bar/SearchBarTrigger.tsx @@ -0,0 +1,15 @@ +import DxcButton from "../button/Button"; +import { SearchBarTriggerProps } from "./types"; + +const DxcSearchBarTrigger = ({ onTriggerClick }: SearchBarTriggerProps) => ( + <DxcButton + onClick={onTriggerClick} + icon="Search" + mode="tertiary" + title="Search" + semantic="default" + size={{ height: "medium" }} + /> +); + +export default DxcSearchBarTrigger; diff --git a/packages/lib/src/search-bar/types.ts b/packages/lib/src/search-bar/types.ts new file mode 100644 index 000000000..6e89ba7d3 --- /dev/null +++ b/packages/lib/src/search-bar/types.ts @@ -0,0 +1,32 @@ +export type SearchBarTriggerProps = { + /** + * Function invoked when the trigger button is clicked. + */ + onTriggerClick?: () => void; +}; +export type SearchBarProps = { + /** + * If true, the search bar input will be focused when rendered. + */ + autoFocus?: boolean; + /** + * Function invoked when the search bar loses focus. + */ + onBlur?: (value: string) => void; + /** + * Function invoked when the user cancels the search. + */ + onCancel?: () => void; + /** + * Function invoked when the user changes the input value. + */ + onChange?: (value: string) => void; + /** + * Function invoked when the Enter key is pressed. + */ + onEnter?: (value: string) => void; + /** + * Placeholder text displayed in the search bar input field. + */ + placeholder?: string; +}; From 3c22103a886cc28c354d3c8fc0f7dbbf541ee47f Mon Sep 17 00:00:00 2001 From: Jialecl <jialestrabajos@gmail.com> Date: Fri, 2 Jan 2026 08:33:08 +0100 Subject: [PATCH 2/8] Corrected some errors in storybook --- packages/lib/src/search-bar/SearchBar.stories.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/lib/src/search-bar/SearchBar.stories.tsx b/packages/lib/src/search-bar/SearchBar.stories.tsx index 3649abe23..c67efef07 100644 --- a/packages/lib/src/search-bar/SearchBar.stories.tsx +++ b/packages/lib/src/search-bar/SearchBar.stories.tsx @@ -1,7 +1,6 @@ import { Meta, StoryObj } from "@storybook/react-vite"; import ExampleContainer from "../../.storybook/components/ExampleContainer"; import Title from "../../.storybook/components/Title"; -import DxcLink from "./Link"; import DxcSearchBarTrigger from "./SearchBarTrigger"; import { useState } from "react"; import DxcSearchBar from "./SearchBar"; @@ -16,7 +15,7 @@ const Link = () => { const [showSearch, setShowSearch] = useState(false); return ( <> - <Title title="With anchor" theme="light" level={2} /> + <Title title="SearchBar component" theme="light" level={2} /> <ExampleContainer> <DxcFlex alignItems="center"> {!showSearch ? ( @@ -41,7 +40,7 @@ const Link = () => { ); }; -type Story = StoryObj<typeof DxcLink>; +type Story = StoryObj<typeof DxcSearchBar>; export const Chromatic: Story = { render: Link, From e28de1a58f851059b107e2bf735368edbae77bc8 Mon Sep 17 00:00:00 2001 From: Pelayo Felgueroso <pfelguerosogalguera@gmail.com> Date: Fri, 2 Jan 2026 10:19:48 +0100 Subject: [PATCH 3/8] Revert AppLayout changes --- packages/lib/src/layout/ApplicationLayout.tsx | 37 ++++++++++++------- 1 file changed, 24 insertions(+), 13 deletions(-) diff --git a/packages/lib/src/layout/ApplicationLayout.tsx b/packages/lib/src/layout/ApplicationLayout.tsx index 905e7f497..16b55ab42 100644 --- a/packages/lib/src/layout/ApplicationLayout.tsx +++ b/packages/lib/src/layout/ApplicationLayout.tsx @@ -40,13 +40,22 @@ const SidenavContainer = styled.div` overflow: auto; `; +const MainContainer = styled.div` + display: flex; + flex-grow: 1; + flex-direction: column; + width: 100%; + height: 100%; + position: relative; + overflow: auto; +`; + const FooterContainer = styled.div` height: fit-content; width: 100%; `; const MainContentContainer = styled.main` - width: 100%; height: 100%; display: grid; grid-template-rows: 1fr auto; @@ -69,18 +78,20 @@ const DxcApplicationLayout = ({ logo, header, sidenav, footer, children }: Appli {header && <HeaderContainer>{header}</HeaderContainer>} <BodyContainer hasSidenav={!!sidenav}> {sidenav && <SidenavContainer>{sidenav}</SidenavContainer>} - <MainContentContainer> - {findChildType(children, Main)} - <FooterContainer> - {footer ?? ( - <DxcFooter - copyright={`© DXC Technology ${year}. All rights reserved.`} - bottomLinks={bottomLinks} - socialLinks={socialLinks} - /> - )} - </FooterContainer> - </MainContentContainer> + <MainContainer> + <MainContentContainer> + {findChildType(children, Main)} + <FooterContainer> + {footer ?? ( + <DxcFooter + copyright={`© DXC Technology ${year}. All rights reserved.`} + bottomLinks={bottomLinks} + socialLinks={socialLinks} + /> + )} + </FooterContainer> + </MainContentContainer> + </MainContainer> </BodyContainer> </ApplicationLayoutContext.Provider> </ApplicationLayoutContainer> From 858a19932437c4ad083d0e74778aa87ea064cb7b Mon Sep 17 00:00:00 2001 From: Pelayo Felgueroso <pfelguerosogalguera@gmail.com> Date: Fri, 2 Jan 2026 14:11:22 +0100 Subject: [PATCH 4/8] Add tests and enhance SearchBar component functionality - Implement accessibility tests for DxcSearchBar and DxcSearchBarTrigger components. - Refactor SearchBar component to manage internal state and handle clear action. - Update SearchBarProps type to include a disabled property. - Improve SearchBar stories to demonstrate various states and interactions. --- .../SearchBar.accessibility.test.tsx | 58 ++++++++ .../lib/src/search-bar/SearchBar.stories.tsx | 110 ++++++++++++--- .../lib/src/search-bar/SearchBar.test.tsx | 99 +++++++++++++ packages/lib/src/search-bar/SearchBar.tsx | 130 +++++++++++++++--- packages/lib/src/search-bar/types.ts | 4 + 5 files changed, 364 insertions(+), 37 deletions(-) create mode 100644 packages/lib/src/search-bar/SearchBar.accessibility.test.tsx create mode 100644 packages/lib/src/search-bar/SearchBar.test.tsx diff --git a/packages/lib/src/search-bar/SearchBar.accessibility.test.tsx b/packages/lib/src/search-bar/SearchBar.accessibility.test.tsx new file mode 100644 index 000000000..36671272f --- /dev/null +++ b/packages/lib/src/search-bar/SearchBar.accessibility.test.tsx @@ -0,0 +1,58 @@ +import { render } from "@testing-library/react"; +import DxcSearchBar from "./SearchBar"; +import DxcSearchBarTrigger from "./SearchBarTrigger"; +import { axe } from "../../test/accessibility/axe-helper"; + +describe("SearchBar component accessibility tests", () => { + it("Should not have basic accessibility issues", async () => { + const { container } = render(<DxcSearchBar />); + const results = await axe(container); + expect(results.violations).toHaveLength(0); + }); + + it("Should not have basic accessibility issues with placeholder", async () => { + const { container } = render(<DxcSearchBar placeholder="Search here..." />); + const results = await axe(container); + expect(results.violations).toHaveLength(0); + }); + + it("Should not have basic accessibility issues with disabled state", async () => { + const { container } = render(<DxcSearchBar disabled />); + const results = await axe(container); + expect(results.violations).toHaveLength(0); + }); + + it("Should not have basic accessibility issues with cancel button", async () => { + const { container } = render(<DxcSearchBar onCancel={() => {}} />); + const results = await axe(container); + expect(results.violations).toHaveLength(0); + }); + + it("Should not have basic accessibility issues with all props", async () => { + const { container } = render( + <DxcSearchBar + placeholder="Search items..." + onChange={() => {}} + onEnter={() => {}} + onBlur={() => {}} + onCancel={() => {}} + /> + ); + const results = await axe(container); + expect(results.violations).toHaveLength(0); + }); +}); + +describe("SearchBarTrigger component accessibility tests", () => { + it("Should not have basic accessibility issues", async () => { + const { container } = render(<DxcSearchBarTrigger />); + const results = await axe(container); + expect(results.violations).toHaveLength(0); + }); + + it("Should not have basic accessibility issues with onTriggerClick", async () => { + const { container } = render(<DxcSearchBarTrigger onTriggerClick={() => {}} />); + const results = await axe(container); + expect(results.violations).toHaveLength(0); + }); +}); diff --git a/packages/lib/src/search-bar/SearchBar.stories.tsx b/packages/lib/src/search-bar/SearchBar.stories.tsx index c67efef07..c287ff6d5 100644 --- a/packages/lib/src/search-bar/SearchBar.stories.tsx +++ b/packages/lib/src/search-bar/SearchBar.stories.tsx @@ -5,36 +5,106 @@ import DxcSearchBarTrigger from "./SearchBarTrigger"; import { useState } from "react"; import DxcSearchBar from "./SearchBar"; import DxcFlex from "../flex/Flex"; +import DxcContainer from "../container/Container"; export default { title: "SearchBar", component: DxcSearchBar, } satisfies Meta<typeof DxcSearchBar>; -const Link = () => { +const SearchBarComponent = () => { const [showSearch, setShowSearch] = useState(false); + + return ( + <DxcFlex alignItems="center"> + {!showSearch ? ( + <DxcSearchBarTrigger onTriggerClick={() => setShowSearch(!showSearch)} /> + ) : ( + <DxcSearchBar + placeholder="Search..." + onBlur={(value) => { + console.log("onBlur", value); + }} + onChange={(value) => console.log("onChange", value)} + onEnter={(value) => { + console.log("onEnter", value); + setShowSearch(false); + }} + onCancel={() => setShowSearch(false)} + /> + )} + </DxcFlex> + ); +}; + +const SearchBar = () => { return ( <> <Title title="SearchBar component" theme="light" level={2} /> <ExampleContainer> - <DxcFlex alignItems="center"> - {!showSearch ? ( - <DxcSearchBarTrigger onTriggerClick={() => setShowSearch(!showSearch)} /> - ) : ( - <DxcSearchBar - placeholder="Search..." - onBlur={(value) => { - console.log("onBlur", value); - }} - onChange={(value) => console.log("onChange", value)} - onEnter={(value) => { - console.log("onEnter", value); - setShowSearch(false); - }} - onCancel={() => setShowSearch(false)} - /> - )} - </DxcFlex> + <SearchBarComponent /> + </ExampleContainer> + + <Title title="States" theme="light" level={2} /> + <ExampleContainer> + <Title title="Default" theme="light" level={4} /> + <DxcSearchBar + placeholder="Search..." + onBlur={(value) => { + console.log("onBlur", value); + }} + onChange={(value) => console.log("onChange", value)} + onEnter={(value) => { + console.log("onEnter", value); + }} + /> + </ExampleContainer> + <ExampleContainer pseudoState="pseudo-hover"> + <Title title="Hover" theme="light" level={4} /> + <DxcSearchBar + placeholder="Search..." + onBlur={(value) => { + console.log("onBlur", value); + }} + onChange={(value) => console.log("onChange", value)} + onEnter={(value) => { + console.log("onEnter", value); + }} + /> + </ExampleContainer> + <ExampleContainer pseudoState="pseudo-focus-within"> + <Title title="Focus" theme="light" level={4} /> + <DxcSearchBar + placeholder="Search..." + onBlur={(value) => { + console.log("onBlur", value); + }} + onChange={(value) => console.log("onChange", value)} + onEnter={(value) => { + console.log("onEnter", value); + }} + /> + </ExampleContainer> + <ExampleContainer> + <Title title="Disabled" theme="light" level={4} /> + <DxcSearchBar + placeholder="Search..." + onBlur={(value) => { + console.log("onBlur", value); + }} + onChange={(value) => console.log("onChange", value)} + onEnter={(value) => { + console.log("onEnter", value); + }} + disabled + /> + </ExampleContainer> + + <Title title="Small SearchBar component" theme="light" level={2} /> + <ExampleContainer> + <DxcContainer width="220px"> + <SearchBarComponent /> + </DxcContainer> </ExampleContainer> </> ); @@ -43,5 +113,5 @@ const Link = () => { type Story = StoryObj<typeof DxcSearchBar>; export const Chromatic: Story = { - render: Link, + render: SearchBar, }; diff --git a/packages/lib/src/search-bar/SearchBar.test.tsx b/packages/lib/src/search-bar/SearchBar.test.tsx new file mode 100644 index 000000000..2a59203c0 --- /dev/null +++ b/packages/lib/src/search-bar/SearchBar.test.tsx @@ -0,0 +1,99 @@ +import { render, fireEvent } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import DxcSearchBar from "./SearchBar"; +import DxcSearchBarTrigger from "./SearchBarTrigger"; + +describe("SearchBarTrigger component tests", () => { + test("Renders correctly", () => { + const { getByRole } = render(<DxcSearchBarTrigger />); + + const button = getByRole("button"); + expect(button).toBeTruthy(); + }); + + test("Calls onTriggerClick when button is clicked", () => { + const onTriggerClick = jest.fn(); + const { getByRole } = render(<DxcSearchBarTrigger onTriggerClick={onTriggerClick} />); + + const button = getByRole("button"); + userEvent.click(button); + + expect(onTriggerClick).toHaveBeenCalledTimes(1); + }); +}); + +describe("SearchBar component tests", () => { + test("Renders correctly", () => { + const { getByPlaceholderText } = render(<DxcSearchBar placeholder="Search..." />); + + const text = getByPlaceholderText("Search..."); + expect(text).toBeTruthy(); + }); + + test("Calls onChange when typing", () => { + const onChange = jest.fn(); + const { getByRole } = render(<DxcSearchBar onChange={onChange} />); + + const input = getByRole("textbox") as HTMLInputElement; + userEvent.type(input, "hello"); + + expect(onChange).toHaveBeenCalled(); + expect(onChange).toHaveBeenLastCalledWith("hello"); + }); + + test("Calls onEnter with value when pressing Enter", () => { + const onEnter = jest.fn(); + const { getByRole } = render(<DxcSearchBar onEnter={onEnter} />); + + const input = getByRole("textbox") as HTMLInputElement; + userEvent.type(input, "search text"); + fireEvent.keyDown(input, { key: "Enter" }); + + expect(onEnter).toHaveBeenCalledTimes(1); + expect(onEnter).toHaveBeenCalledWith("search text"); + }); + + test("Clears value when clicking clear icon", () => { + const { getByRole } = render(<DxcSearchBar />); + + const input = getByRole("textbox") as HTMLInputElement; + userEvent.type(input, "abc"); + + const clearButton = getByRole("button"); + expect(clearButton).toBeTruthy(); + + userEvent.click(clearButton); + expect(input.value).toBe(""); + }); + + test("Clears value when pressing Escape", () => { + const { getByRole } = render(<DxcSearchBar />); + + const input = getByRole("textbox") as HTMLInputElement; + userEvent.type(input, "xyz"); + fireEvent.keyDown(input, { key: "Escape" }); + + expect(input.value).toBe(""); + }); + + test("Calls onBlur with current value when blurred", () => { + const onBlur = jest.fn(); + const { getByRole } = render(<DxcSearchBar onBlur={onBlur} />); + + const input = getByRole("textbox") as HTMLInputElement; + userEvent.type(input, "blur me"); + fireEvent.blur(input); + + expect(onBlur).toHaveBeenCalledWith("blur me"); + }); + + test("Calls onCancel when Cancel button is clicked", () => { + const onCancel = jest.fn(); + const { getByRole } = render(<DxcSearchBar onCancel={onCancel} />); + + const cancelButton = getByRole("button", { name: /Cancel/i }); + userEvent.click(cancelButton); + + expect(onCancel).toHaveBeenCalledTimes(1); + }); +}); diff --git a/packages/lib/src/search-bar/SearchBar.tsx b/packages/lib/src/search-bar/SearchBar.tsx index b3a4bfae6..e4079c24c 100644 --- a/packages/lib/src/search-bar/SearchBar.tsx +++ b/packages/lib/src/search-bar/SearchBar.tsx @@ -2,40 +2,136 @@ import styled from "@emotion/styled"; import DxcButton from "../button/Button"; import DxcFlex from "../flex/Flex"; import { SearchBarProps } from "./types"; +import DxcActionIcon from "../action-icon/ActionIcon"; +import { KeyboardEvent, useContext, useRef, useState } from "react"; +import { HalstackLanguageContext } from "../HalstackContext"; +import { css } from "@emotion/react"; +import DxcIcon from "../icon/Icon"; -const SearchBarStyles = styled.input` +const SearchBarContainer = styled.div<{ disabled: Required<SearchBarProps>["disabled"] }>` width: 100%; min-width: 200px; max-width: 720px; height: var(--height-m); + display: flex; + align-items: center; + gap: var(--spacing-gap-s); border-radius: var(--border-radius-xl); border: var(--border-width-s) var(--border-style-default) var(--border-color-neutral-dark); box-sizing: border-box; padding: 0 var(--spacing-padding-s); color: var(--color-fg-neutral-dark); + + ${({ disabled }) => + !disabled + ? css` + &:hover { + border-color: var(--border-color-primary-strong); + } + &:focus, + &:focus-within { + border-color: transparent; + outline-offset: -2px; + outline: var(--border-width-m) var(--border-style-default) var(--border-color-secondary-medium); + } + ` + : css` + color: var(--color-fg-neutral-medium); + border-color: var(--border-color-neutral-strong); + cursor: not-allowed; + `} +`; + +const SearchBarInput = styled.input<{ disabled: Required<SearchBarProps>["disabled"] }>` + width: 100%; + max-width: 100%; + background: none; + border: none; + outline: none; + padding: 0; font-family: var(--typography-font-family); font-size: var(--typography-label-m); font-weight: var(--typography-label-regular); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; + cursor: ${({ disabled }) => (disabled ? "not-allowed" : "text")}; `; -const DxcSearchBar = ({ autoFocus, onBlur, onCancel, onChange, onEnter, placeholder }: SearchBarProps) => ( - <DxcFlex gap="var(--spacing-gap-m)" alignItems="center" grow={1}> - <SearchBarStyles - placeholder={placeholder} - onBlur={(e) => typeof onBlur === "function" && onBlur(e.target.value)} - onChange={(e) => typeof onChange === "function" && onChange(e.target.value)} - onKeyDown={(e) => - e.key === "Enter" && typeof onEnter === "function" && onEnter((e.target as HTMLInputElement).value) - } - autoFocus={autoFocus} - /> - {typeof onCancel === "function" && ( - <DxcButton label="Cancel" onClick={onCancel} mode="tertiary" size={{ height: "medium" }} /> - )} - </DxcFlex> -); +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 handleClearActionOnClick = () => { + setInnerValue(""); + inputRef.current?.focus(); + }; + + 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; + } + }; + + 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} + /> + )} + </SearchBarContainer> + + {typeof onCancel === "function" && ( + <DxcButton label="Cancel" onClick={onCancel} mode="tertiary" size={{ height: "medium" }} /> + )} + </DxcFlex> + ); +}; export default DxcSearchBar; diff --git a/packages/lib/src/search-bar/types.ts b/packages/lib/src/search-bar/types.ts index 6e89ba7d3..5d67a8f39 100644 --- a/packages/lib/src/search-bar/types.ts +++ b/packages/lib/src/search-bar/types.ts @@ -9,6 +9,10 @@ export type SearchBarProps = { * If true, the search bar input will be focused when rendered. */ autoFocus?: boolean; + /** + * If true, the component will be disabled. + */ + disabled?: boolean; /** * Function invoked when the search bar loses focus. */ From 70355ee6ca41a4582311d559032170579f0e98b7 Mon Sep 17 00:00:00 2001 From: Pelayo Felgueroso <pfelguerosogalguera@gmail.com> Date: Mon, 5 Jan 2026 08:39:55 +0100 Subject: [PATCH 5/8] Enhance SearchBar focus styles to include focus-visible state --- packages/lib/src/search-bar/SearchBar.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/lib/src/search-bar/SearchBar.tsx b/packages/lib/src/search-bar/SearchBar.tsx index e4079c24c..f8d4b537b 100644 --- a/packages/lib/src/search-bar/SearchBar.tsx +++ b/packages/lib/src/search-bar/SearchBar.tsx @@ -29,7 +29,8 @@ const SearchBarContainer = styled.div<{ disabled: Required<SearchBarProps>["disa border-color: var(--border-color-primary-strong); } &:focus, - &:focus-within { + &:focus-within, + &:focus-visible { border-color: transparent; outline-offset: -2px; outline: var(--border-width-m) var(--border-style-default) var(--border-color-secondary-medium); From fb75f4ff83e361eec44b3bd49c21545ebafecd19 Mon Sep 17 00:00:00 2001 From: Pelayo Felgueroso <pfelguerosogalguera@gmail.com> Date: Mon, 5 Jan 2026 08:52:48 +0100 Subject: [PATCH 6/8] Retype stories titles --- packages/lib/src/search-bar/SearchBar.stories.tsx | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/packages/lib/src/search-bar/SearchBar.stories.tsx b/packages/lib/src/search-bar/SearchBar.stories.tsx index c287ff6d5..47e660975 100644 --- a/packages/lib/src/search-bar/SearchBar.stories.tsx +++ b/packages/lib/src/search-bar/SearchBar.stories.tsx @@ -40,7 +40,7 @@ const SearchBarComponent = () => { const SearchBar = () => { return ( <> - <Title title="SearchBar component" theme="light" level={2} /> + <Title title="Searchbar component" theme="light" level={2} /> <ExampleContainer> <SearchBarComponent /> </ExampleContainer> @@ -100,10 +100,19 @@ const SearchBar = () => { /> </ExampleContainer> - <Title title="Small SearchBar component" theme="light" level={2} /> + <Title title="Small Searchbar" theme="light" level={2} /> <ExampleContainer> <DxcContainer width="220px"> - <SearchBarComponent /> + <DxcSearchBar + placeholder="Search..." + onBlur={(value) => { + console.log("onBlur", value); + }} + onChange={(value) => console.log("onChange", value)} + onEnter={(value) => { + console.log("onEnter", value); + }} + /> </DxcContainer> </ExampleContainer> </> From 473a539dd62d97993c0e97b6e194e7ec59272cb6 Mon Sep 17 00:00:00 2001 From: Pelayo Felgueroso <pfelguerosogalguera@gmail.com> Date: Mon, 5 Jan 2026 09:26:04 +0100 Subject: [PATCH 7/8] Retype searchbar stories title --- packages/lib/src/search-bar/SearchBar.stories.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/lib/src/search-bar/SearchBar.stories.tsx b/packages/lib/src/search-bar/SearchBar.stories.tsx index 47e660975..048145b97 100644 --- a/packages/lib/src/search-bar/SearchBar.stories.tsx +++ b/packages/lib/src/search-bar/SearchBar.stories.tsx @@ -8,7 +8,7 @@ import DxcFlex from "../flex/Flex"; import DxcContainer from "../container/Container"; export default { - title: "SearchBar", + title: "Searchbar", component: DxcSearchBar, } satisfies Meta<typeof DxcSearchBar>; From d566e5ead94f9d825a158f64f4c4fb39ea43f7f3 Mon Sep 17 00:00:00 2001 From: Pelayo Felgueroso <pfelguerosogalguera@gmail.com> Date: Thu, 8 Jan 2026 10:29:54 +0100 Subject: [PATCH 8/8] Add tooltip to Cancel button --- packages/lib/src/search-bar/SearchBar.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/lib/src/search-bar/SearchBar.tsx b/packages/lib/src/search-bar/SearchBar.tsx index f8d4b537b..fe3b8e058 100644 --- a/packages/lib/src/search-bar/SearchBar.tsx +++ b/packages/lib/src/search-bar/SearchBar.tsx @@ -129,7 +129,7 @@ const DxcSearchBar = ({ </SearchBarContainer> {typeof onCancel === "function" && ( - <DxcButton label="Cancel" onClick={onCancel} mode="tertiary" size={{ height: "medium" }} /> + <DxcButton label="Cancel" title="Cancel" onClick={onCancel} mode="tertiary" size={{ height: "medium" }} /> )} </DxcFlex> );