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;