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 를 이용하여 리팩토링 합니다.
diff --git a/apps/wow-docs/app/page.tsx b/apps/wow-docs/app/page.tsx
index b12702eb..3d93c599 100644
--- a/apps/wow-docs/app/page.tsx
+++ b/apps/wow-docs/app/page.tsx
@@ -22,8 +22,8 @@ const Home = () => {
-
-
+
+
diff --git a/packages/scripts/generateBuildConfig.ts b/packages/scripts/generateBuildConfig.ts
index 2144e117..36a68f74 100644
--- a/packages/scripts/generateBuildConfig.ts
+++ b/packages/scripts/generateBuildConfig.ts
@@ -19,6 +19,14 @@ type EntryFileKey = string;
type EntryFileValue = string;
type EntryFileObject = { [key: EntryFileKey]: EntryFileValue };
+// 제외할 컴포넌트 목록
+const excludedComponents = [
+ "DropDownTrigger",
+ "DropDownWrapper",
+ "CollectionContext",
+ "DropDownOptionList",
+];
+
const getFilteredComponentFiles = async (directoryPath: string) => {
const files = await fs.readdir(directoryPath, { recursive: true });
@@ -26,7 +34,8 @@ const getFilteredComponentFiles = async (directoryPath: string) => {
(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/components/DropDown/DropDown.stories.tsx b/packages/wow-ui/src/components/DropDown/DropDown.stories.tsx
index 4ff6c341..df853815 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;
@@ -122,13 +150,13 @@ export const WithDefaultValue: Story = {
args: {
children: (
<>
-
-
+
+
>
),
label: "Select an Option",
placeholder: "Please select",
- defaultValue: "Option 2",
+ defaultValue: "option 2",
},
parameters: {
docs: {
diff --git a/packages/wow-ui/src/components/DropDown/DropDownOption.tsx b/packages/wow-ui/src/components/DropDown/DropDownOption.tsx
index ae370dc2..57b74f2d 100644
--- a/packages/wow-ui/src/components/DropDown/DropDownOption.tsx
+++ b/packages/wow-ui/src/components/DropDown/DropDownOption.tsx
@@ -1,39 +1,65 @@
+"use client";
+
import { cva } from "@styled-system/css";
import { styled } from "@styled-system/jsx";
import type { ReactNode } from "react";
-import { forwardRef } from "react";
+import { forwardRef, useCallback, useEffect } from "react";
+
+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;
}
-const DropDownOption = forwardRef(
- function Option({ value, onClick, focused, text, selected }, ref) {
+const DropDownOption = forwardRef(
+ function Option({ value, onClick, text }, ref) {
+ const { focusedValue, selectedValue, handleSelect } = useDropDownContext();
+ const isSelected = selectedValue === value;
+ const isFocused = focusedValue !== null && focusedValue === value;
+
+ const handleOptionClick = useCallback(
+ (value: string, onClick?: () => void) => {
+ if (onClick) onClick();
+ handleSelect(value, text);
+ },
+ [handleSelect, text]
+ );
+
+ const itemMap = useCollection();
+
+ useEffect(() => {
+ const currentItem = itemMap.get(value);
+ if (!currentItem || currentItem !== text) {
+ itemMap.set(value, text);
+ }
+ }, [itemMap, value, text]);
+
return (
- {
+ handleOptionClick(value, onClick);
+ }}
>
{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..22304655
--- /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));
+ 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
new file mode 100644
index 00000000..e712d9a0
--- /dev/null
+++ b/packages/wow-ui/src/components/DropDown/DropDownTrigger.tsx
@@ -0,0 +1,178 @@
+"use client";
+
+import { cva } from "@styled-system/css";
+import { styled } from "@styled-system/jsx";
+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 { useCollection } from "./context/CollectionContext";
+
+interface DropDownTriggerProps {
+ placeholder?: DropDownProps["placeholder"];
+ label?: DropDownProps["label"];
+ trigger?: DropDownProps["trigger"];
+ dropdownId: string;
+}
+
+const DropDownTrigger = ({
+ placeholder,
+ label,
+ trigger,
+ dropdownId,
+}: DropDownTriggerProps) => {
+ const itemMap = useCollection();
+ const { open, selectedValue, setOpen, setFocusedValue } =
+ useDropDownContext();
+
+ const selectedText = itemMap.get(selectedValue);
+
+ 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 (
+ <>
+ {label && (
+
+ {label}
+
+ )}
+
+
+ {selectedValue ? selectedText : placeholder}
+
+
+
+ >
+ );
+};
+
+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/DropDownWrapper.tsx b/packages/wow-ui/src/components/DropDown/DropDownWrapper.tsx
new file mode 100644
index 00000000..f2f60950
--- /dev/null
+++ b/packages/wow-ui/src/components/DropDown/DropDownWrapper.tsx
@@ -0,0 +1,47 @@
+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 { useDropDownContext } from "./context/DropDownContext";
+
+interface DropDownWrapperProps extends PropsWithChildren {
+ dropdownId: string;
+ style?: DropDownProps["style"];
+ className?: DropDownProps["className"];
+ hasCustomTrigger?: boolean;
+}
+export const DropDownWrapper = ({
+ children,
+ dropdownId,
+ hasCustomTrigger,
+ ...rest
+}: DropDownWrapperProps) => {
+ const { setOpen } = useDropDownContext();
+
+ const dropdownRef = useRef(null);
+
+ useClickOutside(
+ dropdownRef,
+ useCallback(() => setOpen(false), [setOpen])
+ );
+
+ 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..09fe9d27
--- /dev/null
+++ b/packages/wow-ui/src/components/DropDown/context/CollectionContext.tsx
@@ -0,0 +1,38 @@
+import type { PropsWithChildren, ReactNode } from "react";
+import { createContext, useMemo } from "react";
+
+import useSafeContext from "@/hooks/useSafeContext";
+
+type ItemData = {
+ value: string;
+ text: ReactNode;
+};
+
+type ContextValue = {
+ itemMap: Map;
+};
+
+const CollectionContext = createContext(null);
+
+export const useCollectionContext = () => {
+ const context = useSafeContext(CollectionContext);
+ return context;
+};
+
+export const CollectionProvider = ({ children }: PropsWithChildren) => {
+ 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
new file mode 100644
index 00000000..09d53349
--- /dev/null
+++ b/packages/wow-ui/src/components/DropDown/context/DropDownContext.ts
@@ -0,0 +1,13 @@
+import { createContext } from "react";
+
+import type useDropDownState from "../../../hooks/useDropDownState";
+import useSafeContext from "../../../hooks/useSafeContext";
+
+export const DropDownContext = createContext | null>(null);
+
+export const useDropDownContext = () => {
+ const context = useSafeContext(DropDownContext);
+ return context;
+};
diff --git a/packages/wow-ui/src/components/DropDown/index.tsx b/packages/wow-ui/src/components/DropDown/index.tsx
index 40733818..811d6fcb 100644
--- a/packages/wow-ui/src/components/DropDown/index.tsx
+++ b/packages/wow-ui/src/components/DropDown/index.tsx
@@ -1,21 +1,20 @@
"use client";
-import { cva } from "@styled-system/css";
-import { Flex, styled } from "@styled-system/jsx";
import type {
CSSProperties,
PropsWithChildren,
ReactElement,
ReactNode,
} from "react";
-import { cloneElement, isValidElement, useRef } from "react";
-import { DownArrow } from "wowds-icons";
+import { useId } from "react";
-import DropDownOption from "@/components/DropDown/DropDownOption";
-import useClickOutside from "@/hooks/useClickOutside";
+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 useFlattenChildren from "@/hooks/useFlattenChildren";
+import { CollectionProvider } from "./context/CollectionContext";
+import { DropDownWrapper } from "./DropDownWrapper";
export interface DropDownWithTriggerProps extends PropsWithChildren {
/**
* @description 드롭다운을 열기 위한 외부 트리거 요소입니다.
@@ -53,8 +52,9 @@ export interface DropDownWithoutTriggerProps extends PropsWithChildren {
}
/**
- * @description 사용자가 외부 트리거 컴포넌트 나 내부 요소를 통해서 선택 옵션 리스트 중에 아이템을 선택할 수 있는 드롭다운 컴포넌트 입니다.
+ * @description 사용자가 외부 트리거 컴포넌트나 내부 요소를 통해서 선택 옵션 리스트 중에 아이템을 선택할 수 있는 드롭다운 컴포넌트 입니다.
*
+ * @param {string} [id] 드롭다운 id 입니다.
* @param {ReactElement} [trigger] 드롭다운을 열기 위한 외부 트리거 요소입니다.
* @param {ReactNode} [label] 외부 트리거를 사용하지 않는 경우 레이블을 사용할 수 있습니다.
* @param {string} [placeholder] 외부 트리거를 사용하지 않는 경우 선택되지 않았을 때 표시되는 플레이스홀더입니다.
@@ -68,6 +68,7 @@ export type DropDownProps = (
| DropDownWithTriggerProps
| DropDownWithoutTriggerProps
) & {
+ id?: string;
value?: string;
defaultValue?: string;
onChange?: (value: {
@@ -79,6 +80,7 @@ export type DropDownProps = (
};
const DropDown = ({
+ id,
children,
trigger,
label,
@@ -88,233 +90,37 @@ const DropDown = ({
onChange,
...rest
}: DropDownProps) => {
- const flattenedChildren = useFlattenChildren(children);
- const {
- selectedValue,
- selectedText,
- open,
- setOpen,
- focusedIndex,
- setFocusedIndex,
- handleSelect,
- handleKeyDown,
- } = useDropDownState({
+ const dropdownState = useDropDownState({
value,
defaultValue,
- children: flattenedChildren,
onChange,
});
- const dropdownRef = useRef(null);
- const optionsRef = useRef<(HTMLDivElement | null)[]>([]);
-
- useClickOutside(dropdownRef, () => setOpen(false));
-
- const toggleDropdown = () => {
- setOpen((prevOpen) => {
- if (!prevOpen) setFocusedIndex(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, {
- onClick: toggleDropdown,
- })}
- >
- );
-
- const renderLabel = () => (
- <>
-
- {label}
-
-
-
- {selectedText ? selectedText : placeholder}
-
- {
- if (e.key === "Enter") toggleDropdown();
- }}
- />
-
- >
- );
-
- 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: selectedValue === child.props.value,
- });
- }
- return child;
- })}
-
- );
+ const defaultId = useId();
+ const dropdownId = id ?? `dropdown-${defaultId}`;
return (
-
- {trigger ? renderTrigger(trigger) : renderLabel()}
- {open && renderOptions()}
-
+
+
+
+
+
+ {children}
+
+
+
+
);
};
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",
- top: "calc(100% + 0.5rem)",
- left: 0,
- zIndex: "dropdown",
- maxHeight: "18.75rem",
- 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",
- },
- },
-});
-
-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/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 be77ccc0..49d696a9 100644
--- a/packages/wow-ui/src/hooks/useDropDownState.ts
+++ b/packages/wow-ui/src/hooks/useDropDownState.ts
@@ -1,10 +1,9 @@
-import type { KeyboardEvent, ReactElement, ReactNode } from "react";
-import { isValidElement, useEffect, useMemo, useState } from "react";
+import type { ReactNode } from "react";
+import { useEffect, useState } from "react";
interface DropDownStateProps {
value?: string;
defaultValue?: string;
- children: ReactNode[];
onChange?: (value: {
selectedValue: string;
selectedText: ReactNode;
@@ -14,77 +13,38 @@ interface DropDownStateProps {
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 [selectedValue, setSelectedValue] = useState(defaultValue || "");
- const [selectedText, setSelectedText] = useState(
- defaultValue ? options[defaultValue] : ""
- );
+ const [selectedValue, setSelectedValue] = useState("");
const [open, setOpen] = useState(false);
- const [focusedIndex, setFocusedIndex] = useState(null);
+ const [focusedValue, setFocusedValue] = useState(null);
useEffect(() => {
if (value !== undefined) {
setSelectedValue(value);
- setSelectedText(options[value]);
}
- }, [options, value]);
+ if (defaultValue !== undefined) {
+ setSelectedValue(defaultValue);
+ }
+ }, [value, defaultValue]);
- const handleSelect = (option: string) => {
+ const handleSelect = (selectedValue: string, selectedText: ReactNode) => {
if (value === undefined) {
- setSelectedValue(option);
- setSelectedText(options[option]);
+ setSelectedValue(selectedValue);
}
- setOpen(false);
if (onChange) {
- onChange({ selectedValue: option, selectedText: options[option] });
- }
- };
-
- const handleKeyDown = (event: KeyboardEvent) => {
- if (!open) return;
-
- const { key } = event;
-
- if (key === "ArrowDown") {
- setFocusedIndex((prevIndex) =>
- prevIndex === null ? 0 : (prevIndex + 1) % children.length
- );
- event.preventDefault();
- } else if (key === "ArrowUp") {
- setFocusedIndex((prevIndex) =>
- prevIndex === null
- ? children.length - 1
- : (prevIndex - 1 + children.length) % children.length
- );
- event.preventDefault();
- } else if (key === "Enter" && focusedIndex !== null) {
- const child = children[focusedIndex] as ReactElement;
- handleSelect(child.props.value);
- event.preventDefault();
+ onChange({ selectedValue, selectedText });
}
+ setOpen(false);
};
return {
selectedValue,
- selectedText,
open,
setOpen,
- focusedIndex,
- setFocusedIndex,
+ focusedValue,
+ setFocusedValue,
handleSelect,
- handleKeyDown,
};
};
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;
diff --git a/packages/wow-ui/src/hooks/useSafeContext.ts b/packages/wow-ui/src/hooks/useSafeContext.ts
new file mode 100644
index 00000000..5ea8dfda
--- /dev/null
+++ b/packages/wow-ui/src/hooks/useSafeContext.ts
@@ -0,0 +1,19 @@
+import type { Context } from "react";
+import { useContext } from "react";
+
+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`);
+ }
+
+ return contextValue as SafeContextType;
+};
+
+export default useSafeContext;