-
Notifications
You must be signed in to change notification settings - Fork 7
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
5b1109e
commit 56751f2
Showing
3 changed files
with
60 additions
and
204 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<HTMLFormElement>) => 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<HTMLInputElement>; | ||
|
||
/** The ref object used to access the underlying HTMLButtonElement */ | ||
buttonRef?: ForwardedRef<HTMLButtonElement>; | ||
|
||
/** 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 = ( | ||
<Icon | ||
name="search" | ||
className="stroke-primary-100 w-6 h-6 pointer-events-none" | ||
/> | ||
), | ||
onSubmit, | ||
}: SearchBarProps) => { | ||
const handleSubmit = (e: FormEvent<HTMLFormElement>) => { | ||
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 ( | ||
<form onSubmit={handleSubmit} className={rootClassName}> | ||
<Input | ||
ref={inputRef} | ||
role="search" | ||
value={value} | ||
placeholder={placeholder} | ||
autoFocus={autoFocus} | ||
className={inputWrapperClassName} | ||
inputClassName={inputClassName} | ||
onChange={onChange} | ||
/> | ||
<Button ref={buttonRef} className={buttonClassName} loading={loading}> | ||
{buttonText} | ||
</Button> | ||
</form> | ||
<div className="p-px gradient-purple rounded-full max-w-lg w-full"> | ||
<div className="body-bg rounded-full"> | ||
<div className="relative flex p-1 bg-white/8 rounded-full w-full"> | ||
{leftSlot} | ||
<input | ||
role="search" | ||
className="py-2.5 px-4 leading-normal rounded-full bg-white/8 flex-1 text-primary-200" | ||
placeholder={placeholder} | ||
value={value} | ||
onChange={(e) => setValue(e.target.value)} | ||
/> | ||
<button | ||
type="button" | ||
className="absolute right-3 p-2.5" | ||
onClick={() => onSubmit?.(value)} | ||
> | ||
{buttonSlot} | ||
</button> | ||
</div> | ||
</div> | ||
</div> | ||
); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 <SearchBar value={value} onChange={handleChange} />; | ||
}; | ||
const mockSubmit = jest.fn(); | ||
|
||
render(<ControllerComponent />); | ||
render(<SearchBar onSubmit={mockSubmit} buttonSlot="submit" />); | ||
|
||
const inputElement = screen.getByRole<HTMLInputElement>("search"); | ||
const buttonElement = screen.getByRole<HTMLInputElement>("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 ( | ||
<SearchBar | ||
value={value} | ||
onChange={setValue} | ||
onSubmit={mockSubmit} | ||
buttonText="查詢" | ||
/> | ||
); | ||
}; | ||
|
||
render(<ControllerComponent />); | ||
|
||
const inputElement = screen.getByRole<HTMLInputElement>("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( | ||
<SearchBar | ||
value="test" | ||
autoFocus | ||
buttonText="search" | ||
className={rootClassName} | ||
buttonClassName={buttonClassName} | ||
inputClassName={inputClassName} | ||
inputWrapperClassName={inputWrapperClassName} | ||
/> | ||
); | ||
|
||
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); | ||
}); | ||
}); |