ui icon indicating copy to clipboard operation
ui copied to clipboard

feat: :sparkles: new date picker v9

Open flixlix opened this issue 1 year ago • 14 comments

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:

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

flixlix avatar Jul 26 '24 13:07 flixlix

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

A member of the Team first needs to authorize it.

vercel[bot] avatar Jul 26 '24 13:07 vercel[bot]

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

efgomes avatar Jul 28 '24 14:07 efgomes

Style issue

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

T04435 avatar Aug 07 '24 05:08 T04435

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

Christophvh avatar Aug 07 '24 11:08 Christophvh

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

flixlix avatar Aug 07 '24 12:08 flixlix

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!

Christophvh avatar Aug 09 '24 08:08 Christophvh

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 :)

flixlix avatar Aug 09 '24 08:08 flixlix

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

Christophvh avatar Aug 09 '24 08:08 Christophvh

Image Code
image image

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

sayyedarib avatar Aug 13 '24 13:08 sayyedarib

Any update?

LenoM avatar Aug 14 '24 14:08 LenoM

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

MKSinghDev avatar Aug 16 '24 11:08 MKSinghDev

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.

musjj avatar Aug 30 '24 15:08 musjj

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 avatar Sep 11 '24 12:09 MHBahrampour

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?

MHBahrampour avatar Sep 11 '24 12:09 MHBahrampour

Great job!

njacob1001 avatar Oct 18 '24 17:10 njacob1001

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

musthafa1996 avatar Nov 05 '24 07:11 musthafa1996

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

benjamin-guibert avatar Nov 05 '24 15:11 benjamin-guibert

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

capaj avatar Nov 11 '24 08:11 capaj

"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 }

dzuloaga avatar Nov 14 '24 23:11 dzuloaga

"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

Blackvz avatar Nov 18 '24 22:11 Blackvz

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

capaj avatar Nov 22 '24 07:11 capaj

Style issue

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

fixed this in the latest commits

flixlix avatar Dec 30 '24 11:12 flixlix

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

fixed this in https://github.com/flixlix/shadcn-date-picker/commit/57f5684d7a5fc65f67b5f7f6bb3817f407b20fce#diff-111083d11e1f991cf2764d274afd3378246acb82aaff5dfe1d61f55d0ebdde47

flixlix avatar Dec 30 '24 11:12 flixlix

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

fixed this in https://github.com/flixlix/shadcn-date-picker/commit/57f5684d7a5fc65f67b5f7f6bb3817f407b20fce#diff-111083d11e1f991cf2764d274afd3378246acb82aaff5dfe1d61f55d0ebdde47

flixlix avatar Dec 30 '24 11:12 flixlix

Will this be released someday?

kamami avatar Jan 26 '25 21:01 kamami

Best proposal, +1

atleugim avatar Jan 30 '25 00:01 atleugim

The version here fixes the console warnings https://github.com/shadcn-ui/ui/pull/4421#issuecomment-2456403155

as well as the version here https://github.com/shadcn-ui/ui/pull/4421#issuecomment-2477597372

The current version by @flixlix doesn't fix those errors, or am I missing something?

michaelklopf avatar Feb 24 '25 16:02 michaelklopf

Is this or #4371 better upgraded to v9? I want to bump the cal version, disappointed that something has not been merged on this yet.

Woofer21 avatar Apr 29 '25 22:04 Woofer21

I find the confusion regarding a good calendar component rather unfortunate. Some people are creating pull requests, that don't get merged. On X you can read that a new calendar component is developed by shadcn himself, but you never heard anything again from that. Here on Github are a lot of discussions about how to upgrade to v9 as well. The lack of communication is really frustrating.

kamami avatar Apr 30 '25 07:04 kamami

@shadcn any general updates from the core team here, at minimum to help set/manage expectations?

robbienohra avatar Apr 30 '25 16:04 robbienohra