Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Refactor] DropDownOption 에 대해서 context 로 분리 #93

Merged
merged 29 commits into from
Jul 28, 2024
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
8f751e6
feat : 서치바 구현
SeieunYoo Jul 16, 2024
4cf6973
feat : 스토리북 추가
SeieunYoo Jul 17, 2024
00c26bf
feat : 서치바 테스트 코드 작성 및 aria 속성 수정
SeieunYoo Jul 17, 2024
5c57c1a
refactor: useFormCOntrol 로 분리
SeieunYoo Jul 19, 2024
69c641d
chore: 체인지셋 작성
SeieunYoo Jul 19, 2024
42500ff
refactor : 드롭다운 컨텍스트로 분리
SeieunYoo Jul 21, 2024
829f096
Merge branch 'main' of https://github.com/GDSC-Hongik/wow-design-syst…
SeieunYoo Jul 21, 2024
ea56104
refactor : 절대경로로 수정
SeieunYoo Jul 21, 2024
dc8c589
fix : ts 로 변경
SeieunYoo Jul 21, 2024
6b3db85
feat : DropDownOption 에 use client 추가, useSafeContext 추가
SeieunYoo Jul 21, 2024
6574310
refactor: useSafeContext 의 타입 추론 강화, aria 속성 추가
SeieunYoo Jul 21, 2024
4343ad0
fix : 접근성 테스트 해결, test ci 해결
SeieunYoo Jul 21, 2024
88f7fce
fix: 텍필 원복
SeieunYoo Jul 21, 2024
c5edc59
style : trigger 가 있을 때는 100% 유지
SeieunYoo Jul 21, 2024
3c38d6b
refactor: useDropDownstate 수정
SeieunYoo Jul 25, 2024
b001317
refactor : DropDownTrigger 로 리팩토링
SeieunYoo Jul 26, 2024
504878c
refactor : collection context 로 분리
SeieunYoo Jul 26, 2024
f8527b2
refactor : 안쓰는 import 삭제, 제외하는 컴포넌트 build 스크립트 추가
SeieunYoo Jul 26, 2024
eb86299
refactor : useCallback 추가해서 렌더링 최소화
SeieunYoo Jul 26, 2024
fdcb756
fix : 필요없는 코드 삭제
SeieunYoo Jul 26, 2024
8be18f5
refactor: values함수 내부로 이동
SeieunYoo Jul 26, 2024
2b66ec1
fix : flat 해주는 훅 삭제
SeieunYoo Jul 26, 2024
ad51ddb
Merge branch 'main' of https://github.com/GDSC-Hongik/wow-design-syst…
SeieunYoo Jul 26, 2024
50d1ee5
chore : changeset 추가
SeieunYoo Jul 26, 2024
d772386
fix : defaultValue 관련 수정, 드롭다운 width 스타일 수정
SeieunYoo Jul 27, 2024
c2ad801
feat : 스토리북 보완
SeieunYoo Jul 27, 2024
b9072e3
fix: 스토리북 수정
SeieunYoo Jul 27, 2024
6ca0434
refacotor: OptionList 에서 focus 로직 처리하도록 분리
SeieunYoo Jul 28, 2024
6440016
refactor : itemMap 객체에서 변경
SeieunYoo Jul 28, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/lazy-radios-battle.md
SeieunYoo marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"wowds-icons": patch
"wowds-ui": patch
---

