-
-
Save mjbalcueva/1fbcb1be9ef68a82c14d778b686a04fa to your computer and use it in GitHub Desktop.
"use client" | |
import * as React from "react" | |
import { buttonVariants } from "@/components/ui/button" | |
import { ScrollArea } from "@/components/ui/scroll-area" | |
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" | |
import { cn } from "@/lib/utils" | |
import { ChevronLeft, ChevronRight } from "lucide-react" | |
import { DayPicker, DropdownProps } from "react-day-picker" | |
export type CalendarProps = React.ComponentProps<typeof DayPicker> | |
function Calendar({ className, classNames, showOutsideDays = true, ...props }: CalendarProps) { | |
return ( | |
<DayPicker | |
showOutsideDays={showOutsideDays} | |
className={cn("p-3", className)} | |
classNames={{ | |
months: "flex flex-col sm:flex-row space-y-4 sm:space-x-4 sm:space-y-0", | |
month: "space-y-4", | |
caption: "flex justify-center pt-1 relative items-center", | |
caption_label: "text-sm font-medium", | |
caption_dropdowns: "flex justify-center gap-1", | |
nav: "space-x-1 flex items-center", | |
nav_button: cn( | |
buttonVariants({ variant: "outline" }), | |
"h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100" | |
), | |
nav_button_previous: "absolute left-1", | |
nav_button_next: "absolute right-1", | |
table: "w-full border-collapse space-y-1", | |
head_row: "flex", | |
head_cell: "text-muted-foreground rounded-md w-9 font-normal text-[0.8rem]", | |
row: "flex w-full mt-2", | |
cell: "text-center text-sm p-0 relative [&:has([aria-selected])]:bg-accent first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md focus-within:relative focus-within:z-20", | |
day: cn(buttonVariants({ variant: "ghost" }), "h-9 w-9 p-0 font-normal aria-selected:opacity-100"), | |
day_selected: | |
"bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground", | |
day_today: "bg-accent text-accent-foreground", | |
day_outside: "text-muted-foreground opacity-50", | |
day_disabled: "text-muted-foreground opacity-50", | |
day_range_middle: "aria-selected:bg-accent aria-selected:text-accent-foreground", | |
day_hidden: "invisible", | |
...classNames, | |
}} | |
components={{ | |
Dropdown: ({ value, onChange, children, ...props }: DropdownProps) => { | |
const options = React.Children.toArray(children) as React.ReactElement<React.HTMLProps<HTMLOptionElement>>[] | |
const selected = options.find((child) => child.props.value === value) | |
const handleChange = (value: string) => { | |
const changeEvent = { | |
target: { value }, | |
} as React.ChangeEvent<HTMLSelectElement> | |
onChange?.(changeEvent) | |
} | |
return ( | |
<Select | |
value={value?.toString()} | |
onValueChange={(value) => { | |
handleChange(value) | |
}} | |
> | |
<SelectTrigger className="pr-1.5 focus:ring-0"> | |
<SelectValue>{selected?.props?.children}</SelectValue> | |
</SelectTrigger> | |
<SelectContent position="popper"> | |
<ScrollArea className="h-80"> | |
{options.map((option, id: number) => ( | |
<SelectItem key={`${option.props.value}-${id}`} value={option.props.value?.toString() ?? ""}> | |
{option.props.children} | |
</SelectItem> | |
))} | |
</ScrollArea> | |
</SelectContent> | |
</Select> | |
) | |
}, | |
IconLeft: ({ ...props }) => <ChevronLeft className="h-4 w-4" />, | |
IconRight: ({ ...props }) => <ChevronRight className="h-4 w-4" />, | |
}} | |
{...props} | |
/> | |
) | |
} | |
Calendar.displayName = "Calendar" | |
export { Calendar } |
/* add this snippet in your globals.css file */ | |
.rdp-vhidden { | |
@apply hidden; | |
} |
"use client" | |
import * as React from "react" | |
import { Button } from "@/components/ui/button" | |
import { Calendar } from "@/components/ui/calendar" | |
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover" | |
import { cn } from "@/lib/utils" | |
import { format } from "date-fns" | |
import { CalendarIcon } from "lucide-react" | |
export function SampleDatePicker() { | |
const [date, setDate] = React.useState<Date>() | |
return ( | |
<Popover> | |
<PopoverTrigger asChild> | |
<Button | |
variant={"outline"} | |
className={cn("w-[240px] justify-start text-left font-normal", !date && "text-muted-foreground")} | |
> | |
<CalendarIcon className="mr-2 h-4 w-4" /> | |
{date ? format(date, "PPP") : <span>Pick a date</span>} | |
</Button> | |
</PopoverTrigger> | |
<PopoverContent align="start" className=" w-auto p-0"> | |
<Calendar | |
mode="single" | |
captionLayout="dropdown-buttons" | |
selected={date} | |
onSelect={setDate} | |
fromYear={1960} | |
toYear={2030} | |
/> | |
</PopoverContent> | |
</Popover> | |
) | |
} |
thanks for sharing this
@mjbalcueva thank you for this. Has anyone used this in a modal?? Having some weird behaviors on the dropdowns
Using this component, after selecting current date selectedValue = undefined. To solve this issue add to calendar prop required
according https://react-day-picker.js.org/basics/selecting-days
A small change, instead of adding to the CSS file
.rdp-vhidden {
@apply hidden;
}
Apply the Tailwind selector directly in the component declaration:
<DayPicker
showOutsideDays={showOutsideDays}
className={cn("p-3", className)}
classNames={{
months: "flex flex-col sm:flex-row space-y-4 sm:space-x-4 sm:space-y-0",
month: "space-y-4",
//...
vhidden: "vhidden hidden", // Add this line
//..
}}
@jramiresbrito Try adding this classname caption_dropdowns: "flex justify-center gap-x-2"
for fixing the dropdowns in a row. If you put the code in a live example, I can try to help with the popover issue.
@jramiresbrito Try adding this classname
caption_dropdowns: "flex justify-center gap-x-2"
for fixing the dropdowns in a row. If you put the code in a live example, I can try to help with the popover issue.
Thanks for replying. I went for an easier solution by just adding classes instead of modifying the component broadly
Thank you!
Thank you for sharing this
Thanks a lot
@mjbalcueva 👏 great work. but I'm always wondering why shadcn-ui does not merges this kind of changes quickly?
@liuhe2020
Hey man, do you know how I can close the modal when the user selects a date, instead of having to click outside again?
onSelect={field.onChange}
I suppose I need to change this but I am not sure how.
here is how you make the datepicker close after selection of date
const [isCalendarOpen, setIsCalendarOpen] = useState(false);
return (
<Popover open={isCalendarOpen} onOpenChange={setIsCalendarOpen}>
<PopoverTrigger asChild>
<Button
variant={"outline"}
className={cn(
"overflow-hidden dark:text-white",
!date && "text-muted-foreground",
className
)}
>
<CalendarIcon className="mr-2 h-4 w-4" />
{date ? (
<span>
{window.innerWidth > 1024
? format(date, "PPP")
: format(date, "d MMM")}
</span>
) : (
<span className="hidden sm:block">Pick a date</span>
)}
<ChevronsUpDown className="sm:ml-2 h-4 w-4 shrink-0 opacity-50 " />
</Button>
</PopoverTrigger>
<PopoverContent className="w-auto p-0">
<Calendar
mode="single"
selected={date}
onSelect={(e) => {
setDate(e);
setIsCalendarOpen(false);
}}
initialFocus
captionLayout="dropdown-buttons"
fromYear={1990}
toYear={2025}
/>
</PopoverContent>
</Popover>
one logic i want my datepicker to have is after i have selected a date at some point and it closes.when i again clicked on it and it opens i want the date to point to the pervious date that i have selected already instead of the default initial state date?how do i implement this any ideas would be apperciated.
here is my implmentation:
export default function MainContent() {
const [date, setDate] = useState<Date | undefined>(new Date());
return (
<div className="w-full mt-24 absolute pr-1 sm:pr-4 ">
<DatePicker
className="w-[100px] sm:w-1/4 lg:w-48"
date={date}
setDate={setDate}
/>
</div>
);
}
type DatePickerProps = {
className: string;
date: Date | undefined;
setDate: React.Dispatch<React.SetStateAction<Date | undefined>>;
};
const DatePicker = ({ className, date, setDate }: DatePickerProps) => {
const [isCalendarOpen, setIsCalendarOpen] = useState(false);
return (
<Popover open={isCalendarOpen} onOpenChange={setIsCalendarOpen}>
<PopoverTrigger asChild>
<Button
variant={"outline"}
className={cn(
"overflow-hidden dark:text-white",
!date && "text-muted-foreground",
className
)}
>
<CalendarIcon className="mr-2 h-4 w-4" />
{date ? (
<span>
{window.innerWidth > 1024
? format(date, "PPP")
: format(date, "d MMM")}
</span>
) : (
<span className="hidden sm:block">Pick a date</span>
)}
<ChevronsUpDown className="sm:ml-2 h-4 w-4 shrink-0 opacity-50 " />
</Button>
</PopoverTrigger>
<PopoverContent className="w-auto p-0">
<Calendar
mode="single"
selected={date}
onSelect={(e) => {
setDate(e);
setIsCalendarOpen(false);
}}
initialFocus
captionLayout="dropdown-buttons"
fromYear={1990}
toYear={2025}
/>
</PopoverContent>
</Popover>
);
};
Thank you man!
This saved me big time!!! Thank you so much!
one logic i want my datepicker to have is after i have selected a date at some point and it closes.when i again clicked on it and it opens i want the date to point to the pervious date that i have selected already instead of the default initial state date?how do i implement this any ideas would be apperciated.
here is how i have come up to implement it by following from the docs and it works as expected.
export default function MainContent() {
const [date, setDate] = useState<Date | undefined>(new Date());
return (
<div className="w-full mt-24 absolute pr-1 sm:pr-4 ">
<DatePicker
className="w-[100px] sm:w-1/4 lg:w-48"
date={date}
setDate={setDate}
/>
</div>
);
}
type DatePickerProps = {
className: string;
date: Date | undefined;
setDate: React.Dispatch<React.SetStateAction<Date | undefined>>;
};
const DatePicker = ({ className, date, setDate }: DatePickerProps) => {
const [ month, setMonth ] = useState<Date | undefined>(new Date());
const [isCalendarOpen, setIsCalendarOpen] = useState(false);
return (
<Popover open={isCalendarOpen} onOpenChange={setIsCalendarOpen}>
<PopoverTrigger asChild>
<Button
variant={"outline"}
className={cn(
"overflow-hidden dark:text-white",
!date && "text-muted-foreground",
className
)}
>
<CalendarIcon className="mr-2 h-4 w-4" />
{date ? (
<span>
{window.innerWidth > 1024
? format(updateDatePart(month as Date,date), "PPP")
: format(updateDatePart(month as Date,date), "d MMM")}
</span>
) : (
<span className="hidden sm:block">Pick a date</span>
)}
<ChevronsUpDown className="sm:ml-2 h-4 w-4 shrink-0 opacity-50 " />
</Button>
</PopoverTrigger>
<PopoverContent className="w-auto p-0">
<Calendar
mode="single"
month={month}
onMonthChange={(month) => setMonth(month)}
selected={date}
onSelect={(e) => {
setDate(e);
setIsCalendarOpen(false);
}}
initialFocus
captionLayout="dropdown-buttons"
fromYear={1990}
toYear={2025}
/>
</PopoverContent>
</Popover>
);
};
lib/utils.tsx
import { format, parse, setDate } from "date-fns"
export const updateDatePart = (month: Date, newDay: Date): Date => {
return setDate(month, newDay.getDate());
};
@ErhanArda no setting defaultMonth={value}
does not allow you to programatically control the month it just sets a default month which means every time it opens it is set to that default month read here, but if you want to programatically control the month, you have to set it like i have put above.you can read here more.
if it does not work for you please check your code so that it matches mine, or create a sandbox and i will check it.it works fine for me and hell yea it should for you to.
Thanks @mjbalcueva 💯
@ErhanArda yes it works but when you select the year and month from the drop down it does not reflect immediately on the button here part
<Button>other code here
{value? formattedDate : <span>Pick a date </span>}
<Button>
until you select a date and the date picker closes, but mine does.
I solved it by doing this @aynuayex:
const handleMonthChange = (newDate: Date) => { if (newDate){ setDate(newDate); onChangeValue && onChangeValue(newDate) } };
and passing this attribute to Calendar:
onMonthChange={handleMonthChange}
@Marco-Antonio-Rodrigues where does onChangeValue
come?
@ErhanArda yes it works but when you select the year and month from the drop down it does not reflect immediately on the button here part
<Button>other code here {value? formattedDate : <span>Pick a date </span>} <Button>
until you select a date and the date picker closes, but mine does.
Instead of doing this you can simply close the popover in onDayClick, it means popover will only close when a date is selected and won't close on any other interaction
@Maliksidk19 I don't feel you, I mean that is the default behavior the date picker closes only on day selection and we are here taking about the input field not reflecting the year and month selection on selection before the picker is closed after selecting day.
https://shadcn-datetime-picker.vercel.app/
take a look @aynuayex
https://shadcn-datetime-picker.vercel.app/
take a look @aynuayex
What part of the CSS or components did you edit for this for the dropdowns?
https://shadcn-datetime-picker.vercel.app/
take a look @aynuayexWhat part of the CSS or components did you edit for this for the dropdowns?
I have used the select component for dropdown
Life Saver
thanks for sharing this!!!
@mjbalcueva thank you , this is so good !