React에는 아주 많은 달력 라이브러리가 있지만 가장 기본적이면서도 심플한 react-datepicker 라이브러리를 활용해봤다 !
사용 방법이 간단하고 제공하는 기능이 꽤나 많다는 점이 좋았다.
라이브러리 설치 및 사용
Install
$ npm i react-datepicker
$ npm i --save-dev @types/react-datepicker
Import
import DatePicker from "react-datepicker";
import "react-datepicker/dist/react-datepicker.css";
커스텀 참고
React Datepicker crafted by HackerOne
reactdatepicker.com
Date Picker
Default
<DatePicker
selected={ }
onChange={ }
/>
selected는 필수는 아니지만 선택한 값을 표시하기 위해 사용했다.
onChange는 DatePicker의 Props 중 유일한 필수 값이다.
index.d.ts
import { Middleware, Placement, UseFloatingOptions } from "@floating-ui/react";
import { Locale } from "date-fns";
import * as React from "react";
type PopperProps = Partial<Omit<UseFloatingOptions, "open" | "whileElementsMounted" | "placement" | "middleware">>;
export interface CalendarContainerProps {
className?: string | undefined;
children?: React.ReactNode | React.ReactNode[] | undefined;
}
export function registerLocale(localeName: string, localeData: Locale): void;
export function setDefaultLocale(localeName: string): void;
export function getDefaultLocale(): string;
export function CalendarContainer(props: CalendarContainerProps): React.ReactElement;
interface HighlightDates {
[className: string]: Date[];
}
interface Holiday {
date: string;
holidayName: string;
}
export interface ReactDatePickerCustomHeaderProps {
monthDate: Date;
date: Date;
changeYear(year: number): void;
changeMonth(month: number): void;
customHeaderCount: number;
decreaseMonth(): void;
increaseMonth(): void;
prevMonthButtonDisabled: boolean;
nextMonthButtonDisabled: boolean;
decreaseYear(): void;
increaseYear(): void;
prevYearButtonDisabled: boolean;
nextYearButtonDisabled: boolean;
}
export interface ReactDatePickerProps<
WithRange extends boolean | undefined = undefined,
WithMultiple extends boolean | undefined = undefined,
> {
adjustDateOnChange?: boolean | undefined;
allowSameDay?: boolean | undefined;
ariaDescribedBy?: string | undefined;
ariaInvalid?: string | undefined;
ariaLabelClose?: string | undefined;
ariaLabelledBy?: string | undefined;
ariaRequired?: string | undefined;
autoComplete?: string | undefined;
autoFocus?: boolean | undefined;
calendarClassName?: string | undefined;
calendarContainer?(props: CalendarContainerProps): React.ReactNode;
calendarIconClassname?: string | undefined;
calendarStartDay?: number | undefined;
children?: React.ReactNode | undefined;
chooseDayAriaLabelPrefix?: string | undefined;
className?: string | undefined;
clearButtonClassName?: string | undefined;
clearButtonTitle?: string | undefined;
closeOnScroll?: boolean | ((e: Event) => boolean) | undefined;
customInput?: React.ReactNode | undefined;
customInputRef?: string | undefined;
customTimeInput?: React.ReactNode | undefined;
dateFormat?: string | string[] | undefined;
dateFormatCalendar?: string | undefined;
dayClassName?(date: Date): string | null;
weekDayClassName?(date: Date): string | null;
monthClassName?(date: Date): string | null;
timeClassName?(date: Date): string | null;
disabledDayAriaLabelPrefix?: string | undefined;
disabled?: boolean | undefined;
disabledKeyboardNavigation?: boolean | undefined;
dropdownMode?: "scroll" | "select" | undefined;
endDate?: Date | null | undefined;
excludeDates?: Date[] | Array<{ date: Date; message: string }> | undefined;
excludeDateIntervals?: Array<{ start: Date; end: Date }> | undefined;
excludeTimes?: Date[] | undefined;
filterDate?(date: Date): boolean;
filterTime?(date: Date): boolean;
fixedHeight?: boolean | undefined;
forceShowMonthNavigation?: boolean | undefined;
formatWeekDay?(day: string): React.ReactNode;
formatWeekNumber?(date: Date): string | number;
highlightDates?: Array<HighlightDates | Date> | undefined;
holidays?: Holiday[] | undefined;
icon?: string | React.ReactElement;
id?: string | undefined;
includeDates?: Date[] | undefined;
includeDateIntervals?: Array<{ start: Date; end: Date }> | undefined;
includeTimes?: Date[] | undefined;
injectTimes?: Date[] | undefined;
inline?: boolean | undefined;
focusSelectedMonth?: boolean | undefined;
isClearable?: boolean | undefined;
locale?: string | Locale | undefined;
maxDate?: Date | null | undefined;
maxTime?: Date | undefined;
minDate?: Date | null | undefined;
minTime?: Date | undefined;
monthsShown?: number | undefined;
name?: string | undefined;
nextMonthAriaLabel?: string | undefined;
nextMonthButtonLabel?: string | React.ReactNode | undefined;
nextYearAriaLabel?: string | undefined;
nextYearButtonLabel?: string | React.ReactNode | undefined;
onBlur?(event: React.FocusEvent<HTMLInputElement>): void;
onCalendarClose?(): void;
onCalendarOpen?(): void;
onChange(
date: WithRange extends false | undefined
? (WithMultiple extends false | undefined ? Date | null : Date[] | null)
: [Date | null, Date | null],
event: React.SyntheticEvent<any> | undefined,
): void;
onChangeRaw?(event: React.FocusEvent<HTMLInputElement>): void;
onClickOutside?(event: React.MouseEvent<HTMLDivElement>): void;
onDayMouseEnter?: ((date: Date) => void) | undefined;
onFocus?(event: React.FocusEvent<HTMLInputElement>): void;
onInputClick?(): void;
onInputError?(err: { code: number; msg: string }): void;
onKeyDown?(event: React.KeyboardEvent<HTMLDivElement>): void;
onMonthChange?(date: Date): void;
onMonthMouseLeave?: (() => void) | undefined;
onSelect?(date: Date, event: React.SyntheticEvent<any> | undefined): void;
onWeekSelect?(
firstDayOfWeek: Date,
weekNumber: string | number,
event: React.SyntheticEvent<any> | undefined,
): void;
onYearChange?(date: Date): void;
open?: boolean | undefined;
openToDate?: Date | undefined;
peekNextMonth?: boolean | undefined;
placeholderText?: string | undefined;
popperClassName?: string | undefined;
popperContainer?(props: { children: React.ReactNode[] }): React.ReactNode;
popperModifiers?: readonly Middleware[] | undefined;
popperPlacement?: Placement | undefined;
popperProps?: PopperProps | undefined;
preventOpenOnFocus?: boolean | undefined;
previousMonthAriaLabel?: string | undefined;
previousMonthButtonLabel?: string | React.ReactNode | undefined;
previousYearAriaLabel?: string | undefined;
previousYearButtonLabel?: string | React.ReactNode | undefined;
readOnly?: boolean | undefined;
renderCustomHeader?(params: ReactDatePickerCustomHeaderProps): React.ReactNode;
renderDayContents?(dayOfMonth: number, date?: Date): React.ReactNode;
renderQuarterContent?(quarter: number, shortQuarterText: string): React.ReactNode;
renderMonthContent?(monthIndex: number, shortMonthText: string, fullMonthText: string): React.ReactNode;
renderYearContent?(year: number): React.ReactNode;
required?: boolean | undefined;
scrollableMonthYearDropdown?: boolean | undefined;
scrollableYearDropdown?: boolean | undefined;
selected?: Date | null | undefined;
selectsEnd?: boolean | undefined;
selectsStart?: boolean | undefined;
selectsRange?: WithRange;
selectsMultiple?: WithMultiple;
selectedDates?: Date[];
shouldCloseOnSelect?: boolean | undefined;
showDisabledMonthNavigation?: boolean | undefined;
showFullMonthYearPicker?: boolean | undefined;
showMonthDropdown?: boolean | undefined;
showMonthYearDropdown?: boolean | undefined;
showMonthYearPicker?: boolean | undefined;
showPopperArrow?: boolean | undefined;
showPreviousMonths?: boolean | undefined;
showQuarterYearPicker?: boolean | undefined;
showTimeInput?: boolean | undefined;
showTimeSelect?: boolean | undefined;
showTimeSelectOnly?: boolean | undefined;
showTwoColumnMonthYearPicker?: boolean | undefined;
showFourColumnMonthYearPicker?: boolean | undefined;
showWeekNumbers?: boolean | undefined;
showWeekPicker?: boolean | undefined;
showYearDropdown?: boolean | undefined;
showYearPicker?: boolean | undefined;
showIcon?: boolean | undefined;
startDate?: Date | null | undefined;
startOpen?: boolean | undefined;
strictParsing?: boolean | undefined;
tabIndex?: number | undefined;
timeCaption?: string | undefined;
timeFormat?: string | undefined;
timeInputLabel?: string | undefined;
timeIntervals?: number | undefined;
title?: string | undefined;
todayButton?: React.ReactNode | undefined;
toggleCalendarOnIconClick?: boolean | undefined;
useShortMonthInDropdown?: boolean | undefined;
useWeekdaysShort?: boolean | undefined;
weekAriaLabelPrefix?: string | undefined;
monthAriaLabelPrefix?: string | undefined;
value?: string | undefined;
weekLabel?: string | undefined;
withPortal?: boolean | undefined;
portalId?: string | undefined;
portalHost?: ShadowRoot | undefined;
wrapperClassName?: string | undefined;
yearDropdownItemNumber?: number | undefined;
excludeScrollbar?: boolean | undefined;
enableTabLoop?: boolean | undefined;
yearItemNumber?: number | undefined;
}
declare class ReactDatePicker<
WithRange extends boolean | undefined = undefined,
WithMultiple extends boolean | undefined = undefined,
> extends React.Component<
ReactDatePickerProps<WithRange, WithMultiple>
> {
readonly setBlur: () => void;
readonly setFocus: () => void;
readonly setOpen: (open: boolean, skipSetBlur?: boolean) => void;
readonly isCalendarOpen: () => boolean;
}
export default ReactDatePicker;
minDate, maxDate와 startDate, endDate의 차이
🍀 min, maxDate
- 사용자가 선택할 수 있는 날짜의 최소(대)값 지정
- 특정 날짜 이전(후)의 날짜를 선택하지 못하도록 제한
🍀 start, endDate
- 선택된 날짜 범위의 시작(끝) 날짜 지정, 주로 date range picker에서 사용
- 날짜 범위를 선택할 때, 시작(끝) 날짜를 설정하여 끝(시작) 날짜를 선택하는 범위 제한
즉, 단일 날짜 선택 → min, max / 날짜 범위 선택 → start, end
언어 지정
import { ko } from "date-fns/locale/ko";
registerLocale("ko", ko);
export default function DateSelector() {
return (
<DatePicker
...
locale={ko}
/>
);
}
Icon
react-icons와 함께 사용
import { MdOutlineCalendarToday } from "react-icons/md";
export default function DateSelector() {
return (
<DatePicker
...
showIcon
icon={<MdOutlineCalendarToday />}
toggleCalendarOnIconClick
/>
);
}
showIcon을 반드시 같이 써줘야 한다.
toggleCalendarOnIconClick → Icon을 눌렀을 때도 달력 팝업이 나오게 하려고 사용
Placeholder
export default function DateSelector() {
return (
<DatePicker
...
placeholderText="YYYY-MM-DD"
/>
);
}
Header Custom
😥 헤더를 굳이 커스텀한 이유
→ prev, next 버튼이 아이콘(svg)이 아니기 때문에 크기 조절이 상당히 어려웠다.
renderCustomHeader 속성 사용
DateCalendarHeader.tsx
// DateCalendarHeader.tsx
import {
CalendarButton,
CalendarButtonWrapper,
PickerCalendarHeader,
} from "./DateCalendarHeader.style";
import { MdNavigateBefore, MdOutlineNavigateNext } from "react-icons/md";
interface Props {
date: Date;
decreaseMonth(): void;
increaseMonth(): void;
prevMonthButtonDisabled: boolean;
nextMonthButtonDisabled: boolean;
}
export default function DateCalendarHeader({
date,
decreaseMonth,
increaseMonth,
prevMonthButtonDisabled,
nextMonthButtonDisabled,
}: Props) {
return (
<PickerCalendarHeader>
<div>
<span>{date.getFullYear()}년 </span>
<span>{date.getMonth() + 1}월</span>
</div>
<CalendarButtonWrapper>
<CalendarButton
onClick={decreaseMonth}
disabled={prevMonthButtonDisabled}
>
<MdNavigateBefore />
</CalendarButton>
<CalendarButton
onClick={increaseMonth}
disabled={nextMonthButtonDisabled}
>
<MdOutlineNavigateNext />
</CalendarButton>
</CalendarButtonWrapper>
</PickerCalendarHeader>
);
}
ReactDatePickerCustomHeaderProps에 정의된 변수들은 전부 필수 값으로 돼있어서 컴포넌트에서 전달 받을 props에 대한 인터페이스를 따로 만들어줬다.
getMonth는 특이하게도 0부터 반환한다. 그렇기 때문에 반환 값에 +1을 해줘야 한다.
DateSelector.tsx
export default function DateSelector() {
return (
<DatePicker
...
renderCustomHeader={({
date,
decreaseMonth,
increaseMonth,
prevMonthButtonDisabled,
nextMonthButtonDisabled,
}) => (
<DateCalendarHeader
date={date}
decreaseMonth={decreaseMonth}
increaseMonth={increaseMonth}
prevMonthButtonDisabled={prevMonthButtonDisabled}
nextMonthButtonDisabled={nextMonthButtonDisabled}
/>
)}
/>
);
}
Date Range Picker
Range
기본 버전
export default function DateRangeSelector() {
return (
<DatePicker
...
selectsRange
startDate={ }
endDate={ }
/>
);
}
BUT,
1️⃣ 가운데 -를 ~로 바꾸고 싶음
2️⃣ 두 날짜 사이 간격을 띄우고 싶음
DatePicker 두개 사용
위의 두 가지 조건을 충족하기 위해 DatePicker를 두 개로 나누고 스타일을 조절했다.
export default function DateRangeSelector() {
return (
<>
<DatePicker
selected={startDate}
onChange={ }
startDate={ }
endDate={ }
selectsStart
/>
~
<DatePicker
selected={endDate}
onChange={ }
startDate={ }
endDate={ }
selectsEnd
/>
</>
);
}
Problem
오늘 날짜와 동일한 일자가 모든 달마다 하이라이트
오늘 날짜 : 5월 30일
BUT 6월 30일에도 동일하게 하이라이팅이 들어가 있다. (다른 달도 동일)
< 참고 >
Today's day is highlighted for every motnth · Issue #2376 · Hacker0x01/react-datepicker
Describe the bug Todays date is highlighted on every month To Reproduce Steps to reproduce the behavior: Go to the demo page Open any calendar, and change months notice that today's date is highlig...
github.com
disabledKeyboardNavigation 속성 추가
달력 Range 색상 이상
‘월’이 범위에 속하지 않아도 ‘일’만으로도 날짜에 스타일이 적용되고 있다.
❌ 방법 1
highlightDates 속성에 선택한 범위의 날짜 배열을 넣어주기
import { eachDayOfInterval } from "date-fns";
export default function DateRangeSelector() {
return (
<>
...
<DatePicker
selected={endDate}
...
highlightDates={eachDayOfInterval({ start: startDate, end: endDate })}
/>
</>
);
}
→ 달라지는 것이 없다…
⭕ 방법 2
CSS 설정을 조절하기
7월 4일 (선택되지 않았는데 색상 O)
<div
class="react-datepicker__day react-datepicker__day--004 react-datepicker__day--in-selecting-range"
tabindex="-1"
aria-label="Choose 2024년 7월 4일 목요일"
role="option"
title=""
aria-disabled="false"
aria-selected="false"
>4</div>
7월 6일 (일반)
<div
class="react-datepicker__day react-datepicker__day--006 react-datepicker__day--weekend"
tabindex="-1"
aria-label="Choose 2024년 7월 6일 토요일"
role="option"
title=""
aria-disabled="false"
aria-selected="false"
>6</div>
6월 3일 (선택 범위 내)
<div
class="react-datepicker__day react-datepicker__day--003 react-datepicker__day--in-range react-datepicker__day--in-selecting-range"
tabindex="-1"
aria-label="Choose 2024년 6월 3일 월요일"
role="option"
title=""
aria-disabled="false"
aria-selected="true"
>3</div>
선택 범위 내의 날짜(6/3)와 선택되지 않았으나 색상이 있는 날짜(7/4)의 클래스 중 react-datepicker__day—in-selecting-range 가 동일하다.
(대체 후자는 왜 저 클래스를 주는건데…)
react-datepicker__day—in-selecting-range 클래스가 하이라이팅을 해주는 컬러 css를 가지고 있으므로 적용된 것이다.
두 날짜의 다른 차이점인 aria-selected 속성을 통해 범위에 속하지 않은 날짜에 대한 하이라이팅을 제거해줬다.
.react-datepicker__day--in-selecting-range[aria-selected="false"] {
color: #000;
background-color: white;
}
'React > 라이브러리' 카테고리의 다른 글
@react-icons/all-files로 모듈 크기 개선 (0) | 2024.05.21 |
---|---|
react-icons 라이브러리 활용 (Figma, 프로젝트) (1) | 2024.05.21 |