SearchBar 컴포넌트를 구현합니다
4 changes: 2 additions & 2 deletions apps/wow-docs/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,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
2 changes: 1 addition & 1 deletion packages/wow-icons/src/component/RightArrow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ const RightArrow = forwardRef<SVGSVGElement, IconProps>(
) => {
return (
<svg
aria-label="rightArrow icon"
aria-label="right-arrow icon"
className={className}
fill="none"
height={height}
Expand Down
47 changes: 47 additions & 0 deletions packages/wow-icons/src/component/Search.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { forwardRef } from "react";
import { color } from "wowds-tokens";

import type { IconProps } from "@/types/Icon.ts";

const Search = forwardRef<SVGSVGElement, IconProps>(
(
{
className,
width = "24",
height = "24",
viewBox = "0 0 24 24",
stroke = "white",
...rest
},
ref
) => {
return (
<svg
aria-label="search icon"
className={className}
fill="none"
height={height}
ref={ref}
viewBox={viewBox}
width={width}
xmlns="http://www.w3.org/2000/svg"
{...rest}
>
<path
d="M18 10C18 13.866 14.866 17 11 17C7.13401 17 4 13.866 4 10C4 6.13401 7.13401 3 11 3C14.866 3 18 6.13401 18 10Z"
stroke={color[stroke]}
strokeWidth="1.4"
/>
<path
d="M15 16L19 21.5"
stroke={color[stroke]}
strokeLinejoin="bevel"
strokeWidth="1.4"
/>
</svg>
);
}
);

Search.displayName = "Search";
export default Search;
1 change: 1 addition & 0 deletions packages/wow-icons/src/component/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
4 changes: 4 additions & 0 deletions packages/wow-icons/src/svg/search.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 5 additions & 0 deletions packages/wow-ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
1 change: 1 addition & 0 deletions packages/wow-ui/rollup.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
20 changes: 17 additions & 3 deletions packages/wow-ui/src/components/DropDown/DropDownOption.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
"use client";

import { cva } from "@styled-system/css";
import { styled } from "@styled-system/jsx";
import type { ReactNode } from "react";
import { forwardRef } from "react";

import { useDropDownContext } from "@/components/DropDown/context/DropDownContext";

/**
* @description 드롭다운 옵션의 props입니다.
*
Expand All @@ -21,16 +25,26 @@ export interface DropDownOptionProps {
}

const DropDownOption = forwardRef<HTMLDivElement, DropDownOptionProps>(
function Option({ value, onClick, focused, text, selected }, ref) {
function Option({ value, onClick, text }, ref) {
const { focusedValue, selectedValue, handleSelect } = useDropDownContext();
SeieunYoo marked this conversation as resolved.
Show resolved Hide resolved
const isSelected = selectedValue === value;
const isFocused = focusedValue !== null && focusedValue === value;

const handleOptionClick = (value: string, onClick?: () => void) => {
if (onClick) onClick();
handleSelect(value);
};
return (
<styled.div
id={`dropdown-option-${value}`}
ref={ref}
tabIndex={-1}
className={optionStyle({
type: selected ? "selected" : focused ? "focused" : "default",
type: isSelected ? "selected" : isFocused ? "focused" : "default",
})}
onClick={onClick}
onClick={() => {
handleOptionClick(value, onClick);
}}
>
{text}
</styled.div>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { createContext } from "react";

import type useDropDownState from "@/hooks/useDropDownState";
import useSafeContext from "@/hooks/useSafeContext";

export const DropDownContext = createContext<ReturnType<
typeof useDropDownState
> | null>(null);

export const useDropDownContext = () => {
const context = useSafeContext(DropDownContext);
return context;
};
121 changes: 67 additions & 54 deletions packages/wow-ui/src/components/DropDown/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@ import type {
ReactElement,
ReactNode,
} from "react";
import { cloneElement, isValidElement, useRef } from "react";
import { cloneElement, useId, useRef } from "react";
import { DownArrow } from "wowds-icons";

import DropDownOption from "@/components/DropDown/DropDownOption";
import { DropDownContext } from "@/components/DropDown/context/DropDownContext";
import useClickOutside from "@/hooks/useClickOutside";
import useDropDownState from "@/hooks/useDropDownState";
import useFlattenChildren from "@/hooks/useFlattenChildren";
Expand Down Expand Up @@ -53,8 +53,9 @@ export interface DropDownWithoutTriggerProps extends PropsWithChildren {
}

/**
* @description 사용자가 외부 트리거 컴포넌트 나 내부 요소를 통해서 선택 옵션 리스트 중에 아이템을 선택할 수 있는 드롭다운 컴포넌트 입니다.
* @description 사용자가 외부 트리거 컴포넌트나 내부 요소를 통해서 선택 옵션 리스트 중에 아이템을 선택할 수 있는 드롭다운 컴포넌트 입니다.
*
* @param {string} [id] 드롭다운 id 입니다.
* @param {ReactElement} [trigger] 드롭다운을 열기 위한 외부 트리거 요소입니다.
* @param {ReactNode} [label] 외부 트리거를 사용하지 않는 경우 레이블을 사용할 수 있습니다.
* @param {string} [placeholder] 외부 트리거를 사용하지 않는 경우 선택되지 않았을 때 표시되는 플레이스홀더입니다.
Expand All @@ -68,6 +69,7 @@ export type DropDownProps = (
| DropDownWithTriggerProps
| DropDownWithoutTriggerProps
) & {
id?: string;
value?: string;
defaultValue?: string;
onChange?: (value: {
Expand All @@ -79,6 +81,7 @@ export type DropDownProps = (
};

const DropDown = ({
id,
children,
trigger,
label,
Expand All @@ -89,62 +92,65 @@ const DropDown = ({
...rest
}: DropDownProps) => {
const flattenedChildren = useFlattenChildren(children);
SeieunYoo marked this conversation as resolved.
Show resolved Hide resolved
const dropdownState = useDropDownState({
value,
defaultValue,
children: flattenedChildren,
onChange,
});

const {
selectedValue,
selectedText,
open,
setFocusedValue,
setOpen,
focusedIndex,
setFocusedIndex,
handleSelect,
handleKeyDown,
} = useDropDownState({
value,
defaultValue,
children: flattenedChildren,
onChange,
});
} = dropdownState;

const defaultId = useId();
const dropdownId = id ?? `dropdown-${defaultId}`;
const dropdownRef = useRef<HTMLDivElement>(null);
const optionsRef = useRef<(HTMLDivElement | null)[]>([]);

useClickOutside(dropdownRef, () => setOpen(false));

const toggleDropdown = () => {
setOpen((prevOpen) => {
if (!prevOpen) setFocusedIndex(null);
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) => (
const renderCustomTrigger = (trigger: ReactElement) => (
<>
{cloneElement(trigger, {
onClick: toggleDropdown,
"aria-expanded": open,
"aria-haspopup": "true",
id: `${dropdownId}-trigger`,
"aria-controls": `${dropdownId}`,
})}
</>
);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

p3;
renderCustomTrigger에서도 cloneElement가 아니라 context api 방식으로 해결해볼 수 있을 거 같다는 생각이 드는데, 별도의 컴포넌트로 분리 후 context api 적용하도록 개선해보면 좋지 않을까 싶습니다!

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

context api 를 적용하려면 trigger 외부에 div 같은 엘리먼트로 하나 감싸서 프로퍼티를 전달해주어야 하는데 요렇게 되면 의도하지 않은 외부 div 가 돔에 생겨서 스타일링에 영향을 줄 수도 있을 것 같은데 어떻게 생각하시나용?!

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

음 그럴 수도 있겠네요!
당장은 best practice가 뭔지 잘 모르겠어서 조금 더 고민해봐야 할 거 같아요!
추후에 더 좋은 방법이 생각난다면 개선해봐도 좋을 거 같은...


const renderLabel = () => (
const renderDefaultTrigger = () => (
<>
<styled.span
SeieunYoo marked this conversation as resolved.
Show resolved Hide resolved
color={open ? "primary" : selectedValue ? "textBlack" : "sub"}
textStyle="label2"
>
{label}
</styled.span>
<Flex
<styled.button
alignItems="center"
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",
})}
Expand All @@ -165,43 +171,41 @@ const DropDown = ({
if (e.key === "Enter") toggleDropdown();
}}
/>
</Flex>
</styled.button>
</>
);

const renderOptions = () => (
<Flex className={dropdownContentStyle()} direction="column">
{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;
<Flex
direction="column"
className={dropdownContentStyle({
type: trigger ? "custom" : "default",
})}
>
{children}
</Flex>
);

return (
<Flex
cursor="pointer"
direction="column"
gap="xs"
outline="none"
position="relative"
ref={dropdownRef}
tabIndex={0}
width={trigger ? "fit-content" : "auto"}
onKeyDown={handleKeyDown}
{...rest}
>
{trigger ? renderTrigger(trigger) : renderLabel()}
{open && renderOptions()}
</Flex>
<DropDownContext.Provider value={dropdownState}>
<Flex
aria-labelledby={`${dropdownId}-trigger`}
SeieunYoo marked this conversation as resolved.
Show resolved Hide resolved
cursor="pointer"
direction="column"
gap="xs"
id={dropdownId}
SeieunYoo marked this conversation as resolved.
Show resolved Hide resolved
outline="none"
position="relative"
ref={dropdownRef}
tabIndex={0}
width={trigger ? "fit-content" : "auto"}
onKeyDown={handleKeyDown}
{...rest}
>
{trigger ? renderCustomTrigger(trigger) : renderDefaultTrigger()}
{open && renderOptions()}
</Flex>
</DropDownContext.Provider>
);
};

Expand Down Expand Up @@ -295,6 +299,15 @@ const dropdownContentStyle = cva({
marginBottom: "2px",
},
},
variants: {
type: {
custom: {
width: "100%",
},
default: {},
},
},
defaultVariants: { type: "default" },
});

const placeholderStyle = cva({
Expand Down
Loading
Loading