Skip to content

Instantly share code, notes, and snippets.

@mjbalcueva
Last active July 1, 2024 05:35
Show Gist options
  • Save mjbalcueva/1fbcb1be9ef68a82c14d778b686a04fa to your computer and use it in GitHub Desktop.
Save mjbalcueva/1fbcb1be9ef68a82c14d778b686a04fa to your computer and use it in GitHub Desktop.
shadcn ui calendar custom year and month dropdown
"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>
)
}
@nopitown
Copy link

@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
Copy link

@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

@KhatriFaiz
Copy link

Thank you!

@nadetastic
Copy link

Thank you for sharing this

@Raczan
Copy link

Raczan commented Mar 29, 2024

Thanks a lot

@aynuayex
Copy link

@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>

@aynuayex
Copy link

aynuayex commented May 20, 2024

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>
  );
};

@HamidDev22
Copy link

Thank you man!

@alexwhb
Copy link

alexwhb commented May 27, 2024

This saved me big time!!! Thank you so much!

@aynuayex
Copy link

aynuayex commented May 28, 2024

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
Copy link

i have a problem here is that despite selecting a date (e.g., 11/06/2025), the calendar opens to May 2024 instead of June 2025. The calendar is not properly focusing on the selected date when reopened.
image

@ErhanArda
Copy link

ErhanArda commented May 28, 2024

i have a problem here is that despite selecting a date (e.g., 11/06/2025), the calendar opens to May 2024 instead of June 2025. The calendar is not properly focusing on the selected date when reopened.

image

defaultMonth={value} solved my problem 💯

@aynuayex
Copy link

aynuayex commented May 28, 2024

@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.

@ErhanArda
Copy link

This also worked

image

@mjcarnaje
Copy link

Thanks @mjbalcueva 💯

@aynuayex
Copy link

aynuayex commented May 30, 2024

@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.

@Marco-Antonio-Rodrigues

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}

image
image

@aynuayex
Copy link

@Marco-Antonio-Rodrigues where does onChangeValue come?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment