* fix(coss-ui): convert /settings/my-account/general to coss-ui * update guide * fix: add menuPosition fixed to TimezoneSelect in TravelScheduleModal Co-Authored-By: eunjae@cal.com <hey@eunjae.dev> * fix: use menuPortalTarget to render TimezoneSelect dropdown above dialog footer Co-Authored-By: eunjae@cal.com <hey@eunjae.dev> * fix: use Object.assign for type-safe styles in TimezoneSelect Co-Authored-By: eunjae@cal.com <hey@eunjae.dev> * revert comment * fix: guard document access for SSR in TravelScheduleModal Addresses Cubic AI review feedback: menuPortalTarget={document.body} will throw during server rendering where document is undefined. Added typeof check to guard against SSR. Co-Authored-By: unknown <> * change dialog backdrop from blur to dim * fix: add menuPlacement auto to open dropdown upward on mobile Co-Authored-By: eunjae@cal.com <hey@eunjae.dev> * fix: use menuPlacement top on mobile for TimezoneSelect dropdown Co-Authored-By: eunjae@cal.com <hey@eunjae.dev> * fix: make timezone button stack vertically on mobile to prevent overflow Co-Authored-By: eunjae@cal.com <hey@eunjae.dev> * fix: make timezone select and button 50/50 width on larger viewports Co-Authored-By: eunjae@cal.com <hey@eunjae.dev> * fix date range picker issue on mobile * prevent travel schedule dialog dismissal with date picker --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
143 lines
4.3 KiB
TypeScript
143 lines
4.3 KiB
TypeScript
"use client";
|
|
|
|
import classNames from "@calcom/ui/classNames";
|
|
import * as Popover from "@radix-ui/react-popover";
|
|
import { format, isBefore, isSameDay } from "date-fns";
|
|
import { type HTMLAttributes, useMemo, useState } from "react";
|
|
import { Button } from "../../button";
|
|
import { Calendar } from "./Calendar";
|
|
import { calculateNewDateRange } from "./dateRangeLogic";
|
|
|
|
type DatePickerWithRangeProps = {
|
|
dates: { startDate?: Date; endDate?: Date };
|
|
onDatesChange: ({ startDate, endDate }: { startDate?: Date; endDate?: Date }) => void;
|
|
disabled?: boolean;
|
|
minDate?: Date | null;
|
|
maxDate?: Date;
|
|
withoutPopover?: boolean;
|
|
popoverModal?: boolean;
|
|
popoverOpen?: boolean;
|
|
onPopoverOpenChange?: (open: boolean) => void;
|
|
"data-testid"?: string;
|
|
strictlyBottom?: boolean;
|
|
allowPastDates?: boolean;
|
|
};
|
|
|
|
export function DatePickerWithRange({
|
|
className,
|
|
dates,
|
|
minDate,
|
|
maxDate,
|
|
onDatesChange,
|
|
disabled,
|
|
withoutPopover,
|
|
popoverModal = true,
|
|
popoverOpen,
|
|
onPopoverOpenChange,
|
|
"data-testid": testId,
|
|
strictlyBottom,
|
|
allowPastDates = false,
|
|
}: HTMLAttributes<HTMLDivElement> & DatePickerWithRangeProps) {
|
|
const [hoveredDate, setHoveredDate] = useState<Date | undefined>(undefined);
|
|
|
|
function handleDayClick(date: Date) {
|
|
const newDates = calculateNewDateRange({
|
|
startDate: dates.startDate,
|
|
endDate: dates.endDate,
|
|
clickedDate: date,
|
|
});
|
|
onDatesChange(newDates);
|
|
setHoveredDate(undefined);
|
|
}
|
|
|
|
function handleDayMouseEnter(date: Date) {
|
|
if (dates.startDate && !dates.endDate) {
|
|
setHoveredDate(date);
|
|
}
|
|
}
|
|
|
|
function handleDayMouseLeave() {
|
|
setHoveredDate(undefined);
|
|
}
|
|
|
|
const fromDate = allowPastDates && minDate === null ? undefined : (minDate ?? new Date());
|
|
|
|
const hoverRangeModifier = useMemo(() => {
|
|
if (!dates.startDate || dates.endDate || !hoveredDate) {
|
|
return undefined;
|
|
}
|
|
if (isSameDay(dates.startDate, hoveredDate)) {
|
|
return undefined;
|
|
}
|
|
if (isBefore(hoveredDate, dates.startDate)) {
|
|
return { from: hoveredDate, to: dates.startDate };
|
|
}
|
|
return { from: dates.startDate, to: hoveredDate };
|
|
}, [dates.startDate, dates.endDate, hoveredDate]);
|
|
|
|
const calendar = (
|
|
<Calendar
|
|
initialFocus
|
|
fromDate={fromDate}
|
|
toDate={maxDate}
|
|
mode="range"
|
|
defaultMonth={dates?.startDate}
|
|
selected={{ from: dates?.startDate, to: dates?.endDate }}
|
|
onDayClick={(day) => handleDayClick(day)}
|
|
onDayMouseEnter={handleDayMouseEnter}
|
|
onDayMouseLeave={handleDayMouseLeave}
|
|
numberOfMonths={1}
|
|
disabled={disabled}
|
|
data-testid={testId}
|
|
modifiers={hoverRangeModifier ? { hoverRange: hoverRangeModifier } : undefined}
|
|
modifiersClassNames={hoverRangeModifier ? { hoverRange: "bg-emphasis" } : undefined}
|
|
/>
|
|
);
|
|
|
|
if (withoutPopover) {
|
|
return calendar;
|
|
}
|
|
|
|
return (
|
|
<div className={classNames("grid gap-2", className)}>
|
|
{/* modal prop required for iOS compatibility when nested inside Dialog modals */}
|
|
<Popover.Root modal={popoverModal} open={popoverOpen} onOpenChange={onPopoverOpenChange}>
|
|
<Popover.Trigger asChild>
|
|
<Button
|
|
data-testid="date-range"
|
|
color="secondary"
|
|
EndIcon="calendar"
|
|
className={classNames("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")} - End</>
|
|
)
|
|
) : (
|
|
<span>Pick a date</span>
|
|
)}
|
|
</Button>
|
|
</Popover.Trigger>
|
|
<Popover.Portal>
|
|
<Popover.Content
|
|
className="bg-default text-emphasis z-50 w-auto rounded-md border p-0 outline-none"
|
|
align="start"
|
|
sideOffset={4}
|
|
side={strictlyBottom ? "bottom" : undefined}
|
|
avoidCollisions={!strictlyBottom}
|
|
onInteractOutside={(event) => {
|
|
if (dates?.startDate && !dates?.endDate) {
|
|
event.preventDefault();
|
|
}
|
|
}}>
|
|
{calendar}
|
|
</Popover.Content>
|
|
</Popover.Portal>
|
|
</Popover.Root>
|
|
</div>
|
|
);
|
|
}
|