From 489a374f1ac5afaada75d6d514333c9e5846d52a Mon Sep 17 00:00:00 2001 From: Yossi Saadi Date: Wed, 17 Apr 2024 17:59:18 +0300 Subject: [PATCH] feat(Search): new Search component (#2064) --- .../__stories__/attentionBox.stories.js | 4 +- .../src/components/BaseInput/BaseInput.tsx | 30 +-- .../components/BaseInput/BaseInput.types.ts | 40 +++- .../BaseInput/__tests__/BaseInput.jest.tsx | 8 +- .../Chips/__stories__/chips.stories.js | 6 +- .../core/src/components/Combobox/Combobox.tsx | 4 +- .../Heading/__stories__/Heading.stories.js | 4 +- .../components/Icon/__stories__/IconsList.tsx | 4 +- .../__stories__/LegacyHeading.stories.tsx | 4 +- .../LegacySearch/LegacySearch.module.scss | 34 +++ .../components/LegacySearch/LegacySearch.tsx | 124 +++++++++++ .../SearchConstants.ts | 0 .../LegacySearch/__stories__/LegacySearch.mdx | 72 +++++++ .../LegacySearch.stories.helpers.tsx} | 0 .../__stories__/LegacySearch.stories.scss} | 0 .../__stories__/LegacySearch.stories.tsx | 83 ++++++++ .../search-snapshot-tests.jest.js.snap | 0 .../__tests__/search-snapshot-tests.jest.js | 2 +- .../Menu/__stories__/Menu.stories.helpers.tsx | 2 +- .../__stories__/ResponsiveList.stories.js | 10 +- .../src/components/Search/Search.module.scss | 46 ++--- .../core/src/components/Search/Search.tsx | 195 +++++++++--------- .../src/components/Search/Search.types.ts | 80 +++++++ .../components/Search/__stories__/Search.mdx | 67 +++++- .../__stories__/Search.stories.module.scss | 3 + .../Search/__stories__/Search.stories.tsx | 123 ++++++----- .../Search/__tests__/Search.jest.tsx | 134 ++++++++++++ packages/core/src/components/index.js | 4 +- .../UseActiveDescendantListFocus.jsx | 1 - packages/core/src/next.ts | 3 +- .../descriptions/search-description.jsx | 4 +- .../Catalog/Catalog.stories.templates.tsx | 9 +- .../playground/react-docgen-output.json | 2 +- .../core/webpack/published-ts-components.js | 4 +- 34 files changed, 868 insertions(+), 238 deletions(-) create mode 100644 packages/core/src/components/LegacySearch/LegacySearch.module.scss create mode 100644 packages/core/src/components/LegacySearch/LegacySearch.tsx rename packages/core/src/components/{Search => LegacySearch}/SearchConstants.ts (100%) create mode 100644 packages/core/src/components/LegacySearch/__stories__/LegacySearch.mdx rename packages/core/src/components/{Search/__stories__/Search.stories.helpers.tsx => LegacySearch/__stories__/LegacySearch.stories.helpers.tsx} (100%) rename packages/core/src/components/{Search/__stories__/Search.stories.scss => LegacySearch/__stories__/LegacySearch.stories.scss} (100%) create mode 100644 packages/core/src/components/LegacySearch/__stories__/LegacySearch.stories.tsx rename packages/core/src/components/{Search => LegacySearch}/__tests__/__snapshots__/search-snapshot-tests.jest.js.snap (100%) rename packages/core/src/components/{Search => LegacySearch}/__tests__/search-snapshot-tests.jest.js (98%) create mode 100644 packages/core/src/components/Search/Search.types.ts create mode 100644 packages/core/src/components/Search/__stories__/Search.stories.module.scss create mode 100644 packages/core/src/components/Search/__tests__/Search.jest.tsx diff --git a/packages/core/src/components/AttentionBox/__stories__/attentionBox.stories.js b/packages/core/src/components/AttentionBox/__stories__/attentionBox.stories.js index f6301973ee..6a0fff0ead 100644 --- a/packages/core/src/components/AttentionBox/__stories__/attentionBox.stories.js +++ b/packages/core/src/components/AttentionBox/__stories__/attentionBox.stories.js @@ -5,7 +5,7 @@ import { createComponentTemplate, StoryDescription } from "vibe-storybook-compon import DialogContentContainer from "../../DialogContentContainer/DialogContentContainer"; import { Info, Invite, ThumbsUp } from "../../Icon/Icons"; import Icon from "../../Icon/Icon"; -import Search from "../../Search/Search"; +import LegacySearch from "../../LegacySearch/LegacySearch"; import Avatar from "../../Avatar/Avatar"; import person from "./assets/person.png"; import Flex from "../../Flex/Flex"; @@ -159,7 +159,7 @@ export const AttentionBoxInsideADialogCombobox = { return ( - +
Suggested people
diff --git a/packages/core/src/components/BaseInput/BaseInput.tsx b/packages/core/src/components/BaseInput/BaseInput.tsx index 186dab93a1..f9b5cecada 100644 --- a/packages/core/src/components/BaseInput/BaseInput.tsx +++ b/packages/core/src/components/BaseInput/BaseInput.tsx @@ -1,43 +1,49 @@ import React, { forwardRef } from "react"; import cx from "classnames"; import styles from "./BaseInput.module.scss"; -import { BaseInputComponent } from "./BaseInput.types"; +import { BaseInputProps } from "./BaseInput.types"; import { getStyle } from "../../helpers/typesciptCssModulesHelper"; -const BaseInput: BaseInputComponent = forwardRef( +const BaseInput = forwardRef( ( { size = "medium", - leftRender, - rightRender, + renderLeft, + renderRight, success, error, wrapperRole, inputRole, className, - wrapperClassName, + inputClassName, ...props - }, - ref + }: BaseInputProps, + ref: React.ForwardedRef ) => { const wrapperClassNames = cx( styles.wrapper, { - [styles.rightThinnerPadding]: !rightRender, + [styles.rightThinnerPadding]: !renderRight, [styles.error]: error, [styles.success]: success, [styles.readOnly]: props.readOnly, [styles.disabled]: props.disabled }, getStyle(styles, size), - wrapperClassName + className ); return (
- {leftRender} - - {rightRender} + {renderLeft} + + {renderRight}
); } diff --git a/packages/core/src/components/BaseInput/BaseInput.types.ts b/packages/core/src/components/BaseInput/BaseInput.types.ts index bca33d94e7..c5f6feaf10 100644 --- a/packages/core/src/components/BaseInput/BaseInput.types.ts +++ b/packages/core/src/components/BaseInput/BaseInput.types.ts @@ -1,21 +1,49 @@ import { AriaRole, InputHTMLAttributes, ReactNode } from "react"; import { VibeComponentProps } from "../../types"; import { BASE_SIZES } from "../../constants"; -import VibeComponent from "../../types/VibeComponent"; export type InputSize = (typeof BASE_SIZES)[keyof typeof BASE_SIZES]; type BaseInputNativeInputProps = Omit, "size" | "role">; type Renderer = ReactNode | ReactNode[]; export interface BaseInputProps extends BaseInputNativeInputProps, VibeComponentProps { + /** + * Size of the input element. Will influence also padding and font size. + */ size?: InputSize; - leftRender?: Renderer; - rightRender?: Renderer; + /** + * A render prop function for adding a component or element to the left side of the input. + * This could be an icon, text, or any custom element that fits within the input's design. + */ + renderLeft?: Renderer; + /** + * Similar to renderLeft, but for adding an element to the right side of the input. + * Useful for clear buttons, password visibility toggles, or custom validation icons. + */ + renderRight?: Renderer; + /** + * When true, indicates that the input has successfully passed validation or meets some criteria. + * This control the visual styling of the input to convey success to the user. + */ success?: boolean; + /** + * When true, indicates that there is an error with the input's current value. + * This control the visual styling of the input to convey error to the user. + */ error?: boolean; + /** + * ARIA role for the input wrapper. This can be used to improve accessibility by + * giving screen readers more context about the input's purpose. + */ wrapperRole?: AriaRole; + /** + * ARIA role for the input. Setting this helps in making the input more + * accessible by providing additional semantic information to assistive technologies. + */ inputRole?: AriaRole; - wrapperClassName?: string; + /** + * Additional CSS class names to be applied to the input element. This allows for custom + * styling on top of the default styles provided by the component. + */ + inputClassName?: string; } - -export type BaseInputComponent = VibeComponent; diff --git a/packages/core/src/components/BaseInput/__tests__/BaseInput.jest.tsx b/packages/core/src/components/BaseInput/__tests__/BaseInput.jest.tsx index 73747cafcb..d868901813 100644 --- a/packages/core/src/components/BaseInput/__tests__/BaseInput.jest.tsx +++ b/packages/core/src/components/BaseInput/__tests__/BaseInput.jest.tsx @@ -22,9 +22,9 @@ describe("BaseInput", () => { }); it("should show left and right elements when provided", () => { - const leftRender =
Left
; - const rightRender =
Right
; - const { getByText } = renderBaseInput({ leftRender, rightRender }); + const renderLeft =
Left
; + const renderRight =
Right
; + const { getByText } = renderBaseInput({ renderLeft, renderRight }); expect(getByText("Left")).toBeInTheDocument(); expect(getByText("Right")).toBeInTheDocument(); @@ -42,7 +42,7 @@ describe("BaseInput", () => { }); it("should apply the className for input and wrapperClassName for wrapper", () => { - const { getByLabelText } = renderBaseInput({ className: "inputClass", wrapperClassName: "customWrapper" }); + const { getByLabelText } = renderBaseInput({ className: "customWrapper", inputClassName: "inputClass" }); expect(getByLabelText("base-input")).toHaveClass("inputClass"); expect(getByLabelText("base-input").parentNode).toHaveClass("customWrapper"); }); diff --git a/packages/core/src/components/Chips/__stories__/chips.stories.js b/packages/core/src/components/Chips/__stories__/chips.stories.js index 609792eb95..b742a80e3a 100644 --- a/packages/core/src/components/Chips/__stories__/chips.stories.js +++ b/packages/core/src/components/Chips/__stories__/chips.stories.js @@ -4,7 +4,7 @@ import Chips from "../Chips"; import Text from "../../Text/Text"; import { createStoryMetaSettingsDecorator } from "../../../storybook"; import { createComponentTemplate } from "vibe-storybook-components"; -import Search from "../../Search/Search"; +import LegacySearch from "../../LegacySearch/LegacySearch"; import Avatar from "../../Avatar/Avatar"; import DialogContentContainer from "../../DialogContentContainer/DialogContentContainer"; import { Email } from "../../Icon/Icons"; @@ -187,7 +187,7 @@ export const ColorfulChipsForDifferentContent = {
- +
@@ -206,7 +206,7 @@ export const ColorfulChipsForDifferentContent = { export const ChipsInAPersonPickerComboBox = { render: () => ( - + diff --git a/packages/core/src/components/Combobox/Combobox.tsx b/packages/core/src/components/Combobox/Combobox.tsx index 84de5f07aa..1a508ceaa3 100644 --- a/packages/core/src/components/Combobox/Combobox.tsx +++ b/packages/core/src/components/Combobox/Combobox.tsx @@ -6,7 +6,7 @@ import { isFunction, noop as NOOP } from "lodash-es"; import { getStyle } from "../../helpers/typesciptCssModulesHelper"; import { ComponentDefaultTestId, getTestId } from "../../tests/test-ids-utils"; import useMergeRef from "../../hooks/useMergeRef"; -import Search from "../Search/Search"; +import LegacySearch from "../LegacySearch/LegacySearch"; import { BASE_SIZES } from "../../constants"; import Button from "../Button/Button"; import Text from "../Text/Text"; @@ -283,7 +283,7 @@ const Combobox: React.FC & { ellipsis={false} >
- - + ; + const { getByText } = renderSearch({ renderAction: AdditionalActionButton }); + expect(getByText("Extra Action")).toBeInTheDocument(); + }); + + describe("a11y", () => { + it("should have default input role when searchResultsContainerId is not provided", () => { + const { getByRole } = renderSearch(); + expect(getByRole("searchbox")).toBeInTheDocument(); + }); + + it("should have role 'combobox' when searchResultsContainerId is provided", () => { + const { getByRole } = renderSearch({ searchResultsContainerId: "results" }); + expect(getByRole("combobox")).toBeInTheDocument(); + }); + + it("should set aria-owns when searchResultsContainerId is provided", () => { + const { getByRole } = renderSearch({ searchResultsContainerId: "search-results" }); + expect(getByRole("combobox")).toHaveAttribute("aria-owns", "search-results"); + }); + + it("should set aria-activedescendant when activeDescendant is provided", () => { + const { getByRole } = renderSearch({ currentAriaResultId: "option-1" }); + expect(getByRole("searchbox")).toHaveAttribute("aria-activedescendant", "option-1"); + }); + + it("should set aria-busy when loading is true", () => { + const { getByRole } = renderSearch({ loading: true }); + expect(getByRole("searchbox")).toHaveAttribute("aria-busy", "true"); + }); + }); + + describe("interactions", () => { + it("should handle focus and blur events", () => { + const onFocus = jest.fn(); + const onBlur = jest.fn(); + const { getByRole } = renderSearch({ onFocus, onBlur }); + const input = getByRole("searchbox"); + userEvent.click(input); + expect(onFocus).toHaveBeenCalled(); + userEvent.tab(); + expect(onBlur).toHaveBeenCalled(); + }); + }); +}); diff --git a/packages/core/src/components/index.js b/packages/core/src/components/index.js index 10600235a1..407e862af6 100644 --- a/packages/core/src/components/index.js +++ b/packages/core/src/components/index.js @@ -1,7 +1,7 @@ export { default as Loader } from "./Loader/Loader"; export { default as Icon } from "./Icon/Icon"; -export { default as SearchComponent } from "./Search/Search"; // TODO: remove when bumping to version 1.0.0 -export { default as Search } from "./Search/Search"; +export { default as SearchComponent } from "./LegacySearch/LegacySearch"; // TODO: remove when bumping to version 1.0.0 +export { default as Search } from "./LegacySearch/LegacySearch"; export { default as InputField } from "./TextField/TextField"; // TODO: remove when bumping to version 1.0.0 export { default as TextField } from "./TextField/TextField"; export { default as Dialog } from "./Dialog/Dialog"; diff --git a/packages/core/src/hooks/useActiveDescendantListFocus/__stories__/hooksDummyComponents/UseActiveDescendantListFocus.jsx b/packages/core/src/hooks/useActiveDescendantListFocus/__stories__/hooksDummyComponents/UseActiveDescendantListFocus.jsx index fbc7708dfb..e8861993b4 100644 --- a/packages/core/src/hooks/useActiveDescendantListFocus/__stories__/hooksDummyComponents/UseActiveDescendantListFocus.jsx +++ b/packages/core/src/hooks/useActiveDescendantListFocus/__stories__/hooksDummyComponents/UseActiveDescendantListFocus.jsx @@ -2,7 +2,6 @@ import React from "react"; import PropTypes from "prop-types"; import useActiveDescendantListFocus from "../../../useActiveDescendantListFocus"; -import Search from "../../../../components/Search/Search"; const UseActiveDescendantListFocus = ({ focusedElementRef, // the reference for the component that listens to keyboard diff --git a/packages/core/src/next.ts b/packages/core/src/next.ts index f15fc33d63..6284fe5f57 100644 --- a/packages/core/src/next.ts +++ b/packages/core/src/next.ts @@ -2,5 +2,6 @@ export * from "./components/Heading/Heading"; import Heading from "./components/Heading/Heading"; export * from "./components/EditableHeading/EditableHeading"; import EditableHeading from "./components/EditableHeading/EditableHeading"; +import Search from "./components/Search/Search"; -export { Heading, EditableHeading }; +export { Heading, EditableHeading, Search }; diff --git a/packages/core/src/storybook/components/related-components/descriptions/search-description.jsx b/packages/core/src/storybook/components/related-components/descriptions/search-description.jsx index 8e9b526abc..9077273ad1 100644 --- a/packages/core/src/storybook/components/related-components/descriptions/search-description.jsx +++ b/packages/core/src/storybook/components/related-components/descriptions/search-description.jsx @@ -1,6 +1,6 @@ import { useMemo } from "react"; import { RelatedComponent } from "vibe-storybook-components"; -import Search from "../../../../components/Search/Search"; +import LegacySearch from "../../../../components/LegacySearch/LegacySearch"; export const SearchDescription = () => { const component = useMemo(() => { @@ -9,7 +9,7 @@ export const SearchDescription = () => { }; return (
- +
); }, []); diff --git a/packages/core/src/storybook/stand-alone-documentaion/catalog/Catalog/Catalog.stories.templates.tsx b/packages/core/src/storybook/stand-alone-documentaion/catalog/Catalog/Catalog.stories.templates.tsx index 7ce77b5b18..aa97187145 100644 --- a/packages/core/src/storybook/stand-alone-documentaion/catalog/Catalog/Catalog.stories.templates.tsx +++ b/packages/core/src/storybook/stand-alone-documentaion/catalog/Catalog/Catalog.stories.templates.tsx @@ -2,7 +2,7 @@ import { useState } from "react"; import { RelatedComponentsDecorator } from "../../../components"; import { RelatedComponents } from "vibe-storybook-components"; import { DESCRIPTION_COMPONENTS_WITHOUT_GENERAL_DESCRIPTION_MAP } from "../../../components/related-components/component-description-map"; -import { Search } from "../../../../components"; +import Search from "../../../../components/Search/Search"; import { CatalogEmptyState } from "../EmptyState/Catalog.stories.EmptyState"; import styles from "./Catalog.stories.templates.module.scss"; @@ -18,12 +18,7 @@ export const CatalogTemplate = () => { return (
- +