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