From 8f751e6af769a03eab1d3724386051a589df1018 Mon Sep 17 00:00:00 2001 From: SeieunYoo Date: Wed, 17 Jul 2024 01:10:50 +0900 Subject: [PATCH 01/27] =?UTF-8?q?feat=20:=20=EC=84=9C=EC=B9=98=EB=B0=94=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../wow-icons/src/component/RightArrow.tsx | 2 +- packages/wow-icons/src/component/Search.tsx | 47 ++++ packages/wow-icons/src/component/index.ts | 1 + .../svg/{rightArrow.svg => right-arrow.svg} | 0 packages/wow-icons/src/svg/search.svg | 4 + .../wow-ui/src/components/SearchBar/index.tsx | 216 ++++++++++++++++++ 6 files changed, 269 insertions(+), 1 deletion(-) create mode 100644 packages/wow-icons/src/component/Search.tsx rename packages/wow-icons/src/svg/{rightArrow.svg => right-arrow.svg} (100%) create mode 100644 packages/wow-icons/src/svg/search.svg create mode 100644 packages/wow-ui/src/components/SearchBar/index.tsx diff --git a/packages/wow-icons/src/component/RightArrow.tsx b/packages/wow-icons/src/component/RightArrow.tsx index bade4e0a..6a857dbf 100644 --- a/packages/wow-icons/src/component/RightArrow.tsx +++ b/packages/wow-icons/src/component/RightArrow.tsx @@ -17,7 +17,7 @@ const RightArrow = forwardRef( ) => { return ( ( + ( + { + className, + width = "24", + height = "24", + viewBox = "0 0 24 24", + stroke = "white", + ...rest + }, + ref + ) => { + return ( + + + + + ); + } +); + +Search.displayName = "Search"; +export default Search; diff --git a/packages/wow-icons/src/component/index.ts b/packages/wow-icons/src/component/index.ts index 6f20186b..e8c0811b 100644 --- a/packages/wow-icons/src/component/index.ts +++ b/packages/wow-icons/src/component/index.ts @@ -3,4 +3,5 @@ export { default as Close } from "./Close.tsx"; export { default as DownArrow } from "./DownArrow.tsx"; export { default as Help } from "./Help.tsx"; export { default as RightArrow } from "./RightArrow.tsx"; +export { default as Search } from "./Search.tsx"; export { default as Warn } from "./Warn.tsx"; diff --git a/packages/wow-icons/src/svg/rightArrow.svg b/packages/wow-icons/src/svg/right-arrow.svg similarity index 100% rename from packages/wow-icons/src/svg/rightArrow.svg rename to packages/wow-icons/src/svg/right-arrow.svg diff --git a/packages/wow-icons/src/svg/search.svg b/packages/wow-icons/src/svg/search.svg new file mode 100644 index 00000000..f7f34b82 --- /dev/null +++ b/packages/wow-icons/src/svg/search.svg @@ -0,0 +1,4 @@ + + + + diff --git a/packages/wow-ui/src/components/SearchBar/index.tsx b/packages/wow-ui/src/components/SearchBar/index.tsx new file mode 100644 index 00000000..14d46645 --- /dev/null +++ b/packages/wow-ui/src/components/SearchBar/index.tsx @@ -0,0 +1,216 @@ +"use client"; + +import { cva } from "@styled-system/css"; +import { Flex, styled } from "@styled-system/jsx"; +import type { + ChangeEvent, + CSSProperties, + FocusEvent, + InputHTMLAttributes, +} from "react"; +import { forwardRef, useId, useLayoutEffect, useRef, useState } from "react"; +import { Search as SearchIcon } from "wowds-icons"; +// import InputBase from "../../components/SearchBar/InputBase"; + +type VariantType = "default" | "typing" | "typed" | "disabled"; + +/** + * @description 사용자가 검색할 텍스트를 입력하는 서치바 컴포넌트입니다. + * + * @param {string} [placeholder] 서치바의 플레이스홀더 텍스트. + * @param {string} [defaultValue] 서치바의 기본 값. + * @param {string} [value] 외부에서 제어할 활성 상태. + * @param {number} [maxLength] 서치바의 최대 입력 길이. + * @param {(value: string) => void} [onChange] 외부 활성 상태가 변경될 때 호출될 콜백 함수. + * @param {() => void} [onBlur] 서치바가 포커스를 잃을 때 호출될 콜백 함수. + * @param {() => void} [onFocus] 서치바가 포커스됐을 때 호출될 콜백 함수. + * @param {InputHTMLAttributes} [inputProps] 서치바에 전달할 추가 textarea 속성. + * @param {CSSProperties} [style] 서치바의 커스텀 스타일 속성. + * @param {string} [className] 서치바에 전달하는 커스텀 클래스명. + * @param {ComponentPropsWithoutRef} rest 렌더링된 요소 또는 컴포넌트에 전달할 추가 props. + * @param {ComponentPropsWithRef["ref"]} ref 렌더링된 요소 또는 컴포넌트에 연결할 ref. + */ +export interface SearchBarProps { + placeholder?: string; + defaultValue?: string; + value?: string; + maxLength?: number; + onChange?: (value: string) => void; + onBlur?: () => void; + onFocus?: () => void; + inputProps?: InputHTMLAttributes; + style?: CSSProperties; + className?: string; + disabled?: boolean; +} + +const SearchBar = forwardRef( + ( + { + onBlur, + onChange, + onFocus, + placeholder = "", + defaultValue, + value: valueProp, + maxLength, + inputProps, + disabled, + ...rest + }, + ref + ) => { + const id = useId(); + const textareaId = inputProps?.id || id; + + const textareaRef = useRef(null); + const textareaElementRef = ref || textareaRef; + + const [value, setValue] = useState(valueProp ?? defaultValue ?? ""); + const [variant, setVariant] = useState("default"); + + useLayoutEffect(() => { + if (disabled) { + setVariant("disabled"); + } else if (defaultValue) { + setVariant("typed"); + } else { + setVariant("default"); + } + }, [defaultValue, disabled]); + + const handleChange = (e: ChangeEvent) => { + const textareaValue = e.target.value; + setVariant("typing"); + + if (maxLength && textareaValue.length > maxLength) { + setValue(textareaValue.slice(0, maxLength)); + } else { + setValue(textareaValue); + onChange?.(textareaValue); + } + }; + + const handleBlur = (e: FocusEvent) => { + const inputValue = e.target.value; + + setVariant(inputValue ? "typed" : "default"); + onBlur?.(); + }; + + const handleFocus = () => { + if (variant !== "typing") { + setVariant("typing"); + } + onFocus?.(); + }; + + return ( + + + + {/* { + setVariant("typing"); + }} + {...rest} + /> */} + + ); + } +); + +SearchBar.displayName = "TextField"; +export default SearchBar; + +const containerStyle = cva({ + base: { + lg: { + minWidth: "19.75rem", + maxWidth: "40.75rem", + }, + smDown: { + width: "100%", + }, + borderRadius: "sm", + borderWidth: "button", + paddingX: "sm", + paddingY: "xs", + textStyle: "body1", + height: "2.625rem", + maxHeight: "7.5rem", + backgroundColor: "backgroundNormal", + _placeholder: { + color: "outline", + }, + _focus: { + outline: "none", + }, + }, + variants: { + type: { + default: { + borderColor: "outline", + color: "outline", + }, + typing: { + borderColor: "primary", + color: "textBlack", + }, + typed: { + borderColor: "sub", + color: "textBlack", + }, + disabled: { + backgroundColor: "backgroundAlternative", + borderColor: "sub", + cursor: "not-allowed", + }, + }, + }, +}); + +const inputStyle = cva({ + base: { + width: "100%", + overflowY: "hidden", + resize: "none", + outline: "none", + }, +}); From 4cf69739979804f445f2d78cf1966a6d82fd8b8b Mon Sep 17 00:00:00 2001 From: SeieunYoo Date: Wed, 17 Jul 2024 14:11:22 +0900 Subject: [PATCH 02/27] =?UTF-8?q?feat=20:=20=EC=8A=A4=ED=86=A0=EB=A6=AC?= =?UTF-8?q?=EB=B6=81=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../SearchBar/SearchBar.stories.tsx | 164 ++++++++++++++++++ 1 file changed, 164 insertions(+) create mode 100644 packages/wow-ui/src/components/SearchBar/SearchBar.stories.tsx diff --git a/packages/wow-ui/src/components/SearchBar/SearchBar.stories.tsx b/packages/wow-ui/src/components/SearchBar/SearchBar.stories.tsx new file mode 100644 index 00000000..f67065d7 --- /dev/null +++ b/packages/wow-ui/src/components/SearchBar/SearchBar.stories.tsx @@ -0,0 +1,164 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import { useState } from "react"; + +import SearchBar from "@/components/SearchBar"; + +const meta: Meta = { + title: "UI/SearchBar", + component: SearchBar, + tags: ["autodocs"], + parameters: { + componentSubtitle: "서치바 컴포넌트", + a11y: { + config: { + rules: [{ id: "color-contrast", enabled: false }], + }, + }, + }, + argTypes: { + placeholder: { + description: "서치바의 플레이스홀더 텍스트.", + table: { + type: { summary: "string" }, + }, + control: { type: "text" }, + }, + defaultValue: { + description: "서치바의 기본 값.", + table: { + type: { summary: "string" }, + }, + control: { type: "text" }, + }, + value: { + description: "외부에서 제어하는 서치바의 값.", + table: { + type: { summary: "string" }, + }, + control: { type: "text" }, + }, + maxLength: { + description: "서치바의 최대 입력 길이.", + table: { + type: { summary: "number" }, + }, + control: { type: "number" }, + }, + onChange: { + description: "값이 변경될 때 호출되는 콜백 함수.", + table: { + type: { summary: "(value: string) => void" }, + }, + action: "changed", + }, + onBlur: { + description: "서치바가 포커스를 잃을 때 호출되는 콜백 함수.", + table: { + type: { summary: "() => void" }, + }, + action: "blurred", + }, + onFocus: { + description: "서치바가 포커스될 때 호출되는 콜백 함수.", + table: { + type: { summary: "() => void" }, + }, + action: "focused", + }, + disabled: { + description: "서치바를 비활성화합니다.", + table: { + type: { summary: "boolean" }, + }, + control: { type: "boolean" }, + }, + }, +}; + +export default meta; + +type Story = StoryObj; + +export const Primary: Story = { + args: { + placeholder: "검색어를 입력하세요", + defaultValue: "", + }, + parameters: { + docs: { + description: { + story: "기본 서치바.", + }, + }, + }, +}; + +export const WithDefaultValue: Story = { + args: { + placeholder: "검색어를 입력하세요", + defaultValue: "안녕하세요!", + }, + parameters: { + docs: { + description: { + story: "기본 값이 설정된 서치바.", + }, + }, + }, +}; + +export const Disabled: Story = { + args: { + placeholder: "검색어를 입력하세요", + defaultValue: "", + disabled: true, + }, + parameters: { + docs: { + description: { + story: "비활성화된 서치바.", + }, + }, + }, +}; + +const ControlledSearchBar = () => { + const [value, setValue] = useState(""); + + const handleChange = (value: string) => { + setValue(value); + }; + + return ( + + ); +}; + +export const ControlledValue: Story = { + render: () => , + parameters: { + docs: { + description: { + story: "외부에서 값이 제어되는 서치바.", + }, + }, + }, +}; + +export const WithMaxLength: Story = { + args: { + placeholder: "검색어를 입력하세요", + maxLength: 10, + }, + parameters: { + docs: { + description: { + story: "최대 입력 길이가 설정된 서치바.", + }, + }, + }, +}; From 00c26bf5e17027224b46495d5b5e5b20762e938e Mon Sep 17 00:00:00 2001 From: SeieunYoo Date: Wed, 17 Jul 2024 15:06:42 +0900 Subject: [PATCH 03/27] =?UTF-8?q?feat=20:=20=EC=84=9C=EC=B9=98=EB=B0=94=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=BD=94=EB=93=9C=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1=20=EB=B0=8F=20aria=20=EC=86=8D=EC=84=B1=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/wow-ui/package.json | 5 + packages/wow-ui/rollup.config.js | 1 + .../components/SearchBar/SearchBar.test.tsx | 144 ++++++++++++++++++ .../wow-ui/src/components/SearchBar/index.tsx | 44 ++---- 4 files changed, 165 insertions(+), 29 deletions(-) create mode 100644 packages/wow-ui/src/components/SearchBar/SearchBar.test.tsx diff --git a/packages/wow-ui/package.json b/packages/wow-ui/package.json index f811b1d1..b4160245 100644 --- a/packages/wow-ui/package.json +++ b/packages/wow-ui/package.json @@ -40,6 +40,11 @@ "require": "./dist/Stepper.cjs", "import": "./dist/Stepper.js" }, + "./SearchBar": { + "types": "./dist/components/SearchBar/index.d.ts", + "require": "./dist/SearchBar.cjs", + "import": "./dist/SearchBar.js" + }, "./RadioButton": { "types": "./dist/components/RadioGroup/RadioButton.d.ts", "require": "./dist/RadioButton.cjs", diff --git a/packages/wow-ui/rollup.config.js b/packages/wow-ui/rollup.config.js index 05b7b61b..5d8fa060 100644 --- a/packages/wow-ui/rollup.config.js +++ b/packages/wow-ui/rollup.config.js @@ -24,6 +24,7 @@ export default { TextButton: "./src/components/TextButton", Switch: "./src/components/Switch", Stepper: "./src/components/Stepper", + SearchBar: "./src/components/SearchBar", RadioButton: "./src/components/RadioGroup/RadioButton", RadioGroup: "./src/components/RadioGroup/RadioGroup", MultiGroup: "./src/components/MultiGroup", diff --git a/packages/wow-ui/src/components/SearchBar/SearchBar.test.tsx b/packages/wow-ui/src/components/SearchBar/SearchBar.test.tsx new file mode 100644 index 00000000..f9d53cb6 --- /dev/null +++ b/packages/wow-ui/src/components/SearchBar/SearchBar.test.tsx @@ -0,0 +1,144 @@ +import { fireEvent, render, waitFor } from "@testing-library/react"; +import { useState } from "react"; + +import SearchBar from "@/components/SearchBar"; + +describe("SearchBar component", () => { + it("should render placeholder", () => { + const { getByPlaceholderText } = render( + + ); + const placeholderText = getByPlaceholderText("검색어를 입력하세요"); + + expect(placeholderText).toBeInTheDocument(); + }); + + it("should render with default value", () => { + const { getByPlaceholderText } = render( + + ); + const searchBar = getByPlaceholderText("검색어를 입력하세요"); + + expect(searchBar).toHaveValue("default value"); + }); + + it("should handle max length correctly", async () => { + const { getByPlaceholderText } = render( + + ); + const searchBar = getByPlaceholderText( + "검색어를 입력하세요" + ) as HTMLInputElement; + + fireEvent.change(searchBar, { target: { value: "12345678910" } }); + + await waitFor(() => { + expect(searchBar.value).toHaveLength(5); + }); + }); + + it("should apply typing style while typing", async () => { + const { getByPlaceholderText } = render( + + ); + const searchBar = getByPlaceholderText("검색어를 입력하세요"); + + fireEvent.change(searchBar, { target: { value: "12345" } }); + + await waitFor(() => { + expect(searchBar).toHaveStyle("borderColor: primary"); + expect(searchBar).toHaveStyle("color: textBlack"); + }); + }); + + it("should apply typed style after typing", async () => { + const { getByPlaceholderText } = render( + + ); + const searchBar = getByPlaceholderText("검색어를 입력하세요"); + + fireEvent.change(searchBar, { target: { value: "12345" } }); + fireEvent.blur(searchBar); + + await waitFor(() => { + expect(searchBar).toHaveStyle("borderColor: sub"); + expect(searchBar).toHaveStyle("color: textBlack"); + }); + }); + + it("should apply disabled style when disabled", () => { + const { getByPlaceholderText } = render( + + ); + const searchBar = getByPlaceholderText("검색어를 입력하세요"); + + expect(searchBar).toHaveStyle("backgroundColor: backgroundAlternative"); + expect(searchBar).toHaveStyle("borderColor: sub"); + }); + + it("should fire onFocus event when focused", () => { + const handleFocus = jest.fn(); + const { getByPlaceholderText } = render( + + ); + const searchBar = getByPlaceholderText("검색어를 입력하세요"); + + fireEvent.focus(searchBar); + + expect(handleFocus).toHaveBeenCalledTimes(1); + }); + + it("should fire onBlur event when focus is lost", () => { + const handleBlur = jest.fn(); + const { getByPlaceholderText } = render( + + ); + const searchBar = getByPlaceholderText("검색어를 입력하세요"); + + fireEvent.click(searchBar); + fireEvent.blur(searchBar); + + expect(handleBlur).toHaveBeenCalledTimes(1); + }); + it("should have appropriate aria attributes", () => { + const { getByPlaceholderText } = render( + + ); + const searchBar = getByPlaceholderText("검색어를 입력하세요"); + + expect(searchBar).toHaveAttribute("aria-describedby", "description-id"); + expect(searchBar).toHaveAttribute("aria-label", "searchbar"); + }); +}); + +describe("external control and events", () => { + it("should fire external onChange event", () => { + const Component = () => { + const [value, setValue] = useState("initial value"); + const handleChange = (newValue: string) => setValue(newValue); + + return ( + + ); + }; + const { getByPlaceholderText } = render(); + const searchBar = getByPlaceholderText("검색어를 입력하세요"); + + expect(searchBar).toHaveValue("initial value"); + + fireEvent.change(searchBar, { target: { value: "updated value" } }); + + expect(searchBar).toHaveValue("updated value"); + }); +}); diff --git a/packages/wow-ui/src/components/SearchBar/index.tsx b/packages/wow-ui/src/components/SearchBar/index.tsx index 14d46645..662cfcdf 100644 --- a/packages/wow-ui/src/components/SearchBar/index.tsx +++ b/packages/wow-ui/src/components/SearchBar/index.tsx @@ -10,7 +10,6 @@ import type { } from "react"; import { forwardRef, useId, useLayoutEffect, useRef, useState } from "react"; import { Search as SearchIcon } from "wowds-icons"; -// import InputBase from "../../components/SearchBar/InputBase"; type VariantType = "default" | "typing" | "typed" | "disabled"; @@ -21,6 +20,7 @@ type VariantType = "default" | "typing" | "typed" | "disabled"; * @param {string} [defaultValue] 서치바의 기본 값. * @param {string} [value] 외부에서 제어할 활성 상태. * @param {number} [maxLength] 서치바의 최대 입력 길이. + * @param {boolean} [disabled] 서치바의 비활성 여부. * @param {(value: string) => void} [onChange] 외부 활성 상태가 변경될 때 호출될 콜백 함수. * @param {() => void} [onBlur] 서치바가 포커스를 잃을 때 호출될 콜백 함수. * @param {() => void} [onFocus] 서치바가 포커스됐을 때 호출될 콜백 함수. @@ -35,13 +35,13 @@ export interface SearchBarProps { defaultValue?: string; value?: string; maxLength?: number; + disabled?: boolean; onChange?: (value: string) => void; onBlur?: () => void; onFocus?: () => void; inputProps?: InputHTMLAttributes; style?: CSSProperties; className?: string; - disabled?: boolean; } const SearchBar = forwardRef( @@ -61,10 +61,10 @@ const SearchBar = forwardRef( ref ) => { const id = useId(); - const textareaId = inputProps?.id || id; + const inputId = inputProps?.id || id; - const textareaRef = useRef(null); - const textareaElementRef = ref || textareaRef; + const inputRef = useRef(null); + const inputElementRef = ref || inputRef; const [value, setValue] = useState(valueProp ?? defaultValue ?? ""); const [variant, setVariant] = useState("default"); @@ -120,43 +120,26 @@ const SearchBar = forwardRef( /> - {/* { - setVariant("typing"); - }} - {...rest} - /> */} ); } ); -SearchBar.displayName = "TextField"; +SearchBar.displayName = "SearchBar"; export default SearchBar; const containerStyle = cva({ @@ -200,7 +183,7 @@ const containerStyle = cva({ disabled: { backgroundColor: "backgroundAlternative", borderColor: "sub", - cursor: "not-allowed", + cursor: "none", }, }, }, @@ -210,7 +193,10 @@ const inputStyle = cva({ base: { width: "100%", overflowY: "hidden", - resize: "none", + cursor: "inherit", outline: "none", + _disabled: { + pointerEvents: "none", + }, }, }); From 5c57c1a2b52186089a42a5ee2f8be0a437c7d07f Mon Sep 17 00:00:00 2001 From: SeieunYoo Date: Fri, 19 Jul 2024 18:40:26 +0900 Subject: [PATCH 04/27] =?UTF-8?q?refactor:=20useFormCOntrol=20=EB=A1=9C=20?= =?UTF-8?q?=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../wow-ui/src/components/SearchBar/index.tsx | 100 ++++++++---------- packages/wow-ui/src/hooks/useFormControl.ts | 73 +++++++++++++ 2 files changed, 119 insertions(+), 54 deletions(-) create mode 100644 packages/wow-ui/src/hooks/useFormControl.ts diff --git a/packages/wow-ui/src/components/SearchBar/index.tsx b/packages/wow-ui/src/components/SearchBar/index.tsx index 662cfcdf..5a5d54a3 100644 --- a/packages/wow-ui/src/components/SearchBar/index.tsx +++ b/packages/wow-ui/src/components/SearchBar/index.tsx @@ -2,16 +2,14 @@ import { cva } from "@styled-system/css"; import { Flex, styled } from "@styled-system/jsx"; -import type { - ChangeEvent, - CSSProperties, - FocusEvent, - InputHTMLAttributes, -} from "react"; -import { forwardRef, useId, useLayoutEffect, useRef, useState } from "react"; +import type { CSSProperties, InputHTMLAttributes } from "react"; +import { forwardRef, useId, useLayoutEffect, useRef } from "react"; import { Search as SearchIcon } from "wowds-icons"; -type VariantType = "default" | "typing" | "typed" | "disabled"; +import type { BaseVariantType } from "@/hooks/useFormControl"; +import { useFormControl } from "@/hooks/useFormControl"; + +type CustomVariantType = BaseVariantType | "disabled"; /** * @description 사용자가 검색할 텍스트를 입력하는 서치바 컴포넌트입니다. @@ -66,58 +64,32 @@ const SearchBar = forwardRef( const inputRef = useRef(null); const inputElementRef = ref || inputRef; - const [value, setValue] = useState(valueProp ?? defaultValue ?? ""); - const [variant, setVariant] = useState("default"); + const { + value, + variant, + setVariant, + handleChange, + handleBlur, + handleFocus, + } = useFormControl({ + defaultValue, + value: valueProp, + maxLength, + disabled, + onChange, + onBlur, + onFocus, + }); useLayoutEffect(() => { if (disabled) { setVariant("disabled"); - } else if (defaultValue) { - setVariant("typed"); - } else { - setVariant("default"); - } - }, [defaultValue, disabled]); - - const handleChange = (e: ChangeEvent) => { - const textareaValue = e.target.value; - setVariant("typing"); - - if (maxLength && textareaValue.length > maxLength) { - setValue(textareaValue.slice(0, maxLength)); - } else { - setValue(textareaValue); - onChange?.(textareaValue); - } - }; - - const handleBlur = (e: FocusEvent) => { - const inputValue = e.target.value; - - setVariant(inputValue ? "typed" : "default"); - onBlur?.(); - }; - - const handleFocus = () => { - if (variant !== "typing") { - setVariant("typing"); } - onFocus?.(); - }; + }, [disabled, setVariant]); return ( - + ( SearchBar.displayName = "SearchBar"; export default SearchBar; +const getStrokeColor = (variant: CustomVariantType) => { + if (variant === "default") { + return "outline"; + } + if (variant === "typing") { + return "primary"; + } + if (variant === "typed") { + return "sub"; + } + if (variant === "disabled") { + return "outline"; + } +}; + const containerStyle = cva({ base: { lg: { @@ -153,6 +140,7 @@ const containerStyle = cva({ }, borderRadius: "sm", borderWidth: "button", + borderStyle: "solid", paddingX: "sm", paddingY: "xs", textStyle: "body1", @@ -182,8 +170,12 @@ const containerStyle = cva({ }, disabled: { backgroundColor: "backgroundAlternative", - borderColor: "sub", - cursor: "none", + borderColor: "outline", + cursor: "not-allowed", + _placeholder: { + color: "outline", + }, + color: "outline", }, }, }, diff --git a/packages/wow-ui/src/hooks/useFormControl.ts b/packages/wow-ui/src/hooks/useFormControl.ts new file mode 100644 index 00000000..c8e3a88d --- /dev/null +++ b/packages/wow-ui/src/hooks/useFormControl.ts @@ -0,0 +1,73 @@ +import type { ChangeEvent, FocusEvent } from "react"; +import { useLayoutEffect, useState } from "react"; + +export type BaseVariantType = "default" | "typing" | "typed"; + +interface FormControlProps { + defaultValue?: string; + value?: string; + maxLength?: number; + disabled?: boolean; + onChange?: (value: string) => void; + onBlur?: () => void; + onFocus?: () => void; + variant?: VariantType; +} + +export function useFormControl({ + defaultValue, + value: valueProp, + maxLength, + disabled, + onChange, + onBlur, + onFocus, +}: FormControlProps) { + const [value, setValue] = useState(valueProp ?? defaultValue ?? ""); + const [variant, setVariant] = useState( + "default" + ); + + useLayoutEffect(() => { + if (defaultValue) { + setVariant("typed"); + } else { + setVariant("default"); + } + }, [defaultValue]); + + const handleChange = (e: ChangeEvent) => { + const inputValue = e.target.value; + if (!disabled) setVariant("typing"); + + if (maxLength && inputValue.length > maxLength) { + setValue(inputValue.slice(0, maxLength)); + } else { + setValue(inputValue); + onChange?.(inputValue); + } + }; + + const handleBlur = (e: FocusEvent) => { + const inputValue = e.target.value; + + if (!disabled) setVariant(inputValue ? "typed" : "default"); + onBlur?.(); + }; + + const handleFocus = () => { + if (!disabled && variant !== "typing") { + setVariant("typing"); + } + onFocus?.(); + }; + + return { + value, + variant, + setVariant, + handleChange, + handleBlur, + handleFocus, + }; +} From 69c641d02e834855125d0b08c45d6d49d69572c8 Mon Sep 17 00:00:00 2001 From: SeieunYoo Date: Fri, 19 Jul 2024 19:05:17 +0900 Subject: [PATCH 05/27] =?UTF-8?q?chore:=20=EC=B2=B4=EC=9D=B8=EC=A7=80?= =?UTF-8?q?=EC=85=8B=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .changeset/lazy-radios-battle.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changeset/lazy-radios-battle.md diff --git a/.changeset/lazy-radios-battle.md b/.changeset/lazy-radios-battle.md new file mode 100644 index 00000000..c39b0176 --- /dev/null +++ b/.changeset/lazy-radios-battle.md @@ -0,0 +1,6 @@ +--- +"wowds-icons": patch +"wowds-ui": patch +--- + +SearchBar 컴포넌트를 구현합니다 From 42500ffa08631111595b9cae837fdc5ad82e7927 Mon Sep 17 00:00:00 2001 From: SeieunYoo Date: Sun, 21 Jul 2024 14:34:59 +0900 Subject: [PATCH 06/27] =?UTF-8?q?refactor=20:=20=EB=93=9C=EB=A1=AD?= =?UTF-8?q?=EB=8B=A4=EC=9A=B4=20=EC=BB=A8=ED=85=8D=EC=8A=A4=ED=8A=B8?= =?UTF-8?q?=EB=A1=9C=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/DropDown/DropDownOption.tsx | 18 +++- .../DropDown/context/DropDownContext.tsx | 29 ++++++ .../wow-ui/src/components/DropDown/index.tsx | 96 +++++++------------ .../wow-ui/src/components/TextField/index.tsx | 10 +- packages/wow-ui/src/hooks/useClickOutside.ts | 2 +- packages/wow-ui/src/hooks/useDropDownState.ts | 71 +++++++++----- 6 files changed, 137 insertions(+), 89 deletions(-) create mode 100644 packages/wow-ui/src/components/DropDown/context/DropDownContext.tsx diff --git a/packages/wow-ui/src/components/DropDown/DropDownOption.tsx b/packages/wow-ui/src/components/DropDown/DropDownOption.tsx index ae370dc2..8094e18b 100644 --- a/packages/wow-ui/src/components/DropDown/DropDownOption.tsx +++ b/packages/wow-ui/src/components/DropDown/DropDownOption.tsx @@ -3,6 +3,8 @@ import { styled } from "@styled-system/jsx"; import type { ReactNode } from "react"; import { forwardRef } from "react"; +import { useDropDownContext } from "../../components/DropDown/context/DropDownContext"; + /** * @description 드롭다운 옵션의 props입니다. * @@ -21,16 +23,26 @@ export interface DropDownOptionProps { } const DropDownOption = forwardRef( - function Option({ value, onClick, focused, text, selected }, ref) { + function Option({ value, onClick, text }, ref) { + const { focusedValue, selectedValue, handleSelect } = useDropDownContext(); + const isSelected = selectedValue === value; + const isFocused = focusedValue !== null && focusedValue === value; + + const handleOptionClick = (value: string, onClick?: () => void) => { + if (onClick) onClick(); + handleSelect(value); + }; return ( { + handleOptionClick(value, onClick); + }} > {text} diff --git a/packages/wow-ui/src/components/DropDown/context/DropDownContext.tsx b/packages/wow-ui/src/components/DropDown/context/DropDownContext.tsx new file mode 100644 index 00000000..f51c0b3e --- /dev/null +++ b/packages/wow-ui/src/components/DropDown/context/DropDownContext.tsx @@ -0,0 +1,29 @@ +"use client"; + +import type React from "react"; +import { createContext, useContext } from "react"; + +interface DropDownContextType { + selectedValue: string; + selectedText: React.ReactNode; + open: boolean; + focusedValue: string | null; + setOpen: React.Dispatch>; + setFocusedValue: React.Dispatch>; + handleSelect: (option: string) => void; + handleKeyDown: (event: React.KeyboardEvent) => void; +} + +export const DropDownContext = createContext( + undefined +); + +export const useDropDownContext = () => { + const context = useContext(DropDownContext); + if (!context) { + throw new Error( + "useDropDownContext must be used within a DropDownProvider" + ); + } + return context; +}; diff --git a/packages/wow-ui/src/components/DropDown/index.tsx b/packages/wow-ui/src/components/DropDown/index.tsx index ddbd0912..6ac03402 100644 --- a/packages/wow-ui/src/components/DropDown/index.tsx +++ b/packages/wow-ui/src/components/DropDown/index.tsx @@ -8,13 +8,13 @@ import type { ReactElement, ReactNode, } from "react"; -import { cloneElement, isValidElement, useRef } from "react"; +import { cloneElement, useRef } from "react"; import { DownArrow } from "wowds-icons"; -import DropDownOption from "@/components/DropDown/DropDownOption"; -import useClickOutside from "@/hooks/useClickOutside"; -import useDropDownState from "@/hooks/useDropDownState"; -import useFlattenChildren from "@/hooks/useFlattenChildren"; +import { DropDownContext } from "../../components/DropDown/context/DropDownContext"; +import useClickOutside from "../../hooks/useClickOutside"; +import useDropDownState from "../../hooks/useDropDownState"; +import useFlattenChildren from "../../hooks/useFlattenChildren"; export interface DropDownWithTriggerProps extends PropsWithChildren { /** @@ -70,7 +70,10 @@ export type DropDownProps = ( ) & { value?: string; defaultValue?: string; - onChange?: (value: string) => void; + onChange?: (value: { + selectedValue: string; + selectedText: ReactNode; + }) => void; style?: CSSProperties; className?: string; }; @@ -86,42 +89,26 @@ const DropDown = ({ ...rest }: DropDownProps) => { const flattenedChildren = useFlattenChildren(children); - const { - selected, - open, - setOpen, - focusedIndex, - setFocusedIndex, - handleSelect, - handleKeyDown, - } = useDropDownState({ + const dropdownState = useDropDownState({ value, defaultValue, children: flattenedChildren, onChange, }); + const { selectedValue, selectedText, open, setFocusedValue, setOpen } = + dropdownState; const dropdownRef = useRef(null); - const optionsRef = useRef<(HTMLDivElement | null)[]>([]); useClickOutside(dropdownRef, () => setOpen(false)); const toggleDropdown = () => { - setOpen((prevOpen) => { - if (!prevOpen) setFocusedIndex(null); + dropdownState.setOpen((prevOpen) => { + if (!prevOpen) setFocusedValue(null); return !prevOpen; }); }; - const handleOptionClick = (value: string, onClick?: () => void) => () => { - if (onClick) onClick(); - handleSelect(value); - }; - - const setOptionRef = (index: number) => (el: HTMLDivElement | null) => { - optionsRef.current[index] = el; - }; - const renderTrigger = (trigger: ReactElement) => ( <> {cloneElement(trigger, { @@ -133,7 +120,7 @@ const DropDown = ({ const renderLabel = () => ( <> {label} @@ -142,20 +129,20 @@ const DropDown = ({ alignItems="center" justifyContent="space-between" className={dropdownStyle({ - type: open ? "focused" : selected ? "selected" : "default", + type: open ? "focused" : selectedValue ? "selected" : "default", })} onClick={toggleDropdown} > - {selected ? selected : placeholder} + {selectedText ? selectedText : placeholder} { if (e.key === "Enter") toggleDropdown(); @@ -167,37 +154,28 @@ const DropDown = ({ const renderOptions = () => ( - {flattenedChildren.map((child, index) => { - if (isValidElement(child) && child.type === DropDownOption) { - return cloneElement(child as ReactElement, { - key: child.props.value, - ref: setOptionRef(index), - onClick: handleOptionClick(child.props.value, child.props.onClick), - focused: focusedIndex === index, - selected: selected === child.props.value, - }); - } - return child; - })} + {children} ); return ( - - {trigger ? renderTrigger(trigger) : renderLabel()} - {open && renderOptions()} - + + + {trigger ? renderTrigger(trigger) : renderLabel()} + {dropdownState.open && renderOptions()} + + ); }; diff --git a/packages/wow-ui/src/components/TextField/index.tsx b/packages/wow-ui/src/components/TextField/index.tsx index c4545491..1044381e 100644 --- a/packages/wow-ui/src/components/TextField/index.tsx +++ b/packages/wow-ui/src/components/TextField/index.tsx @@ -12,7 +12,8 @@ import type { } from "react"; import { forwardRef, useId, useLayoutEffect, useRef, useState } from "react"; -import { useTextareaAutosize } from "@/hooks/useTextareaAutosize"; +import InputBase from "../../base/InputBase"; +import { useTextareaAutosize } from "../../hooks/useTextareaAutosize"; type VariantType = "default" | "typing" | "typed" | "success" | "error"; @@ -78,11 +79,13 @@ const TextField = forwardRef( const helperTextId = `${textareaId}-helper-text`; const descriptionId = error ? `${errorMessageId}` : `${helperTextId}`; + const [value, setValue] = useState(valueProp ?? defaultValue ?? ""); + const [variant, setVariant] = useState("default"); + const textareaRef = useRef(null); const textareaElementRef = ref || textareaRef; - const [value, setValue] = useState(valueProp ?? defaultValue ?? ""); - const [variant, setVariant] = useState("default"); + useTextareaAutosize(textareaElementRef as RefObject); useLayoutEffect(() => { if (success) { @@ -147,6 +150,7 @@ const TextField = forwardRef( placeholder={placeholder} ref={textareaElementRef} rows={1} + //setValue={setValue} value={value} onBlur={handleBlur} onChange={handleChange} diff --git a/packages/wow-ui/src/hooks/useClickOutside.ts b/packages/wow-ui/src/hooks/useClickOutside.ts index b08788a1..b0b7040d 100644 --- a/packages/wow-ui/src/hooks/useClickOutside.ts +++ b/packages/wow-ui/src/hooks/useClickOutside.ts @@ -1,6 +1,6 @@ import { useEffect } from "react"; -import { isMobile } from "@/utils"; +import { isMobile } from "../utils"; const useClickOutside = ( ref: React.RefObject, diff --git a/packages/wow-ui/src/hooks/useDropDownState.ts b/packages/wow-ui/src/hooks/useDropDownState.ts index a12ab555..64751f78 100644 --- a/packages/wow-ui/src/hooks/useDropDownState.ts +++ b/packages/wow-ui/src/hooks/useDropDownState.ts @@ -1,11 +1,14 @@ import type { KeyboardEvent, ReactElement, ReactNode } from "react"; -import { useEffect, useState } from "react"; +import { isValidElement, useEffect, useMemo, useState } from "react"; interface DropDownStateProps { value?: string; defaultValue?: string; children: ReactNode[]; - onChange?: (value: string) => void; + onChange?: (value: { + selectedValue: string; + selectedText: ReactNode; + }) => void; } const useDropDownState = ({ @@ -14,54 +17,76 @@ const useDropDownState = ({ children, onChange, }: DropDownStateProps) => { - const [selected, setSelected] = useState(defaultValue || ""); + const options = useMemo(() => { + const opts: { [key: string]: ReactNode } = {}; + children.forEach((child) => { + if (isValidElement(child)) { + opts[child.props.value] = child.props.text; + } + }); + return opts; + }, [children]); + const [selectedValue, setSelectedValue] = useState(defaultValue || ""); + const [selectedText, setSelectedText] = useState( + defaultValue ? options[defaultValue] : "" + ); const [open, setOpen] = useState(false); - const [focusedIndex, setFocusedIndex] = useState(null); + const [focusedValue, setFocusedValue] = useState(null); useEffect(() => { if (value !== undefined) { - setSelected(value); + setSelectedValue(value); + setSelectedText(options[value]); } - }, [value]); + }, [options, value]); const handleSelect = (option: string) => { if (value === undefined) { - setSelected(option); + setSelectedValue(option); + setSelectedText(options[option]); } setOpen(false); - onChange && onChange(option); + if (onChange) { + onChange({ selectedValue: option, selectedText: options[option] }); + } }; const handleKeyDown = (event: KeyboardEvent) => { if (!open) return; const { key } = event; + const values = Object.keys(options); if (key === "ArrowDown") { - setFocusedIndex((prevIndex) => - prevIndex === null ? 0 : (prevIndex + 1) % children.length - ); + setFocusedValue((prevValue) => { + const currentIndex = values.indexOf(prevValue ?? ""); + const nextIndex = + currentIndex === -1 ? 0 : (currentIndex + 1) % values.length; + return values[nextIndex]; + }); event.preventDefault(); } else if (key === "ArrowUp") { - setFocusedIndex((prevIndex) => - prevIndex === null - ? children.length - 1 - : (prevIndex - 1 + children.length) % children.length - ); + setFocusedValue((prevValue) => { + const currentIndex = values.indexOf(prevValue ?? ""); + const nextIndex = + currentIndex === -1 + ? values.length - 1 + : (currentIndex - 1 + values.length) % values.length; + return values[nextIndex]; + }); event.preventDefault(); - } else if (key === "Enter" && focusedIndex !== null) { - const child = children[focusedIndex] as ReactElement; - handleSelect(child.props.value); + } else if (key === "Enter" && focusedValue !== null) { + handleSelect(focusedValue); event.preventDefault(); } }; - return { - selected, + selectedValue, + selectedText, open, setOpen, - focusedIndex, - setFocusedIndex, + focusedValue, + setFocusedValue, handleSelect, handleKeyDown, }; From ea561044a8e28b319349b27971de4888daca913e Mon Sep 17 00:00:00 2001 From: SeieunYoo Date: Sun, 21 Jul 2024 14:47:17 +0900 Subject: [PATCH 07/27] =?UTF-8?q?refactor=20:=20=EC=A0=88=EB=8C=80?= =?UTF-8?q?=EA=B2=BD=EB=A1=9C=EB=A1=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/DropDown/DropDownOption.tsx | 2 +- .../DropDown/context/DropDownContext.tsx | 18 ++++-------------- .../wow-ui/src/components/DropDown/index.tsx | 8 ++++---- packages/wow-ui/src/hooks/useClickOutside.ts | 2 +- packages/wow-ui/src/hooks/useDropDownState.ts | 2 +- 5 files changed, 11 insertions(+), 21 deletions(-) diff --git a/packages/wow-ui/src/components/DropDown/DropDownOption.tsx b/packages/wow-ui/src/components/DropDown/DropDownOption.tsx index 8094e18b..e44c5195 100644 --- a/packages/wow-ui/src/components/DropDown/DropDownOption.tsx +++ b/packages/wow-ui/src/components/DropDown/DropDownOption.tsx @@ -3,7 +3,7 @@ import { styled } from "@styled-system/jsx"; import type { ReactNode } from "react"; import { forwardRef } from "react"; -import { useDropDownContext } from "../../components/DropDown/context/DropDownContext"; +import { useDropDownContext } from "@/components/DropDown/context/DropDownContext"; /** * @description 드롭다운 옵션의 props입니다. diff --git a/packages/wow-ui/src/components/DropDown/context/DropDownContext.tsx b/packages/wow-ui/src/components/DropDown/context/DropDownContext.tsx index f51c0b3e..81516300 100644 --- a/packages/wow-ui/src/components/DropDown/context/DropDownContext.tsx +++ b/packages/wow-ui/src/components/DropDown/context/DropDownContext.tsx @@ -1,22 +1,12 @@ "use client"; -import type React from "react"; import { createContext, useContext } from "react"; -interface DropDownContextType { - selectedValue: string; - selectedText: React.ReactNode; - open: boolean; - focusedValue: string | null; - setOpen: React.Dispatch>; - setFocusedValue: React.Dispatch>; - handleSelect: (option: string) => void; - handleKeyDown: (event: React.KeyboardEvent) => void; -} +import type useDropDownState from "@/hooks/useDropDownState"; -export const DropDownContext = createContext( - undefined -); +export const DropDownContext = createContext< + ReturnType | undefined +>(undefined); export const useDropDownContext = () => { const context = useContext(DropDownContext); diff --git a/packages/wow-ui/src/components/DropDown/index.tsx b/packages/wow-ui/src/components/DropDown/index.tsx index 531b0fbe..613689cd 100644 --- a/packages/wow-ui/src/components/DropDown/index.tsx +++ b/packages/wow-ui/src/components/DropDown/index.tsx @@ -11,10 +11,10 @@ import type { import { cloneElement, useRef } from "react"; import { DownArrow } from "wowds-icons"; -import { DropDownContext } from "../../components/DropDown/context/DropDownContext"; -import useClickOutside from "../../hooks/useClickOutside"; -import useDropDownState from "../../hooks/useDropDownState"; -import useFlattenChildren from "../../hooks/useFlattenChildren"; +import { DropDownContext } from "@/components/DropDown/context/DropDownContext"; +import useClickOutside from "@/hooks/useClickOutside"; +import useDropDownState from "@/hooks/useDropDownState"; +import useFlattenChildren from "@/hooks/useFlattenChildren"; export interface DropDownWithTriggerProps extends PropsWithChildren { /** diff --git a/packages/wow-ui/src/hooks/useClickOutside.ts b/packages/wow-ui/src/hooks/useClickOutside.ts index b0b7040d..b08788a1 100644 --- a/packages/wow-ui/src/hooks/useClickOutside.ts +++ b/packages/wow-ui/src/hooks/useClickOutside.ts @@ -1,6 +1,6 @@ import { useEffect } from "react"; -import { isMobile } from "../utils"; +import { isMobile } from "@/utils"; const useClickOutside = ( ref: React.RefObject, diff --git a/packages/wow-ui/src/hooks/useDropDownState.ts b/packages/wow-ui/src/hooks/useDropDownState.ts index 9f4a8932..3498cd27 100644 --- a/packages/wow-ui/src/hooks/useDropDownState.ts +++ b/packages/wow-ui/src/hooks/useDropDownState.ts @@ -1,4 +1,4 @@ -import type { KeyboardEvent, ReactElement, ReactNode } from "react"; +import type { KeyboardEvent, ReactNode } from "react"; import { isValidElement, useEffect, useMemo, useState } from "react"; interface DropDownStateProps { From dc8c58910eff7ed266de39d9a5b2c75b4019daa4 Mon Sep 17 00:00:00 2001 From: SeieunYoo Date: Sun, 21 Jul 2024 14:55:16 +0900 Subject: [PATCH 08/27] =?UTF-8?q?fix=20:=20ts=20=EB=A1=9C=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DropDown/context/{DropDownContext.tsx => DropDownContext.ts} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename packages/wow-ui/src/components/DropDown/context/{DropDownContext.tsx => DropDownContext.ts} (100%) diff --git a/packages/wow-ui/src/components/DropDown/context/DropDownContext.tsx b/packages/wow-ui/src/components/DropDown/context/DropDownContext.ts similarity index 100% rename from packages/wow-ui/src/components/DropDown/context/DropDownContext.tsx rename to packages/wow-ui/src/components/DropDown/context/DropDownContext.ts From 6b3db85cd1a4615907182768a5767b3c367f7926 Mon Sep 17 00:00:00 2001 From: SeieunYoo Date: Mon, 22 Jul 2024 00:31:37 +0900 Subject: [PATCH 09/27] =?UTF-8?q?feat=20:=20DropDownOption=20=EC=97=90=20u?= =?UTF-8?q?se=20client=20=EC=B6=94=EA=B0=80,=20useSafeContext=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/wow-docs/app/page.tsx | 4 ++-- .../src/components/DropDown/DropDownOption.tsx | 2 ++ .../components/DropDown/context/DropDownContext.ts | 10 ++-------- packages/wow-ui/src/hooks/useSafeContext.ts | 12 ++++++++++++ 4 files changed, 18 insertions(+), 10 deletions(-) create mode 100644 packages/wow-ui/src/hooks/useSafeContext.ts diff --git a/apps/wow-docs/app/page.tsx b/apps/wow-docs/app/page.tsx index 282a6da3..0abc9d85 100644 --- a/apps/wow-docs/app/page.tsx +++ b/apps/wow-docs/app/page.tsx @@ -18,8 +18,8 @@ const Home = () => { - - + + diff --git a/packages/wow-ui/src/components/DropDown/DropDownOption.tsx b/packages/wow-ui/src/components/DropDown/DropDownOption.tsx index e44c5195..df9e44d9 100644 --- a/packages/wow-ui/src/components/DropDown/DropDownOption.tsx +++ b/packages/wow-ui/src/components/DropDown/DropDownOption.tsx @@ -1,3 +1,5 @@ +"use client"; + import { cva } from "@styled-system/css"; import { styled } from "@styled-system/jsx"; import type { ReactNode } from "react"; diff --git a/packages/wow-ui/src/components/DropDown/context/DropDownContext.ts b/packages/wow-ui/src/components/DropDown/context/DropDownContext.ts index 81516300..c164e4cf 100644 --- a/packages/wow-ui/src/components/DropDown/context/DropDownContext.ts +++ b/packages/wow-ui/src/components/DropDown/context/DropDownContext.ts @@ -1,19 +1,13 @@ -"use client"; - import { createContext, useContext } from "react"; import type useDropDownState from "@/hooks/useDropDownState"; +import useSafeContext from "@/hooks/useSafeContext"; export const DropDownContext = createContext< ReturnType | undefined >(undefined); export const useDropDownContext = () => { - const context = useContext(DropDownContext); - if (!context) { - throw new Error( - "useDropDownContext must be used within a DropDownProvider" - ); - } + const context = useSafeContext(DropDownContext); return context; }; diff --git a/packages/wow-ui/src/hooks/useSafeContext.ts b/packages/wow-ui/src/hooks/useSafeContext.ts new file mode 100644 index 00000000..d9543ca5 --- /dev/null +++ b/packages/wow-ui/src/hooks/useSafeContext.ts @@ -0,0 +1,12 @@ +import type { Context } from "react"; +import { useContext } from "react"; + +const useSafeContext = (context: Context): T => { + const contextValue = useContext(context); + if (contextValue === undefined) { + throw new Error(`useSafeContext must be used within a context provider`); + } + return contextValue; +}; + +export default useSafeContext; From 6574310421e15e14173bc8b762c56a7b3ec34890 Mon Sep 17 00:00:00 2001 From: SeieunYoo Date: Mon, 22 Jul 2024 01:28:38 +0900 Subject: [PATCH 10/27] =?UTF-8?q?refactor:=20useSafeContext=20=EC=9D=98=20?= =?UTF-8?q?=ED=83=80=EC=9E=85=20=EC=B6=94=EB=A1=A0=20=EA=B0=95=ED=99=94,?= =?UTF-8?q?=20aria=20=EC=86=8D=EC=84=B1=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DropDown/context/DropDownContext.ts | 8 ++-- .../wow-ui/src/components/DropDown/index.tsx | 42 ++++++++++++++----- packages/wow-ui/src/hooks/useSafeContext.ts | 13 ++++-- 3 files changed, 46 insertions(+), 17 deletions(-) diff --git a/packages/wow-ui/src/components/DropDown/context/DropDownContext.ts b/packages/wow-ui/src/components/DropDown/context/DropDownContext.ts index c164e4cf..d0229174 100644 --- a/packages/wow-ui/src/components/DropDown/context/DropDownContext.ts +++ b/packages/wow-ui/src/components/DropDown/context/DropDownContext.ts @@ -1,11 +1,11 @@ -import { createContext, useContext } from "react"; +import { createContext } from "react"; import type useDropDownState from "@/hooks/useDropDownState"; import useSafeContext from "@/hooks/useSafeContext"; -export const DropDownContext = createContext< - ReturnType | undefined ->(undefined); +export const DropDownContext = createContext | null>(null); export const useDropDownContext = () => { const context = useSafeContext(DropDownContext); diff --git a/packages/wow-ui/src/components/DropDown/index.tsx b/packages/wow-ui/src/components/DropDown/index.tsx index 613689cd..1201c7d1 100644 --- a/packages/wow-ui/src/components/DropDown/index.tsx +++ b/packages/wow-ui/src/components/DropDown/index.tsx @@ -8,7 +8,7 @@ import type { ReactElement, ReactNode, } from "react"; -import { cloneElement, useRef } from "react"; +import { cloneElement, useId, useRef } from "react"; import { DownArrow } from "wowds-icons"; import { DropDownContext } from "@/components/DropDown/context/DropDownContext"; @@ -53,8 +53,9 @@ export interface DropDownWithoutTriggerProps extends PropsWithChildren { } /** - * @description 사용자가 외부 트리거 컴포넌트 나 내부 요소를 통해서 선택 옵션 리스트 중에 아이템을 선택할 수 있는 드롭다운 컴포넌트 입니다. + * @description 사용자가 외부 트리거 컴포넌트나 내부 요소를 통해서 선택 옵션 리스트 중에 아이템을 선택할 수 있는 드롭다운 컴포넌트 입니다. * + * @param {string} [id] 드롭다운 id 입니다. * @param {ReactElement} [trigger] 드롭다운을 열기 위한 외부 트리거 요소입니다. * @param {ReactNode} [label] 외부 트리거를 사용하지 않는 경우 레이블을 사용할 수 있습니다. * @param {string} [placeholder] 외부 트리거를 사용하지 않는 경우 선택되지 않았을 때 표시되는 플레이스홀더입니다. @@ -68,6 +69,7 @@ export type DropDownProps = ( | DropDownWithTriggerProps | DropDownWithoutTriggerProps ) & { + id?: string; value?: string; defaultValue?: string; onChange?: (value: { @@ -79,6 +81,7 @@ export type DropDownProps = ( }; const DropDown = ({ + id, children, trigger, label, @@ -96,28 +99,41 @@ const DropDown = ({ onChange, }); - const { selectedValue, selectedText, open, setFocusedValue, setOpen } = - dropdownState; + const { + selectedValue, + selectedText, + open, + setFocusedValue, + setOpen, + handleKeyDown, + } = dropdownState; + + const defaultId = useId(); + const dropdownId = id ?? `dropdown-${defaultId}`; const dropdownRef = useRef(null); useClickOutside(dropdownRef, () => setOpen(false)); const toggleDropdown = () => { - dropdownState.setOpen((prevOpen) => { + setOpen((prevOpen) => { if (!prevOpen) setFocusedValue(null); return !prevOpen; }); }; - const renderTrigger = (trigger: ReactElement) => ( + const renderCustomTrigger = (trigger: ReactElement) => ( <> {cloneElement(trigger, { onClick: toggleDropdown, + "aria-expanded": open, + "aria-haspopup": "true", + id: `${dropdownId}-trigger`, + "aria-controls": `${dropdownId}`, })} ); - const renderLabel = () => ( + const renderDefaultTrigger = () => ( <> - {trigger ? renderTrigger(trigger) : renderLabel()} - {dropdownState.open && renderOptions()} + {trigger ? renderCustomTrigger(trigger) : renderDefaultTrigger()} + {open && renderOptions()} ); diff --git a/packages/wow-ui/src/hooks/useSafeContext.ts b/packages/wow-ui/src/hooks/useSafeContext.ts index d9543ca5..5ea8dfda 100644 --- a/packages/wow-ui/src/hooks/useSafeContext.ts +++ b/packages/wow-ui/src/hooks/useSafeContext.ts @@ -1,12 +1,19 @@ import type { Context } from "react"; import { useContext } from "react"; -const useSafeContext = (context: Context): T => { +type SafeContextType = + T extends Context ? Exclude : never; + +const useSafeContext = >( + context: T +): SafeContextType => { const contextValue = useContext(context); + if (contextValue === undefined) { - throw new Error(`useSafeContext must be used within a context provider`); + throw new Error(`useSafeContext must be used within a context Provider`); } - return contextValue; + + return contextValue as SafeContextType; }; export default useSafeContext; From 4343ad028bdd5d9bebcb2cda1584dc057bc339b5 Mon Sep 17 00:00:00 2001 From: SeieunYoo Date: Mon, 22 Jul 2024 02:07:39 +0900 Subject: [PATCH 11/27] =?UTF-8?q?fix=20:=20=EC=A0=91=EA=B7=BC=EC=84=B1=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=20=ED=95=B4=EA=B2=B0,=20=20test=20c?= =?UTF-8?q?i=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/wow-ui/src/components/DropDown/index.tsx | 5 +++-- packages/wow-ui/src/components/TextField/index.tsx | 3 +-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/wow-ui/src/components/DropDown/index.tsx b/packages/wow-ui/src/components/DropDown/index.tsx index 1201c7d1..38516908 100644 --- a/packages/wow-ui/src/components/DropDown/index.tsx +++ b/packages/wow-ui/src/components/DropDown/index.tsx @@ -141,11 +141,12 @@ const DropDown = ({ > {label} - - + ); diff --git a/packages/wow-ui/src/components/TextField/index.tsx b/packages/wow-ui/src/components/TextField/index.tsx index 695c9341..91dd1961 100644 --- a/packages/wow-ui/src/components/TextField/index.tsx +++ b/packages/wow-ui/src/components/TextField/index.tsx @@ -12,8 +12,7 @@ import type { } from "react"; import { forwardRef, useId, useLayoutEffect, useRef, useState } from "react"; -import InputBase from "../../base/InputBase"; -import { useTextareaAutosize } from "../../hooks/useTextareaAutosize"; +import { useTextareaAutosize } from "@/hooks/useTextareaAutosize"; type VariantType = "default" | "typing" | "typed" | "success" | "error"; From 88f7fcec295c00aebd3eaceec40b717dbf079ebb Mon Sep 17 00:00:00 2001 From: SeieunYoo Date: Mon, 22 Jul 2024 02:19:21 +0900 Subject: [PATCH 12/27] =?UTF-8?q?fix:=20=ED=85=8D=ED=95=84=20=EC=9B=90?= =?UTF-8?q?=EB=B3=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/wow-ui/src/components/TextField/index.tsx | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/packages/wow-ui/src/components/TextField/index.tsx b/packages/wow-ui/src/components/TextField/index.tsx index 91dd1961..199a5a33 100644 --- a/packages/wow-ui/src/components/TextField/index.tsx +++ b/packages/wow-ui/src/components/TextField/index.tsx @@ -78,13 +78,11 @@ const TextField = forwardRef( const helperTextId = `${textareaId}-helper-text`; const descriptionId = error ? `${errorMessageId}` : `${helperTextId}`; - const [value, setValue] = useState(valueProp ?? defaultValue ?? ""); - const [variant, setVariant] = useState("default"); - const textareaRef = useRef(null); const textareaElementRef = ref || textareaRef; - useTextareaAutosize(textareaElementRef as RefObject); + const [value, setValue] = useState(valueProp ?? defaultValue ?? ""); + const [variant, setVariant] = useState("default"); useLayoutEffect(() => { if (success) { @@ -149,7 +147,6 @@ const TextField = forwardRef( placeholder={placeholder} ref={textareaElementRef} rows={1} - //setValue={setValue} value={value} onBlur={handleBlur} onChange={handleChange} From c5edc599944c83391bc02d14c158cbf50810ce23 Mon Sep 17 00:00:00 2001 From: SeieunYoo Date: Mon, 22 Jul 2024 02:33:17 +0900 Subject: [PATCH 13/27] =?UTF-8?q?style=20:=20trigger=20=EA=B0=80=20?= =?UTF-8?q?=EC=9E=88=EC=9D=84=20=EB=95=8C=EB=8A=94=20=20100%=20=EC=9C=A0?= =?UTF-8?q?=EC=A7=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../wow-ui/src/components/DropDown/index.tsx | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/packages/wow-ui/src/components/DropDown/index.tsx b/packages/wow-ui/src/components/DropDown/index.tsx index 38516908..6b0c5b18 100644 --- a/packages/wow-ui/src/components/DropDown/index.tsx +++ b/packages/wow-ui/src/components/DropDown/index.tsx @@ -146,9 +146,11 @@ const DropDown = ({ aria-controls={dropdownId} aria-expanded={open} aria-haspopup={true} + cursor="pointer" display="flex" id={`${dropdownId}-trigger`} justifyContent="space-between" + outline="none" className={dropdownStyle({ type: open ? "focused" : selectedValue ? "selected" : "default", })} @@ -174,7 +176,12 @@ const DropDown = ({ ); const renderOptions = () => ( - + {children} ); @@ -292,6 +299,15 @@ const dropdownContentStyle = cva({ marginBottom: "2px", }, }, + variants: { + type: { + custom: { + width: "100%", + }, + default: {}, + }, + }, + defaultVariants: { type: "default" }, }); const placeholderStyle = cva({ From 3c38d6b01a05625325d9c7bffb69aa190f5e7b68 Mon Sep 17 00:00:00 2001 From: SeieunYoo Date: Thu, 25 Jul 2024 23:38:01 +0900 Subject: [PATCH 14/27] =?UTF-8?q?refactor:=20useDropDownstate=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/wow-ui/src/hooks/useDropDownState.ts | 67 +++++++++++-------- 1 file changed, 39 insertions(+), 28 deletions(-) diff --git a/packages/wow-ui/src/hooks/useDropDownState.ts b/packages/wow-ui/src/hooks/useDropDownState.ts index 3498cd27..9506010e 100644 --- a/packages/wow-ui/src/hooks/useDropDownState.ts +++ b/packages/wow-ui/src/hooks/useDropDownState.ts @@ -11,6 +11,11 @@ interface DropDownStateProps { }) => void; } +interface SelectedOption { + selectedValue: string; + selectedText: ReactNode; +} + const useDropDownState = ({ value, defaultValue, @@ -27,63 +32,69 @@ const useDropDownState = ({ return opts; }, [children]); - const [selectedValue, setSelectedValue] = useState(defaultValue || ""); - const [selectedText, setSelectedText] = useState( - defaultValue ? options[defaultValue] : "" + const getDefaultSelectedOption = () => { + if (defaultValue && options[defaultValue]) { + return { + selectedValue: defaultValue, + selectedText: options[defaultValue], + }; + } + return { selectedValue: "", selectedText: "" }; + }; + + const [selectedOption, setSelectedOption] = useState( + getDefaultSelectedOption() ); const [open, setOpen] = useState(false); const [focusedValue, setFocusedValue] = useState(null); useEffect(() => { - if (value !== undefined) { - setSelectedValue(value); - setSelectedText(options[value]); + if (value !== undefined && options[value]) { + setSelectedOption({ selectedValue: value, selectedText: options[value] }); } }, [options, value]); - const handleSelect = (option: string) => { + const handleSelect = (selectedValue: string) => { + const selectedText = options[selectedValue]; if (value === undefined) { - setSelectedValue(option); - setSelectedText(options[option]); + setSelectedOption({ selectedValue, selectedText }); } - setOpen(false); if (onChange) { - onChange({ selectedValue: option, selectedText: options[option] }); + onChange({ selectedValue, selectedText }); } + setOpen(false); + }; + + const updateFocusedValue = (direction: number) => { + const values = Object.keys(options); + setFocusedValue((prevValue) => { + const currentIndex = values.indexOf(prevValue ?? ""); + const nextIndex = + (currentIndex + direction + values.length) % values.length; + return values[nextIndex] ?? ""; + }); }; const handleKeyDown = (event: KeyboardEvent) => { if (!open) return; const { key } = event; - const values = Object.keys(options); if (key === "ArrowDown") { - setFocusedValue((prevValue) => { - const currentIndex = values.indexOf(prevValue ?? ""); - const nextIndex = - currentIndex === -1 ? 0 : (currentIndex + 1) % values.length; - return values[nextIndex] ?? ""; - }); + updateFocusedValue(1); event.preventDefault(); } else if (key === "ArrowUp") { - setFocusedValue((prevValue) => { - const currentIndex = values.indexOf(prevValue ?? ""); - const nextIndex = - currentIndex === -1 - ? values.length - 1 - : (currentIndex - 1 + values.length) % values.length; - return values[nextIndex] ?? ""; - }); + updateFocusedValue(-1); event.preventDefault(); } else if (key === "Enter" && focusedValue !== null) { handleSelect(focusedValue); event.preventDefault(); } }; + return { - selectedValue, - selectedText, + selectedValue: selectedOption.selectedValue, + selectedText: selectedOption.selectedText, open, setOpen, focusedValue, From b001317cc3e0c4478ef963b7adda0a1028dff42a Mon Sep 17 00:00:00 2001 From: SeieunYoo Date: Fri, 26 Jul 2024 16:40:08 +0900 Subject: [PATCH 15/27] =?UTF-8?q?refactor=20:=20DropDownTrigger=20?= =?UTF-8?q?=EB=A1=9C=20=EB=A6=AC=ED=8C=A9=ED=86=A0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/DropDown/DropDownOption.tsx | 2 +- .../components/DropDown/DropDownTrigger.tsx | 166 +++++++++++++++ .../wow-ui/src/components/DropDown/index.tsx | 190 +++--------------- 3 files changed, 191 insertions(+), 167 deletions(-) create mode 100644 packages/wow-ui/src/components/DropDown/DropDownTrigger.tsx diff --git a/packages/wow-ui/src/components/DropDown/DropDownOption.tsx b/packages/wow-ui/src/components/DropDown/DropDownOption.tsx index df9e44d9..3ec6a221 100644 --- a/packages/wow-ui/src/components/DropDown/DropDownOption.tsx +++ b/packages/wow-ui/src/components/DropDown/DropDownOption.tsx @@ -5,7 +5,7 @@ import { styled } from "@styled-system/jsx"; import type { ReactNode } from "react"; import { forwardRef } from "react"; -import { useDropDownContext } from "@/components/DropDown/context/DropDownContext"; +import { useDropDownContext } from "../../components/DropDown/context/DropDownContext"; /** * @description 드롭다운 옵션의 props입니다. diff --git a/packages/wow-ui/src/components/DropDown/DropDownTrigger.tsx b/packages/wow-ui/src/components/DropDown/DropDownTrigger.tsx new file mode 100644 index 00000000..2f30a58e --- /dev/null +++ b/packages/wow-ui/src/components/DropDown/DropDownTrigger.tsx @@ -0,0 +1,166 @@ +import { cva } from "@styled-system/css"; +import { styled } from "@styled-system/jsx"; +import type { ReactElement } from "react"; +import React, { cloneElement } from "react"; +import { DownArrow } from "wowds-icons"; + +import { useDropDownContext } from "../../components/DropDown/context/DropDownContext"; + +interface DropDownTriggerProps { + placeholder?: string; + label?: React.ReactNode; + trigger?: ReactElement; + dropdownId: string; +} + +const DropDownTrigger = ({ + placeholder, + label, + trigger, + dropdownId, +}: DropDownTriggerProps) => { + const { open, selectedValue, selectedText, setOpen, setFocusedValue } = + useDropDownContext(); + const toggleDropdown = () => { + setOpen((prevOpen) => { + if (!prevOpen) setFocusedValue(null); + return !prevOpen; + }); + }; + + if (trigger) { + return ( + <> + {cloneElement(trigger, { + onClick: toggleDropdown, + "aria-expanded": open, + "aria-haspopup": "true", + id: `${dropdownId}-trigger`, + "aria-controls": `${dropdownId}`, + })} + + ); + } + + return ( + <> + + {label} + + + + {selectedText ? selectedText : placeholder} + + { + if (e.key === "Enter") toggleDropdown(); + }} + /> + + + ); +}; + +export default DropDownTrigger; + +const iconStyle = cva({ + base: { + transition: "transform 1s ease", + }, + variants: { + type: { + up: { + transform: "rotate(180deg)", + }, + down: { + transform: "rotate(0deg)", + }, + }, + }, +}); + +const dropdownStyle = cva({ + base: { + lg: { + maxWidth: "22.375rem", + }, + smDown: { + width: "100%", + }, + backgroundColor: "backgroundNormal", + border: "1px solid", + borderRadius: "sm", + borderColor: "outline", + paddingY: "xs", + paddingX: "sm", + }, + variants: { + type: { + default: { + borderColor: "outline", + _hover: { + borderColor: "sub", + }, + _pressed: { + backgroundColor: "monoBackgroundPressed", + }, + }, + focused: { + borderColor: "primary", + color: "primary", + }, + selected: { + borderColor: "sub", + }, + }, + }, + defaultVariants: { + type: "default", + }, +}); + +const placeholderStyle = cva({ + base: { + textStyle: "body1", + }, + variants: { + type: { + default: { + color: "outline", + _hover: { + color: "sub", + }, + }, + focused: { + color: "primary", + }, + selected: { + color: "textBlack", + }, + }, + }, +}); diff --git a/packages/wow-ui/src/components/DropDown/index.tsx b/packages/wow-ui/src/components/DropDown/index.tsx index 6b0c5b18..239d3fbe 100644 --- a/packages/wow-ui/src/components/DropDown/index.tsx +++ b/packages/wow-ui/src/components/DropDown/index.tsx @@ -1,20 +1,20 @@ "use client"; import { cva } from "@styled-system/css"; -import { Flex, styled } from "@styled-system/jsx"; +import { Flex } from "@styled-system/jsx"; import type { CSSProperties, PropsWithChildren, ReactElement, ReactNode, } from "react"; -import { cloneElement, useId, useRef } from "react"; -import { DownArrow } from "wowds-icons"; +import { useId, useRef } from "react"; -import { DropDownContext } from "@/components/DropDown/context/DropDownContext"; -import useClickOutside from "@/hooks/useClickOutside"; -import useDropDownState from "@/hooks/useDropDownState"; -import useFlattenChildren from "@/hooks/useFlattenChildren"; +import { DropDownContext } from "../../components/DropDown/context/DropDownContext"; +import DropDownTrigger from "../../components/DropDown/DropDownTrigger"; +import useClickOutside from "../../hooks/useClickOutside"; +import useDropDownState from "../../hooks/useDropDownState"; +import useFlattenChildren from "../../hooks/useFlattenChildren"; export interface DropDownWithTriggerProps extends PropsWithChildren { /** @@ -99,14 +99,7 @@ const DropDown = ({ onChange, }); - const { - selectedValue, - selectedText, - open, - setFocusedValue, - setOpen, - handleKeyDown, - } = dropdownState; + const { open, setOpen, handleKeyDown } = dropdownState; const defaultId = useId(); const dropdownId = id ?? `dropdown-${defaultId}`; @@ -114,78 +107,6 @@ const DropDown = ({ useClickOutside(dropdownRef, () => setOpen(false)); - const toggleDropdown = () => { - setOpen((prevOpen) => { - if (!prevOpen) setFocusedValue(null); - return !prevOpen; - }); - }; - - const renderCustomTrigger = (trigger: ReactElement) => ( - <> - {cloneElement(trigger, { - onClick: toggleDropdown, - "aria-expanded": open, - "aria-haspopup": "true", - id: `${dropdownId}-trigger`, - "aria-controls": `${dropdownId}`, - })} - - ); - - const renderDefaultTrigger = () => ( - <> - - {label} - - - - {selectedText ? selectedText : placeholder} - - { - if (e.key === "Enter") toggleDropdown(); - }} - /> - - - ); - - const renderOptions = () => ( - - {children} - - ); - return ( - {trigger ? renderCustomTrigger(trigger) : renderDefaultTrigger()} - {open && renderOptions()} + + {open && ( + + {children} + + )} ); @@ -212,61 +147,6 @@ const DropDown = ({ DropDown.displayName = "DropDown"; export default DropDown; -const iconStyle = cva({ - base: { - transition: "transform 1s ease", - }, - variants: { - type: { - up: { - transform: "rotate(180deg)", - }, - down: { - transform: "rotate(0deg)", - }, - }, - }, -}); -const dropdownStyle = cva({ - base: { - lg: { - maxWidth: "22.375rem", - }, - smDown: { - width: "100%", - }, - backgroundColor: "backgroundNormal", - border: "1px solid", - borderRadius: "sm", - borderColor: "outline", - paddingY: "xs", - paddingX: "sm", - }, - variants: { - type: { - default: { - borderColor: "outline", - _hover: { - borderColor: "sub", - }, - _pressed: { - backgroundColor: "monoBackgroundPressed", - }, - }, - focused: { - borderColor: "primary", - color: "primary", - }, - selected: { - borderColor: "sub", - }, - }, - }, - defaultVariants: { - type: "default", - }, -}); - const dropdownContentStyle = cva({ base: { position: "absolute", @@ -309,25 +189,3 @@ const dropdownContentStyle = cva({ }, defaultVariants: { type: "default" }, }); - -const placeholderStyle = cva({ - base: { - textStyle: "body1", - }, - variants: { - type: { - default: { - color: "outline", - _hover: { - color: "sub", - }, - }, - focused: { - color: "primary", - }, - selected: { - color: "textBlack", - }, - }, - }, -}); From 504878c6bebddcc121da0cbc5d831dfabf984139 Mon Sep 17 00:00:00 2001 From: SeieunYoo Date: Fri, 26 Jul 2024 17:26:24 +0900 Subject: [PATCH 16/27] =?UTF-8?q?refactor=20:=20collection=20context=20?= =?UTF-8?q?=EB=A1=9C=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/DropDown/DropDownOption.tsx | 16 +++- .../components/DropDown/DropDownTrigger.tsx | 24 +++-- .../components/DropDown/DropDownWrapper.tsx | 76 ++++++++++++++++ .../DropDown/context/CollectionContext.tsx | 40 +++++++++ .../DropDown/context/DropDownContext.ts | 4 +- .../wow-ui/src/components/DropDown/index.tsx | 62 +++++-------- packages/wow-ui/src/hooks/useDropDownState.ts | 87 ++++++------------- 7 files changed, 199 insertions(+), 110 deletions(-) create mode 100644 packages/wow-ui/src/components/DropDown/DropDownWrapper.tsx create mode 100644 packages/wow-ui/src/components/DropDown/context/CollectionContext.tsx diff --git a/packages/wow-ui/src/components/DropDown/DropDownOption.tsx b/packages/wow-ui/src/components/DropDown/DropDownOption.tsx index 3ec6a221..8a588e07 100644 --- a/packages/wow-ui/src/components/DropDown/DropDownOption.tsx +++ b/packages/wow-ui/src/components/DropDown/DropDownOption.tsx @@ -3,10 +3,13 @@ import { cva } from "@styled-system/css"; import { styled } from "@styled-system/jsx"; import type { ReactNode } from "react"; -import { forwardRef } from "react"; +import { forwardRef, useEffect } from "react"; import { useDropDownContext } from "../../components/DropDown/context/DropDownContext"; - +import { + useCollection, + useCollectionContext, +} from "./context/CollectionContext"; /** * @description 드롭다운 옵션의 props입니다. * @@ -32,8 +35,15 @@ const DropDownOption = forwardRef( const handleOptionClick = (value: string, onClick?: () => void) => { if (onClick) onClick(); - handleSelect(value); + handleSelect(value, text); }; + + const itemMap = useCollection(); + + useEffect(() => { + itemMap.set(value, { text }); + }, [itemMap, value, text]); + return ( { - const { open, selectedValue, selectedText, setOpen, setFocusedValue } = + const { open, selectedValue, setOpen, setFocusedValue } = useDropDownContext(); + + const itemMap = useCollection(); + const selectedText = itemMap.get(selectedValue)?.text; + const toggleDropdown = () => { setOpen((prevOpen) => { if (!prevOpen) setFocusedValue(null); @@ -70,13 +78,13 @@ const DropDownTrigger = ({ type: open ? "focused" : selectedValue ? "selected" : "default", })} > - {selectedText ? selectedText : placeholder} + {selectedValue ? selectedText : placeholder} { + onKeyDown={(e: KeyboardEvent) => { if (e.key === "Enter") toggleDropdown(); }} /> diff --git a/packages/wow-ui/src/components/DropDown/DropDownWrapper.tsx b/packages/wow-ui/src/components/DropDown/DropDownWrapper.tsx new file mode 100644 index 00000000..5c84d662 --- /dev/null +++ b/packages/wow-ui/src/components/DropDown/DropDownWrapper.tsx @@ -0,0 +1,76 @@ +import { cva } from "@styled-system/css"; +import { Flex } from "@styled-system/jsx"; +import { type CSSProperties, type PropsWithChildren, useRef } from "react"; + +import type { DropDownProps } from "../../components/DropDown"; +import useClickOutside from "../../hooks/useClickOutside"; +import { + useCollection, + useCollectionContext, +} from "./context/CollectionContext"; +import { useDropDownContext } from "./context/DropDownContext"; + +interface DropDownWrapperProps extends PropsWithChildren { + dropdownId: string; + style?: DropDownProps["style"]; + className?: DropDownProps["className"]; +} +export const DropDownWrapper = ({ + children, + dropdownId, + ...rest +}: DropDownWrapperProps) => { + const { open, setOpen, setFocusedValue, focusedValue, handleSelect } = + useDropDownContext(); + const itemMap = useCollection(); + const dropdownRef = useRef(null); + + useClickOutside(dropdownRef, () => setOpen(false)); + console.log("DropDownWrapper", itemMap); + + const updateFocusedValue = (direction: number) => { + const values = Array.from(itemMap.keys()); + setFocusedValue((prevValue) => { + const currentIndex = values.indexOf(prevValue ?? ""); + const nextIndex = + (currentIndex + direction + values.length) % values.length; + return values[nextIndex] ?? ""; + }); + }; + + const handleKeyDown = (event: React.KeyboardEvent) => { + if (!open) return; + + const { key } = event; + + if (key === "ArrowDown") { + updateFocusedValue(1); + event.preventDefault(); + } else if (key === "ArrowUp") { + updateFocusedValue(-1); + event.preventDefault(); + } else if (key === "Enter" && focusedValue !== null) { + handleSelect(focusedValue, itemMap.get(focusedValue)?.text); + event.preventDefault(); + } + }; + + return ( + + {children} + + ); +}; diff --git a/packages/wow-ui/src/components/DropDown/context/CollectionContext.tsx b/packages/wow-ui/src/components/DropDown/context/CollectionContext.tsx new file mode 100644 index 00000000..f331d736 --- /dev/null +++ b/packages/wow-ui/src/components/DropDown/context/CollectionContext.tsx @@ -0,0 +1,40 @@ +import type { PropsWithChildren, ReactNode } from "react"; +import { createContext, useMemo, useRef } from "react"; + +import useSafeContext from "../../../hooks/useSafeContext"; + +type ItemData = { + value: string; + text: ReactNode; +}; + +type ContextValue = { + itemMap: Map; + //collectionRef: React.RefObject; +}; + +const CollectionContext = createContext(null); + +export const useCollectionContext = () => { + const context = useSafeContext(CollectionContext); + return context; +}; + +export const CollectionProvider = ({ children }: PropsWithChildren) => { + //const collectionRef = useRef(null); + const itemMap = useMemo( + () => new Map(), + [] + ); + + return ( + + {children} + + ); +}; + +export const useCollection = () => { + const context = useCollectionContext(); + return context.itemMap; +}; diff --git a/packages/wow-ui/src/components/DropDown/context/DropDownContext.ts b/packages/wow-ui/src/components/DropDown/context/DropDownContext.ts index d0229174..09d53349 100644 --- a/packages/wow-ui/src/components/DropDown/context/DropDownContext.ts +++ b/packages/wow-ui/src/components/DropDown/context/DropDownContext.ts @@ -1,7 +1,7 @@ import { createContext } from "react"; -import type useDropDownState from "@/hooks/useDropDownState"; -import useSafeContext from "@/hooks/useSafeContext"; +import type useDropDownState from "../../../hooks/useDropDownState"; +import useSafeContext from "../../../hooks/useSafeContext"; export const DropDownContext = createContext { - const flattenedChildren = useFlattenChildren(children); const dropdownState = useDropDownState({ value, defaultValue, - children: flattenedChildren, onChange, }); - const { open, setOpen, handleKeyDown } = dropdownState; + const { open } = dropdownState; const defaultId = useId(); const dropdownId = id ?? `dropdown-${defaultId}`; - const dropdownRef = useRef(null); - - useClickOutside(dropdownRef, () => setOpen(false)); return ( - - - {open && ( - - {children} - - )} - + + + + {open && ( + + {children} + + )} + + ); }; diff --git a/packages/wow-ui/src/hooks/useDropDownState.ts b/packages/wow-ui/src/hooks/useDropDownState.ts index 9506010e..134649fe 100644 --- a/packages/wow-ui/src/hooks/useDropDownState.ts +++ b/packages/wow-ui/src/hooks/useDropDownState.ts @@ -1,10 +1,11 @@ import type { KeyboardEvent, ReactNode } from "react"; import { isValidElement, useEffect, useMemo, useState } from "react"; +import { useCollectionContext } from "../components/DropDown/context/CollectionContext"; + interface DropDownStateProps { value?: string; defaultValue?: string; - children: ReactNode[]; onChange?: (value: { selectedValue: string; selectedText: ReactNode; @@ -19,45 +20,41 @@ interface SelectedOption { const useDropDownState = ({ value, defaultValue, - children, onChange, }: DropDownStateProps) => { - const options = useMemo(() => { - const opts: { [key: string]: ReactNode } = {}; - children.forEach((child) => { - if (isValidElement(child)) { - opts[child.props.value] = child.props.text; - } - }); - return opts; - }, [children]); + // const options = useMemo(() => { + // const opts: { [key: string]: ReactNode } = {}; + // children.forEach((child) => { + // if (isValidElement(child)) { + // opts[child.props.value] = child.props.text; + // } + // }); + // return opts; + // }, [children]); - const getDefaultSelectedOption = () => { - if (defaultValue && options[defaultValue]) { - return { - selectedValue: defaultValue, - selectedText: options[defaultValue], - }; - } - return { selectedValue: "", selectedText: "" }; - }; + // const getDefaultSelectedOption = () => { + // if (defaultValue && options[defaultValue]) { + // return { + // selectedValue: defaultValue, + // selectedText: options[defaultValue], + // }; + // } + // return { selectedValue: "", selectedText: "" }; + // }; - const [selectedOption, setSelectedOption] = useState( - getDefaultSelectedOption() - ); + const [selectedValue, setSelectedValue] = useState(defaultValue || ""); const [open, setOpen] = useState(false); const [focusedValue, setFocusedValue] = useState(null); useEffect(() => { - if (value !== undefined && options[value]) { - setSelectedOption({ selectedValue: value, selectedText: options[value] }); + if (value !== undefined) { + setSelectedValue(value); } - }, [options, value]); + }, [value]); - const handleSelect = (selectedValue: string) => { - const selectedText = options[selectedValue]; + const handleSelect = (selectedValue: string, selectedText: ReactNode) => { if (value === undefined) { - setSelectedOption({ selectedValue, selectedText }); + setSelectedValue(selectedValue); } if (onChange) { onChange({ selectedValue, selectedText }); @@ -65,42 +62,14 @@ const useDropDownState = ({ setOpen(false); }; - const updateFocusedValue = (direction: number) => { - const values = Object.keys(options); - setFocusedValue((prevValue) => { - const currentIndex = values.indexOf(prevValue ?? ""); - const nextIndex = - (currentIndex + direction + values.length) % values.length; - return values[nextIndex] ?? ""; - }); - }; - - const handleKeyDown = (event: KeyboardEvent) => { - if (!open) return; - - const { key } = event; - - if (key === "ArrowDown") { - updateFocusedValue(1); - event.preventDefault(); - } else if (key === "ArrowUp") { - updateFocusedValue(-1); - event.preventDefault(); - } else if (key === "Enter" && focusedValue !== null) { - handleSelect(focusedValue); - event.preventDefault(); - } - }; - return { - selectedValue: selectedOption.selectedValue, - selectedText: selectedOption.selectedText, + selectedValue: selectedValue, open, setOpen, focusedValue, setFocusedValue, handleSelect, - handleKeyDown, + //handleKeyDown, }; }; From f8527b281fc554598293f6902e858495ead33be1 Mon Sep 17 00:00:00 2001 From: SeieunYoo Date: Fri, 26 Jul 2024 17:50:53 +0900 Subject: [PATCH 17/27] =?UTF-8?q?refactor=20:=20=EC=95=88=EC=93=B0?= =?UTF-8?q?=EB=8A=94=20import=20=EC=82=AD=EC=A0=9C,=20=EC=A0=9C=EC=99=B8?= =?UTF-8?q?=ED=95=98=EB=8A=94=20=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20bui?= =?UTF-8?q?ld=20=EC=8A=A4=ED=81=AC=EB=A6=BD=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/scripts/generateBuildConfig.ts | 11 +- packages/wow-ui/src/base/InputBase.tsx | 161 ++++++++++++++++++ .../components/DropDown/DropDownOption.tsx | 15 +- .../components/DropDown/DropDownTrigger.tsx | 14 +- .../components/DropDown/DropDownWrapper.tsx | 14 +- .../DropDown/context/CollectionContext.tsx | 6 +- .../wow-ui/src/components/DropDown/index.tsx | 11 +- packages/wow-ui/src/hooks/useDropDownState.ts | 34 +--- 8 files changed, 197 insertions(+), 69 deletions(-) create mode 100644 packages/wow-ui/src/base/InputBase.tsx diff --git a/packages/scripts/generateBuildConfig.ts b/packages/scripts/generateBuildConfig.ts index 2144e117..9b2b4974 100644 --- a/packages/scripts/generateBuildConfig.ts +++ b/packages/scripts/generateBuildConfig.ts @@ -19,14 +19,23 @@ type EntryFileKey = string; type EntryFileValue = string; type EntryFileObject = { [key: EntryFileKey]: EntryFileValue }; +// 제외할 컴포넌트 목록 +const excludedComponents = [ + "DropDownTrigger", + "DropDownWrapper", + "CollectionContext", +]; + const getFilteredComponentFiles = async (directoryPath: string) => { const files = await fs.readdir(directoryPath, { recursive: true }); + console.log(directoryPath, files); return files.filter( (file) => file.endsWith(".tsx") && !file.includes("test") && - !file.includes("stories") + !file.includes("stories") && + !excludedComponents.some((excluded) => file.includes(excluded)) ); }; diff --git a/packages/wow-ui/src/base/InputBase.tsx b/packages/wow-ui/src/base/InputBase.tsx new file mode 100644 index 00000000..553aadfc --- /dev/null +++ b/packages/wow-ui/src/base/InputBase.tsx @@ -0,0 +1,161 @@ +// "use client"; + +// import { styled } from "@styled-system/jsx"; +// import type { CSSProperties, InputHTMLAttributes } from "react"; +// import { forwardRef, useLayoutEffect, useRef } from "react"; + +// import { useFormControl } from "../hooks/useFormControl"; + +// export interface InputBaseProps +// extends Omit, "onChange"> { +// style?: CSSProperties; +// className?: string; +// defaultValue?: string; +// value?: string; +// maxLength?: number; +// disabled?: boolean; +// onChange?: (value: string) => void; +// onBlur?: () => void; +// onFocus?: () => void; +// } + +// const InputBase = forwardRef( +// ( +// { +// defaultValue, +// value: valueProp, +// maxLength, +// disabled, +// onChange, +// onBlur, +// onFocus, +// className, +// ...props +// }, +// ref +// ) => { +// const { value, setVariant, handleChange, handleBlur, handleFocus } = +// useFormControl({ +// defaultValue, +// value: valueProp, +// maxLength, +// disabled, +// onChange, +// onBlur, +// onFocus, +// }); + +// useLayoutEffect(() => { +// if (disabled) { +// setVariant("disabled"); +// } else if (defaultValue) { +// setVariant("typed"); +// } else { +// setVariant("default"); +// } +// }, [defaultValue, disabled, setVariant]); + +// const inputRef = useRef(null); +// const inputElementRef = ref || inputRef; + +// return ( +// +// ); +// } +// ); + +// InputBase.displayName = "InputBase"; +// export default InputBase; + +"use client"; + +import { cva } from "@styled-system/css"; +import { styled } from "@styled-system/jsx"; +import type { CSSProperties, InputHTMLAttributes } from "react"; +import { forwardRef, useLayoutEffect } from "react"; + +import type { BaseVariantType } from "../hooks/useFormControl"; +import { useFormControl } from "../hooks/useFormControl"; + +// Extend the BaseVariantType with CustomVariantType +export type CustomVariantType = BaseVariantType | "disabled"; + +export interface InputBaseProps + extends Omit< + InputHTMLAttributes, + | "defaultValue" + | "value" + | "maxLength" + | "disabled" + | "onChange" + | "onBlur" + | "onFocus" + > { + disabled?: boolean; + style?: CSSProperties; + className?: string; + variant?: BaseVariantType; +} + +const InputBase = forwardRef( + ({ className, disabled, ...props }, ref) => { + const { value, setVariant, handleChange, handleBlur, handleFocus } = + useFormControl(props); + + useLayoutEffect(() => { + if (disabled) { + setVariant("disabled"); + } + }, [disabled, setVariant]); + + const handleChange = (event, ...args) => { + if (!isControlled) { + const element = event.target || inputRef.current; + if (element == null) { + throw new MuiError( + "MUI: Expected valid input target. " + + "Did you use a custom `inputComponent` and forget to forward refs? " + + "See https://mui.com/r/input-component-ref-interface for more info." + ); + } + + checkDirty({ + value: element.value, + }); + } + + if (inputPropsProp.onChange) { + inputPropsProp.onChange(event, ...args); + } + + // Perform in the willUpdate + if (onChange) { + onChange(event, ...args); + } + }; + + return ( + + ); + } +); + +InputBase.displayName = "InputBase"; +export default InputBase; diff --git a/packages/wow-ui/src/components/DropDown/DropDownOption.tsx b/packages/wow-ui/src/components/DropDown/DropDownOption.tsx index 8a588e07..349d8353 100644 --- a/packages/wow-ui/src/components/DropDown/DropDownOption.tsx +++ b/packages/wow-ui/src/components/DropDown/DropDownOption.tsx @@ -5,23 +5,18 @@ import { styled } from "@styled-system/jsx"; import type { ReactNode } from "react"; import { forwardRef, useEffect } from "react"; -import { useDropDownContext } from "../../components/DropDown/context/DropDownContext"; -import { - useCollection, - useCollectionContext, -} from "./context/CollectionContext"; +import { useDropDownContext } from "@/components/DropDown/context/DropDownContext"; + +import { useCollection } from "./context/CollectionContext"; + /** - * @description 드롭다운 옵션의 props입니다. + * @description DropDown의 옵션을 나타내는 DropDownOption 컴포넌트입니다. * - * @param {boolean} [focused] 옵션이 포커스된 상태인지 여부를 나타냅니다. - * @param {boolean} [selected] 옵션이 선택된 상태인지 여부를 나타냅니다. * @param {string} value 옵션의 값입니다. * @param {() => void} [onClick] 옵션이 클릭되었을 때 호출되는 함수입니다. * @param {React.ReactNode} [text] 드롭다운 옵션에 들어갈 텍스트. */ export interface DropDownOptionProps { - focused?: boolean; - selected?: boolean; value: string; onClick?: () => void; text: ReactNode; diff --git a/packages/wow-ui/src/components/DropDown/DropDownTrigger.tsx b/packages/wow-ui/src/components/DropDown/DropDownTrigger.tsx index 403cda4c..6c9c4d37 100644 --- a/packages/wow-ui/src/components/DropDown/DropDownTrigger.tsx +++ b/packages/wow-ui/src/components/DropDown/DropDownTrigger.tsx @@ -1,15 +1,13 @@ import { cva } from "@styled-system/css"; import { styled } from "@styled-system/jsx"; -import type { KeyboardEvent, ReactElement } from "react"; -import React, { cloneElement } from "react"; +import type { KeyboardEvent } from "react"; +import { cloneElement } from "react"; import { DownArrow } from "wowds-icons"; -import type { DropDownProps } from "../../components/DropDown"; -import { useDropDownContext } from "../../components/DropDown/context/DropDownContext"; -import { - useCollection, - useCollectionContext, -} from "./context/CollectionContext"; +import type { DropDownProps } from "@/components/DropDown"; +import { useDropDownContext } from "@/components/DropDown/context/DropDownContext"; + +import { useCollection } from "./context/CollectionContext"; interface DropDownTriggerProps { placeholder?: DropDownProps["placeholder"]; label?: DropDownProps["label"]; diff --git a/packages/wow-ui/src/components/DropDown/DropDownWrapper.tsx b/packages/wow-ui/src/components/DropDown/DropDownWrapper.tsx index 5c84d662..14a6c823 100644 --- a/packages/wow-ui/src/components/DropDown/DropDownWrapper.tsx +++ b/packages/wow-ui/src/components/DropDown/DropDownWrapper.tsx @@ -1,13 +1,10 @@ -import { cva } from "@styled-system/css"; import { Flex } from "@styled-system/jsx"; -import { type CSSProperties, type PropsWithChildren, useRef } from "react"; +import { type PropsWithChildren, useRef } from "react"; -import type { DropDownProps } from "../../components/DropDown"; -import useClickOutside from "../../hooks/useClickOutside"; -import { - useCollection, - useCollectionContext, -} from "./context/CollectionContext"; +import type { DropDownProps } from "@/components/DropDown"; +import useClickOutside from "@/hooks/useClickOutside"; + +import { useCollection } from "./context/CollectionContext"; import { useDropDownContext } from "./context/DropDownContext"; interface DropDownWrapperProps extends PropsWithChildren { @@ -26,7 +23,6 @@ export const DropDownWrapper = ({ const dropdownRef = useRef(null); useClickOutside(dropdownRef, () => setOpen(false)); - console.log("DropDownWrapper", itemMap); const updateFocusedValue = (direction: number) => { const values = Array.from(itemMap.keys()); diff --git a/packages/wow-ui/src/components/DropDown/context/CollectionContext.tsx b/packages/wow-ui/src/components/DropDown/context/CollectionContext.tsx index f331d736..0ac3ba5b 100644 --- a/packages/wow-ui/src/components/DropDown/context/CollectionContext.tsx +++ b/packages/wow-ui/src/components/DropDown/context/CollectionContext.tsx @@ -1,7 +1,7 @@ import type { PropsWithChildren, ReactNode } from "react"; -import { createContext, useMemo, useRef } from "react"; +import { createContext, useMemo } from "react"; -import useSafeContext from "../../../hooks/useSafeContext"; +import useSafeContext from "@/hooks/useSafeContext"; type ItemData = { value: string; @@ -10,7 +10,6 @@ type ItemData = { type ContextValue = { itemMap: Map; - //collectionRef: React.RefObject; }; const CollectionContext = createContext(null); @@ -21,7 +20,6 @@ export const useCollectionContext = () => { }; export const CollectionProvider = ({ children }: PropsWithChildren) => { - //const collectionRef = useRef(null); const itemMap = useMemo( () => new Map(), [] diff --git a/packages/wow-ui/src/components/DropDown/index.tsx b/packages/wow-ui/src/components/DropDown/index.tsx index 6b7bb0c5..855a76ba 100644 --- a/packages/wow-ui/src/components/DropDown/index.tsx +++ b/packages/wow-ui/src/components/DropDown/index.tsx @@ -8,13 +8,12 @@ import type { ReactElement, ReactNode, } from "react"; -import { useId, useRef } from "react"; +import { useId } from "react"; + +import { DropDownContext } from "@/components/DropDown/context/DropDownContext"; +import DropDownTrigger from "@/components/DropDown/DropDownTrigger"; +import useDropDownState from "@/hooks/useDropDownState"; -import { DropDownContext } from "../../components/DropDown/context/DropDownContext"; -import DropDownTrigger from "../../components/DropDown/DropDownTrigger"; -import useClickOutside from "../../hooks/useClickOutside"; -import useDropDownState from "../../hooks/useDropDownState"; -import useFlattenChildren from "../../hooks/useFlattenChildren"; import { CollectionProvider } from "./context/CollectionContext"; import { DropDownWrapper } from "./DropDownWrapper"; export interface DropDownWithTriggerProps extends PropsWithChildren { diff --git a/packages/wow-ui/src/hooks/useDropDownState.ts b/packages/wow-ui/src/hooks/useDropDownState.ts index 134649fe..4ed78d33 100644 --- a/packages/wow-ui/src/hooks/useDropDownState.ts +++ b/packages/wow-ui/src/hooks/useDropDownState.ts @@ -1,7 +1,5 @@ -import type { KeyboardEvent, ReactNode } from "react"; -import { isValidElement, useEffect, useMemo, useState } from "react"; - -import { useCollectionContext } from "../components/DropDown/context/CollectionContext"; +import type { ReactNode } from "react"; +import { useEffect, useState } from "react"; interface DropDownStateProps { value?: string; @@ -12,36 +10,11 @@ interface DropDownStateProps { }) => void; } -interface SelectedOption { - selectedValue: string; - selectedText: ReactNode; -} - const useDropDownState = ({ value, defaultValue, onChange, }: DropDownStateProps) => { - // const options = useMemo(() => { - // const opts: { [key: string]: ReactNode } = {}; - // children.forEach((child) => { - // if (isValidElement(child)) { - // opts[child.props.value] = child.props.text; - // } - // }); - // return opts; - // }, [children]); - - // const getDefaultSelectedOption = () => { - // if (defaultValue && options[defaultValue]) { - // return { - // selectedValue: defaultValue, - // selectedText: options[defaultValue], - // }; - // } - // return { selectedValue: "", selectedText: "" }; - // }; - const [selectedValue, setSelectedValue] = useState(defaultValue || ""); const [open, setOpen] = useState(false); const [focusedValue, setFocusedValue] = useState(null); @@ -63,13 +36,12 @@ const useDropDownState = ({ }; return { - selectedValue: selectedValue, + selectedValue, open, setOpen, focusedValue, setFocusedValue, handleSelect, - //handleKeyDown, }; }; From eb86299c1443a9e58f8389ca3e42b41ab89b9250 Mon Sep 17 00:00:00 2001 From: SeieunYoo Date: Fri, 26 Jul 2024 18:00:19 +0900 Subject: [PATCH 18/27] =?UTF-8?q?refactor=20:=20useCallback=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=ED=95=B4=EC=84=9C=20=EB=A0=8C=EB=8D=94=EB=A7=81=20?= =?UTF-8?q?=EC=B5=9C=EC=86=8C=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/DropDown/DropDownOption.tsx | 18 +++-- .../components/DropDown/DropDownTrigger.tsx | 43 +++++++----- .../components/DropDown/DropDownWrapper.tsx | 66 ++++++++++++------- 3 files changed, 78 insertions(+), 49 deletions(-) diff --git a/packages/wow-ui/src/components/DropDown/DropDownOption.tsx b/packages/wow-ui/src/components/DropDown/DropDownOption.tsx index 349d8353..db29ec3b 100644 --- a/packages/wow-ui/src/components/DropDown/DropDownOption.tsx +++ b/packages/wow-ui/src/components/DropDown/DropDownOption.tsx @@ -3,7 +3,7 @@ import { cva } from "@styled-system/css"; import { styled } from "@styled-system/jsx"; import type { ReactNode } from "react"; -import { forwardRef, useEffect } from "react"; +import { forwardRef, useCallback, useEffect } from "react"; import { useDropDownContext } from "@/components/DropDown/context/DropDownContext"; @@ -28,15 +28,21 @@ const DropDownOption = forwardRef( const isSelected = selectedValue === value; const isFocused = focusedValue !== null && focusedValue === value; - const handleOptionClick = (value: string, onClick?: () => void) => { - if (onClick) onClick(); - handleSelect(value, text); - }; + const handleOptionClick = useCallback( + (value: string, onClick?: () => void) => { + if (onClick) onClick(); + handleSelect(value, text); + }, + [handleSelect, text] + ); const itemMap = useCollection(); useEffect(() => { - itemMap.set(value, { text }); + const currentItem = itemMap.get(value); + if (!currentItem || currentItem.text !== text) { + itemMap.set(value, { text }); + } }, [itemMap, value, text]); return ( diff --git a/packages/wow-ui/src/components/DropDown/DropDownTrigger.tsx b/packages/wow-ui/src/components/DropDown/DropDownTrigger.tsx index 6c9c4d37..9c738e79 100644 --- a/packages/wow-ui/src/components/DropDown/DropDownTrigger.tsx +++ b/packages/wow-ui/src/components/DropDown/DropDownTrigger.tsx @@ -1,13 +1,16 @@ +"use client"; + import { cva } from "@styled-system/css"; import { styled } from "@styled-system/jsx"; import type { KeyboardEvent } from "react"; -import { cloneElement } from "react"; +import { cloneElement, useCallback, useMemo } from "react"; import { DownArrow } from "wowds-icons"; import type { DropDownProps } from "@/components/DropDown"; import { useDropDownContext } from "@/components/DropDown/context/DropDownContext"; import { useCollection } from "./context/CollectionContext"; + interface DropDownTriggerProps { placeholder?: DropDownProps["placeholder"]; label?: DropDownProps["label"]; @@ -25,27 +28,33 @@ const DropDownTrigger = ({ useDropDownContext(); const itemMap = useCollection(); - const selectedText = itemMap.get(selectedValue)?.text; + const selectedText = useMemo( + () => itemMap.get(selectedValue)?.text, + [itemMap, selectedValue] + ); - const toggleDropdown = () => { + const toggleDropdown = useCallback(() => { setOpen((prevOpen) => { if (!prevOpen) setFocusedValue(null); return !prevOpen; }); - }; + }, [setOpen, setFocusedValue]); + + const handleKeyDown = useCallback( + (e: KeyboardEvent) => { + if (e.key === "Enter") toggleDropdown(); + }, + [toggleDropdown] + ); if (trigger) { - return ( - <> - {cloneElement(trigger, { - onClick: toggleDropdown, - "aria-expanded": open, - "aria-haspopup": "true", - id: `${dropdownId}-trigger`, - "aria-controls": `${dropdownId}`, - })} - - ); + return cloneElement(trigger, { + onClick: toggleDropdown, + "aria-expanded": open, + "aria-haspopup": "true", + id: `${dropdownId}-trigger`, + "aria-controls": `${dropdownId}`, + }); } return ( @@ -82,9 +91,7 @@ const DropDownTrigger = ({ className={iconStyle({ type: open ? "up" : "down" })} stroke={open ? "primary" : selectedValue ? "sub" : "outline"} tabIndex={0} - onKeyDown={(e: KeyboardEvent) => { - if (e.key === "Enter") toggleDropdown(); - }} + onKeyDown={handleKeyDown} /> diff --git a/packages/wow-ui/src/components/DropDown/DropDownWrapper.tsx b/packages/wow-ui/src/components/DropDown/DropDownWrapper.tsx index 14a6c823..63e3940a 100644 --- a/packages/wow-ui/src/components/DropDown/DropDownWrapper.tsx +++ b/packages/wow-ui/src/components/DropDown/DropDownWrapper.tsx @@ -1,5 +1,11 @@ import { Flex } from "@styled-system/jsx"; -import { type PropsWithChildren, useRef } from "react"; +import { + type KeyboardEvent, + type PropsWithChildren, + useCallback, + useMemo, + useRef, +} from "react"; import type { DropDownProps } from "@/components/DropDown"; import useClickOutside from "@/hooks/useClickOutside"; @@ -22,34 +28,44 @@ export const DropDownWrapper = ({ const itemMap = useCollection(); const dropdownRef = useRef(null); - useClickOutside(dropdownRef, () => setOpen(false)); + useClickOutside( + dropdownRef, + useCallback(() => setOpen(false), [setOpen]) + ); + + const values = useMemo(() => Array.from(itemMap.keys()), [itemMap]); - const updateFocusedValue = (direction: number) => { - const values = Array.from(itemMap.keys()); - setFocusedValue((prevValue) => { - const currentIndex = values.indexOf(prevValue ?? ""); - const nextIndex = - (currentIndex + direction + values.length) % values.length; - return values[nextIndex] ?? ""; - }); - }; + const updateFocusedValue = useCallback( + (direction: number) => { + setFocusedValue((prevValue) => { + const currentIndex = values.indexOf(prevValue ?? ""); + const nextIndex = + (currentIndex + direction + values.length) % values.length; + return values[nextIndex] ?? ""; + }); + }, + [setFocusedValue, values] + ); - const handleKeyDown = (event: React.KeyboardEvent) => { - if (!open) return; + const handleKeyDown = useCallback( + (event: KeyboardEvent) => { + if (!open) return; - const { key } = event; + const { key } = event; - if (key === "ArrowDown") { - updateFocusedValue(1); - event.preventDefault(); - } else if (key === "ArrowUp") { - updateFocusedValue(-1); - event.preventDefault(); - } else if (key === "Enter" && focusedValue !== null) { - handleSelect(focusedValue, itemMap.get(focusedValue)?.text); - event.preventDefault(); - } - }; + if (key === "ArrowDown") { + updateFocusedValue(1); + event.preventDefault(); + } else if (key === "ArrowUp") { + updateFocusedValue(-1); + event.preventDefault(); + } else if (key === "Enter" && focusedValue !== null) { + handleSelect(focusedValue, itemMap.get(focusedValue)?.text); + event.preventDefault(); + } + }, + [open, focusedValue, updateFocusedValue, handleSelect, itemMap] + ); return ( Date: Fri, 26 Jul 2024 18:14:30 +0900 Subject: [PATCH 19/27] =?UTF-8?q?fix=20:=20=ED=95=84=EC=9A=94=EC=97=86?= =?UTF-8?q?=EB=8A=94=20=EC=BD=94=EB=93=9C=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/scripts/generateBuildConfig.ts | 1 - packages/wow-ui/src/base/InputBase.tsx | 161 ------------------------ 2 files changed, 162 deletions(-) delete mode 100644 packages/wow-ui/src/base/InputBase.tsx diff --git a/packages/scripts/generateBuildConfig.ts b/packages/scripts/generateBuildConfig.ts index 9b2b4974..bc4c04d0 100644 --- a/packages/scripts/generateBuildConfig.ts +++ b/packages/scripts/generateBuildConfig.ts @@ -29,7 +29,6 @@ const excludedComponents = [ const getFilteredComponentFiles = async (directoryPath: string) => { const files = await fs.readdir(directoryPath, { recursive: true }); - console.log(directoryPath, files); return files.filter( (file) => file.endsWith(".tsx") && diff --git a/packages/wow-ui/src/base/InputBase.tsx b/packages/wow-ui/src/base/InputBase.tsx deleted file mode 100644 index 553aadfc..00000000 --- a/packages/wow-ui/src/base/InputBase.tsx +++ /dev/null @@ -1,161 +0,0 @@ -// "use client"; - -// import { styled } from "@styled-system/jsx"; -// import type { CSSProperties, InputHTMLAttributes } from "react"; -// import { forwardRef, useLayoutEffect, useRef } from "react"; - -// import { useFormControl } from "../hooks/useFormControl"; - -// export interface InputBaseProps -// extends Omit, "onChange"> { -// style?: CSSProperties; -// className?: string; -// defaultValue?: string; -// value?: string; -// maxLength?: number; -// disabled?: boolean; -// onChange?: (value: string) => void; -// onBlur?: () => void; -// onFocus?: () => void; -// } - -// const InputBase = forwardRef( -// ( -// { -// defaultValue, -// value: valueProp, -// maxLength, -// disabled, -// onChange, -// onBlur, -// onFocus, -// className, -// ...props -// }, -// ref -// ) => { -// const { value, setVariant, handleChange, handleBlur, handleFocus } = -// useFormControl({ -// defaultValue, -// value: valueProp, -// maxLength, -// disabled, -// onChange, -// onBlur, -// onFocus, -// }); - -// useLayoutEffect(() => { -// if (disabled) { -// setVariant("disabled"); -// } else if (defaultValue) { -// setVariant("typed"); -// } else { -// setVariant("default"); -// } -// }, [defaultValue, disabled, setVariant]); - -// const inputRef = useRef(null); -// const inputElementRef = ref || inputRef; - -// return ( -// -// ); -// } -// ); - -// InputBase.displayName = "InputBase"; -// export default InputBase; - -"use client"; - -import { cva } from "@styled-system/css"; -import { styled } from "@styled-system/jsx"; -import type { CSSProperties, InputHTMLAttributes } from "react"; -import { forwardRef, useLayoutEffect } from "react"; - -import type { BaseVariantType } from "../hooks/useFormControl"; -import { useFormControl } from "../hooks/useFormControl"; - -// Extend the BaseVariantType with CustomVariantType -export type CustomVariantType = BaseVariantType | "disabled"; - -export interface InputBaseProps - extends Omit< - InputHTMLAttributes, - | "defaultValue" - | "value" - | "maxLength" - | "disabled" - | "onChange" - | "onBlur" - | "onFocus" - > { - disabled?: boolean; - style?: CSSProperties; - className?: string; - variant?: BaseVariantType; -} - -const InputBase = forwardRef( - ({ className, disabled, ...props }, ref) => { - const { value, setVariant, handleChange, handleBlur, handleFocus } = - useFormControl(props); - - useLayoutEffect(() => { - if (disabled) { - setVariant("disabled"); - } - }, [disabled, setVariant]); - - const handleChange = (event, ...args) => { - if (!isControlled) { - const element = event.target || inputRef.current; - if (element == null) { - throw new MuiError( - "MUI: Expected valid input target. " + - "Did you use a custom `inputComponent` and forget to forward refs? " + - "See https://mui.com/r/input-component-ref-interface for more info." - ); - } - - checkDirty({ - value: element.value, - }); - } - - if (inputPropsProp.onChange) { - inputPropsProp.onChange(event, ...args); - } - - // Perform in the willUpdate - if (onChange) { - onChange(event, ...args); - } - }; - - return ( - - ); - } -); - -InputBase.displayName = "InputBase"; -export default InputBase; From 8be18f540b209a051b222b87d19de3921a96c605 Mon Sep 17 00:00:00 2001 From: SeieunYoo Date: Fri, 26 Jul 2024 18:49:52 +0900 Subject: [PATCH 20/27] =?UTF-8?q?refactor:=20values=ED=95=A8=EC=88=98=20?= =?UTF-8?q?=EB=82=B4=EB=B6=80=EB=A1=9C=20=EC=9D=B4=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../wow-ui/src/components/DropDown/DropDownWrapper.tsx | 7 +++---- .../src/components/DropDown/context/DropDownContext.ts | 4 ++-- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/packages/wow-ui/src/components/DropDown/DropDownWrapper.tsx b/packages/wow-ui/src/components/DropDown/DropDownWrapper.tsx index 63e3940a..3c73a5dd 100644 --- a/packages/wow-ui/src/components/DropDown/DropDownWrapper.tsx +++ b/packages/wow-ui/src/components/DropDown/DropDownWrapper.tsx @@ -3,7 +3,6 @@ import { type KeyboardEvent, type PropsWithChildren, useCallback, - useMemo, useRef, } from "react"; @@ -33,10 +32,9 @@ export const DropDownWrapper = ({ useCallback(() => setOpen(false), [setOpen]) ); - const values = useMemo(() => Array.from(itemMap.keys()), [itemMap]); - const updateFocusedValue = useCallback( (direction: number) => { + const values = Array.from(itemMap.keys()); setFocusedValue((prevValue) => { const currentIndex = values.indexOf(prevValue ?? ""); const nextIndex = @@ -44,13 +42,14 @@ export const DropDownWrapper = ({ return values[nextIndex] ?? ""; }); }, - [setFocusedValue, values] + [itemMap, setFocusedValue] ); const handleKeyDown = useCallback( (event: KeyboardEvent) => { if (!open) return; + //console.log(event); const { key } = event; if (key === "ArrowDown") { diff --git a/packages/wow-ui/src/components/DropDown/context/DropDownContext.ts b/packages/wow-ui/src/components/DropDown/context/DropDownContext.ts index 09d53349..d0229174 100644 --- a/packages/wow-ui/src/components/DropDown/context/DropDownContext.ts +++ b/packages/wow-ui/src/components/DropDown/context/DropDownContext.ts @@ -1,7 +1,7 @@ import { createContext } from "react"; -import type useDropDownState from "../../../hooks/useDropDownState"; -import useSafeContext from "../../../hooks/useSafeContext"; +import type useDropDownState from "@/hooks/useDropDownState"; +import useSafeContext from "@/hooks/useSafeContext"; export const DropDownContext = createContext Date: Fri, 26 Jul 2024 18:53:09 +0900 Subject: [PATCH 21/27] =?UTF-8?q?fix=20:=20flat=20=ED=95=B4=EC=A3=BC?= =?UTF-8?q?=EB=8A=94=20=ED=9B=85=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../wow-ui/src/hooks/useFlattenChildren.ts | 20 ------------------- 1 file changed, 20 deletions(-) delete mode 100644 packages/wow-ui/src/hooks/useFlattenChildren.ts diff --git a/packages/wow-ui/src/hooks/useFlattenChildren.ts b/packages/wow-ui/src/hooks/useFlattenChildren.ts deleted file mode 100644 index 18d7b2c2..00000000 --- a/packages/wow-ui/src/hooks/useFlattenChildren.ts +++ /dev/null @@ -1,20 +0,0 @@ -import type { ReactNode } from "react"; -import { Children, isValidElement, useCallback, useMemo } from "react"; - -const useFlattenChildren = (children: ReactNode): ReactNode[] => { - const flattenChildren = useCallback((children: ReactNode): ReactNode[] => { - const result: ReactNode[] = []; - Children.forEach(children, (child) => { - if (isValidElement(child) && child.props.children) { - result.push(...flattenChildren(child.props.children)); - } else { - result.push(child); - } - }); - return result; - }, []); - - return useMemo(() => flattenChildren(children), [children, flattenChildren]); -}; - -export default useFlattenChildren; From 50d1ee5357daf5aa29be27c84f1c04c1d0ed8e69 Mon Sep 17 00:00:00 2001 From: SeieunYoo Date: Fri, 26 Jul 2024 20:46:13 +0900 Subject: [PATCH 22/27] =?UTF-8?q?chore=20:=20changeset=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .changeset/four-peaches-approve.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/four-peaches-approve.md diff --git a/.changeset/four-peaches-approve.md b/.changeset/four-peaches-approve.md new file mode 100644 index 00000000..d963ec32 --- /dev/null +++ b/.changeset/four-peaches-approve.md @@ -0,0 +1,5 @@ +--- +"wowds-ui": patch +--- + +DropDownOption 컴포넌트를 context 를 이용하여 리팩토링 합니다. From d772386205bd4e277d3609396df085303cd78db1 Mon Sep 17 00:00:00 2001 From: SeieunYoo Date: Sat, 27 Jul 2024 21:24:26 +0900 Subject: [PATCH 23/27] =?UTF-8?q?fix=20:=20defaultValue=20=EA=B4=80?= =?UTF-8?q?=EB=A0=A8=20=EC=88=98=EC=A0=95,=20=EB=93=9C=EB=A1=AD=EB=8B=A4?= =?UTF-8?q?=EC=9A=B4=20width=20=EC=8A=A4=ED=83=80=EC=9D=BC=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/DropDown/DropDownOption.tsx | 3 +- .../components/DropDown/DropDownTrigger.tsx | 23 +++++++------- .../components/DropDown/DropDownWrapper.tsx | 5 +-- .../wow-ui/src/components/DropDown/index.tsx | 31 +++++++++++-------- packages/wow-ui/src/hooks/useDropDownState.ts | 7 +++-- 5 files changed, 39 insertions(+), 30 deletions(-) diff --git a/packages/wow-ui/src/components/DropDown/DropDownOption.tsx b/packages/wow-ui/src/components/DropDown/DropDownOption.tsx index db29ec3b..45dbd1ed 100644 --- a/packages/wow-ui/src/components/DropDown/DropDownOption.tsx +++ b/packages/wow-ui/src/components/DropDown/DropDownOption.tsx @@ -49,7 +49,8 @@ const DropDownOption = forwardRef( { + const itemMap = useCollection(); const { open, selectedValue, setOpen, setFocusedValue } = useDropDownContext(); - const itemMap = useCollection(); - const selectedText = useMemo( - () => itemMap.get(selectedValue)?.text, - [itemMap, selectedValue] - ); + const selectedText = itemMap.get(selectedValue)?.text; const toggleDropdown = useCallback(() => { setOpen((prevOpen) => { @@ -59,12 +56,14 @@ const DropDownTrigger = ({ return ( <> - - {label} - + {label && ( + + {label} + + )} { const { open, setOpen, setFocusedValue, focusedValue, handleSelect } = @@ -49,7 +51,6 @@ export const DropDownWrapper = ({ (event: KeyboardEvent) => { if (!open) return; - //console.log(event); const { key } = event; if (key === "ArrowDown") { @@ -77,7 +78,7 @@ export const DropDownWrapper = ({ position="relative" ref={dropdownRef} tabIndex={0} - width="auto" + width={hasCustomTrigger ? "fit-content" : "auto"} onKeyDown={handleKeyDown} {...rest} > diff --git a/packages/wow-ui/src/components/DropDown/index.tsx b/packages/wow-ui/src/components/DropDown/index.tsx index 855a76ba..289dfdde 100644 --- a/packages/wow-ui/src/components/DropDown/index.tsx +++ b/packages/wow-ui/src/components/DropDown/index.tsx @@ -105,24 +105,28 @@ const DropDown = ({ return ( - + - {open && ( - - {children} - - )} + + {children} + @@ -139,6 +143,7 @@ const dropdownContentStyle = cva({ left: 0, zIndex: "dropdown", maxHeight: "18.75rem", + width: "100%", lg: { maxWidth: "22.375rem", }, @@ -167,7 +172,7 @@ const dropdownContentStyle = cva({ variants: { type: { custom: { - width: "100%", + lg: {}, }, default: {}, }, diff --git a/packages/wow-ui/src/hooks/useDropDownState.ts b/packages/wow-ui/src/hooks/useDropDownState.ts index 4ed78d33..49d696a9 100644 --- a/packages/wow-ui/src/hooks/useDropDownState.ts +++ b/packages/wow-ui/src/hooks/useDropDownState.ts @@ -15,7 +15,7 @@ const useDropDownState = ({ defaultValue, onChange, }: DropDownStateProps) => { - const [selectedValue, setSelectedValue] = useState(defaultValue || ""); + const [selectedValue, setSelectedValue] = useState(""); const [open, setOpen] = useState(false); const [focusedValue, setFocusedValue] = useState(null); @@ -23,7 +23,10 @@ const useDropDownState = ({ if (value !== undefined) { setSelectedValue(value); } - }, [value]); + if (defaultValue !== undefined) { + setSelectedValue(defaultValue); + } + }, [value, defaultValue]); const handleSelect = (selectedValue: string, selectedText: ReactNode) => { if (value === undefined) { From c2ad801f3d813812d9ca3c23d300a5f796fc0593 Mon Sep 17 00:00:00 2001 From: SeieunYoo Date: Sat, 27 Jul 2024 21:29:15 +0900 Subject: [PATCH 24/27] =?UTF-8?q?feat=20:=20=EC=8A=A4=ED=86=A0=EB=A6=AC?= =?UTF-8?q?=EB=B6=81=20=EB=B3=B4=EC=99=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/DropDown/DropDown.stories.tsx | 30 ++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/packages/wow-ui/src/components/DropDown/DropDown.stories.tsx b/packages/wow-ui/src/components/DropDown/DropDown.stories.tsx index 4ff6c341..1f00b766 100644 --- a/packages/wow-ui/src/components/DropDown/DropDown.stories.tsx +++ b/packages/wow-ui/src/components/DropDown/DropDown.stories.tsx @@ -10,12 +10,19 @@ const meta = { component: DropDown, tags: ["autodocs"], parameters: { - componentSubtitle: "드롭다운 컴포넌트", + componentSubtitle: + "사용자가 외부 트리거 컴포넌트나 내부 요소를 통해서 선택 옵션 리스트 중에 아이템을 선택할 수 있는 드롭다운 컴포넌트 입니다.", a11y: { config: { rules: [{ id: "color-contrast", enabled: false }], }, }, + docs: { + description: { + component: + "children 에 DropDownOption 컴포넌트를 넣어서 사용합니다. 외부 트리거를 사용할 경우 trigger 를 사용합니다.", + }, + }, }, argTypes: { children: { @@ -48,6 +55,13 @@ const meta = { }, control: { type: "text" }, }, + id: { + description: "드롭다운의 id 를 나타냅니다.", + table: { + type: { summary: "string" }, + }, + control: { type: "text" }, + }, value: { description: "현재 선택된 값을 나타냅니다.", table: { @@ -72,6 +86,20 @@ const meta = { }, action: "changed", }, + style: { + description: "드롭다운의 커스텀 스타일을 설정할 수 있습니다.", + table: { + type: { summary: "CSSProperties" }, + }, + control: "object", + }, + className: { + description: "드롭다운 전달하는 커스텀 클래스를 설정합니다.", + table: { + type: { summary: "string" }, + }, + control: "text", + }, }, } satisfies Meta; From b9072e3fec8a261eb7a22de328439580248fb17b Mon Sep 17 00:00:00 2001 From: SeieunYoo Date: Sat, 27 Jul 2024 21:50:16 +0900 Subject: [PATCH 25/27] =?UTF-8?q?fix:=20=EC=8A=A4=ED=86=A0=EB=A6=AC?= =?UTF-8?q?=EB=B6=81=20=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../wow-ui/src/components/DropDown/DropDown.stories.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/wow-ui/src/components/DropDown/DropDown.stories.tsx b/packages/wow-ui/src/components/DropDown/DropDown.stories.tsx index 1f00b766..df853815 100644 --- a/packages/wow-ui/src/components/DropDown/DropDown.stories.tsx +++ b/packages/wow-ui/src/components/DropDown/DropDown.stories.tsx @@ -150,13 +150,13 @@ export const WithDefaultValue: Story = { args: { children: ( <> - - + + ), label: "Select an Option", placeholder: "Please select", - defaultValue: "Option 2", + defaultValue: "option 2", }, parameters: { docs: { From 6ca04346d622c36070e0d3f198dab1f25115eff2 Mon Sep 17 00:00:00 2001 From: SeieunYoo Date: Sun, 28 Jul 2024 13:55:21 +0900 Subject: [PATCH 26/27] =?UTF-8?q?refacotor:=20OptionList=20=EC=97=90?= =?UTF-8?q?=EC=84=9C=20focus=20=EB=A1=9C=EC=A7=81=20=EC=B2=98=EB=A6=AC?= =?UTF-8?q?=ED=95=98=EB=8F=84=EB=A1=9D=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/DropDown/DropDownOption.tsx | 9 +- .../DropDown/DropDownOptionList.tsx | 127 ++++++++++++++++++ .../components/DropDown/DropDownTrigger.tsx | 5 +- .../components/DropDown/DropDownWrapper.tsx | 54 +------- .../DropDown/context/CollectionContext.tsx | 2 +- .../DropDown/context/DropDownContext.ts | 4 +- .../wow-ui/src/components/DropDown/index.tsx | 68 +--------- packages/wow-ui/src/hooks/useClickOutside.ts | 2 +- 8 files changed, 149 insertions(+), 122 deletions(-) create mode 100644 packages/wow-ui/src/components/DropDown/DropDownOptionList.tsx diff --git a/packages/wow-ui/src/components/DropDown/DropDownOption.tsx b/packages/wow-ui/src/components/DropDown/DropDownOption.tsx index 45dbd1ed..50e6c4e6 100644 --- a/packages/wow-ui/src/components/DropDown/DropDownOption.tsx +++ b/packages/wow-ui/src/components/DropDown/DropDownOption.tsx @@ -5,8 +5,7 @@ import { styled } from "@styled-system/jsx"; import type { ReactNode } from "react"; import { forwardRef, useCallback, useEffect } from "react"; -import { useDropDownContext } from "@/components/DropDown/context/DropDownContext"; - +import { useDropDownContext } from "../../components/DropDown/context/DropDownContext"; import { useCollection } from "./context/CollectionContext"; /** @@ -22,7 +21,7 @@ export interface DropDownOptionProps { text: ReactNode; } -const DropDownOption = forwardRef( +const DropDownOption = forwardRef( function Option({ value, onClick, text }, ref) { const { focusedValue, selectedValue, handleSelect } = useDropDownContext(); const isSelected = selectedValue === value; @@ -46,7 +45,7 @@ const DropDownOption = forwardRef( }, [itemMap, value, text]); return ( - ( }} > {text} - + ); } ); diff --git a/packages/wow-ui/src/components/DropDown/DropDownOptionList.tsx b/packages/wow-ui/src/components/DropDown/DropDownOptionList.tsx new file mode 100644 index 00000000..e985faad --- /dev/null +++ b/packages/wow-ui/src/components/DropDown/DropDownOptionList.tsx @@ -0,0 +1,127 @@ +import { cva } from "@styled-system/css"; +import { styled } from "@styled-system/jsx"; +import { + type KeyboardEvent, + type PropsWithChildren, + useCallback, + useEffect, + useRef, +} from "react"; + +import { useCollection } from "./context/CollectionContext"; +import { useDropDownContext } from "./context/DropDownContext"; + +interface DropDownWrapperProps extends PropsWithChildren { + hasCustomTrigger?: boolean; +} +export const DropDownOptionList = ({ + children, + hasCustomTrigger, +}: DropDownWrapperProps) => { + const { open, setFocusedValue, focusedValue, handleSelect } = + useDropDownContext(); + const itemMap = useCollection(); + const listRef = useRef(null); + + useEffect(() => { + if (open && listRef.current) { + listRef.current.focus(); + } + }, [open]); + + const updateFocusedValue = useCallback( + (direction: number) => { + const values = Array.from(itemMap.keys()); + setFocusedValue((prevValue) => { + const currentIndex = values.indexOf(prevValue ?? ""); + const nextIndex = + (currentIndex + direction + values.length) % values.length; + return values[nextIndex] ?? ""; + }); + }, + [itemMap, setFocusedValue] + ); + + const handleKeyDown = useCallback( + (event: KeyboardEvent) => { + if (!open) return; + + const { key } = event; + + if (key === "ArrowDown") { + updateFocusedValue(1); + event.preventDefault(); + } else if (key === "ArrowUp") { + updateFocusedValue(-1); + event.preventDefault(); + } else if (key === "Enter" && focusedValue !== null) { + handleSelect(focusedValue, itemMap.get(focusedValue)?.text); + event.preventDefault(); + } + }, + [open, focusedValue, updateFocusedValue, handleSelect, itemMap] + ); + + return ( + + {children} + + ); +}; + +const dropdownContentStyle = cva({ + base: { + position: "absolute", + outline: "none", + top: "calc(100% + 0.5rem)", + left: 0, + zIndex: "dropdown", + maxHeight: "18.75rem", + width: "100%", + lg: { + maxWidth: "22.375rem", + }, + smDown: { + width: "100%", + }, + backgroundColor: "backgroundNormal", + border: "1px solid", + borderRadius: "sm", + borderColor: "outline", + overflow: "auto", + _scrollbar: { + width: "2px", + }, + _scrollbarThumb: { + width: "2px", + height: "65px", + borderRadius: "sm", + backgroundColor: "outline", + }, + _scrollbarTrack: { + marginTop: "2px", + marginBottom: "2px", + }, + }, + variants: { + type: { + custom: { + lg: {}, + }, + default: {}, + }, + }, + defaultVariants: { type: "default" }, +}); diff --git a/packages/wow-ui/src/components/DropDown/DropDownTrigger.tsx b/packages/wow-ui/src/components/DropDown/DropDownTrigger.tsx index 126a78db..52a3e8a8 100644 --- a/packages/wow-ui/src/components/DropDown/DropDownTrigger.tsx +++ b/packages/wow-ui/src/components/DropDown/DropDownTrigger.tsx @@ -6,9 +6,8 @@ import type { KeyboardEvent } from "react"; import { cloneElement, useCallback } from "react"; import { DownArrow } from "wowds-icons"; -import type { DropDownProps } from "@/components/DropDown"; -import { useDropDownContext } from "@/components/DropDown/context/DropDownContext"; - +import type { DropDownProps } from "../../components/DropDown"; +import { useDropDownContext } from "../../components/DropDown/context/DropDownContext"; import { useCollection } from "./context/CollectionContext"; interface DropDownTriggerProps { diff --git a/packages/wow-ui/src/components/DropDown/DropDownWrapper.tsx b/packages/wow-ui/src/components/DropDown/DropDownWrapper.tsx index 9bf26a61..f006ff48 100644 --- a/packages/wow-ui/src/components/DropDown/DropDownWrapper.tsx +++ b/packages/wow-ui/src/components/DropDown/DropDownWrapper.tsx @@ -1,15 +1,8 @@ import { Flex } from "@styled-system/jsx"; -import { - type KeyboardEvent, - type PropsWithChildren, - useCallback, - useRef, -} from "react"; +import { type PropsWithChildren, useCallback, useRef } from "react"; -import type { DropDownProps } from "@/components/DropDown"; -import useClickOutside from "@/hooks/useClickOutside"; - -import { useCollection } from "./context/CollectionContext"; +import type { DropDownProps } from "../../components/DropDown"; +import useClickOutside from "../../hooks/useClickOutside"; import { useDropDownContext } from "./context/DropDownContext"; interface DropDownWrapperProps extends PropsWithChildren { @@ -24,9 +17,8 @@ export const DropDownWrapper = ({ hasCustomTrigger, ...rest }: DropDownWrapperProps) => { - const { open, setOpen, setFocusedValue, focusedValue, handleSelect } = - useDropDownContext(); - const itemMap = useCollection(); + const { setOpen } = useDropDownContext(); + const dropdownRef = useRef(null); useClickOutside( @@ -34,39 +26,6 @@ export const DropDownWrapper = ({ useCallback(() => setOpen(false), [setOpen]) ); - const updateFocusedValue = useCallback( - (direction: number) => { - const values = Array.from(itemMap.keys()); - setFocusedValue((prevValue) => { - const currentIndex = values.indexOf(prevValue ?? ""); - const nextIndex = - (currentIndex + direction + values.length) % values.length; - return values[nextIndex] ?? ""; - }); - }, - [itemMap, setFocusedValue] - ); - - const handleKeyDown = useCallback( - (event: KeyboardEvent) => { - if (!open) return; - - const { key } = event; - - if (key === "ArrowDown") { - updateFocusedValue(1); - event.preventDefault(); - } else if (key === "ArrowUp") { - updateFocusedValue(-1); - event.preventDefault(); - } else if (key === "Enter" && focusedValue !== null) { - handleSelect(focusedValue, itemMap.get(focusedValue)?.text); - event.preventDefault(); - } - }, - [open, focusedValue, updateFocusedValue, handleSelect, itemMap] - ); - return ( {children} diff --git a/packages/wow-ui/src/components/DropDown/context/CollectionContext.tsx b/packages/wow-ui/src/components/DropDown/context/CollectionContext.tsx index 0ac3ba5b..aef9e7e1 100644 --- a/packages/wow-ui/src/components/DropDown/context/CollectionContext.tsx +++ b/packages/wow-ui/src/components/DropDown/context/CollectionContext.tsx @@ -1,7 +1,7 @@ import type { PropsWithChildren, ReactNode } from "react"; import { createContext, useMemo } from "react"; -import useSafeContext from "@/hooks/useSafeContext"; +import useSafeContext from "../../../hooks/useSafeContext"; type ItemData = { value: string; diff --git a/packages/wow-ui/src/components/DropDown/context/DropDownContext.ts b/packages/wow-ui/src/components/DropDown/context/DropDownContext.ts index d0229174..09d53349 100644 --- a/packages/wow-ui/src/components/DropDown/context/DropDownContext.ts +++ b/packages/wow-ui/src/components/DropDown/context/DropDownContext.ts @@ -1,7 +1,7 @@ import { createContext } from "react"; -import type useDropDownState from "@/hooks/useDropDownState"; -import useSafeContext from "@/hooks/useSafeContext"; +import type useDropDownState from "../../../hooks/useDropDownState"; +import useSafeContext from "../../../hooks/useSafeContext"; export const DropDownContext = createContext - + {children} - + @@ -135,47 +123,3 @@ const DropDown = ({ DropDown.displayName = "DropDown"; export default DropDown; - -const dropdownContentStyle = cva({ - base: { - position: "absolute", - top: "calc(100% + 0.5rem)", - left: 0, - zIndex: "dropdown", - maxHeight: "18.75rem", - width: "100%", - lg: { - maxWidth: "22.375rem", - }, - smDown: { - width: "100%", - }, - backgroundColor: "backgroundNormal", - border: "1px solid", - borderRadius: "sm", - borderColor: "outline", - overflow: "auto", - _scrollbar: { - width: "2px", - }, - _scrollbarThumb: { - width: "2px", - height: "65px", - borderRadius: "sm", - backgroundColor: "outline", - }, - _scrollbarTrack: { - marginTop: "2px", - marginBottom: "2px", - }, - }, - variants: { - type: { - custom: { - lg: {}, - }, - default: {}, - }, - }, - defaultVariants: { type: "default" }, -}); diff --git a/packages/wow-ui/src/hooks/useClickOutside.ts b/packages/wow-ui/src/hooks/useClickOutside.ts index b08788a1..b0b7040d 100644 --- a/packages/wow-ui/src/hooks/useClickOutside.ts +++ b/packages/wow-ui/src/hooks/useClickOutside.ts @@ -1,6 +1,6 @@ import { useEffect } from "react"; -import { isMobile } from "@/utils"; +import { isMobile } from "../utils"; const useClickOutside = ( ref: React.RefObject, From 6440016dcdaf32148c6660d85a2dda867439b051 Mon Sep 17 00:00:00 2001 From: SeieunYoo Date: Sun, 28 Jul 2024 14:31:09 +0900 Subject: [PATCH 27/27] =?UTF-8?q?refactor=20:=20itemMap=20=EA=B0=9D?= =?UTF-8?q?=EC=B2=B4=EC=97=90=EC=84=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/scripts/generateBuildConfig.ts | 1 + .../wow-ui/src/components/DropDown/DropDownOption.tsx | 7 ++++--- .../src/components/DropDown/DropDownOptionList.tsx | 2 +- .../wow-ui/src/components/DropDown/DropDownTrigger.tsx | 7 ++++--- .../wow-ui/src/components/DropDown/DropDownWrapper.tsx | 5 +++-- .../components/DropDown/context/CollectionContext.tsx | 6 +++--- packages/wow-ui/src/components/DropDown/index.tsx | 9 +++++---- 7 files changed, 21 insertions(+), 16 deletions(-) diff --git a/packages/scripts/generateBuildConfig.ts b/packages/scripts/generateBuildConfig.ts index bc4c04d0..36a68f74 100644 --- a/packages/scripts/generateBuildConfig.ts +++ b/packages/scripts/generateBuildConfig.ts @@ -24,6 +24,7 @@ const excludedComponents = [ "DropDownTrigger", "DropDownWrapper", "CollectionContext", + "DropDownOptionList", ]; const getFilteredComponentFiles = async (directoryPath: string) => { diff --git a/packages/wow-ui/src/components/DropDown/DropDownOption.tsx b/packages/wow-ui/src/components/DropDown/DropDownOption.tsx index 50e6c4e6..57b74f2d 100644 --- a/packages/wow-ui/src/components/DropDown/DropDownOption.tsx +++ b/packages/wow-ui/src/components/DropDown/DropDownOption.tsx @@ -5,7 +5,8 @@ import { styled } from "@styled-system/jsx"; import type { ReactNode } from "react"; import { forwardRef, useCallback, useEffect } from "react"; -import { useDropDownContext } from "../../components/DropDown/context/DropDownContext"; +import { useDropDownContext } from "@/components/DropDown/context/DropDownContext"; + import { useCollection } from "./context/CollectionContext"; /** @@ -39,8 +40,8 @@ const DropDownOption = forwardRef( useEffect(() => { const currentItem = itemMap.get(value); - if (!currentItem || currentItem.text !== text) { - itemMap.set(value, { text }); + if (!currentItem || currentItem !== text) { + itemMap.set(value, text); } }, [itemMap, value, text]); diff --git a/packages/wow-ui/src/components/DropDown/DropDownOptionList.tsx b/packages/wow-ui/src/components/DropDown/DropDownOptionList.tsx index e985faad..22304655 100644 --- a/packages/wow-ui/src/components/DropDown/DropDownOptionList.tsx +++ b/packages/wow-ui/src/components/DropDown/DropDownOptionList.tsx @@ -55,7 +55,7 @@ export const DropDownOptionList = ({ updateFocusedValue(-1); event.preventDefault(); } else if (key === "Enter" && focusedValue !== null) { - handleSelect(focusedValue, itemMap.get(focusedValue)?.text); + handleSelect(focusedValue, itemMap.get(focusedValue)); event.preventDefault(); } }, diff --git a/packages/wow-ui/src/components/DropDown/DropDownTrigger.tsx b/packages/wow-ui/src/components/DropDown/DropDownTrigger.tsx index 52a3e8a8..e712d9a0 100644 --- a/packages/wow-ui/src/components/DropDown/DropDownTrigger.tsx +++ b/packages/wow-ui/src/components/DropDown/DropDownTrigger.tsx @@ -6,8 +6,9 @@ import type { KeyboardEvent } from "react"; import { cloneElement, useCallback } from "react"; import { DownArrow } from "wowds-icons"; -import type { DropDownProps } from "../../components/DropDown"; -import { useDropDownContext } from "../../components/DropDown/context/DropDownContext"; +import type { DropDownProps } from "@/components/DropDown"; +import { useDropDownContext } from "@/components/DropDown/context/DropDownContext"; + import { useCollection } from "./context/CollectionContext"; interface DropDownTriggerProps { @@ -27,7 +28,7 @@ const DropDownTrigger = ({ const { open, selectedValue, setOpen, setFocusedValue } = useDropDownContext(); - const selectedText = itemMap.get(selectedValue)?.text; + const selectedText = itemMap.get(selectedValue); const toggleDropdown = useCallback(() => { setOpen((prevOpen) => { diff --git a/packages/wow-ui/src/components/DropDown/DropDownWrapper.tsx b/packages/wow-ui/src/components/DropDown/DropDownWrapper.tsx index f006ff48..f2f60950 100644 --- a/packages/wow-ui/src/components/DropDown/DropDownWrapper.tsx +++ b/packages/wow-ui/src/components/DropDown/DropDownWrapper.tsx @@ -1,8 +1,9 @@ import { Flex } from "@styled-system/jsx"; import { type PropsWithChildren, useCallback, useRef } from "react"; -import type { DropDownProps } from "../../components/DropDown"; -import useClickOutside from "../../hooks/useClickOutside"; +import type { DropDownProps } from "@/components/DropDown"; +import useClickOutside from "@/hooks/useClickOutside"; + import { useDropDownContext } from "./context/DropDownContext"; interface DropDownWrapperProps extends PropsWithChildren { diff --git a/packages/wow-ui/src/components/DropDown/context/CollectionContext.tsx b/packages/wow-ui/src/components/DropDown/context/CollectionContext.tsx index aef9e7e1..09fe9d27 100644 --- a/packages/wow-ui/src/components/DropDown/context/CollectionContext.tsx +++ b/packages/wow-ui/src/components/DropDown/context/CollectionContext.tsx @@ -1,7 +1,7 @@ import type { PropsWithChildren, ReactNode } from "react"; import { createContext, useMemo } from "react"; -import useSafeContext from "../../../hooks/useSafeContext"; +import useSafeContext from "@/hooks/useSafeContext"; type ItemData = { value: string; @@ -9,7 +9,7 @@ type ItemData = { }; type ContextValue = { - itemMap: Map; + itemMap: Map; }; const CollectionContext = createContext(null); @@ -21,7 +21,7 @@ export const useCollectionContext = () => { export const CollectionProvider = ({ children }: PropsWithChildren) => { const itemMap = useMemo( - () => new Map(), + () => new Map(), [] ); diff --git a/packages/wow-ui/src/components/DropDown/index.tsx b/packages/wow-ui/src/components/DropDown/index.tsx index 138570d2..811d6fcb 100644 --- a/packages/wow-ui/src/components/DropDown/index.tsx +++ b/packages/wow-ui/src/components/DropDown/index.tsx @@ -8,10 +8,11 @@ import type { } from "react"; import { useId } from "react"; -import { DropDownContext } from "../../components/DropDown/context/DropDownContext"; -import { DropDownOptionList } from "../../components/DropDown/DropDownOptionList"; -import DropDownTrigger from "../../components/DropDown/DropDownTrigger"; -import useDropDownState from "../../hooks/useDropDownState"; +import { DropDownContext } from "@/components/DropDown/context/DropDownContext"; +import { DropDownOptionList } from "@/components/DropDown/DropDownOptionList"; +import DropDownTrigger from "@/components/DropDown/DropDownTrigger"; +import useDropDownState from "@/hooks/useDropDownState"; + import { CollectionProvider } from "./context/CollectionContext"; import { DropDownWrapper } from "./DropDownWrapper"; export interface DropDownWithTriggerProps extends PropsWithChildren {