feat: date range picker (#14713)
* feat: add DateRangePicker.tsx and Calendar.tsx to match DS * chore:cleanup * tests: remove as these are duplicate tests from the library themselves * fix: mobile sizes --------- Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
This commit is contained in:
co-authored by
Udit Takkar
parent
ceb555460e
commit
386006fad0
@@ -474,8 +474,10 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
|
||||
name="periodDates"
|
||||
render={({ field: { onChange } }) => (
|
||||
<DateRangePicker
|
||||
startDate={formMethods.getValues("periodDates").startDate}
|
||||
endDate={formMethods.getValues("periodDates").endDate}
|
||||
dates={{
|
||||
startDate: formMethods.getValues("periodDates").startDate,
|
||||
endDate: formMethods.getValues("periodDates").endDate,
|
||||
}}
|
||||
disabled={periodTypeLocked.disabled}
|
||||
onDatesChange={({ startDate, endDate }) => {
|
||||
onChange({
|
||||
|
||||
@@ -99,10 +99,13 @@ const TravelScheduleModal = ({
|
||||
<>
|
||||
<Label className="mt-2">{t("time_range")}</Label>
|
||||
<DateRangePicker
|
||||
startDate={startDate}
|
||||
endDate={endDate ?? startDate}
|
||||
dates={{
|
||||
startDate,
|
||||
endDate: endDate ?? startDate,
|
||||
}}
|
||||
onDatesChange={({ startDate: newStartDate, endDate: newEndDate }) => {
|
||||
setStartDate(newStartDate);
|
||||
// If newStartDate does become undefined - we resort back to to-todays date
|
||||
setStartDate(newStartDate ?? new Date());
|
||||
setEndDate(newEndDate);
|
||||
setErrorMessage("");
|
||||
}}
|
||||
|
||||
@@ -74,7 +74,7 @@ const CreateOutOfOfficeEntryModal = ({
|
||||
label: member.name || "",
|
||||
})) || [];
|
||||
|
||||
const { handleSubmit, setValue, getValues, control, register } = useForm<BookingRedirectForm>({
|
||||
const { handleSubmit, setValue, control, register } = useForm<BookingRedirectForm>({
|
||||
defaultValues: {
|
||||
dateRange: {
|
||||
startDate: dateRange.startDate,
|
||||
@@ -132,15 +132,11 @@ const CreateOutOfOfficeEntryModal = ({
|
||||
name="dateRange"
|
||||
control={control}
|
||||
defaultValue={dateRange}
|
||||
render={() => (
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<DateRangePicker
|
||||
startDate={getValues("dateRange").startDate}
|
||||
endDate={getValues("dateRange").endDate}
|
||||
onDatesChange={({ startDate, endDate }) => {
|
||||
setValue("dateRange", {
|
||||
startDate,
|
||||
endDate,
|
||||
});
|
||||
dates={{ startDate: value.startDate, endDate: value.endDate }}
|
||||
onDatesChange={(values) => {
|
||||
onChange(values);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import { DayPicker } from "react-day-picker";
|
||||
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import { classNames as cn } from "@calcom/lib";
|
||||
|
||||
import { Icon } from "../../../index";
|
||||
import { buttonClasses } from "../../button/Button";
|
||||
|
||||
export type CalendarProps = React.ComponentProps<typeof DayPicker>;
|
||||
|
||||
function Calendar({ className, classNames, showOutsideDays = true, ...props }: CalendarProps) {
|
||||
return (
|
||||
<DayPicker
|
||||
fromDate={new Date()}
|
||||
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 pt-1 relative items-center justify-between",
|
||||
caption_label: "text-sm font-medium",
|
||||
nav: "flex items-center",
|
||||
head: "",
|
||||
head_row: "flex w-full items-center justify-between",
|
||||
head_cell: "w-8 md:w-11 h-8 text-sm font-medium text-default",
|
||||
nav_button: cn(buttonClasses({ color: "minimal", variant: "icon" })),
|
||||
table: "w-full border-collapse space-y-1",
|
||||
row: "flex w-full mt-2 gap-0.5",
|
||||
cell: "h-8 w-8 md:h-11 md:w-11 text-center text-sm p-0 relative [&:has([aria-selected].day-range-end)]:rounded-r-md [&:has([aria-selected].day-outside)]:bg-accent/50 [&: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(
|
||||
buttonClasses({ color: "minimal" }),
|
||||
"w-8 h-8 md:h-11 md:w-11 p-0 text-sm font-medium aria-selected:opacity-100 inline-flex items-center justify-center"
|
||||
),
|
||||
day_range_end: "day-range-end",
|
||||
day_selected: "bg-inverted text-inverted",
|
||||
day_today: "",
|
||||
day_outside: "",
|
||||
day_disabled: "text-muted opacity-50",
|
||||
day_range_middle: "aria-selected:bg-emphasis aria-selected:text-emphasis",
|
||||
day_hidden: "invisible",
|
||||
...classNames,
|
||||
}}
|
||||
components={{
|
||||
CaptionLabel: (capLabelProps) => (
|
||||
<div className="px-2">
|
||||
<span className="text-emphasis leadning-none font-semibold">
|
||||
{dayjs(capLabelProps.displayMonth).format("MMMM")}{" "}
|
||||
</span>
|
||||
<span className="text-subtle font-medium leading-none">
|
||||
{dayjs(capLabelProps.displayMonth).format("YYYY")}
|
||||
</span>
|
||||
</div>
|
||||
),
|
||||
IconLeft: () => <Icon name="chevron-left" className="h-4 w-4 stroke-2" />,
|
||||
IconRight: () => <Icon name="chevron-right" className="h-4 w-4 stroke-2" />,
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
Calendar.displayName = "Calendar";
|
||||
|
||||
export { Calendar };
|
||||
@@ -1,35 +1,70 @@
|
||||
// @see: https://github.com/wojtekmaj/react-daterange-picker/issues/91
|
||||
import "@wojtekmaj/react-daterange-picker/dist/DateRangePicker.css";
|
||||
import PrimitiveDateRangePicker from "@wojtekmaj/react-daterange-picker/dist/entry.nostyle";
|
||||
"use client";
|
||||
|
||||
import { Icon } from "../../..";
|
||||
import "./styles.css";
|
||||
import { format } from "date-fns";
|
||||
import * as React from "react";
|
||||
import type { DateRange } from "react-day-picker";
|
||||
|
||||
type Props = {
|
||||
disabled?: boolean | undefined;
|
||||
startDate: Date;
|
||||
endDate: Date;
|
||||
onDatesChange?: ((arg: { startDate: Date; endDate: Date }) => void) | undefined;
|
||||
import { classNames as cn } from "@calcom/lib";
|
||||
|
||||
import { Button } from "../../button";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "../../popover";
|
||||
import { Calendar } from "./Calendar";
|
||||
|
||||
type DatePickerWithRangeProps = {
|
||||
dates: { startDate: Date; endDate?: Date };
|
||||
onDatesChange: ({ startDate, endDate }: { startDate?: Date; endDate?: Date }) => void;
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
const DateRangePicker = ({ disabled, startDate, endDate, onDatesChange }: Props) => {
|
||||
export function DatePickerWithRange({
|
||||
className,
|
||||
dates,
|
||||
onDatesChange,
|
||||
disabled,
|
||||
}: React.HTMLAttributes<HTMLDivElement> & DatePickerWithRangeProps) {
|
||||
// Even though this is uncontrolled we need to do a bit of logic to improve the UX when selecting dates
|
||||
function _onDatesChange(onChangeValues: DateRange | undefined) {
|
||||
if (onChangeValues?.from && !onChangeValues?.to) {
|
||||
onDatesChange({ startDate: onChangeValues.from, endDate: onChangeValues.from });
|
||||
} else {
|
||||
onDatesChange({ startDate: onChangeValues?.from, endDate: onChangeValues?.to });
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<PrimitiveDateRangePicker
|
||||
disabled={disabled || false}
|
||||
className="border-default rounded-sm text-sm"
|
||||
clearIcon={null}
|
||||
calendarIcon={<Icon name="calendar" className="text-subtle h-4 w-4" />}
|
||||
rangeDivider={<Icon name="arrow-right" className="text-muted h-4 w-4 ltr:mr-2 rtl:ml-2" />}
|
||||
value={[startDate, endDate]}
|
||||
onChange={([startDate, endDate]: [Date, Date]) => {
|
||||
if (typeof onDatesChange === "function") onDatesChange({ startDate, endDate });
|
||||
}}
|
||||
nextLabel={<Icon name="chevron-right" className="text-subtle h-4 w-4" />}
|
||||
prevLabel={<Icon name="chevron-left" className="text-subtle h-4 w-4" />}
|
||||
/>
|
||||
</>
|
||||
<div className={cn("grid gap-2", className)}>
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
id="date"
|
||||
color="secondary"
|
||||
EndIcon="calendar"
|
||||
className={cn("justify-between text-left font-normal", !dates && "text-subtle")}>
|
||||
{dates?.startDate ? (
|
||||
dates?.endDate ? (
|
||||
<>
|
||||
{format(dates.startDate, "LLL dd, y")} - {format(dates.endDate, "LLL dd, y")}
|
||||
</>
|
||||
) : (
|
||||
format(dates.startDate, "LLL dd, y")
|
||||
)
|
||||
) : (
|
||||
<span>Pick a date</span>
|
||||
)}
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-auto p-0" align="start">
|
||||
<Calendar
|
||||
initialFocus
|
||||
mode="range"
|
||||
defaultMonth={dates?.startDate}
|
||||
selected={{ from: dates?.startDate, to: dates?.endDate }}
|
||||
onSelect={(values) => _onDatesChange(values)}
|
||||
numberOfMonths={1}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DateRangePicker;
|
||||
}
|
||||
|
||||
@@ -1,77 +0,0 @@
|
||||
/* eslint-disable playwright/missing-playwright-await */
|
||||
import { render, fireEvent } from "@testing-library/react";
|
||||
import { format } from "date-fns";
|
||||
import { vi } from "vitest";
|
||||
|
||||
import DateRangePicker from "./DateRangePicker";
|
||||
|
||||
describe("Tests for DateRangePicker component", () => {
|
||||
const startDate = new Date("2023-07-15");
|
||||
const endDate = new Date("2023-07-20");
|
||||
|
||||
test("Should display the selected start and end dates", () => {
|
||||
const { getByDisplayValue } = render(<DateRangePicker startDate={startDate} endDate={endDate} />);
|
||||
|
||||
const expectedStartDate = format(new Date(startDate), "yyyy-MM-dd");
|
||||
const expectedEndDate = format(new Date(endDate), "yyyy-MM-dd");
|
||||
|
||||
expect(getByDisplayValue(expectedStartDate)).toBeInTheDocument();
|
||||
expect(getByDisplayValue(expectedEndDate)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("Should disable the DateRangePicker when disabled prop is true", () => {
|
||||
const { getByDisplayValue } = render(
|
||||
<DateRangePicker startDate={startDate} endDate={endDate} disabled />
|
||||
);
|
||||
|
||||
const startDateInput = getByDisplayValue(format(startDate, "yyyy-MM-dd")) as HTMLInputElement;
|
||||
const endDateInput = getByDisplayValue(format(endDate, "yyyy-MM-dd")) as HTMLInputElement;
|
||||
expect(startDateInput).toBeDisabled();
|
||||
expect(endDateInput).toBeDisabled();
|
||||
});
|
||||
|
||||
test("Should call the onDatesChange callback when the selected dates change", () => {
|
||||
const startDate = new Date("2023-07-14");
|
||||
const endDate = new Date("2023-07-19");
|
||||
|
||||
const mockOnDatesChange = vi.fn();
|
||||
|
||||
const { getByDisplayValue } = render(
|
||||
<DateRangePicker startDate={startDate} endDate={endDate} onDatesChange={mockOnDatesChange} />
|
||||
);
|
||||
|
||||
const startDateInput = getByDisplayValue(format(startDate, "yyyy-MM-dd")) as HTMLInputElement;
|
||||
const endDateInput = getByDisplayValue(format(endDate, "yyyy-MM-dd")) as HTMLInputElement;
|
||||
|
||||
const newStartDate = new Date(startDate);
|
||||
newStartDate.setDate(newStartDate.getDate() + 1);
|
||||
|
||||
const newEndDate = new Date(endDate);
|
||||
newEndDate.setDate(newEndDate.getDate() + 1);
|
||||
|
||||
const eventStartDate = { target: { value: format(newStartDate, "yyyy-MM-dd") } };
|
||||
const eventEndDate = { target: { value: format(newEndDate, "yyyy-MM-dd") } };
|
||||
fireEvent.change(startDateInput, eventStartDate);
|
||||
fireEvent.change(endDateInput, eventEndDate);
|
||||
|
||||
const formattedStartDate = mockOnDatesChange.mock.calls[0][0].startDate;
|
||||
const formattedEndDate = mockOnDatesChange.mock.calls[1][0].endDate;
|
||||
|
||||
expect(mockOnDatesChange).toHaveBeenCalled();
|
||||
|
||||
expect(mockOnDatesChange).toHaveBeenCalledWith({
|
||||
startDate: expect.any(Date),
|
||||
endDate: expect.any(Date),
|
||||
});
|
||||
|
||||
expect(formattedStartDate.getDate()).toEqual(newStartDate.getDate());
|
||||
expect(formattedStartDate.getMonth()).toEqual(newStartDate.getMonth());
|
||||
expect(formattedStartDate.getFullYear()).toEqual(newStartDate.getFullYear());
|
||||
|
||||
expect(formattedEndDate.getDate()).toEqual(newEndDate.getDate());
|
||||
expect(formattedEndDate.getMonth()).toEqual(newEndDate.getMonth());
|
||||
expect(formattedEndDate.getFullYear()).toEqual(newEndDate.getFullYear());
|
||||
});
|
||||
});
|
||||
|
||||
HTMLCanvasElement.prototype.getContext = vi.fn() as never;
|
||||
@@ -1,5 +1,5 @@
|
||||
import dynamic from "next/dynamic";
|
||||
|
||||
export const DateRangePickerLazy = dynamic(
|
||||
() => import("./DateRangePicker")
|
||||
) as unknown as typeof import("./DateRangePicker").default;
|
||||
export const DateRangePickerLazy = dynamic(() =>
|
||||
import("./DateRangePicker").then((mod) => mod.DatePickerWithRange)
|
||||
);
|
||||
|
||||
@@ -40,12 +40,14 @@
|
||||
"class-variance-authority": "^0.4.0",
|
||||
"cmdk": "^0.2.0",
|
||||
"cmk": "^0.1.1",
|
||||
"date-fns": "^3.6.0",
|
||||
"downshift": "^6.1.9",
|
||||
"lucide-react": "^0.364.0",
|
||||
"next": "^13.5.4",
|
||||
"next-seo": "^6.0.0",
|
||||
"react": "^18.2.0",
|
||||
"react-colorful": "^5.6.0",
|
||||
"react-day-picker": "^8.10.1",
|
||||
"react-feather": "^2.0.10",
|
||||
"react-hook-form": "^7.43.3",
|
||||
"react-select": "^5.7.0",
|
||||
|
||||
Reference in New Issue
Block a user