Skip to content

Commit

Permalink
[Refactor] DropDownOption 에 대해서 context 로 분리 (#93)
Browse files Browse the repository at this point in the history
* feat : 서치바 구현

* feat : 스토리북 추가

* feat : 서치바 테스트 코드 작성 및 aria 속성 수정

* refactor: useFormCOntrol 로 분리

* chore: 체인지셋 작성

* refactor : 드롭다운 컨텍스트로 분리

* refactor : 절대경로로 수정

* fix : ts 로 변경

* feat : DropDownOption 에 use client 추가, useSafeContext 추가

* refactor: useSafeContext 의 타입 추론 강화, aria 속성 추가

* fix : 접근성 테스트 해결,  test ci 해결

* fix: 텍필 원복

* style : trigger 가 있을 때는  100% 유지

* refactor: useDropDownstate 수정

* refactor : DropDownTrigger 로 리팩토링

* refactor : collection context 로 분리

* refactor : 안쓰는 import 삭제, 제외하는 컴포넌트 build 스크립트 추가

* refactor : useCallback 추가해서 렌더링 최소화

* fix : 필요없는 코드 삭제

* refactor: values함수 내부로 이동

* fix : flat 해주는 훅 삭제

* chore : changeset 추가

* fix : defaultValue 관련 수정, 드롭다운 width 스타일 수정

* feat : 스토리북 보완

* fix: 스토리북  수정

* refacotor: OptionList 에서 focus 로직 처리하도록 분리

* refactor : itemMap 객체에서 변경
  • Loading branch information
SeieunYoo authored Jul 28, 2024
1 parent 15f3271 commit 940b704
Show file tree
Hide file tree
Showing 15 changed files with 557 additions and 321 deletions.
5 changes: 5 additions & 0 deletions .changeset/four-peaches-approve.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"wowds-ui": patch
---

DropDownOption 컴포넌트를 context 를 이용하여 리팩토링 합니다.
4 changes: 2 additions & 2 deletions apps/wow-docs/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ const Home = () => {
<RadioButton label="2학년" value="2학년" />
</RadioGroup>
<DropDown placeholder="목록을 입력하세요">
<DropDownOption text="option 1" value="option 1" />
<DropDownOption text="option 1" value="option 2" />
<DropDownOption text=" 1" value="option 1" />
<DropDownOption text="옵 2" value="option 2" />
</DropDown>
<MultiGroup variant="checkbox">
<Checkbox label="checkbox1" value="checkbox1" />
Expand Down
11 changes: 10 additions & 1 deletion packages/scripts/generateBuildConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,23 @@ 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 });

return files.filter(
(file) =>
file.endsWith(".tsx") &&
!file.includes("test") &&
!file.includes("stories")
!file.includes("stories") &&
!excludedComponents.some((excluded) => file.includes(excluded))
);
};

Expand Down
36 changes: 32 additions & 4 deletions packages/wow-ui/src/components/DropDown/DropDown.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down Expand Up @@ -48,6 +55,13 @@ const meta = {
},
control: { type: "text" },
},
id: {
description: "드롭다운의 id 를 나타냅니다.",
table: {
type: { summary: "string" },
},
control: { type: "text" },
},
value: {
description: "현재 선택된 값을 나타냅니다.",
table: {
Expand All @@ -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<typeof DropDown>;

Expand Down Expand Up @@ -122,13 +150,13 @@ export const WithDefaultValue: Story = {
args: {
children: (
<>
<DropDownOption text="option 1" value="option 1" />
<DropDownOption text="option 2" value="option 2" />
<DropDownOption text="옵션 1" value="option 1" />
<DropDownOption text="옵션 2" value="option 2" />
</>
),
label: "Select an Option",
placeholder: "Please select",
defaultValue: "Option 2",
defaultValue: "option 2",
},
parameters: {
docs: {
Expand Down
52 changes: 39 additions & 13 deletions packages/wow-ui/src/components/DropDown/DropDownOption.tsx
Original file line number Diff line number Diff line change
@@ -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<HTMLDivElement, DropDownOptionProps>(
function Option({ value, onClick, focused, text, selected }, ref) {
const DropDownOption = forwardRef<HTMLLIElement, DropDownOptionProps>(
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 (
<styled.div
<styled.li
id={`dropdown-option-${value}`}
ref={ref}
tabIndex={-1}
role="option"
tabIndex={isSelected ? 0 : -1}
className={optionStyle({
type: selected ? "selected" : focused ? "focused" : "default",
type: isSelected ? "selected" : isFocused ? "focused" : "default",
})}
onClick={onClick}
onClick={() => {
handleOptionClick(value, onClick);
}}
>
{text}
</styled.div>
</styled.li>
);
}
);
Expand Down
127 changes: 127 additions & 0 deletions packages/wow-ui/src/components/DropDown/DropDownOptionList.tsx
Original file line number Diff line number Diff line change
@@ -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<HTMLUListElement>(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<HTMLUListElement>) => {
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 (
<styled.ul
display="flex"
flexDirection="column"
ref={listRef}
role="listbox"
style={{ visibility: open ? "visible" : "hidden" }}
tabIndex={0}
visibility={open ? "visible" : "hidden"}
className={dropdownContentStyle({
type: hasCustomTrigger ? "custom" : "default",
})}
onKeyDown={handleKeyDown}
>
{children}
</styled.ul>
);
};

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" },
});
Loading

0 comments on commit 940b704

Please sign in to comment.