Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: ✨ new date picker v9 #4421

Open
wants to merge 3 commits into
base: main
Choose a base branch
from

Conversation

flixlix
Copy link
Contributor

@flixlix flixlix commented Jul 26, 2024

Added support for react-day-picker v9.

Currently using this version in conjunction with the calendar component leads to this broken view:

image

The fix leads to this view (should be the same as before):

image

Had to change quite a bit in the calendar component, but tried to keep the visual aspect as close to before as possible.


Year Picker

Also added a new feature that is especially useful when trying to select a birth date. Before, a user would have to navigate through each month at a time. This obviously is not very good UX, the older the user, the worse the experience. ;)

image

Here is a demo of the new functionality, to add it to the date picker component, the user only needs to pass the showYearSwitcher prop to the calendar component.

image

notice this label can now be a button. When clicking this button, a new view will appear.

image

This is the year view, the user can now navigate and switch between years instead of only one month.

The amount of years shown in this view can also be customised, by passing the number of years desired to the yearRange prop. By default this is 12.

In the meantime, this component can be used and tested by anyone using this link:

https://date-picker.luca-felix.com/

Copy link

vercel bot commented Jul 26, 2024

@flixlix is attempting to deploy a commit to the shadcn-pro Team on Vercel.

A member of the Team first needs to authorize it.

@efgomes
Copy link

efgomes commented Jul 28, 2024

Added support for react-day-picker v9.

Currently using this version in conjunction with the calendar component leads to this broken view:

image

The fix leads to this view (should be the same as before):

image

Had to change quite a bit in the calendar component, but tried to keep the visual aspect as close to before as possible.

Year Picker

Also added a new feature that is especially useful when trying to select a birth date. Before, a user would have to navigate through each month at a time. This obviously is not very good UX, the older the user, the worse the experience. ;)

image

Here is a demo of the new functionality, to add it to the date picker component, the user only needs to pass the showYearSwitcher prop to the calendar component.

image

notice this label can now be a button. When clicking this button, a new view will appear.

image

This is the year view, the user can now navigate and switch between years instead of only one month.

The amount of years shown in this view can also be customised, by passing the number of years desired to the yearRange prop. By default this is 12.

In the meantime, this component can be used and tested by anyone using this link:

https://date-picker.luca-felix.com/

image
ajusting style

@shadcn shadcn added component: calendar area: roadmap This looks great. We'll add it to the roadmap, review and merge. labels Aug 3, 2024
@T04435
Copy link

T04435 commented Aug 7, 2024

Style issue

issue
Screenshot 2024-08-07 at 2 44 04 PM
og
Screenshot 2024-08-07 at 2 49 27 PM

@Christophvh
Copy link

React daypicker already has functionality for month/year select. So it would probably be better to use that:
https://daypicker.dev/docs/navigation#hidenavigation

@flixlix
Copy link
Contributor Author

flixlix commented Aug 7, 2024

React daypicker already has functionality for month/year select. So it would probably be better to use that: https://daypicker.dev/docs/navigation#hidenavigation

I did see this, but in my opinion the select dropdown is not as user friendly, especially considering might want to enter their birth dates in some cases

@Christophvh
Copy link

React daypicker already has functionality for month/year select. So it would probably be better to use that: https://daypicker.dev/docs/navigation#hidenavigation

I did see this, but in my opinion the select dropdown is not as user friendly, especially considering might want to enter their birth dates in some cases

While i agree that it isn't as clean as your solution. It still exists in the original component, and since this library is a wrapper it feels a bit weird to not support that. Since this library gives you the code, we can extend it ourselves for better UX like you did, but not sure it should be in the base component. Not hating on your work because it looks great!

@flixlix
Copy link
Contributor Author

flixlix commented Aug 9, 2024

React daypicker already has functionality for month/year select. So it would probably be better to use that: https://daypicker.dev/docs/navigation#hidenavigation

I did see this, but in my opinion the select dropdown is not as user friendly, especially considering might want to enter their birth dates in some cases

While i agree that it isn't as clean as your solution. It still exists in the original component, and since this library is a wrapper it feels a bit weird to not support that. Since this library gives you the code, we can extend it ourselves for better UX like you did, but not sure it should be in the base component. Not hating on your work because it looks great!

