From f1a2f31e1a8282cc88a98d2f6aa06aae4f98c85b Mon Sep 17 00:00:00 2001 From: Yossi Saadi Date: Tue, 9 Apr 2024 16:33:06 +0300 Subject: [PATCH 01/28] chore(BaseInput): reusable, internal Input component --- .../BaseInput/BaseInput.module.scss | 112 ++++++++++++++++++ .../src/components/BaseInput/BaseInput.tsx | 35 ++++++ .../components/BaseInput/BaseInput.types.ts | 16 +++ 3 files changed, 163 insertions(+) create mode 100644 packages/core/src/components/BaseInput/BaseInput.module.scss create mode 100644 packages/core/src/components/BaseInput/BaseInput.tsx create mode 100644 packages/core/src/components/BaseInput/BaseInput.types.ts diff --git a/packages/core/src/components/BaseInput/BaseInput.module.scss b/packages/core/src/components/BaseInput/BaseInput.module.scss new file mode 100644 index 0000000000..e3043909d5 --- /dev/null +++ b/packages/core/src/components/BaseInput/BaseInput.module.scss @@ -0,0 +1,112 @@ +@import "../../styles/Typography"; + +.wrapper { + width: 100%; + position: relative; + display: flex; + align-items: center; + gap: var(--spacing-small); + padding-block: var(--spacing-xs); + padding-inline: var(--spacing-medium) var(--spacing-xs); + + @include vibe-text(text1, normal); + @include smoothing-text; + + outline: none; + border: 1px solid var(--ui-border-color); + border-radius: var(--border-radius-small); + color: var(--primary-text-color); + background-color: var(--secondary-background-color); + transition: border-color var(--motion-productive-medium) ease-in; + + &.small { + @include vibe-text(text2, normal); + } + + &.large { + padding-block: var(--spacing-small); + } + + &.underline { + border-left: none; + border-right: none; + border-top: none; + padding: var(--spacing-small) var(--spacing-large) var(--spacing-small) var(--spacing-xs); + border-radius: 0 !important; + } + + &.rightThinnerPadding { + padding-inline-end: var(--spacing-medium); + } + + &:hover { + border-color: var(--primary-text-color); + } + + &:has(.input:active, .input:focus) { + border-color: var(--primary-color); + } + + // TODO in next major remove withReadOnlyStyle, should only be determined by read-only existence on input + &:has(.input:read-only).withReadOnlyStyle { + background-color: var(--allgrey-background-color); + border: none; + + .input { + background-color: var(--allgrey-background-color); + } + } + + &:has(.input:disabled) { + cursor: not-allowed; + user-select: none; + border: none; + pointer-events: none; + background-color: var(--disabled-background-color); + } + + &.success { + border-color: var(--positive-color); + + &:hover { + border-color: var(--positive-color); + } + + &:has(.input:active, .input:focus) { + border-color: var(--positive-color); + } + } + + &:has(.input[aria-invalid="true"]) { + border-color: var(--negative-color); + + &:hover { + border-color: var(--negative-color); + } + + &:has(.input:active, .input:focus) { + border-color: var(--negative-color); + } + } + + .input { + all: unset; + + width: 100%; + height: 100%; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + border: none; + outline: none; + + &::placeholder { + color: var(--secondary-text-color); + font-weight: 400; + } + + &:disabled::placeholder { + color: var(--disabled-text-color); + } + } +} diff --git a/packages/core/src/components/BaseInput/BaseInput.tsx b/packages/core/src/components/BaseInput/BaseInput.tsx new file mode 100644 index 0000000000..2e93f52ccb --- /dev/null +++ b/packages/core/src/components/BaseInput/BaseInput.tsx @@ -0,0 +1,35 @@ +import { forwardRef } from "react"; +import cx from "classnames"; +import styles from "./BaseInput.module.scss"; +import { BaseInputProps } from "./BaseInput.types"; +import VibeComponent from "../../types/VibeComponent"; +import { getStyle } from "../../helpers/typesciptCssModulesHelper"; + +const BaseInput: VibeComponent = forwardRef( + ({ inputSize = "medium", leftRender, rightRender, withReadOnlyStyle, underline, success, error, ...props }, ref) => { + const wrapperClassNames = cx( + styles.wrapper, + { + [styles.rightThinnerPadding]: !rightRender, + [styles.withReadOnlyStyle]: withReadOnlyStyle, + [styles.underline]: underline, + [styles.success]: success + }, + getStyle(styles, inputSize) + ); + + return ( +
+ {leftRender} + + {rightRender} +
+ ); + } +); + +export default BaseInput; + +const App = () => { + return ; +}; diff --git a/packages/core/src/components/BaseInput/BaseInput.types.ts b/packages/core/src/components/BaseInput/BaseInput.types.ts new file mode 100644 index 0000000000..2195c51be3 --- /dev/null +++ b/packages/core/src/components/BaseInput/BaseInput.types.ts @@ -0,0 +1,16 @@ +import { InputHTMLAttributes, ReactNode } from "react"; +import { VibeComponentProps } from "../../types"; +import { BASE_SIZES } from "../../constants"; + +type InputSize = (typeof BASE_SIZES)[keyof typeof BASE_SIZES]; +type Renderer = ReactNode | ReactNode[]; + +export interface BaseInputProps extends InputHTMLAttributes, VibeComponentProps { + inputSize?: InputSize; + leftRender?: Renderer; + rightRender?: Renderer; + withReadOnlyStyle?: boolean; + underline?: boolean; + success?: boolean; + error?: boolean; +} From 985042e03ccd10120de82604c806f16ed79206d8 Mon Sep 17 00:00:00 2001 From: Yossi Saadi Date: Tue, 9 Apr 2024 18:07:13 +0300 Subject: [PATCH 02/28] chore(BaseInput): set hard coded height for sizes --- .../core/src/components/BaseInput/BaseInput.module.scss | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/core/src/components/BaseInput/BaseInput.module.scss b/packages/core/src/components/BaseInput/BaseInput.module.scss index e3043909d5..b8025a995b 100644 --- a/packages/core/src/components/BaseInput/BaseInput.module.scss +++ b/packages/core/src/components/BaseInput/BaseInput.module.scss @@ -20,10 +20,16 @@ transition: border-color var(--motion-productive-medium) ease-in; &.small { + height: 32px; @include vibe-text(text2, normal); } + &.medium { + height: 40px; + } + &.large { + height: 48px; padding-block: var(--spacing-small); } From a141fe42171cff67c0e18f44f6cef58c410a1b51 Mon Sep 17 00:00:00 2001 From: Yossi Saadi Date: Tue, 9 Apr 2024 18:08:05 +0300 Subject: [PATCH 03/28] refactor(BaseInput): remove mistakenly inserted example --- packages/core/src/components/BaseInput/BaseInput.tsx | 4 ---- 1 file changed, 4 deletions(-) diff --git a/packages/core/src/components/BaseInput/BaseInput.tsx b/packages/core/src/components/BaseInput/BaseInput.tsx index 2e93f52ccb..8ee76ec5c9 100644 --- a/packages/core/src/components/BaseInput/BaseInput.tsx +++ b/packages/core/src/components/BaseInput/BaseInput.tsx @@ -29,7 +29,3 @@ const BaseInput: VibeComponent = forwardRef( ); export default BaseInput; - -const App = () => { - return ; -}; From 0c2be3921ffdb3e0c828c4a2e8e808690dd53ff0 Mon Sep 17 00:00:00 2001 From: Yossi Saadi Date: Thu, 11 Apr 2024 17:31:21 +0300 Subject: [PATCH 04/28] feat(BaseInput): update api, allow change role of wrapper and input, add className for wrapper --- .../src/components/BaseInput/BaseInput.tsx | 32 +++++++++++++------ .../components/BaseInput/BaseInput.types.ts | 16 +++++++--- 2 files changed, 34 insertions(+), 14 deletions(-) diff --git a/packages/core/src/components/BaseInput/BaseInput.tsx b/packages/core/src/components/BaseInput/BaseInput.tsx index 8ee76ec5c9..fc0242dd80 100644 --- a/packages/core/src/components/BaseInput/BaseInput.tsx +++ b/packages/core/src/components/BaseInput/BaseInput.tsx @@ -1,27 +1,41 @@ -import { forwardRef } from "react"; +import React, { forwardRef } from "react"; import cx from "classnames"; import styles from "./BaseInput.module.scss"; -import { BaseInputProps } from "./BaseInput.types"; -import VibeComponent from "../../types/VibeComponent"; +import { BaseInputComponent } from "./BaseInput.types"; import { getStyle } from "../../helpers/typesciptCssModulesHelper"; -const BaseInput: VibeComponent = forwardRef( - ({ inputSize = "medium", leftRender, rightRender, withReadOnlyStyle, underline, success, error, ...props }, ref) => { +const BaseInput: BaseInputComponent = forwardRef( + ( + { + size = "medium", + leftRender, + rightRender, + withReadOnlyStyle, + success, + error, + wrapperRole, + inputRole, + className, + wrapperClassName, + ...props + }, + ref + ) => { const wrapperClassNames = cx( styles.wrapper, { [styles.rightThinnerPadding]: !rightRender, [styles.withReadOnlyStyle]: withReadOnlyStyle, - [styles.underline]: underline, [styles.success]: success }, - getStyle(styles, inputSize) + getStyle(styles, size), + wrapperClassName ); return ( -
+
{leftRender} - + {rightRender}
); diff --git a/packages/core/src/components/BaseInput/BaseInput.types.ts b/packages/core/src/components/BaseInput/BaseInput.types.ts index 2195c51be3..4ec530a2ec 100644 --- a/packages/core/src/components/BaseInput/BaseInput.types.ts +++ b/packages/core/src/components/BaseInput/BaseInput.types.ts @@ -1,16 +1,22 @@ -import { InputHTMLAttributes, ReactNode } from "react"; +import { AriaRole, InputHTMLAttributes, ReactNode } from "react"; import { VibeComponentProps } from "../../types"; import { BASE_SIZES } from "../../constants"; +import VibeComponent from "../../types/VibeComponent"; -type InputSize = (typeof BASE_SIZES)[keyof typeof BASE_SIZES]; +export type InputSize = (typeof BASE_SIZES)[keyof typeof BASE_SIZES]; +type BaseInputNativeInputProps = Omit, "size" | "role">; type Renderer = ReactNode | ReactNode[]; -export interface BaseInputProps extends InputHTMLAttributes, VibeComponentProps { - inputSize?: InputSize; +export interface BaseInputProps extends BaseInputNativeInputProps, VibeComponentProps { + size?: InputSize; leftRender?: Renderer; rightRender?: Renderer; withReadOnlyStyle?: boolean; - underline?: boolean; success?: boolean; error?: boolean; + wrapperRole?: AriaRole; + inputRole?: AriaRole; + wrapperClassName?: string; } + +export type BaseInputComponent = VibeComponent; From 3e7f95813cbcc4f5158a4a7d8e050a55eba1ad9e Mon Sep 17 00:00:00 2001 From: Yossi Saadi Date: Thu, 11 Apr 2024 17:32:02 +0300 Subject: [PATCH 05/28] feat(BaseInput): remove underline, allow input to always take maximum available space --- .../core/src/components/BaseInput/BaseInput.module.scss | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/packages/core/src/components/BaseInput/BaseInput.module.scss b/packages/core/src/components/BaseInput/BaseInput.module.scss index b8025a995b..9cd6b4157b 100644 --- a/packages/core/src/components/BaseInput/BaseInput.module.scss +++ b/packages/core/src/components/BaseInput/BaseInput.module.scss @@ -33,14 +33,6 @@ padding-block: var(--spacing-small); } - &.underline { - border-left: none; - border-right: none; - border-top: none; - padding: var(--spacing-small) var(--spacing-large) var(--spacing-small) var(--spacing-xs); - border-radius: 0 !important; - } - &.rightThinnerPadding { padding-inline-end: var(--spacing-medium); } @@ -98,6 +90,7 @@ .input { all: unset; + flex: 1; width: 100%; height: 100%; white-space: nowrap; From 400f06daaea40282503e7d7e1d50ac58bdb67b39 Mon Sep 17 00:00:00 2001 From: Yossi Saadi Date: Thu, 11 Apr 2024 17:33:16 +0300 Subject: [PATCH 06/28] test(BaseInput): add tests for basic usage of component --- .../BaseInput/__tests__/BaseInput.jest.tsx | 109 ++++++++++++++++++ 1 file changed, 109 insertions(+) create mode 100644 packages/core/src/components/BaseInput/__tests__/BaseInput.jest.tsx diff --git a/packages/core/src/components/BaseInput/__tests__/BaseInput.jest.tsx b/packages/core/src/components/BaseInput/__tests__/BaseInput.jest.tsx new file mode 100644 index 0000000000..5439e8c96f --- /dev/null +++ b/packages/core/src/components/BaseInput/__tests__/BaseInput.jest.tsx @@ -0,0 +1,109 @@ +import React from "react"; +import { render } from "@testing-library/react"; +import "@testing-library/jest-dom"; +import userEvent from "@testing-library/user-event"; +import BaseInput from "../BaseInput"; +import { BaseInputProps } from "../BaseInput.types"; + +function renderBaseInput(props?: Partial) { + return render(); +} + +describe("BaseInput", () => { + it("should render correctly", () => { + const { getByLabelText } = renderBaseInput(); + expect(getByLabelText("base-input")).toBeInTheDocument(); + }); + + describe("with declared props", () => { + it("should apply the size class", () => { + const { getByLabelText } = renderBaseInput({ size: "large" }); + expect(getByLabelText("base-input").parentNode).toHaveClass("large"); + }); + + it("should show left and right elements when provided", () => { + const leftRender =
Left
; + const rightRender =
Right
; + const { getByText } = renderBaseInput({ leftRender, rightRender }); + + expect(getByText("Left")).toBeInTheDocument(); + expect(getByText("Right")).toBeInTheDocument(); + }); + + it("should apply the success class", () => { + const { getByLabelText } = renderBaseInput({ success: true }); + expect(getByLabelText("base-input").parentNode).toHaveClass("success"); + }); + + it("should apply wrapper and input role correctly", () => { + const { getByRole } = renderBaseInput({ wrapperRole: "search", inputRole: "combobox" }); + expect(getByRole("search")).toBeInTheDocument(); + expect(getByRole("combobox")).toBeInTheDocument(); + }); + + it("should apply the className for input and wrapperClassName for wrapper", () => { + const { getByLabelText } = renderBaseInput({ className: "inputClass", wrapperClassName: "customWrapper" }); + expect(getByLabelText("base-input")).toHaveClass("inputClass"); + expect(getByLabelText("base-input").parentNode).toHaveClass("customWrapper"); + }); + + it("should forward ref to the input element", () => { + const ref = React.createRef(); + const { getByLabelText } = render(); + expect(ref.current).toBe(getByLabelText("input-base")); + }); + }); + + describe("a11y", () => { + it("should not apply aria-invalid when error prop is not supplied", () => { + const { getByLabelText } = renderBaseInput(); + expect(getByLabelText("base-input")).not.toHaveAttribute("aria-invalid"); + }); + + it("should apply aria-invalid when error prop is supplied", () => { + const { getByLabelText } = renderBaseInput({ error: true }); + expect(getByLabelText("base-input")).toHaveAttribute("aria-invalid", "true"); + }); + }); + + describe("interactions", () => { + it("should capture user input correctly", () => { + const { getByLabelText } = renderBaseInput(); + const input = getByLabelText("base-input"); + userEvent.type(input, "Hello, World!"); + expect(input).toHaveValue("Hello, World!"); + }); + + it("should call onChange on every input", () => { + const onChange = jest.fn(); + const { getByLabelText } = renderBaseInput({ onChange }); + const input = getByLabelText("base-input"); + userEvent.type(input, "Hello, World!"); + expect(onChange).toHaveBeenCalledTimes("Hello, World!".length); + }); + + it("should handle focus and blur events", () => { + const onFocus = jest.fn(); + const onBlur = jest.fn(); + const { getByLabelText } = renderBaseInput({ onFocus, onBlur }); + const input = getByLabelText("base-input"); + userEvent.click(input); + expect(onFocus).toHaveBeenCalled(); + userEvent.tab(); + expect(onBlur).toHaveBeenCalled(); + }); + + it("should handle key down and up", () => { + const onEnterDown = jest.fn(); + const onKeyDown = (e: React.KeyboardEvent) => e.key === "Enter" && onEnterDown(); + const onEnterUp = jest.fn(); + const onKeyUp = (e: React.KeyboardEvent) => e.key === "Enter" && onEnterUp(); + const { getByLabelText } = renderBaseInput({ onKeyDown, onKeyUp }); + + const input = getByLabelText("base-input"); + userEvent.type(input, "{enter}"); + expect(onEnterDown).toHaveBeenCalled(); + expect(onEnterUp).toHaveBeenCalled(); + }); + }); +}); From 15c9695d77676a8511a2b1fc5c11b03f7cc8e618 Mon Sep 17 00:00:00 2001 From: Yossi Saadi Date: Thu, 11 Apr 2024 17:49:07 +0300 Subject: [PATCH 07/28] docs(BaseInput): internal story for component (hidden in prod) --- .../__stories__/BaseInput.stories.tsx | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 packages/core/src/components/BaseInput/__stories__/BaseInput.stories.tsx diff --git a/packages/core/src/components/BaseInput/__stories__/BaseInput.stories.tsx b/packages/core/src/components/BaseInput/__stories__/BaseInput.stories.tsx new file mode 100644 index 0000000000..b1aa37adcc --- /dev/null +++ b/packages/core/src/components/BaseInput/__stories__/BaseInput.stories.tsx @@ -0,0 +1,21 @@ +import { createStoryMetaSettingsDecorator } from "../../../storybook"; +import { createComponentTemplate } from "vibe-storybook-components"; +import BaseInput from "../BaseInput"; + +const metaSettings = createStoryMetaSettingsDecorator({ + component: BaseInput +}); + +export default { + title: "Internal/BaseInput", + component: BaseInput, + argTypes: metaSettings.argTypes, + decorators: metaSettings.decorators, + tags: ["internal"] +}; + +const baseInputTemplate = createComponentTemplate(BaseInput); + +export const Overview = { + render: baseInputTemplate.bind({}) +}; From 73e5f24fe7b439f479b729d6b3f9111bde3ce669 Mon Sep 17 00:00:00 2001 From: Yossi Saadi Date: Mon, 15 Apr 2024 15:16:49 +0300 Subject: [PATCH 08/28] feat(BaseInput): add flex-shrink 0, to avoid the fixed height not being respected --- packages/core/src/components/BaseInput/BaseInput.module.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/core/src/components/BaseInput/BaseInput.module.scss b/packages/core/src/components/BaseInput/BaseInput.module.scss index 9cd6b4157b..0dceb5e14d 100644 --- a/packages/core/src/components/BaseInput/BaseInput.module.scss +++ b/packages/core/src/components/BaseInput/BaseInput.module.scss @@ -5,6 +5,7 @@ position: relative; display: flex; align-items: center; + flex-shrink: 0; gap: var(--spacing-small); padding-block: var(--spacing-xs); padding-inline: var(--spacing-medium) var(--spacing-xs); From b5ecf9fb31187c31f456f1257a804ce66357bab5 Mon Sep 17 00:00:00 2001 From: Yossi Saadi Date: Wed, 17 Apr 2024 09:44:50 +0300 Subject: [PATCH 09/28] fix(BaseInput): fix import of typography styles, add import to mixins --- packages/core/src/components/BaseInput/BaseInput.module.scss | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/core/src/components/BaseInput/BaseInput.module.scss b/packages/core/src/components/BaseInput/BaseInput.module.scss index 0dceb5e14d..7776bbb9b3 100644 --- a/packages/core/src/components/BaseInput/BaseInput.module.scss +++ b/packages/core/src/components/BaseInput/BaseInput.module.scss @@ -1,4 +1,5 @@ -@import "../../styles/Typography"; +@import "../../styles/typography"; +@import "~monday-ui-style/dist/mixins"; .wrapper { width: 100%; From 82d95977473de4915a0b65e3e42f522c6dece949 Mon Sep 17 00:00:00 2001 From: Yossi Saadi Date: Wed, 17 Apr 2024 13:09:38 +0300 Subject: [PATCH 10/28] refactor(BaseInput): only use has for supported browsers, otherwise use class for wrapper --- .../BaseInput/BaseInput.module.scss | 110 +++++++++++++----- .../src/components/BaseInput/BaseInput.tsx | 7 +- .../components/BaseInput/BaseInput.types.ts | 1 - 3 files changed, 87 insertions(+), 31 deletions(-) diff --git a/packages/core/src/components/BaseInput/BaseInput.module.scss b/packages/core/src/components/BaseInput/BaseInput.module.scss index 7776bbb9b3..3a2c6e97bd 100644 --- a/packages/core/src/components/BaseInput/BaseInput.module.scss +++ b/packages/core/src/components/BaseInput/BaseInput.module.scss @@ -43,49 +43,105 @@ border-color: var(--primary-text-color); } - &:has(.input:active, .input:focus) { - border-color: var(--primary-color); - } - - // TODO in next major remove withReadOnlyStyle, should only be determined by read-only existence on input - &:has(.input:read-only).withReadOnlyStyle { - background-color: var(--allgrey-background-color); - border: none; + @supports selector(:has(*)) { + &:has(.input:active, .input:focus) { + border-color: var(--primary-color); + } - .input { + &:has(.input:read-only) { background-color: var(--allgrey-background-color); + border: none; + + .input { + background-color: var(--allgrey-background-color); + } } - } - &:has(.input:disabled) { - cursor: not-allowed; - user-select: none; - border: none; - pointer-events: none; - background-color: var(--disabled-background-color); - } + &:has(.input:disabled) { + cursor: not-allowed; + user-select: none; + border: none; + pointer-events: none; + background-color: var(--disabled-background-color); - &.success { - border-color: var(--positive-color); + .input { + background-color: var(--disabled-background-color); + } + } - &:hover { + &.success { border-color: var(--positive-color); + + &:hover { + border-color: var(--positive-color); + } + + &:has(.input:active, .input:focus) { + border-color: var(--positive-color); + } } - &:has(.input:active, .input:focus) { - border-color: var(--positive-color); + &:has(.input[aria-invalid="true"]) { + border-color: var(--negative-color); + + &:hover { + border-color: var(--negative-color); + } + + &:has(.input:active, .input:focus) { + border-color: var(--negative-color); + } } } - &:has(.input[aria-invalid="true"]) { - border-color: var(--negative-color); + @supports not selector(:has(*)) { + &:focus-within { + border-color: var(--primary-color); + } - &:hover { - border-color: var(--negative-color); + &.readOnly { + background-color: var(--allgrey-background-color); + border: none; + + .input { + background-color: var(--allgrey-background-color); + } } - &:has(.input:active, .input:focus) { + &.disabled { + cursor: not-allowed; + user-select: none; + border: none; + pointer-events: none; + background-color: var(--disabled-background-color); + + .input { + background-color: var(--disabled-background-color); + } + } + + &.success { + border-color: var(--positive-color); + + &:hover { + border-color: var(--positive-color); + } + + &:focus-within { + border-color: var(--positive-color); + } + } + + &.error { border-color: var(--negative-color); + + &:hover { + border-color: var(--negative-color); + } + + &:focus-within { + border-color: var(--negative-color); + } } } diff --git a/packages/core/src/components/BaseInput/BaseInput.tsx b/packages/core/src/components/BaseInput/BaseInput.tsx index fc0242dd80..186dab93a1 100644 --- a/packages/core/src/components/BaseInput/BaseInput.tsx +++ b/packages/core/src/components/BaseInput/BaseInput.tsx @@ -10,7 +10,6 @@ const BaseInput: BaseInputComponent = forwardRef( size = "medium", leftRender, rightRender, - withReadOnlyStyle, success, error, wrapperRole, @@ -25,8 +24,10 @@ const BaseInput: BaseInputComponent = forwardRef( styles.wrapper, { [styles.rightThinnerPadding]: !rightRender, - [styles.withReadOnlyStyle]: withReadOnlyStyle, - [styles.success]: success + [styles.error]: error, + [styles.success]: success, + [styles.readOnly]: props.readOnly, + [styles.disabled]: props.disabled }, getStyle(styles, size), wrapperClassName diff --git a/packages/core/src/components/BaseInput/BaseInput.types.ts b/packages/core/src/components/BaseInput/BaseInput.types.ts index 4ec530a2ec..bca33d94e7 100644 --- a/packages/core/src/components/BaseInput/BaseInput.types.ts +++ b/packages/core/src/components/BaseInput/BaseInput.types.ts @@ -11,7 +11,6 @@ export interface BaseInputProps extends BaseInputNativeInputProps, VibeComponent size?: InputSize; leftRender?: Renderer; rightRender?: Renderer; - withReadOnlyStyle?: boolean; success?: boolean; error?: boolean; wrapperRole?: AriaRole; From 61b9e15f50d91594b9b5e62a50b68747b177ee26 Mon Sep 17 00:00:00 2001 From: Yossi Saadi Date: Wed, 17 Apr 2024 13:34:37 +0300 Subject: [PATCH 11/28] test(BaseInput): refactor test --- .../components/BaseInput/__tests__/BaseInput.jest.tsx | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/core/src/components/BaseInput/__tests__/BaseInput.jest.tsx b/packages/core/src/components/BaseInput/__tests__/BaseInput.jest.tsx index 5439e8c96f..73747cafcb 100644 --- a/packages/core/src/components/BaseInput/__tests__/BaseInput.jest.tsx +++ b/packages/core/src/components/BaseInput/__tests__/BaseInput.jest.tsx @@ -68,18 +68,20 @@ describe("BaseInput", () => { describe("interactions", () => { it("should capture user input correctly", () => { + const expectedValue = "Hello, World!"; const { getByLabelText } = renderBaseInput(); const input = getByLabelText("base-input"); - userEvent.type(input, "Hello, World!"); - expect(input).toHaveValue("Hello, World!"); + userEvent.type(input, expectedValue); + expect(input).toHaveValue(expectedValue); }); it("should call onChange on every input", () => { + const expectedValue = "Hello, World!"; const onChange = jest.fn(); const { getByLabelText } = renderBaseInput({ onChange }); const input = getByLabelText("base-input"); - userEvent.type(input, "Hello, World!"); - expect(onChange).toHaveBeenCalledTimes("Hello, World!".length); + userEvent.type(input, expectedValue); + expect(onChange).toHaveBeenCalledTimes(expectedValue.length); }); it("should handle focus and blur events", () => { From 2978e90a8734794a3367f57ae140b3da22c9ff6a Mon Sep 17 00:00:00 2001 From: Yossi Saadi Date: Thu, 11 Apr 2024 18:23:28 +0300 Subject: [PATCH 12/28] feat(Search): new Search component --- .../src/components/Search/Search.module.scss | 48 ++--- .../core/src/components/Search/Search.tsx | 193 +++++++++--------- .../src/components/Search/Search.types.ts | 31 +++ .../Search/__stories__/Search.stories.tsx | 3 +- packages/core/src/tests/constants.ts | 1 + 5 files changed, 151 insertions(+), 125 deletions(-) create mode 100644 packages/core/src/components/Search/Search.types.ts diff --git a/packages/core/src/components/Search/Search.module.scss b/packages/core/src/components/Search/Search.module.scss index 2931570865..ae6ebbf3e4 100644 --- a/packages/core/src/components/Search/Search.module.scss +++ b/packages/core/src/components/Search/Search.module.scss @@ -1,34 +1,26 @@ -.searchWrapper input[type="search"] { - -webkit-appearance: textfield; -} - -.searchWrapper input[type="search"]::-webkit-search-decoration, -.searchWrapper input[type="search"]::-webkit-search-cancel-button, -.searchWrapper input[type="search"]::-webkit-search-results-button, -.searchWrapper input[type="search"]::-webkit-search-results-decoration { - -webkit-appearance: none; -} +.searchWrapper { + .search { + -webkit-appearance: textfield; -.searchWrapper:focus-within .searchFocusElementFirst { - animation: dashForward 5s linear forwards; -} - -.searchWrapper:focus-within .searchFocusElementSecond { - animation: dashBackwards 5s linear forwards; -} - -.search.round { - border-radius: 50px !important; -} + &::-webkit-search-decoration, + &::-webkit-search-cancel-button, + &::-webkit-search-results-button, + &::-webkit-search-results-decoration { + -webkit-appearance: none; + } + } -@keyframes dashForward { - to { - stroke-dashoffset: 0; + .loader { + margin-right: var(--spacing-xs); } -} -@keyframes dashBackwards { - to { - stroke-dashoffset: 2000; + .icon { + opacity: 1; + color: inherit; + transition: opacity var(--motion-productive-short); + + &.empty { + opacity: 0; + } } } diff --git a/packages/core/src/components/Search/Search.tsx b/packages/core/src/components/Search/Search.tsx index c7f184f421..bcd4b232ad 100644 --- a/packages/core/src/components/Search/Search.tsx +++ b/packages/core/src/components/Search/Search.tsx @@ -1,124 +1,127 @@ import cx from "classnames"; -import React, { forwardRef } from "react"; -import TextField from "../TextField/TextField"; +import React, { forwardRef, useCallback, useRef } from "react"; import useMergeRef from "../../hooks/useMergeRef"; -import { SearchDefaultIconNames, SearchType } from "./SearchConstants"; -import CloseIcon from "../Icon/Icons/components/CloseSmall"; +import CloseSmallIcon from "../Icon/Icons/components/CloseSmall"; import SearchIcon from "../Icon/Icons/components/Search"; -import { NOOP } from "../../utils/function-utils"; -import { SubIcon, VibeComponentProps, VibeComponent, withStaticProps } from "../../types"; -import { TextFieldTextType } from "../TextField/TextFieldConstants"; -import { BASE_SIZES } from "../../constants"; import { ComponentDefaultTestId, getTestId } from "../../tests/test-ids-utils"; import styles from "./Search.module.scss"; +import BaseInput from "../BaseInput/BaseInput"; +import useDebounceEvent from "../../hooks/useDebounceEvent"; +import IconButton from "../IconButton/IconButton"; +import Icon from "../Icon/Icon"; +import { SearchComponent } from "./Search.types"; +import Loader from "../Loader/Loader"; -export interface SearchProps extends VibeComponentProps { - secondaryIconName?: SubIcon; - iconName?: SubIcon; - onChange?: (value: string) => void; - autoFocus?: boolean; - value?: string; - placeholder?: string; - disabled?: boolean; - debounceRate?: number; - onBlur?: (event: React.FocusEvent) => void; - onFocus?: (event: React.FocusEvent) => void; - wrapperClassName?: string; - setRef?: () => void; - autoComplete?: string; - /* BASE_SIZES is exposed on the component itself */ - size?: (typeof BASE_SIZES)[keyof typeof BASE_SIZES]; - /* TYPES is exposed on the component itself */ - type?: SearchType; - validation?: - | { - status: "error" | "success"; - text: string; - } - | { text: string }; - inputAriaLabel?: string; - searchResultsContainerId?: string; - activeDescendant?: string; - /* Icon names labels for a11y */ - iconNames?: { - layout: string; - primary: string; - secondary: string; - }; - /** shows loading animation */ - loading?: boolean; -} - -const Search: VibeComponent & { - sizes?: typeof BASE_SIZES; - types?: typeof SearchType; -} = forwardRef( +const Search: SearchComponent = forwardRef( ( { - secondaryIconName = CloseIcon, - iconName = SearchIcon, - onChange = NOOP, - autoFocus = false, - value = "", - placeholder = "", - disabled = false, - debounceRate = 200, - onBlur = NOOP, - onFocus = NOOP, - wrapperClassName = "", - setRef = NOOP, + searchIconName = SearchIcon, + clearIconName = CloseSmallIcon, + clearIconLabel = "Clear", + additionalActionRender: AdditionalAction, + value, + placeholder, + size = "medium", + disabled, + loading, + autoFocus, autoComplete = "off", - size = BASE_SIZES.MEDIUM, - type = SearchType.SQUARE, - className, - id = "search", - validation = null, inputAriaLabel, - searchResultsContainerId = "", - activeDescendant = "", - iconNames = SearchDefaultIconNames, - loading = false, + debounceRate = 200, + searchResultsContainerId, + activeDescendant, + onChange, + onFocus, + onBlur, + wrapperClassName, + className, + id, "data-testid": dataTestId }, ref ) => { - const mergedRef = useMergeRef(ref, setRef); + const inputRef = useRef(null); + const mergedRef = useMergeRef(ref, inputRef); + + const { inputValue, onEventChanged, clearValue } = useDebounceEvent({ + delay: debounceRate, + onChange, + initialStateValue: value + }); + + const onClearIconClick = useCallback(() => { + if (disabled) { + return; + } + + inputRef.current?.focus?.(); + clearValue(); + }, [disabled, clearValue]); + + const SearchIcon = ( + + ); + + const ClearIcon = ( + + ); + + const RightRender = ( + <> + {loading && ( + + )} + {inputValue && !disabled && ClearIcon} + {AdditionalAction} + + ); + return ( - ); } ); -export default withStaticProps(Search, { - sizes: BASE_SIZES, - types: SearchType -}); +export default Search; diff --git a/packages/core/src/components/Search/Search.types.ts b/packages/core/src/components/Search/Search.types.ts new file mode 100644 index 0000000000..6e6da61051 --- /dev/null +++ b/packages/core/src/components/Search/Search.types.ts @@ -0,0 +1,31 @@ +import { SubIcon, VibeComponent, VibeComponentProps } from "../../types"; +import { ReactElement, FocusEvent, AriaAttributes } from "react"; +import { InputSize } from "../BaseInput/BaseInput.types"; +import IconButton from "../IconButton/IconButton"; +import MenuButton from "../MenuButton/MenuButton"; + +type AdditionalActionRender = ReactElement; + +export interface SearchProps extends VibeComponentProps { + searchIconName?: SubIcon; + clearIconName?: SubIcon; + clearIconLabel?: string; + additionalActionRender?: AdditionalActionRender; + value?: HTMLInputElement["value"]; + placeholder?: HTMLInputElement["placeholder"]; + size?: InputSize; + disabled?: HTMLInputElement["disabled"]; + loading?: boolean; + autoFocus?: HTMLInputElement["autofocus"]; + autoComplete?: HTMLInputElement["autocomplete"]; + inputAriaLabel?: AriaAttributes["aria-label"]; + debounceRate?: number; + searchResultsContainerId?: string; + activeDescendant?: AriaAttributes["aria-activedescendant"]; + onChange?: (value: string) => void; + onBlur?: (event: FocusEvent) => void; + onFocus?: (event: FocusEvent) => void; + wrapperClassName?: string; +} + +export type SearchComponent = VibeComponent; diff --git a/packages/core/src/components/Search/__stories__/Search.stories.tsx b/packages/core/src/components/Search/__stories__/Search.stories.tsx index e9f355dbd5..076389c1b2 100644 --- a/packages/core/src/components/Search/__stories__/Search.stories.tsx +++ b/packages/core/src/components/Search/__stories__/Search.stories.tsx @@ -7,8 +7,7 @@ import "./Search.stories.scss"; const metaSettings = createStoryMetaSettingsDecorator({ component: Search, - enumPropNamesArray: ["type", "size"], - iconPropNamesArray: ["secondaryIconName", "iconName"] + iconPropNamesArray: ["searchIconName", "clearIconName"] }); const searchTemplate = createComponentTemplate(Search); diff --git a/packages/core/src/tests/constants.ts b/packages/core/src/tests/constants.ts index 2761576332..929389125a 100644 --- a/packages/core/src/tests/constants.ts +++ b/packages/core/src/tests/constants.ts @@ -24,6 +24,7 @@ export enum ComponentDefaultTestId { TEXT_FIELD = "text-field", TEXT_FIELD_SECONDARY_BUTTON = "text-field-secondary-button", SEARCH = "search", + SEARCH_ICON = "search-icon", CLEAN_SEARCH_BUTTON = "clean-search-button", COLOR_PICKER_ITEM = "color-picker-item", ICON_BUTTON = "icon-button", From a6da48906a80f5c11dfeb5cf66d63621bb7733f8 Mon Sep 17 00:00:00 2001 From: Yossi Saadi Date: Sun, 14 Apr 2024 14:13:35 +0300 Subject: [PATCH 13/28] test(Search): add tests to new component --- .../Search/__tests__/Search.jest.tsx | 139 ++++++++++++++++++ 1 file changed, 139 insertions(+) create mode 100644 packages/core/src/components/Search/__tests__/Search.jest.tsx diff --git a/packages/core/src/components/Search/__tests__/Search.jest.tsx b/packages/core/src/components/Search/__tests__/Search.jest.tsx new file mode 100644 index 0000000000..7ec8a7bd88 --- /dev/null +++ b/packages/core/src/components/Search/__tests__/Search.jest.tsx @@ -0,0 +1,139 @@ +import React from "react"; +import { render } from "@testing-library/react"; +import "@testing-library/jest-dom"; +import userEvent from "@testing-library/user-event"; +import Search from "../Search"; +import { SearchProps } from "../Search.types"; + +function renderSearch(props?: Partial) { + return render(); +} + +describe("Search", () => { + it("should render correctly", () => { + const { getByRole } = renderSearch(); + expect(getByRole("searchbox")).toBeInTheDocument(); + }); + + it("should display the search icon by default", () => { + const { getByTestId } = renderSearch(); + expect(getByTestId("search-icon")).toBeInTheDocument(); + }); + + it("should not display the clear icon when the input is empty", () => { + const { queryByLabelText } = renderSearch(); + expect(queryByLabelText("Clear")).toBeNull(); + }); + + it("should display both the search icon and clear icon when input has value", async () => { + const { getByTestId } = renderSearch({ value: "Test" }); + expect(getByTestId("search-icon")).toBeInTheDocument(); + expect(getByTestId("clean-search-button")).toBeInTheDocument(); + }); + + it("should clear the input value when the clear icon is clicked", async () => { + const { getByRole, getByLabelText } = renderSearch({ value: "Test" }); + userEvent.click(getByLabelText("Clear")); + expect(getByRole("searchbox")).toHaveValue(""); + }); + + it("should display the clear icon once user inputs", async () => { + const { getByRole, getByTestId } = renderSearch(); + userEvent.type(getByRole("searchbox"), "Test"); + expect(getByTestId("clean-search-button")).toBeInTheDocument(); + }); + + it("should display a loader when the loading prop is true", () => { + const { getByTestId } = renderSearch({ loading: true }); + expect(getByTestId("loader")).toBeInTheDocument(); + }); + + it("should apply the wrapperClassName to the wrapper element", () => { + const { container } = renderSearch({ wrapperClassName: "customWrapper" }); + expect(container.firstChild).toHaveClass("customWrapper"); + }); + + it("should trigger onChange with the correct value when typing without debounce", () => { + const onChange = jest.fn(); + const { getByRole } = renderSearch({ onChange, debounceRate: 0 }); + userEvent.type(getByRole("searchbox"), "Hello, World!"); + expect(onChange).toHaveBeenCalledTimes("Hello, World!".length); + expect(onChange).toHaveBeenLastCalledWith("Hello, World!"); + }); + + it("should debounce the onChange call", async () => { + jest.useFakeTimers(); + const onChange = jest.fn(); + + const { getByRole } = renderSearch({ onChange, debounceRate: 100 }); + userEvent.type(getByRole("searchbox"), "Hello"); + expect(onChange).not.toHaveBeenCalled(); + jest.advanceTimersByTime(100); + expect(onChange).toHaveBeenCalledWith("Hello"); + jest.useRealTimers(); + }); + + it("should respect the autoFocus prop", () => { + const { getByRole } = renderSearch({ autoFocus: true }); + expect(getByRole("searchbox")).toHaveFocus(); + }); + + it("should not allow input when disabled is true", () => { + const { getByRole } = renderSearch({ value: "Test", disabled: true }); + const input = getByRole("searchbox"); + expect(input).toBeDisabled(); + userEvent.type(input, "Hello, World!"); + expect(input).toHaveValue("Test"); + }); + + it("should not show clear when disabled is true", () => { + const { queryByLabelText } = renderSearch({ value: "Test", disabled: true }); + expect(queryByLabelText("Clear")).not.toBeInTheDocument(); + }); + + it("should display additional action render if provided", () => { + const AdditionalActionButton = ; + const { getByText } = renderSearch({ additionalActionRender: 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({ activeDescendant: "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(); + }); + }); +}); From 29d2917296e03f7f76928f4ec54351e8f002f171 Mon Sep 17 00:00:00 2001 From: Yossi Saadi Date: Sun, 14 Apr 2024 14:56:04 +0300 Subject: [PATCH 14/28] chore(Search): export old Search from LegacySearch path, export new Search from Search /next path --- packages/core/src/components/index.js | 4 ++-- packages/core/src/next.ts | 3 ++- packages/core/webpack/published-ts-components.js | 4 ++-- 3 files changed, 6 insertions(+), 5 deletions(-) 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/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/webpack/published-ts-components.js b/packages/core/webpack/published-ts-components.js index 6f7aed4580..a934167df9 100644 --- a/packages/core/webpack/published-ts-components.js +++ b/packages/core/webpack/published-ts-components.js @@ -50,8 +50,8 @@ const publishedTSComponents = { AttentionBox: "components/AttentionBox/AttentionBox", SplitButton: "components/SplitButton/SplitButton", SplitButtonMenu: "components/SplitButton/SplitButtonMenu/SplitButtonMenu", - SearchComponent: "components/Search/Search", // TODO: remove when bumping to version 1.0.0 - Search: "components/Search/Search", + SearchComponent: "components/LegacySearch/LegacySearch", // TODO: remove when bumping to version 1.0.0 + Search: "components/LegacySearch/LegacySearch", Skeleton: "components/Skeleton/Skeleton", Steps: "components/Steps/Steps", DatePicker: "components/DatePicker/DatePicker", From bb8471c3bb6ed384a368ffed9fe53b6ec947d276 Mon Sep 17 00:00:00 2001 From: Yossi Saadi Date: Sun, 14 Apr 2024 14:57:03 +0300 Subject: [PATCH 15/28] chore(LegacySearch): update naming --- .../LegacySearch/LegacySearch.module.scss | 34 +++++ .../components/LegacySearch/LegacySearch.tsx | 124 ++++++++++++++++++ .../SearchConstants.ts | 0 .../__stories__/LegacySearch.mdx} | 20 +-- .../LegacySearch.stories.helpers.tsx} | 0 .../__stories__/LegacySearch.stories.scss} | 0 .../__stories__/LegacySearch.stories.tsx} | 21 +-- .../search-snapshot-tests.jest.js.snap | 0 .../__tests__/search-snapshot-tests.jest.js | 70 ++++++++++ .../__tests__/search-snapshot-tests.jest.js | 70 ---------- 10 files changed, 249 insertions(+), 90 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%) rename packages/core/src/components/{Search/__stories__/Search.mdx => LegacySearch/__stories__/LegacySearch.mdx} (65%) 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%) rename packages/core/src/components/{Search/__stories__/Search.stories.tsx => LegacySearch/__stories__/LegacySearch.stories.tsx} (73%) rename packages/core/src/components/{Search => LegacySearch}/__tests__/__snapshots__/search-snapshot-tests.jest.js.snap (100%) create mode 100644 packages/core/src/components/LegacySearch/__tests__/search-snapshot-tests.jest.js delete mode 100644 packages/core/src/components/Search/__tests__/search-snapshot-tests.jest.js diff --git a/packages/core/src/components/LegacySearch/LegacySearch.module.scss b/packages/core/src/components/LegacySearch/LegacySearch.module.scss new file mode 100644 index 0000000000..2931570865 --- /dev/null +++ b/packages/core/src/components/LegacySearch/LegacySearch.module.scss @@ -0,0 +1,34 @@ +.searchWrapper input[type="search"] { + -webkit-appearance: textfield; +} + +.searchWrapper input[type="search"]::-webkit-search-decoration, +.searchWrapper input[type="search"]::-webkit-search-cancel-button, +.searchWrapper input[type="search"]::-webkit-search-results-button, +.searchWrapper input[type="search"]::-webkit-search-results-decoration { + -webkit-appearance: none; +} + +.searchWrapper:focus-within .searchFocusElementFirst { + animation: dashForward 5s linear forwards; +} + +.searchWrapper:focus-within .searchFocusElementSecond { + animation: dashBackwards 5s linear forwards; +} + +.search.round { + border-radius: 50px !important; +} + +@keyframes dashForward { + to { + stroke-dashoffset: 0; + } +} + +@keyframes dashBackwards { + to { + stroke-dashoffset: 2000; + } +} diff --git a/packages/core/src/components/LegacySearch/LegacySearch.tsx b/packages/core/src/components/LegacySearch/LegacySearch.tsx new file mode 100644 index 0000000000..4f74535eed --- /dev/null +++ b/packages/core/src/components/LegacySearch/LegacySearch.tsx @@ -0,0 +1,124 @@ +import cx from "classnames"; +import React, { forwardRef } from "react"; +import TextField from "../TextField/TextField"; +import useMergeRef from "../../hooks/useMergeRef"; +import { SearchDefaultIconNames, SearchType } from "./SearchConstants"; +import CloseIcon from "../Icon/Icons/components/CloseSmall"; +import SearchIcon from "../Icon/Icons/components/Search"; +import { NOOP } from "../../utils/function-utils"; +import { SubIcon, VibeComponentProps, VibeComponent, withStaticProps } from "../../types"; +import { TextFieldTextType } from "../TextField/TextFieldConstants"; +import { BASE_SIZES } from "../../constants"; +import { ComponentDefaultTestId, getTestId } from "../../tests/test-ids-utils"; +import styles from "./LegacySearch.module.scss"; + +export interface SearchProps extends VibeComponentProps { + secondaryIconName?: SubIcon; + iconName?: SubIcon; + onChange?: (value: string) => void; + autoFocus?: boolean; + value?: string; + placeholder?: string; + disabled?: boolean; + debounceRate?: number; + onBlur?: (event: React.FocusEvent) => void; + onFocus?: (event: React.FocusEvent) => void; + wrapperClassName?: string; + setRef?: () => void; + autoComplete?: string; + /* BASE_SIZES is exposed on the component itself */ + size?: (typeof BASE_SIZES)[keyof typeof BASE_SIZES]; + /* TYPES is exposed on the component itself */ + type?: SearchType; + validation?: + | { + status: "error" | "success"; + text: string; + } + | { text: string }; + inputAriaLabel?: string; + searchResultsContainerId?: string; + activeDescendant?: string; + /* Icon names labels for a11y */ + iconNames?: { + layout: string; + primary: string; + secondary: string; + }; + /** shows loading animation */ + loading?: boolean; +} + +const LegacySearch: VibeComponent & { + sizes?: typeof BASE_SIZES; + types?: typeof SearchType; +} = forwardRef( + ( + { + secondaryIconName = CloseIcon, + iconName = SearchIcon, + onChange = NOOP, + autoFocus = false, + value = "", + placeholder = "", + disabled = false, + debounceRate = 200, + onBlur = NOOP, + onFocus = NOOP, + wrapperClassName = "", + setRef = NOOP, + autoComplete = "off", + size = BASE_SIZES.MEDIUM, + type = SearchType.SQUARE, + className, + id = "search", + validation = null, + inputAriaLabel, + searchResultsContainerId = "", + activeDescendant = "", + iconNames = SearchDefaultIconNames, + loading = false, + "data-testid": dataTestId + }, + ref + ) => { + const mergedRef = useMergeRef(ref, setRef); + return ( + + ); + } +); + +export default withStaticProps(LegacySearch, { + sizes: BASE_SIZES, + types: SearchType +}); diff --git a/packages/core/src/components/Search/SearchConstants.ts b/packages/core/src/components/LegacySearch/SearchConstants.ts similarity index 100% rename from packages/core/src/components/Search/SearchConstants.ts rename to packages/core/src/components/LegacySearch/SearchConstants.ts diff --git a/packages/core/src/components/Search/__stories__/Search.mdx b/packages/core/src/components/LegacySearch/__stories__/LegacySearch.mdx similarity index 65% rename from packages/core/src/components/Search/__stories__/Search.mdx rename to packages/core/src/components/LegacySearch/__stories__/LegacySearch.mdx index 7edd4b1846..29e899e706 100644 --- a/packages/core/src/components/Search/__stories__/Search.mdx +++ b/packages/core/src/components/LegacySearch/__stories__/LegacySearch.mdx @@ -5,12 +5,12 @@ import { DROPDOWN, TEXT_FIELD } from "../../../storybook/components/related-components/component-description-map"; -import { TipCombobox } from "./Search.stories.helpers"; -import * as SearchStories from "./Search.stories"; +import { TipCombobox } from "./LegacySearch.stories.helpers"; +import * as LegacySearchStories from "./LegacySearch.stories"; - + -# Search +# LegacySearch - [Overview](#overview) - [Props](#props) @@ -23,9 +23,9 @@ import * as SearchStories from "./Search.stories"; ## Overview -Search lets users quickly find relevant content. A search field allows a user to type a word or phrase to filter through a large amount of data without navigation. +LegacySearch lets users quickly find relevant content. A search field allows a user to type a word or phrase to filter through a large amount of data without navigation. - + ## Props @@ -35,8 +35,8 @@ Search lets users quickly find relevant content. A search field allows a user to + ## Use cases and examples ### Filter in combobox - + ## Related components diff --git a/packages/core/src/components/Search/__stories__/Search.stories.helpers.tsx b/packages/core/src/components/LegacySearch/__stories__/LegacySearch.stories.helpers.tsx similarity index 100% rename from packages/core/src/components/Search/__stories__/Search.stories.helpers.tsx rename to packages/core/src/components/LegacySearch/__stories__/LegacySearch.stories.helpers.tsx diff --git a/packages/core/src/components/Search/__stories__/Search.stories.scss b/packages/core/src/components/LegacySearch/__stories__/LegacySearch.stories.scss similarity index 100% rename from packages/core/src/components/Search/__stories__/Search.stories.scss rename to packages/core/src/components/LegacySearch/__stories__/LegacySearch.stories.scss diff --git a/packages/core/src/components/Search/__stories__/Search.stories.tsx b/packages/core/src/components/LegacySearch/__stories__/LegacySearch.stories.tsx similarity index 73% rename from packages/core/src/components/Search/__stories__/Search.stories.tsx rename to packages/core/src/components/LegacySearch/__stories__/LegacySearch.stories.tsx index 076389c1b2..f5dfa0fd12 100644 --- a/packages/core/src/components/Search/__stories__/Search.stories.tsx +++ b/packages/core/src/components/LegacySearch/__stories__/LegacySearch.stories.tsx @@ -1,20 +1,21 @@ import { createComponentTemplate } from "vibe-storybook-components"; -import Search from "../Search"; +import LegacySearch from "../LegacySearch"; import { createStoryMetaSettingsDecorator } from "../../../storybook"; import DialogContentContainer from "../../DialogContentContainer/DialogContentContainer"; import Combobox from "../../Combobox/Combobox"; -import "./Search.stories.scss"; +import "./LegacySearch.stories.scss"; const metaSettings = createStoryMetaSettingsDecorator({ - component: Search, - iconPropNamesArray: ["searchIconName", "clearIconName"] + component: LegacySearch, + enumPropNamesArray: ["type", "size"], + iconPropNamesArray: ["secondaryIconName", "iconName"] }); -const searchTemplate = createComponentTemplate(Search); +const searchTemplate = createComponentTemplate(LegacySearch); export default { - title: "Inputs/Search", - component: Search, + title: "Inputs/LegacySearch [deprecated]", + component: LegacySearch, argTypes: metaSettings.argTypes, decorators: metaSettings.decorators }; @@ -32,9 +33,9 @@ export const Overview = { export const Sizes = { render: () => (
- - - + + +
), diff --git a/packages/core/src/components/Search/__tests__/__snapshots__/search-snapshot-tests.jest.js.snap b/packages/core/src/components/LegacySearch/__tests__/__snapshots__/search-snapshot-tests.jest.js.snap similarity index 100% rename from packages/core/src/components/Search/__tests__/__snapshots__/search-snapshot-tests.jest.js.snap rename to packages/core/src/components/LegacySearch/__tests__/__snapshots__/search-snapshot-tests.jest.js.snap diff --git a/packages/core/src/components/LegacySearch/__tests__/search-snapshot-tests.jest.js b/packages/core/src/components/LegacySearch/__tests__/search-snapshot-tests.jest.js new file mode 100644 index 0000000000..81b729f670 --- /dev/null +++ b/packages/core/src/components/LegacySearch/__tests__/search-snapshot-tests.jest.js @@ -0,0 +1,70 @@ +import React from "react"; +import renderer from "react-test-renderer"; +import LegacySearch from "../LegacySearch"; + +describe("LegacySearch renders correctly", () => { + it("without props", () => { + const tree = renderer.create().toJSON(); + expect(tree).toMatchSnapshot(); + }); + + it("with placeholder", () => { + const tree = renderer.create().toJSON(); + expect(tree).toMatchSnapshot(); + }); + + it("with value", () => { + const tree = renderer.create().toJSON(); + expect(tree).toMatchSnapshot(); + }); + + it("when disabled", () => { + const tree = renderer.create().toJSON(); + expect(tree).toMatchSnapshot(); + }); + + it("when underline", () => { + const tree = renderer.create().toJSON(); + expect(tree).toMatchSnapshot(); + }); + + it("with wrapperClassName", () => { + const tree = renderer.create().toJSON(); + expect(tree).toMatchSnapshot(); + }); + + it("with className", () => { + const tree = renderer.create().toJSON(); + expect(tree).toMatchSnapshot(); + }); + + it("with id", () => { + const tree = renderer.create().toJSON(); + expect(tree).toMatchSnapshot(); + }); + + it("with validation", () => { + const tree = renderer.create().toJSON(); + expect(tree).toMatchSnapshot(); + }); + + it("with loading", () => { + const tree = renderer.create().toJSON(); + expect(tree).toMatchSnapshot(); + }); + + it("with underline type", () => { + const tree = renderer.create().toJSON(); + expect(tree).toMatchSnapshot(); + }); + + it("with icon", () => { + const tree = renderer.create().toJSON(); + expect(tree).toMatchSnapshot(); + }); + + it("with secondaryIconName", () => { + const tree = renderer.create().toJSON(); + expect(tree).toMatchSnapshot(); + }); +}); diff --git a/packages/core/src/components/Search/__tests__/search-snapshot-tests.jest.js b/packages/core/src/components/Search/__tests__/search-snapshot-tests.jest.js deleted file mode 100644 index e0bdb87352..0000000000 --- a/packages/core/src/components/Search/__tests__/search-snapshot-tests.jest.js +++ /dev/null @@ -1,70 +0,0 @@ -import React from "react"; -import renderer from "react-test-renderer"; -import Search from "../Search"; - -describe("Search renders correctly", () => { - it("without props", () => { - const tree = renderer.create().toJSON(); - expect(tree).toMatchSnapshot(); - }); - - it("with placeholder", () => { - const tree = renderer.create().toJSON(); - expect(tree).toMatchSnapshot(); - }); - - it("with value", () => { - const tree = renderer.create().toJSON(); - expect(tree).toMatchSnapshot(); - }); - - it("when disabled", () => { - const tree = renderer.create().toJSON(); - expect(tree).toMatchSnapshot(); - }); - - it("when underline", () => { - const tree = renderer.create().toJSON(); - expect(tree).toMatchSnapshot(); - }); - - it("with wrapperClassName", () => { - const tree = renderer.create().toJSON(); - expect(tree).toMatchSnapshot(); - }); - - it("with className", () => { - const tree = renderer.create().toJSON(); - expect(tree).toMatchSnapshot(); - }); - - it("with id", () => { - const tree = renderer.create().toJSON(); - expect(tree).toMatchSnapshot(); - }); - - it("with validation", () => { - const tree = renderer.create().toJSON(); - expect(tree).toMatchSnapshot(); - }); - - it("with loading", () => { - const tree = renderer.create().toJSON(); - expect(tree).toMatchSnapshot(); - }); - - it("with underline type", () => { - const tree = renderer.create().toJSON(); - expect(tree).toMatchSnapshot(); - }); - - it("with icon", () => { - const tree = renderer.create().toJSON(); - expect(tree).toMatchSnapshot(); - }); - - it("with secondaryIconName", () => { - const tree = renderer.create().toJSON(); - expect(tree).toMatchSnapshot(); - }); -}); From 2983824fed2fc81bbefd858dd37d330b7bc6a6dc Mon Sep 17 00:00:00 2001 From: Yossi Saadi Date: Sun, 14 Apr 2024 15:02:13 +0300 Subject: [PATCH 16/28] chore(LegacySearch): use LegacySearch in existing components and stories --- .../AttentionBox/__stories__/attentionBox.stories.js | 4 ++-- .../src/components/Chips/__stories__/chips.stories.js | 6 +++--- packages/core/src/components/Combobox/Combobox.tsx | 4 ++-- .../components/Heading/__stories__/Heading.stories.js | 4 ++-- .../core/src/components/Icon/__stories__/IconsList.tsx | 4 ++-- .../__stories__/LegacyHeading.stories.tsx | 4 ++-- .../__stories__/ResponsiveList.stories.js | 10 +++++----- .../UseActiveDescendantListFocus.jsx | 1 - .../descriptions/search-description.jsx | 4 ++-- .../catalog/Catalog/Catalog.stories.templates.tsx | 4 ++-- .../playground/react-docgen-output.json | 2 +- 11 files changed, 23 insertions(+), 24 deletions(-) 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/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({ additionalActionRender: AdditionalActionButton }); + const { getByText } = renderSearch({ renderAction: AdditionalActionButton }); expect(getByText("Extra Action")).toBeInTheDocument(); }); @@ -114,7 +109,7 @@ describe("Search", () => { }); it("should set aria-activedescendant when activeDescendant is provided", () => { - const { getByRole } = renderSearch({ activeDescendant: "option-1" }); + const { getByRole } = renderSearch({ currentAriaResultId: "option-1" }); expect(getByRole("searchbox")).toHaveAttribute("aria-activedescendant", "option-1"); }); 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 cdb78e4745..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 @@ -18,12 +18,7 @@ export const CatalogTemplate = () => { return (
- + Date: Wed, 17 Apr 2024 16:43:32 +0300 Subject: [PATCH 26/28] docs(BaseInput): jsdoc for props --- .../components/BaseInput/BaseInput.types.ts | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/packages/core/src/components/BaseInput/BaseInput.types.ts b/packages/core/src/components/BaseInput/BaseInput.types.ts index fac0803d96..c5f6feaf10 100644 --- a/packages/core/src/components/BaseInput/BaseInput.types.ts +++ b/packages/core/src/components/BaseInput/BaseInput.types.ts @@ -7,12 +7,43 @@ type BaseInputNativeInputProps = Omit, "si type Renderer = ReactNode | ReactNode[]; export interface BaseInputProps extends BaseInputNativeInputProps, VibeComponentProps { + /** + * Size of the input element. Will influence also padding and font size. + */ size?: InputSize; + /** + * 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; + /** + * 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; } From a04f47010b4606fb734c6b6e1247b91ae57834b0 Mon Sep 17 00:00:00 2001 From: Yossi Saadi Date: Wed, 17 Apr 2024 16:59:31 +0300 Subject: [PATCH 27/28] docs(Search): fix story prop --- .../core/src/components/Search/__stories__/Search.stories.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/src/components/Search/__stories__/Search.stories.tsx b/packages/core/src/components/Search/__stories__/Search.stories.tsx index 3069b5176d..b40c793a01 100644 --- a/packages/core/src/components/Search/__stories__/Search.stories.tsx +++ b/packages/core/src/components/Search/__stories__/Search.stories.tsx @@ -61,7 +61,7 @@ export const WithAdditionalAction: Story = { render: () => ( } + renderAction={} /> ), From b304a9de0742fcc64cb2f85f401e584dc69b2a8d Mon Sep 17 00:00:00 2001 From: Yossi Saadi Date: Wed, 17 Apr 2024 17:02:23 +0300 Subject: [PATCH 28/28] style(Search): prettier --- packages/core/src/components/BaseInput/BaseInput.tsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/core/src/components/BaseInput/BaseInput.tsx b/packages/core/src/components/BaseInput/BaseInput.tsx index 934c25049b..f9b5cecada 100644 --- a/packages/core/src/components/BaseInput/BaseInput.tsx +++ b/packages/core/src/components/BaseInput/BaseInput.tsx @@ -36,7 +36,13 @@ const BaseInput = forwardRef( return (
{renderLeft} - + {renderRight}
);