DateSelection 컴포넌트의 진짜 마지막 기능을 구현하기 위해 3탄으로 왔다.
// ./components/DateSelection.jsx
const DateSelection = ({ format = 'YYYY-MM-DD', autoFormatting = true }) => {
const inputRef = useRef(null);
const calendarRef = useRef(null);
...
const handleClickOut = useCallback(
(e) => {
if (open && inputRef?.current && calendarRef?.current) {
const inputArea = inputRef.current;
const calendarArea = calendarRef.current;
const { target } = e;
const outArea =
!inputArea.contains(target) && !calendarArea.contains(target);
if (outArea) {
setOpen(false);
}
}
},
[open, inputRef, calendarRef]
);
useEffect(() => {
if (open && inputRef?.current && calendarRef?.current) {
document.addEventListener('click', handleClickOut);
}
}, [open, inputRef, calendarRef, handleClickOut]);
return (
<section>
<div ref={inputRef}>
<input ... />
<button type='button' onClick={handleClickButton}> ...
</div>
{open && (
<div className='calendar' ref={calendarRef}>
...
많은 사용자들은 Modal이나 Popover와 같은 컴포넌트가 뜨면 컴포넌트 바깥을 눌러 닫는 행동을 한다.
대략적인 로직은 마우스가 가리킨 위치가 입력창이나 달력이 아닌 곳일 경우에 달력을 닫아주는 것이다.
useRef
를 사용하여 DOM에 접근할 수 있다.
input
과 button
을 감싸는 div
에 inputRef
를, Datetime
을 감싸는 div
에 calendarRef
를 부여한다.
open
상태이면서 inputRef
와 calendarRef
의 요소가 존재한다면 document 전체에 click 이벤트를 넣어준다.
dependency 안에는 open
, inputRef
, calendarRef
를 넣으며, 이 값들 중 하나만 바뀌더라고 코드가 실행된다.
click 이벤트가 발생하면 handleClickout
으로 클릭 위치에 따른 조작을 한다.
useCallback
을 사용한 이유는 useEffect
의 dependency가 변경될 때마다 handleClickout
이 호출되므로 불필요한 호출을 방지하기 위함이다.
inputRef
영역과 calendarRef
영역에 둘 다 클릭한 영역인 target
이 포함되어 있지 않다면 그 곳이 바깥 영역인 outArea
이다.
outArea
가 true인 경우 열려 있는 달력을 닫아준다.
// ./components/DateSelection.jsx
...
const handleKeyDownInput = (e) => {
if (e.code === 'Enter') {
setOpen(!open);
}
};
const handleKeyDownButton = (e) => {
if (e.code === 'Tab' && open) {
setOpen(false);
}
};
...
return (
<section>
<div ref={inputRef}>
<input
...
onKeyDown={handleKeyDownInput}
/>
<button
...
onKeyDown={handleKeyDownButton}
>
<BsFillCalendarHeartFill />
</button>
...
input
에 focus가 된 상태에서 키보드를 입력할 때 해당 이벤트의 코드가 Enter
인 경우에 달력을 열고 닫아준다.
button
에 focus가 된 상태에서 키보드를 입력할 때 해당 이벤트의 코드가 Tab
이고 달력이 열려 있는 경우에 달력을 닫아준다.
개인적으로 달력에도 키보드 이벤트를 넣고 싶었지만 react-datetime
에서 제공하지 않는 것 같다.
// ./components/DateSelection.jsx
import 'react-datetime/css/react-datetime.css';
import '../App.css';
...
return (
<section className='dateselection'>
<div className='input-wrapper' ref={inputRef}>
<input
className='input'
...
/>
<button
className='input-button'
...
>
<BsFillCalendarHeartFill />
</button>
</div>
{open && (
<div className='calendar' ...>
...
// ./DateSelection.css
.dateselection {
position: relative;
width: 272px;
}
.input-wrapper {
position: relative;
width: 100%;
}
.input {
width: calc(100% - 34px);
padding: 8px 16px;
border-radius: 4px;
border: 1px solid #e0e2e7;
outline: none;
}
.input-button {
position: absolute;
top: 50%;
right: 8px;
transform: translateY(-50%);
padding: 0;
background: transparent;
border: none;
cursor: pointer;
}
.calendar {
width: 100%;
position: absolute;
left: 50%;
transform: translateX(-50%);
}
.calendar td {
border-radius: 100px;
}
.rdtPicker {
border: 1px solid #e0e2e7 !important;
border-radius: 4px !important;
}
td.rdtToday {
border: 1px solid #428bca !important;
}
td.rdtToday::before {
display: none !important;
}
보기에 깔끔한 정도로만 CSS를 작성해보았다.
react-datetime
에서 제공하는 기본적인 스타일을 넣어주고 추가로 커스텀하였다.
!important
를 꼭 넣어야만 기본 스타일을 덮어 씌운다.
사내에서 사용될 UI를 구축하다보니 여러 경우의 수를 생각하고 더욱 완벽한 컴포넌트를 구현하기 위해 힘을 쓰게 되었다.
개인 프로젝트인 인생네컷에도 DateSelection 컴포넌트를 도입해야겠다.
(전체 코드는 깃허브 링크로 올리도록 하겠습니다!)