Alright I see your point, maybe I'll try contributing to RDP directly and hopefully we can use it directly in shadcn after that :)

@Christophvh
Copy link

@flixlix
The vue version already made an implementation i think:
https://www.shadcn-vue.com/docs/components/calendar.html#advanced-customization

@sayyedarib
Copy link

Image Code
image image

Make sure to fix this as well, when "showOutsideDays" is set to false, the UI breaks a little bit

@LenoM
Copy link

LenoM commented Aug 14, 2024

Any update?

@BadgerBloke
Copy link

image ajusting style

Hi @flixlix
The fix which I'm using after copying the code from PR apps/www/registry/default/ui/calendar.tsx

className={cn('p-2', className)}
month_caption: 'relative mx-10 mb-3 mt-1 flex h-7 items-center justify-center',
month_grid: 'm-1',

image

After above fix
image
image

(_, i) => {
const isBefore =
differenceInCalendarDays(
new Date(displayYears.from + i, 12, 31),

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please beware of the second argument being monthIndex and is zero-based:

Suggested change
new Date(displayYears.from + i, 12, 31),
new Date(displayYears.from + i, 11, 31),

disabled: "text-muted-foreground opacity-50",
range_middle:
"rounded-none aria-selected:bg-accent aria-selected:text-accent-foreground hover:aria-selected:!bg-accent hover:aria-selected:text-accent-foreground",
hidden: "invisible hidden",

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm observing that when showOutsideDays is false for a 2 month calendar, the "hidden" property messes with the flex positioning since the hidden days still need to take up space along the row

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here. The hidden class should be removed

@musjj
Copy link

musjj commented Aug 30, 2024

This is probably my favorite among the previous attempts to extend the date picker. It feels really nice and intuitive to use.

Btw, not sure if react-day-picker does anything special to prevent this issue, but I'm getting this lint error:

Do not define components during render. React will see a new component type on every render and destroy the entire subtree’s DOM nodes and state (https://reactjs.org/docs/reconciliation.html#elements-of-different-types). Instead, move this component definition out of the parent component “Calendar” and pass data as props. If you want to allow component creation in props, set allowAsProps option to true. eslintreact/no-unstable-nested-components

Maybe it'd be better to move the components outside.

@kachkaev kachkaev mentioned this pull request Aug 30, 2024
2 tasks
@MHBahrampour
Copy link

I'll try contributing to RDP directly and hopefully we can use it directly in shadcn after that :)

That would be great. Nice job man.

@MHBahrampour
Copy link

but not sure it should be in the base component

It would be nice to have this functionality, it's modern and other libraries are doing something similar.
If not as the base component, maybe a variant of it like Toast and Sonner?

@njacob1001
Copy link

Great job!

@musthafa1996
Copy link

musthafa1996 commented Nov 5, 2024

Here's an improved version of this calendar. This code fixes the following:

  • the errors shown in the screenshots below
  • a bug in the navigation of year view
"use client"

import * as React from "react"
import { ChevronLeftIcon, ChevronRightIcon } from "@radix-ui/react-icons"
import { differenceInCalendarDays } from "date-fns"
import {
  DayPicker,
  labelNext,
  labelPrevious,
  useDayPicker,
} from "react-day-picker"
import { cn } from "@/lib/utils"
import { Button, buttonVariants } from "./button"
import { omit } from "lodash"

export type CalendarProps = React.ComponentProps<typeof DayPicker> & {
  /**
   * In the year view, the number of years to display at once.
   * @default 12
   */
  yearRange?: number
  /**
   * Wether to let user switch between months and years view.
   * @default false
   */
  showYearSwitcher?: boolean
}

function Calendar({
  className,
  classNames,
  showOutsideDays = true,
  yearRange = 12,
  showYearSwitcher = false,
  numberOfMonths,
  ...props
}: CalendarProps) {
  const [navView, setNavView] = React.useState<"days" | "years">("days")
  const [displayYears, setDisplayYears] = React.useState<{
    from: number
    to: number
  }>(
    React.useMemo(() => {
      const currentYear = new Date().getFullYear()
      return {
        from: currentYear - Math.floor(yearRange / 2 - 1),
        to: currentYear + Math.ceil(yearRange / 2),
      }
    }, [yearRange])
  )
  const { onNextClick, onPrevClick, startMonth, endMonth } = props

  const columnsDisplayed = navView === "years" ? 1 : numberOfMonths

  return (
    <DayPicker
      showOutsideDays={showOutsideDays}
      className={cn("p-3", className)}
      style={{
        width: 248.8 * (columnsDisplayed ?? 1) + "px",
      }}
      classNames={{
        months: "relative flex flex-col gap-y-4 sm:flex-row sm:gap-y-0",
        month_caption: "relative mx-10 flex h-7 items-center justify-center",
        weekdays: "flex flex-row",
        weekday: "w-8 text-[0.8rem] font-normal text-muted-foreground",
        month: "w-full gap-y-4 overflow-x-hidden",
        caption: "relative flex items-center justify-center pt-1",
        caption_label: "truncate text-sm font-medium",
        button_next: cn(
          buttonVariants({
            variant: "outline",
            className:
              "absolute right-0 h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100",
          })
        ),
        button_previous: cn(
          buttonVariants({
            variant: "outline",
            className:
              "absolute left-0 h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100",
          })
        ),
        nav: "flex items-start",
        month_grid: "mt-4",
        week: "mt-2 flex w-full",
        day: "flex size-8 flex-1 items-center justify-center rounded-md p-0 text-sm [&:has(button)]:hover:!bg-accent [&:has(button)]:hover:text-accent-foreground [&:has(button)]:hover:aria-selected:!bg-primary [&:has(button)]:hover:aria-selected:text-primary-foreground",
        day_button: cn(
          buttonVariants({ variant: "ghost" }),
          "h-8 w-8 p-0 font-normal transition-none hover:bg-transparent hover:text-inherit aria-selected:opacity-100"
        ),
        range_start: "day-range-start rounded-s-md",
        range_end: "day-range-end rounded-e-md",
        selected:
          "bg-primary text-primary-foreground hover:!bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground",
        today: "bg-accent text-accent-foreground",
        outside:
          "day-outside text-muted-foreground opacity-50 aria-selected:bg-accent/50 aria-selected:text-muted-foreground aria-selected:opacity-30",
        disabled: "text-muted-foreground opacity-50",
        range_middle:
          "rounded-none aria-selected:bg-accent aria-selected:text-accent-foreground hover:aria-selected:!bg-accent hover:aria-selected:text-accent-foreground",
        hidden: "invisible hidden",
        ...classNames,
      }}
      components={{
        Chevron: ({ orientation }) => {
          const Icon =
            orientation === "left" ? ChevronLeftIcon : ChevronRightIcon
          return <Icon className="h-4 w-4" />
        },
        Nav: ({ className, children, ...props }) => {
          const navProps = omit(props, [
            "onPreviousClick",
            "onNextClick",
            "previousMonth",
            "nextMonth"
          ]);

          const { nextMonth, previousMonth, goToMonth } = useDayPicker()

          const isPreviousDisabled = (() => {
            if (navView === "years") {
              return (
                (startMonth &&
                  differenceInCalendarDays(
                    new Date(displayYears.from - 1, 0, 1),
                    startMonth
                  ) < 0) ||
                (endMonth &&
                  differenceInCalendarDays(
                    new Date(displayYears.from - 1, 0, 1),
                    endMonth
                  ) > 0)
              )
            }
            return !previousMonth
          })()

          const isNextDisabled = (() => {
            if (navView === "years") {
              return (
                (startMonth &&
                  differenceInCalendarDays(
                    new Date(displayYears.to + 1, 0, 1),
                    startMonth
                  ) < 0) ||
                (endMonth &&
                  differenceInCalendarDays(
                    new Date(displayYears.to + 1, 0, 1),
                    endMonth
                  ) > 0)
              )
            }
            return !nextMonth
          })()

          const handlePreviousClick = React.useCallback(() => {
            if (navView === "years") {
              setDisplayYears((prev) => ({
                from: prev.from - (prev.to - prev.from + 1),
                to: prev.to - (prev.to - prev.from + 1),
              }))
              onPrevClick?.(
                new Date(
                  displayYears.from - (displayYears.to - displayYears.from),
                  0,
                  1
                )
              )
              return
            }

            if (!previousMonth) return

            goToMonth(previousMonth)
            onPrevClick?.(previousMonth)
          }, [previousMonth, goToMonth])

          const handleNextClick = React.useCallback(() => {
            if (navView === "years") {
              setDisplayYears((prev) => ({
                from: prev.from + (prev.to - prev.from + 1),
                to: prev.to + (prev.to - prev.from + 1),
              }))
              onNextClick?.(
                new Date(
                  displayYears.from + (displayYears.to - displayYears.from),
                  0,
                  1
                )
              )
              return
            }

            if (!nextMonth) return

            goToMonth(nextMonth)
            onNextClick?.(nextMonth)
          }, [goToMonth, nextMonth])

          return (
            <nav className={cn("flex items-center", className)} {...navProps}>
              <Button
                variant="outline"
                className="absolute left-0 h-7 w-7 bg-transparent p-0 opacity-80 hover:opacity-100"
                type="button"
                tabIndex={isPreviousDisabled ? undefined : -1}
                disabled={isPreviousDisabled}
                aria-label={
                  navView === "years"
                    ? `Go to the previous ${displayYears.to - displayYears.from + 1
                    } years`
                    : labelPrevious(previousMonth)
                }
                onClick={handlePreviousClick}
              >
                <ChevronLeftIcon className="h-4 w-4" />
              </Button>

              <Button
                variant="outline"
                className="absolute right-0 h-7 w-7 bg-transparent p-0 opacity-80 hover:opacity-100"
                type="button"
                tabIndex={isNextDisabled ? undefined : -1}
                disabled={isNextDisabled}
                aria-label={
                  navView === "years"
                    ? `Go to the next ${displayYears.to - displayYears.from + 1
                    } years`
                    : labelNext(nextMonth)
                }
                onClick={handleNextClick}
              >
                <ChevronRightIcon className="h-4 w-4" />
              </Button>
            </nav>
          )
        },
        CaptionLabel: ({ children, ...props }) => {
          if (!showYearSwitcher) return <span {...props}>{children}</span>

          return (
            <Button
              className="h-7 w-full truncate text-sm font-medium"
              variant="ghost"
              size="sm"
              onClick={() =>
                setNavView((prev) => (prev === "days" ? "years" : "days"))
              }
            >
              {navView === "days"
                ? children
                : displayYears.from + " - " + displayYears.to}
            </Button>
          )
        },
        MonthGrid: ({ className, children, ...props }) => {
          const { goToMonth } = useDayPicker()
          if (navView === "years") {
            return (
              <div
                className={cn("grid grid-cols-4 gap-y-2", className)}
                {...props}
              >
                {Array.from(
                  { length: displayYears.to - displayYears.from + 1 },
                  (_, i) => {
                    const isBefore =
                      differenceInCalendarDays(
                        new Date(displayYears.from + i, 12, 31),
                        startMonth!
                      ) < 0

                    const isAfter =
                      differenceInCalendarDays(
                        new Date(displayYears.from + i, 0, 0),
                        endMonth!
                      ) > 0

                    const isDisabled = isBefore || isAfter
                    return (
                      <Button
                        key={i}
                        className={cn(
                          "h-7 w-full text-sm font-normal text-foreground",
                          displayYears.from + i === new Date().getFullYear() &&
                          "bg-accent font-medium text-accent-foreground"
                        )}
                        variant="ghost"
                        onClick={() => {
                          setNavView("days")
                          goToMonth(
                            new Date(
                              displayYears.from + i,
                              new Date().getMonth()
                            )
                          )
                        }}
                        disabled={navView === "years" ? isDisabled : undefined}
                      >
                        {displayYears.from + i}
                      </Button>
                    )
                  }
                )}
              </div>
            )
          }
          return (
            <table className={className} {...props}>
              {children}
            </table>
          )
        },
      }}
      numberOfMonths={
        // we need to override the number of months if we are in years view to 1
        columnsDisplayed
      }
      {...props}
    />
  )
}
Calendar.displayName = "Calendar"

export { Calendar }

Error screenshots:
Screenshot 2024-11-05 at 12 34 31 PM
Screenshot 2024-11-05 at 12 35 03 PM
Screenshot 2024-11-05 at 12 35 19 PM
Screenshot 2024-11-05 at 12 35 34 PM

@benjamin-guibert
Copy link

Hello, do we know when the fix will be released? Thanks!

@capaj
Copy link

capaj commented Nov 11, 2024

@musthafa1996 your component lacks some styles polish-for example when I hover the corners are not rounded:
image

@dzuloaga
Copy link

dzuloaga commented Nov 14, 2024

"use client"
import "react-day-picker/style.css";

import { ChevronLeftIcon, ChevronRightIcon } from "lucide-react"
import {
  DayPicker,
  labelNext,
  labelPrevious,
  useDayPicker,
} from "react-day-picker"
import { cn } from "@/lib/utils"
import { Button, buttonVariants } from "@/components/ui/button"
import { Select, SelectTrigger, SelectContent, SelectItem, SelectValue, SelectGroup } from '@/components/ui/select'

function Calendar({
  className,
  classNames,
  showOutsideDays = true,
  numberOfMonths,
  ...props
}: React.ComponentProps<typeof DayPicker>) {

  return (
    <DayPicker
      showOutsideDays={showOutsideDays}
      className={cn("py-2", className)}
      classNames={{
        months: "relative flex flex-col gap-y-4 sm:flex-row sm:gap-y-0",
        month_caption: "relative mx-10 flex h-7 items-center justify-center",
        weekdays: "flex flex-row",
        weekday: "w-8 text-[0.8rem] font-normal text-muted-foreground",
        month: "w-full gap-y-4 overflow-x-hidden",
        caption: "relative flex items-center justify-center pt-1",
        caption_label: "truncate text-sm font-medium",
        button_next: cn(
          buttonVariants({
            variant: "outline",
            className:
              "absolute right-0 h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100",
          })
        ),
        button_previous: cn(
          buttonVariants({
            variant: "outline",
            className:
              "absolute left-0 h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100",
          })
        ),
        nav: "flex items-start",
        month_grid: "my-2 mx-2",
        week: "mt-2 flex w-full",
        day: "flex h-9 w-9 flex-1 items-center justify-center rounded-md p-0 text-sm [&:has(button)]:hover:!bg-accent [&:has(button)]:hover:text-accent-foreground [&:has(button)]:hover:aria-selected:!bg-primary [&:has(button)]:hover:aria-selected:text-primary-foreground",
        day_button: cn(
          buttonVariants({ variant: "ghost" }),
          "h-9 w-9 p-0 font-normal transition-none hover:bg-transparent hover:text-inherit aria-selected:opacity-100"
        ),
        range_start: "day-range-start rounded-s-md",
        range_end: "day-range-end rounded-e-md",
        selected:
          "bg-primary text-primary-foreground hover:!bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground",
        today: "bg-accent text-accent-foreground",
        outside:
          "day-outside text-muted-foreground opacity-50 aria-selected:bg-accent/50 aria-selected:text-muted-foreground aria-selected:opacity-30",
        disabled: "text-muted-foreground opacity-50",
        range_middle:
          "rounded-none aria-selected:bg-accent aria-selected:text-accent-foreground hover:aria-selected:!bg-accent hover:aria-selected:text-accent-foreground",
        hidden: "invisible hidden",
        chevron: `inline-block fill-muted-foreground`,
        ...classNames,
      }}
      components={{
        Dropdown: ({ children, ...props }) => {
          const { options, className, disabled } = props;
          const { goToMonth, months } = useDayPicker();
          const currentShown = months[0].date;

          const currentSelection =
            className === "rdp-years_dropdown"
              ? currentShown.getFullYear().toString()
              : currentShown.getMonth().toString();

          const updateDayPickerState = (value: string) => {
            const newDate = new Date(currentShown);
            if (className === "rdp-years_dropdown") {
              newDate.setFullYear(parseInt(value));
            } else if (className === "rdp-months_dropdown") {
              newDate.setMonth(parseInt(value));
            }
            goToMonth(newDate);
          };

          return (
            <Select
              value={currentSelection}
              onValueChange={updateDayPickerState}
              disabled={disabled}
            >
              <SelectTrigger className="w-full border-0 ring-0 focus:ring-0 px-2 py-1">
                <SelectValue />
              </SelectTrigger>
              <SelectContent>
                <SelectGroup>
                  {options?.map((option) => (
                    <SelectItem
                      key={option.value}
                      value={option.value.toString()}
                    >
                      {option.label}
                    </SelectItem>
                  ))}
                </SelectGroup>
              </SelectContent>

            </Select>
          );
        },
        YearsDropdown: ({ children, ...props }) => {
          const { components } = useDayPicker()
          // sort years in descending order
          const sortedOptions = props.options?.sort((a, b) => b.value - a.value)
          return <components.Dropdown {...props} options={sortedOptions} />
        },
        PreviousMonthButton: ({ className, children, ...props }) => {
          const previousMonth = useDayPicker().previousMonth
          return (
            <Button
              variant="outline"
              className={cn(
                buttonVariants({ variant: "outline" }),
                "absolute left-0 h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100 ml-2",
                className
              )}
              type="button"
              tabIndex={previousMonth ? -1 : undefined}
              disabled={!previousMonth}
              aria-label={labelPrevious(previousMonth)}
              onClick={() => {
                props.onClick()
              }}
            >
              <ChevronLeftIcon className="h-4 w-4" />
            </Button>
          )
        },
        NextMonthButton: ({ className, children, ...props }) => {
          const nextMonth = useDayPicker().nextMonth
          return (
            <Button
              variant="outline"
              className={cn(
                buttonVariants({ variant: "outline" }),
                "absolute right-0 h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100 mr-2",
                className
              )}
              type="button"
              tabIndex={nextMonth ? -1 : undefined}
              disabled={!nextMonth}
              aria-label={labelNext(nextMonth)}
              onClick={() => {
                props.onClick()
              }}
            >
              <ChevronRightIcon className="h-4 w-4" />
            </Button>
          )
        },
      }}
      {...props}
    />
  )
}
Calendar.displayName = "Calendar"

export { Calendar }

@Blackvz
Copy link

Blackvz commented Nov 18, 2024

"use client"
import "react-day-picker/style.css";

import { ChevronLeftIcon, ChevronRightIcon } from "lucide-react"
import {
  DayPicker,
  labelNext,
  labelPrevious,
  useDayPicker,
} from "react-day-picker"
import { cn } from "@/lib/utils"
import { Button, buttonVariants } from "@/components/ui/button"
import { Select, SelectTrigger, SelectContent, SelectItem, SelectValue, SelectGroup } from '@/components/ui/select'

function Calendar({
  className,
  classNames,
  showOutsideDays = true,
  numberOfMonths,
  ...props
}: React.ComponentProps<typeof DayPicker>) {

  return (
    <DayPicker
      showOutsideDays={showOutsideDays}
      className={cn("py-2", className)}
      classNames={{
        months: "relative flex flex-col gap-y-4 sm:flex-row sm:gap-y-0",
        month_caption: "relative mx-10 flex h-7 items-center justify-center",
        weekdays: "flex flex-row",
        weekday: "w-8 text-[0.8rem] font-normal text-muted-foreground",
        month: "w-full gap-y-4 overflow-x-hidden",
        caption: "relative flex items-center justify-center pt-1",
        caption_label: "truncate text-sm font-medium",
        button_next: cn(
          buttonVariants({
            variant: "outline",
            className:
              "absolute right-0 h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100",
          })
        ),
        button_previous: cn(
          buttonVariants({
            variant: "outline",
            className:
              "absolute left-0 h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100",
          })
        ),
        nav: "flex items-start",
        month_grid: "my-2 mx-2",
        week: "mt-2 flex w-full",
        day: "flex h-9 w-9 flex-1 items-center justify-center rounded-md p-0 text-sm [&:has(button)]:hover:!bg-accent [&:has(button)]:hover:text-accent-foreground [&:has(button)]:hover:aria-selected:!bg-primary [&:has(button)]:hover:aria-selected:text-primary-foreground",
        day_button: cn(
          buttonVariants({ variant: "ghost" }),
          "h-9 w-9 p-0 font-normal transition-none hover:bg-transparent hover:text-inherit aria-selected:opacity-100"
        ),
        range_start: "day-range-start rounded-s-md",
        range_end: "day-range-end rounded-e-md",
        selected:
          "bg-primary text-primary-foreground hover:!bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground",
        today: "bg-accent text-accent-foreground",
        outside:
          "day-outside text-muted-foreground opacity-50 aria-selected:bg-accent/50 aria-selected:text-muted-foreground aria-selected:opacity-30",
        disabled: "text-muted-foreground opacity-50",
        range_middle:
          "rounded-none aria-selected:bg-accent aria-selected:text-accent-foreground hover:aria-selected:!bg-accent hover:aria-selected:text-accent-foreground",
        hidden: "invisible hidden",
        chevron: `inline-block fill-muted-foreground`,
        ...classNames,
      }}
      components={{
        Dropdown: ({ children, ...props }) => {
          const { options, className, disabled } = props;
          const { goToMonth, months } = useDayPicker();
          const currentShown = months[0].date;

          const currentSelection =
            className === "rdp-years_dropdown"
              ? currentShown.getFullYear().toString()
              : currentShown.getMonth().toString();

          const updateDayPickerState = (value: string) => {
            const newDate = new Date(currentShown);
            if (className === "rdp-years_dropdown") {
              newDate.setFullYear(parseInt(value));
            } else if (className === "rdp-months_dropdown") {
              newDate.setMonth(parseInt(value));
            }
            goToMonth(newDate);
          };

          return (
            <Select
              value={currentSelection}
              onValueChange={updateDayPickerState}
              disabled={disabled}
            >
              <SelectTrigger className="w-full border-0 ring-0 focus:ring-0 px-2 py-1">
                <SelectValue />
              </SelectTrigger>
              <SelectContent>
                <SelectGroup>
                  {options?.map((option) => (
                    <SelectItem
                      key={option.value}
                      value={option.value.toString()}
                    >
                      {option.label}
                    </SelectItem>
                  ))}
                </SelectGroup>
              </SelectContent>

            </Select>
          );
        },
        YearsDropdown: ({ children, ...props }) => {
          const { components } = useDayPicker()
          // sort years in descending order
          const sortedOptions = props.options?.sort((a, b) => b.value - a.value)
          return <components.Dropdown {...props} options={sortedOptions} />
        },
        PreviousMonthButton: ({ className, children, ...props }) => {
          const previousMonth = useDayPicker().previousMonth
          return (
            <Button
              variant="outline"
              className={cn(
                buttonVariants({ variant: "outline" }),
                "absolute left-0 h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100 ml-2",
                className
              )}
              type="button"
              tabIndex={previousMonth ? -1 : undefined}
              disabled={!previousMonth}
              aria-label={labelPrevious(previousMonth)}
              onClick={() => {
                props.onClick()
              }}
            >
              <ChevronLeftIcon className="h-4 w-4" />
            </Button>
          )
        },
        NextMonthButton: ({ className, children, ...props }) => {
          const nextMonth = useDayPicker().nextMonth
          return (
            <Button
              variant="outline"
              className={cn(
                buttonVariants({ variant: "outline" }),
                "absolute right-0 h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100 mr-2",
                className
              )}
              type="button"
              tabIndex={nextMonth ? -1 : undefined}
              disabled={!nextMonth}
              aria-label={labelNext(nextMonth)}
              onClick={() => {
                props.onClick()
              }}
            >
              <ChevronRightIcon className="h-4 w-4" />
            </Button>
          )
        },
      }}
      {...props}
    />
  )
}
Calendar.displayName = "Calendar"

export { Calendar }

Thanks, you saved me some work. Finally a version which works with captureLayout="dropdown" <3

@capaj
Copy link

capaj commented Nov 22, 2024

@Blackvz works, but does not display two calendars side by side in range select mode 👎

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area: roadmap This looks great. We'll add it to the roadmap, review and merge. component: calendar
Projects
None yet
Development

Successfully merging this pull request may close these issues.