Dropdown (2)
Dropdown 을 만들어보고있다.
이전에 포스팅 했던 Dropdown 의 컨셉을 그대로 가져와서 사용해보려고 하고 있는데,
Trigger 는 외부로 부터 받아와 button 의 children 으로 사용하고자 했다.
const DropdownTrigger = (props: { trigger: ReactNode }) => {
const [, dispatch] = useDropdownContext();
const { trigger } = props;
const handleTrigger = () => {
dispatch({ type: 'TOGGLE_OPEN' });
};
return (
<button type="button" onClick={handleTrigger}>
{trigger}
</button>
);
};
// UserInfoCircle
const UserInfoCircle: React.FC = () => {
const { data } = useFetchMeInterval();
return (
<Dropdown>
<Dropdown.Trigger
trigger={
<img
css={css`
width: 2.4rem;
height: 2.4rem;
border-radius: 1.2rem;
`}
src={data?.results?.avatarUrl}
alt={data?.results?.nickname}
/>
}
/>
<Dropdown.List>
<Dropdown.Item>안녕하세요</Dropdown.Item>
<Dropdown.Item>무야호</Dropdown.Item>
</Dropdown.List>
</Dropdown>
);
};
여기서 Dropdown List 의 position 을 지정해주지 않으면 당연히 List 가 생기면서 Element 가 밀릴 것이다.
그래서 DropdownList 가 기본적으로 position absolute 를 갖도록 하였다.
const DropdownList = (props) => {
const [{ isOpen }] = useDropdownContext();
const { children } = props;
return isOpen ? (
<ul
css={css`
position: absolute;
`}
{...props}
>
{children}
</ul>
) : null;
};
이제 List element 에 top, left 등의 값을 지정해줘야 하는데,
Trigger 의 자식으로 태그를 넣어서 사용하면 relative, absolute 관계를 이용해 쉽게 위치를 조정할 수 있겠지만 List 가 Trigger 의 자식태그라는 관계는 절대 아니라고 생각했다.
기존에 Context Provider 의 역할로만 사용했던 Dropdown 컴포넌트에 layout 을 잡기위한 부모태그를 하나 생성하여 사용하였다.
const Dropdown = ({ children }) => {
const dropdownReducer = useReducer(reducer, initState);
return (
<DropdownContext.Provider value={dropdownReducer}>
<div css={{ position: 'relative' }}>{children}</div>
</DropdownContext.Provider>
);
};
이제 top 과 left 값을 계산해야 하는데, top 은 trigger 로 사용중인 element 의 height 만큼 띄우면 될 것이고, left 는 element 의 왼쪽을 기준으로 띄우던지, 오른쪽을 기준으로 띄우던지 선택할 수 있어야겠다고 생각했다. (추후 width를 기준으로 center 도 만들 수 있지 않을까?)
그래서 trigger 의 height 값이 필요하다고 생각이 들어, useRef 를 통해 triggerRef 를 설정했다.
const DropdownTrigger = (props: { trigger: ReactNode }) => {
const triggerRef = useRef<HTMLButtonElement>(null);
const [, dispatch] = useDropdownContext();
const { trigger } = props;
const handleTrigger = () => {
dispatch({ type: 'TOGGLE_OPEN' });
};
useEffect(() => {
if (triggerRef.current) {
dispatch({
type: 'UPDATE_TRIGGER_POSITION',
height: triggerRef.current.offsetHeight,
});
}
}, [dispatch]);
return (
<button type="button" onClick={handleTrigger} ref={triggerRef}>
{trigger}
</button>
);
};
triggerRef 는 렌더링 된 이후 설정이 되니까 useEffect 에서 trigger 의 height 정보를 dispatch 하도록 접근하였다.
그런데 생각해보니 Trigger 는 렌더링이 될 것이고, handleTrigger 핸들러는 trigger 가 생성된 이후 실행될 것이므로
TOGGLE_OPEN action 에서 height 를 지정해줘도 되겠다는 생각이 들었다.
그러니까, 초기 로딩에서 앱 렌더링 -> (useEffect 에서 trigger height dispatch -> 리렌더링) 에서 리렌더링(괄호)을 하나 줄이는 것이지..
const DropdownTrigger = (props: { trigger: ReactNode }) => {
const triggerRef = useRef<HTMLButtonElement>(null);
const [, dispatch] = useDropdownContext();
const { trigger } = props;
const handleTrigger = () => {
if (!triggerRef.current) return;
dispatch({ type: 'TOGGLE_OPEN', height: triggerRef.current.offsetHeight });
};
return (
<button type="button" onClick={handleTrigger} ref={triggerRef}>
{trigger}
</button>
);
};
일단은 잘 된다. 그런데 테스트해보는 과정에서 layout shift 라는 것을 발견하는데...
Dropdown click away
dropdown 의 클릭 이벤트 중, 드롭다운 이외의 영역을 클릭 시 isOpen state 가 변경되도록 하는 로직을 만들었다.
CSS 의 가상요소를 활용했는데, Open State 를 변경하기위한 별도의 태그를 만들기 보다 해당 방법이 의도하는 바를 알기 쉽다고 생각하여 선택했다.
const Dropdown = ({ children }) => {
const dropdownReducer = useReducer(reducer, initState);
const [state, dispatch] = dropdownReducer;
return (
<DropdownContext.Provider value={dropdownReducer}>
<div
css={css`
position: relative;
&::before {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
content: '';
z-index: 90;
display: ${state.isOpen ? 'block' : 'none'};
}
`}
onClick={() => {
if (state.isOpen) {
dispatch({ type: 'TOGGLE_OPEN' });
}
}}
>
{children}
</div>
</DropdownContext.Provider>
);
};
trigger 의 위치를 기반으로 position 을 잡기위해 만들었던 div 태그에 ::before 가상요소를 만들어 Dropdown 의 상태를 Toggle 하도록 하였다.
그리고 Click Away 라는 상황을 Dropdown 이외에서도 다양하게 쓰여질 수 있다고 생각하여 해당 로직을 분리하였다.
+) positioning 을 위해 다르게 시도해본 방법
mui 는 anchorEl 이라는 element 의 위치를 getBoundingClientRect 를 통해 얻어 position 을 주는 것 처럼 보였다. 링크
Popover 라는 컴포넌트를 선언하여 사용하고, anchorEl 이라는 state 를 만들어 trigger 로 만들어 사용하는 것으로 보여졌다.
나처럼 Dropdown 이라는 Container 같은 component 를 만들어 사용하는 법도 있을 것이고, 저런 방법도 있겠구나 싶었다.
저 방법으로 시도해 보았을 때에는 relative 와 absolute 관계를 맺기가 힘들어 직접 top, left 를 정해 지정해주는 식으로 사용했었는데
viewport 의 크기가 달라지면 대응하기 위한 로직을 추가로 작성해야했다.
그래서 일단은 현재 방식대로 쓰는걸로,