From 68d2283b0868b550a45c2ccd1e7af9efdd4a03a9 Mon Sep 17 00:00:00 2001 From: Johnson Mao Date: Fri, 26 Jul 2024 23:32:31 +0800 Subject: [PATCH] feat: update search bar --- components/shared/SearchBar/SearchBar.tsx | 127 +++++------------- .../shared/SearchBar/searchBar.stories.tsx | 31 ++--- .../shared/SearchBar/searchBar.test.tsx | 106 +-------------- 3 files changed, 60 insertions(+), 204 deletions(-) diff --git a/components/shared/SearchBar/SearchBar.tsx b/components/shared/SearchBar/SearchBar.tsx index da349e34..57747926 100644 --- a/components/shared/SearchBar/SearchBar.tsx +++ b/components/shared/SearchBar/SearchBar.tsx @@ -1,110 +1,57 @@ -import type { ClassValue } from "clsx"; -import { FormEvent, ForwardedRef } from "react"; -import { cn } from "@/lib/utils"; - -import Button from "../Button"; -import Input, { ChangeHandler } from "../Input"; - -type SubmitHandler = (value: string, event: FormEvent) => void; +import { ReactNode, useState } from "react"; +import Icon from "../Icon/v2/Icon"; interface SearchBarProps { - /** The current value of the input */ - value: string; - /** The placeholder text displayed in the search bar when it is empty */ placeholder?: string; - /** For button text content */ - buttonText?: string; - - /** If `true`, the button will be in loading mode */ - loading?: boolean; - - /** If `true`, the input will receive autofocus when rendered. */ - autoFocus?: boolean; - - /** The ref object used to access the underlying HTMLInputElement */ - inputRef?: ForwardedRef; - - /** The ref object used to access the underlying HTMLButtonElement */ - buttonRef?: ForwardedRef; - - /** For root class name */ - className?: ClassValue; - - /** For input wrapper class name */ - inputWrapperClassName?: ClassValue; - - /** For input class name */ - inputClassName?: ClassValue; + /** An optional React node that will be rendered on the left side of the input field */ + leftSlot?: ReactNode; - /** For button class name */ - buttonClassName?: ClassValue; - - /** - * Callback function that is called when the value of the input changes. - * @param value - The new value of the input. - * @param event - The event object associated with the change event. - */ - onChange?: ChangeHandler; + /** An optional React node that will be rendered on the submit button */ + buttonSlot?: ReactNode; /** * Callback function triggered when the user submits the search. * @param value - The current input value of the search bar. - * @param event - The form submission event. */ - onSubmit?: SubmitHandler; + onSubmit?: (value: string) => void; } export const SearchBar = ({ - value, - placeholder = "請輸入你想搜尋的遊戲,玩家名稱,帖子關鍵字...", - buttonText = "搜索", - loading, - autoFocus, - className, - inputRef, - buttonRef, - inputWrapperClassName: inputWrapperClassNameProps, - inputClassName: inputClassNameProps, - buttonClassName: buttonClassNameProps, - onChange, + placeholder = "在此輸入今天想玩的遊戲", + leftSlot, + buttonSlot = ( + + ), onSubmit, }: SearchBarProps) => { - const handleSubmit = (e: FormEvent) => { - e.preventDefault(); - onSubmit?.(value, e); - }; - - const rootClassName = cn( - "group flex rounded-[10px] border border-[#2D2D2E] shadow focus-within:border-[#2F88FF]/80 focus-within:shadow-[#2F88FF]/40 transition-[box-shadow,border]", - className - ); - - const inputWrapperClassName = cn("flex-1", inputWrapperClassNameProps); - - const inputClassName = cn("rounded-r-none border-0", inputClassNameProps); - - const buttonClassName = cn( - "leading-none bg-[#2D2D2E] rounded-l-none rounded-r shadow-none group-focus-within:bg-[#2F88FF]/80 active:group-focus-within:bg-[#2173DD]", - buttonClassNameProps - ); + const [value, setValue] = useState(""); return ( -
- - -
+
+
+
+ {leftSlot} + setValue(e.target.value)} + /> + +
+
+
); }; diff --git a/components/shared/SearchBar/searchBar.stories.tsx b/components/shared/SearchBar/searchBar.stories.tsx index a0e0d8e5..bc27629b 100644 --- a/components/shared/SearchBar/searchBar.stories.tsx +++ b/components/shared/SearchBar/searchBar.stories.tsx @@ -1,7 +1,5 @@ import type { Meta, StoryObj } from "@storybook/react"; -import { useArgs } from "@storybook/preview-api"; -import type { ChangeHandler } from "../Input"; import SearchBar from "."; const meta: Meta = { @@ -10,33 +8,36 @@ const meta: Meta = { tags: ["autodocs"], decorators: [ (Story, ctx) => { - const [, setArgs] = useArgs(); - - const onChange: ChangeHandler = (value, event) => { - ctx.args.onChange?.(value, event); - - // Check if the component is controlled - if (typeof ctx.args.value !== undefined) { - setArgs({ value }); - } + const onSubmit = (value: string) => { + ctx.args.onSubmit?.(value); }; return (
- +
); }, ], argTypes: { - onChange: { - type: "function", - }, onSubmit: { type: "function", }, + leftSlot: { + control: { type: "select" }, + options: ["Empty", "Button"], + defaultValue: "Button", + mapping: { + Empty: null, + Button: ( + + ), + }, + }, }, }; diff --git a/components/shared/SearchBar/searchBar.test.tsx b/components/shared/SearchBar/searchBar.test.tsx index 2e60a651..125d6ee4 100644 --- a/components/shared/SearchBar/searchBar.test.tsx +++ b/components/shared/SearchBar/searchBar.test.tsx @@ -1,122 +1,30 @@ -import React, { useState } from "react"; -import { act, fireEvent, render, screen } from "@testing-library/react"; +import React from "react"; +import { act, render, screen } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import "@testing-library/jest-dom"; -import type { ChangeHandler } from "../Input"; import SearchBar from "."; describe("SearchBar", () => { it("should renders input component with correct value", async () => { - const initValue = "test"; - const mockChange = jest.fn(); - - const ControllerComponent = () => { - const [value, setValue] = useState(initValue); - const handleChange: ChangeHandler = (v, e) => { - setValue(v); - mockChange(v, e); - }; - - return ; - }; + const mockSubmit = jest.fn(); - render(); + render(); const inputElement = screen.getByRole("search"); + const buttonElement = screen.getByRole("button"); expect(inputElement).toBeInTheDocument(); - expect(inputElement.value).toBe(initValue); const typingValue = "search"; await act(async () => { await userEvent.type(inputElement, typingValue); - }); - - expect(mockChange).toHaveBeenCalledTimes(typingValue.length); - expect(mockChange).toHaveBeenLastCalledWith( - initValue + typingValue, - expect.any(Object) - ); - expect(inputElement.value).toBe(initValue + typingValue); - }); - - it("should call the onSubmit event handler with the correct value and event object", async () => { - const initValue = "test-submit"; - const mockSubmit = jest.fn(); - - const ControllerComponent = () => { - const [value, setValue] = useState(initValue); - - return ( - - ); - }; - - render(); - - const inputElement = screen.getByRole("search"); - const buttonElement = screen.getByRole("button", { - name: "查詢", - }); - - expect(buttonElement).toBeInTheDocument(); - - await act(async () => { await userEvent.click(buttonElement); }); expect(mockSubmit).toHaveBeenCalledTimes(1); - expect(mockSubmit).toHaveBeenLastCalledWith(initValue, expect.any(Object)); - - const typingValue = "-complete"; - - await act(async () => { - await userEvent.type(inputElement, typingValue); - await userEvent.click(buttonElement); - }); - - expect(mockSubmit).toHaveBeenCalledTimes(2); - expect(mockSubmit).toHaveBeenLastCalledWith( - initValue + typingValue, - expect.any(Object) - ); - }); - - it("should renders correct class name", async () => { - const rootClassName = "test-root"; - const buttonClassName = "test-button"; - const inputClassName = "test-input"; - const inputWrapperClassName = "test-input-wrapper"; - - const { container } = render( - - ); - - const rootElement = container.querySelector("form"); - const inputElement = container.querySelector("input"); - const inputWrapperElement = container.querySelector("div"); - const buttonElement = screen.getByRole("button", { - name: "search", - }); - - expect(rootElement).toHaveClass(rootClassName); - expect(buttonElement).toHaveClass(buttonClassName); - expect(inputElement).toHaveClass(inputClassName); - expect(inputWrapperElement).toHaveClass(inputWrapperClassName); + expect(mockSubmit).toHaveBeenLastCalledWith(typingValue); + expect(inputElement.value).toBe(typingValue); }); });