[common/components] feat: InputDatePicker 컴포넌트 추가
This commit is contained in:
@@ -57,6 +57,7 @@
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"date-fns": "^3.6.0",
|
||||
"dayjs": "^1.11.13",
|
||||
"embla-carousel-react": "^8.6.0",
|
||||
"js-cookie": "^3.0.5",
|
||||
"lucide-react": "^0.474.0",
|
||||
|
||||
@@ -7,6 +7,7 @@ export * from "./carousel";
|
||||
export * from "./checkbox";
|
||||
export * from "./data-table";
|
||||
export * from "./date-picker";
|
||||
export * from "./input-date-picker";
|
||||
export * from "./date-range-picker";
|
||||
export * from "./dialog";
|
||||
export * from "./dropdown-menu";
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
"use client";
|
||||
|
||||
import dayjs from "dayjs";
|
||||
import { useEffect, useState } from "react";
|
||||
import { CalendarIcon } from "lucide-react";
|
||||
|
||||
import { Button } from "../button";
|
||||
import { Calendar } from "../calendar";
|
||||
import { Input } from "../input";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "../popover";
|
||||
|
||||
import "./input-date-picker.css";
|
||||
|
||||
export interface InputDatePickerProps {
|
||||
value?: Date;
|
||||
defaultValue?: Date;
|
||||
onChange?: (date: Date | undefined) => void;
|
||||
placeholder?: string;
|
||||
disabled?: boolean;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function InputDatePicker({
|
||||
value: controlledValue,
|
||||
defaultValue,
|
||||
onChange,
|
||||
placeholder,
|
||||
disabled,
|
||||
className,
|
||||
}: InputDatePickerProps) {
|
||||
const isControlled = controlledValue !== undefined;
|
||||
|
||||
const [open, setOpen] = useState(false);
|
||||
const [internalDate, setInternalDate] = useState<Date | undefined>(
|
||||
defaultValue,
|
||||
);
|
||||
const [month, setMonth] = useState<Date | undefined>(
|
||||
isControlled ? controlledValue : defaultValue,
|
||||
);
|
||||
|
||||
// 실제로 사용할 date 값 (제어/비제어 모두 지원)
|
||||
const date = isControlled ? controlledValue : internalDate;
|
||||
|
||||
// 표시할 문자열 값
|
||||
const [displayValue, setDisplayValue] = useState(date);
|
||||
|
||||
// 제어 컴포넌트일 경우 외부 값 변경 감지
|
||||
useEffect(() => {
|
||||
if (isControlled) {
|
||||
setDisplayValue(controlledValue);
|
||||
setMonth(controlledValue);
|
||||
}
|
||||
}, [isControlled, controlledValue]);
|
||||
|
||||
// 내부 상태 변경 핸들러
|
||||
const handleDateChange = (newDate: Date | undefined) => {
|
||||
if (!isControlled) {
|
||||
setInternalDate(newDate);
|
||||
}
|
||||
|
||||
if (onChange) {
|
||||
onChange(newDate);
|
||||
}
|
||||
|
||||
setDisplayValue(newDate);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="relative flex gap-2">
|
||||
<Input
|
||||
id="date"
|
||||
type="date"
|
||||
value={displayValue ? dayjs(displayValue).format("YYYY-MM-DD") : ""}
|
||||
disabled={disabled}
|
||||
className={`input-date-picker bg-background pr-10 ${className || ""}`}
|
||||
onChange={(e) => {
|
||||
const newDate = new Date(e.target.value);
|
||||
setDisplayValue(newDate);
|
||||
|
||||
if (isValidDate(newDate)) {
|
||||
handleDateChange(newDate);
|
||||
setMonth(newDate);
|
||||
}
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "ArrowDown") {
|
||||
e.preventDefault();
|
||||
setOpen(true);
|
||||
}
|
||||
}}
|
||||
placeholder={placeholder}
|
||||
/>
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
id="date-picker"
|
||||
variant="ghost"
|
||||
color="white"
|
||||
disabled={disabled}
|
||||
className="absolute right-2 top-1/2 size-7 -translate-y-1/2 rounded-[0.4rem] border-none p-0"
|
||||
>
|
||||
<CalendarIcon className="!size-full" />
|
||||
<span className="sr-only">Select date</span>
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
className="w-auto overflow-hidden p-0"
|
||||
align="end"
|
||||
alignOffset={-8}
|
||||
sideOffset={10}
|
||||
>
|
||||
<Calendar
|
||||
mode="single"
|
||||
selected={date}
|
||||
captionLayout="dropdown"
|
||||
month={month}
|
||||
onMonthChange={setMonth}
|
||||
onSelect={(newDate) => {
|
||||
handleDateChange(newDate);
|
||||
setOpen(false);
|
||||
}}
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function isValidDate(date: Date | undefined) {
|
||||
if (!date) {
|
||||
return false;
|
||||
}
|
||||
return !isNaN(date.getTime());
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
@tailwind components;
|
||||
|
||||
@layer components {
|
||||
.input-date-picker::-webkit-calendar-picker-indicator {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
Generated
+9
-6
@@ -758,6 +758,9 @@ importers:
|
||||
date-fns:
|
||||
specifier: ^3.6.0
|
||||
version: 3.6.0
|
||||
dayjs:
|
||||
specifier: ^1.11.13
|
||||
version: 1.11.13
|
||||
embla-carousel-react:
|
||||
specifier: ^8.6.0
|
||||
version: 8.6.0(react@18.3.1)
|
||||
@@ -2405,7 +2408,7 @@ packages:
|
||||
magic-string: 0.27.0
|
||||
react-docgen-typescript: 2.2.2(typescript@5.6.3)
|
||||
typescript: 5.6.3
|
||||
vite: 6.2.0(@types/node@22.13.9)
|
||||
vite: 6.2.0(@types/node@22.13.5)
|
||||
dev: true
|
||||
|
||||
/@jridgewell/gen-mapping@0.3.8:
|
||||
@@ -2497,7 +2500,7 @@ packages:
|
||||
dnd-multi-backend: 8.1.2(dnd-core@16.0.1)
|
||||
framer-motion: 11.18.2(react-dom@18.3.1)(react@18.3.1)
|
||||
react: 18.3.1
|
||||
react-dnd: 16.0.1(@types/node@22.13.9)(@types/react@18.3.18)(react@18.3.1)
|
||||
react-dnd: 16.0.1(@types/node@22.13.5)(@types/react@18.3.18)(react@18.3.1)
|
||||
react-dnd-html5-backend: 16.0.1
|
||||
react-dnd-touch-backend: 16.0.1
|
||||
react-dom: 18.3.1(react@18.3.1)
|
||||
@@ -3998,7 +4001,7 @@ packages:
|
||||
browser-assert: 1.2.1
|
||||
storybook: 8.6.0(prettier@3.5.2)
|
||||
ts-dedent: 2.2.0
|
||||
vite: 6.2.0(@types/node@22.13.9)
|
||||
vite: 6.2.0(@types/node@22.13.5)
|
||||
dev: true
|
||||
|
||||
/@storybook/components@8.6.0(storybook@8.6.0):
|
||||
@@ -4130,7 +4133,7 @@ packages:
|
||||
resolve: 1.22.10
|
||||
storybook: 8.6.0(prettier@3.5.2)
|
||||
tsconfig-paths: 4.2.0
|
||||
vite: 6.2.0(@types/node@22.13.9)
|
||||
vite: 6.2.0(@types/node@22.13.5)
|
||||
transitivePeerDependencies:
|
||||
- rollup
|
||||
- supports-color
|
||||
@@ -4605,7 +4608,7 @@ packages:
|
||||
'@babel/plugin-transform-react-jsx-source': 7.25.9(@babel/core@7.26.9)
|
||||
'@types/babel__core': 7.20.5
|
||||
react-refresh: 0.14.2
|
||||
vite: 6.2.0(@types/node@22.13.9)
|
||||
vite: 6.2.0(@types/node@22.13.5)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: true
|
||||
@@ -9903,7 +9906,7 @@ packages:
|
||||
debug: 4.4.0
|
||||
globrex: 0.1.2
|
||||
tsconfck: 3.1.5(typescript@5.6.3)
|
||||
vite: 6.2.0(@types/node@22.13.9)
|
||||
vite: 6.2.0(@types/node@22.13.5)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
- typescript
|
||||
|
||||
Reference in New Issue
Block a user