Skip to content
Merged
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
39 changes: 39 additions & 0 deletions apps/website/screens/principles/localization/LocalizationPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -551,6 +551,45 @@ const sections = [
</DxcTable>
),
},
{
title: "searchBar",
content: (
<DxcTable>
<thead>
<tr>
<th>Label Name</th>
<th>Default value</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<Code>clearFieldActionTitle</Code>
</td>
<td>Clear field</td>
</tr>
<tr>
<td>
<Code>inputAriaLabel</Code>
</td>
<td>Search input</td>
</tr>
<tr>
<td>
<Code>triggerTitle</Code>
</td>
<td>Search</td>
</tr>
<tr>
<td>
<Code>cancelButtonLabel</Code>
</td>
<td>Cancel</td>
</tr>
</tbody>
</DxcTable>
),
},
{
title: "select",
content: (
Expand Down
6 changes: 6 additions & 0 deletions packages/lib/src/common/variables.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,12 @@ export const defaultTranslatedComponentLabels = {
radioGroup: {
optionalItemLabelDefault: "N/A",
},
searchBar: {
clearFieldActionTitle: "Clear field",
inputAriaLabel: "Search input",
triggerTitle: "Search",
cancelButtonLabel: "Cancel",
},
select: {
actionClearSelectionTitle: "Clear selection",
actionClearSearchTitle: "Clear search",
Expand Down
43 changes: 43 additions & 0 deletions packages/lib/src/search-bar/SearchBar.accessibility.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { render } from "@testing-library/react";
import DxcSearchBar from "./SearchBar";
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);
});
});
158 changes: 158 additions & 0 deletions packages/lib/src/search-bar/SearchBar.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
import { Meta, StoryObj } from "@storybook/react-vite";
import ExampleContainer from "../../.storybook/components/ExampleContainer";
import Title from "../../.storybook/components/Title";
import DxcSearchBarTrigger from "./SearchBarTrigger";
import { useState } from "react";
import DxcSearchBar from "./SearchBar";
import DxcFlex from "../flex/Flex";
import DxcContainer from "../container/Container";
import { userEvent, within } from "storybook/internal/test";

export default {
title: "Search Bar",
component: DxcSearchBar,
} satisfies Meta<typeof DxcSearchBar>;

const SearchBarComponent = () => {
const [showSearch, setShowSearch] = useState(false);

return (
<DxcFlex alignItems="center">
{!showSearch ? (
<DxcSearchBarTrigger onTriggerClick={() => setShowSearch(!showSearch)} />
) : (
<DxcSearchBar
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="Search Bar component" theme="light" level={2} />
<ExampleContainer>
<SearchBarComponent />
</ExampleContainer>

<Title title="States" theme="light" level={2} />
<ExampleContainer>
<Title title="Default" theme="light" level={4} />
<DxcSearchBarTrigger onTriggerClick={() => {}} />
<DxcSearchBar
placeholder="Search..."
onBlur={(value) => {
console.log("onBlur", value);
}}
onChange={(value) => console.log("onChange", value)}
onEnter={(value) => {
console.log("onEnter", value);
}}
onCancel={() => {}}
/>
</ExampleContainer>
<ExampleContainer pseudoState="pseudo-hover">
<Title title="Hover" theme="light" level={4} />
<DxcSearchBarTrigger onTriggerClick={() => {}} />
<DxcSearchBar
placeholder="Search..."
onBlur={(value) => {
console.log("onBlur", value);
}}
onChange={(value) => console.log("onChange", value)}
onEnter={(value) => {
console.log("onEnter", value);
}}
onCancel={() => {}}
/>
</ExampleContainer>
<ExampleContainer pseudoState={["pseudo-focus", "pseudo-focus-within"]}>
<Title title="Focus" theme="light" level={4} />
<DxcSearchBarTrigger onTriggerClick={() => {}} />
<DxcSearchBar
placeholder="Search..."
onBlur={(value) => {
console.log("onBlur", value);
}}
onChange={(value) => console.log("onChange", value)}
onEnter={(value) => {
console.log("onEnter", value);
}}
onCancel={() => {}}
/>
</ExampleContainer>
<ExampleContainer pseudoState={["pseudo-focus", "pseudo-active"]}>
<Title title="Focus and active" theme="light" level={4} />
<DxcSearchBarTrigger onTriggerClick={() => {}} />
<DxcSearchBar
placeholder="Search..."
onBlur={(value) => {
console.log("onBlur", value);
}}
onChange={(value) => console.log("onChange", value)}
onEnter={(value) => {
console.log("onEnter", value);
}}
onCancel={() => {}}
/>
</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);
}}
onCancel={() => {}}
disabled
/>
</ExampleContainer>

<Title title="Small Searchbar" theme="light" level={2} />
<ExampleContainer>
<DxcContainer width="220px">
<DxcSearchBar
placeholder="Search..."
onBlur={(value) => {
console.log("onBlur", value);
}}
onChange={(value) => console.log("onChange", value)}
onEnter={(value) => {
console.log("onEnter", value);
}}
/>
</DxcContainer>
</ExampleContainer>
</>
);
};

type Story = StoryObj<typeof DxcSearchBar>;

export const Chromatic: Story = {
render: SearchBar,
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const user = userEvent.setup();

const enabledInputs = (await canvas.findAllByRole("textbox")).filter((input) => !input.hasAttribute("disabled"));

for (let i = 0; i < enabledInputs.length - 1; i++) {
await user.type(enabledInputs[i]!, "Lorem ipsum");
}
},
};
99 changes: 99 additions & 0 deletions packages/lib/src/search-bar/SearchBar.test.tsx
Original file line number Diff line number Diff line change
@@ -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);
});
});
Loading