8c39210a12
* chore: add new view for two step slot selection for embed * fix: make sure two step slot selection is false by default * chore: update embed playground to showcase two step slot selection * chore: add tests * chore: update embed snippet generator code to include two step slot selection * chore: update docs * fix: back button styling * chore: implement feedback from cubic * fix: add missing isEnableTwoStepSlotSelectionVisible properties to test mock store Co-Authored-By: unknown <> * chore: implement PR feedback * chore: update slot selection modal * chore: add prop to hide available times header * fix: scope two-step slot selection visibility to mobile only Restores the isMobile check in the timeslot visibility guard to ensure desktop embeds continue to show the booking UI when two-step slot selection is enabled. The feature should only hide the timeslot list on mobile devices. Addresses Cubic AI review feedback (confidence 9/10). Co-Authored-By: unknown <> * fixup: use translations for loading instead of actual word * fix: type check * fix: add window.matchMedia mock for jsdom test environment Co-Authored-By: rajiv@cal.com <sahalrajiv6900@gmail.com> * fix: add window.matchMedia mock to packages/testing/src/setupVitest.ts Co-Authored-By: rajiv@cal.com <sahalrajiv6900@gmail.com> * fix: unit tests failing * add a width style to the two-step slot selection embed container. * refactor: rename enableTwoStepSlotSelection to useSlotsViewOnSmallScreen - Rename external-facing embed config option from enableTwoStepSlotSelection to useSlotsViewOnSmallScreen - Update URL parameter parsing in embed-iframe.ts - Update type definitions in types.ts and index.d.ts - Update internal variable names to slotsViewOnSmallScreen for consistency - Update documentation, playground examples, and tests Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com> * Add config to prefill all booking fields so that slot has confirm button along with it * chore: implement PR feedback * fix: move twoStepSlotSelection embed outside misc-embeds div to fix e2e test The e2e test was failing because the misc-embeds div content was intercepting pointer events on mobile viewport. This follows the same pattern used for skeletonDemo - the embed is now outside misc-embeds and the div is hidden when testing this namespace. Co-Authored-By: rajiv@cal.com <sahalrajiv6900@gmail.com> * fix: prevent pointer event interception in twoStepSlotSelection embed Hide heading and note elements and set pointer-events: none on container elements while keeping pointer-events: auto on the iframe container. This prevents the container from intercepting clicks on the iframe during mobile viewport e2e tests. Co-Authored-By: rajiv@cal.com <sahalrajiv6900@gmail.com> * fix: hide all non-essential content for twoStepSlotSelection e2e test Co-Authored-By: rajiv@cal.com <sahalrajiv6900@gmail.com> * fix: disable pointer-events on body for twoStepSlotSelection e2e test Co-Authored-By: rajiv@cal.com <sahalrajiv6900@gmail.com> * fix: remove pointer-events: auto on container to let clicks pass through to iframe Co-Authored-By: rajiv@cal.com <sahalrajiv6900@gmail.com> * fix: explicitly set pointer-events: none on container and inline-embed-container Co-Authored-By: rajiv@cal.com <sahalrajiv6900@gmail.com> * fix: also set pointer-events: none on html element to prevent interception Co-Authored-By: rajiv@cal.com <sahalrajiv6900@gmail.com> * fix: make .place div fill viewport for twoStepSlotSelection e2e test Co-Authored-By: rajiv@cal.com <sahalrajiv6900@gmail.com> * fix: failing embed tests * fixup * fixup fixup * fix: failing tests * fix: update checks for verifying prefilled date --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Co-authored-by: Peer Richelsen <peeroke@gmail.com> Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
556 lines
18 KiB
TypeScript
556 lines
18 KiB
TypeScript
import { useEffect } from "react";
|
|
import { shallow } from "zustand/shallow";
|
|
|
|
import type { Dayjs } from "@calcom/dayjs";
|
|
import dayjs from "@calcom/dayjs";
|
|
import { useEmbedStyles } from "@calcom/embed-core/embed-iframe";
|
|
import { useBookerStoreContext } from "@calcom/features/bookings/Booker/BookerStoreProvider";
|
|
import { getAvailableDatesInMonth } from "@calcom/features/calendars/lib/getAvailableDatesInMonth";
|
|
import type { Slots } from "@calcom/features/calendars/lib/types";
|
|
import { daysInMonth, yyyymmdd } from "@calcom/lib/dayjs";
|
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
|
import { weekdayNames } from "@calcom/lib/weekday";
|
|
import type { PeriodData } from "@calcom/types/Event";
|
|
import classNames from "@calcom/ui/classNames";
|
|
import { Button } from "@calcom/ui/components/button";
|
|
import { SkeletonText } from "@calcom/ui/components/skeleton";
|
|
import { Tooltip } from "@calcom/ui/components/tooltip";
|
|
|
|
import NoAvailabilityDialog from "./NoAvailabilityDialog";
|
|
import { useSlotsViewOnSmallScreen } from "@calcom/features/bookings/Booker/components/hooks/useSlotsViewOnSmallScreen";
|
|
|
|
export type DatePickerProps = {
|
|
/** which day of the week to render the calendar. Usually Sunday (=0) or Monday (=1) - default: Sunday */
|
|
weekStart?: 0 | 1 | 2 | 3 | 4 | 5 | 6;
|
|
/** Fires whenever a selected date is changed. */
|
|
onChange: (date: Dayjs | null, omitUpdatingParams?: boolean) => void;
|
|
/** Fires when the month is changed. */
|
|
onMonthChange?: (date: Dayjs) => void;
|
|
/** which date or dates are currently selected (not tracked from here) */
|
|
selected?: Dayjs | Dayjs[] | null;
|
|
/** defaults to current date. */
|
|
minDate?: Date;
|
|
/** Furthest date selectable in the future, default = UNLIMITED */
|
|
maxDate?: Date;
|
|
/** locale, any IETF language tag, e.g. "hu-HU" - defaults to Browser settings */
|
|
locale: string;
|
|
/** Defaults to [], which dates are not bookable. Array of valid dates like: ["2022-04-23", "2022-04-24"] */
|
|
excludedDates?: string[];
|
|
/** defaults to all, which dates are bookable (inverse of excludedDates) */
|
|
includedDates?: string[];
|
|
/** allows adding classes to the container */
|
|
className?: string;
|
|
/** Shows a small loading spinner next to the month name */
|
|
isLoading?: boolean;
|
|
/** used to query the multiple selected dates */
|
|
eventSlug?: string;
|
|
/** To identify days that are not available and should display OOO and redirect if toUser exists */
|
|
slots?: Slots;
|
|
periodData?: PeriodData;
|
|
// Whether this is a compact sidebar view or main monthly view
|
|
isCompact?: boolean;
|
|
// Whether to show the no availability dialog
|
|
showNoAvailabilityDialog?: boolean;
|
|
};
|
|
|
|
const Day = ({
|
|
date,
|
|
active,
|
|
disabled,
|
|
away,
|
|
emoji,
|
|
customClassName,
|
|
showMonthTooltip,
|
|
isFirstDayOfNextMonth,
|
|
...props
|
|
}: JSX.IntrinsicElements["button"] & {
|
|
active: boolean;
|
|
date: Dayjs;
|
|
away?: boolean;
|
|
emoji?: string | null;
|
|
customClassName?: {
|
|
dayContainer?: string;
|
|
dayActive?: string;
|
|
};
|
|
showMonthTooltip?: boolean;
|
|
isFirstDayOfNextMonth?: boolean;
|
|
}) => {
|
|
const { t } = useLocale();
|
|
const enabledDateButtonEmbedStyles = useEmbedStyles("enabledDateButton");
|
|
const disabledDateButtonEmbedStyles = useEmbedStyles("disabledDateButton");
|
|
|
|
const buttonContent = (
|
|
<button
|
|
type="button"
|
|
style={
|
|
disabled
|
|
? { ...disabledDateButtonEmbedStyles }
|
|
: { ...enabledDateButtonEmbedStyles }
|
|
}
|
|
className={classNames(
|
|
"disabled:text-bookinglighter absolute bottom-0 left-0 right-0 top-0 mx-auto w-full rounded-md border-2 border-transparent text-center text-sm font-medium transition disabled:cursor-default disabled:border-transparent disabled:font-light ",
|
|
active
|
|
? "bg-brand-default text-brand"
|
|
: !disabled
|
|
? `${
|
|
!customClassName?.dayActive
|
|
? "hover:border-brand-default text-emphasis bg-emphasis"
|
|
: `hover:border-brand-default ${customClassName.dayActive}`
|
|
}`
|
|
: `${customClassName ? "" : " text-mute"}`
|
|
)}
|
|
data-testid="day"
|
|
data-disabled={disabled}
|
|
disabled={disabled}
|
|
{...props}
|
|
>
|
|
{away && <span data-testid="away-emoji">{emoji}</span>}
|
|
{!away && date.date()}
|
|
{date.isToday() && (
|
|
<span
|
|
className={classNames(
|
|
"bg-brand-default absolute left-1/2 top-1/2 flex h-[5px] w-[5px] -translate-x-1/2 translate-y-[8px] items-center justify-center rounded-full align-middle sm:translate-y-[12px]",
|
|
active && "bg-brand-accent"
|
|
)}
|
|
>
|
|
<span className="sr-only">{t("today")}</span>
|
|
</span>
|
|
)}
|
|
</button>
|
|
);
|
|
|
|
const content = showMonthTooltip ? (
|
|
<Tooltip content={date.format("MMMM")}>{buttonContent}</Tooltip>
|
|
) : (
|
|
buttonContent
|
|
);
|
|
|
|
return (
|
|
<>
|
|
{isFirstDayOfNextMonth && (
|
|
<div
|
|
className={classNames(
|
|
"absolute top-0 z-10 mx-auto w-fit rounded-full font-semibold uppercase tracking-wide",
|
|
active ? "text-white" : "text-default",
|
|
disabled && "bg-emphasis"
|
|
)}
|
|
style={{
|
|
fontSize: "10px",
|
|
lineHeight: "13px",
|
|
padding: disabled ? "0 3px" : "3px 3px 3px 4px",
|
|
}}
|
|
>
|
|
{date.format("MMM")}
|
|
</div>
|
|
)}
|
|
{content}
|
|
</>
|
|
);
|
|
};
|
|
|
|
const Days = ({
|
|
minDate,
|
|
excludedDates = [],
|
|
browsingDate,
|
|
weekStart,
|
|
DayComponent = Day,
|
|
selected,
|
|
month,
|
|
nextMonthButton,
|
|
eventSlug,
|
|
slots,
|
|
customClassName,
|
|
isBookingInPast,
|
|
periodData,
|
|
isCompact,
|
|
showNoAvailabilityDialog = true,
|
|
...props
|
|
}: Omit<DatePickerProps, "locale" | "className" | "weekStart"> & {
|
|
DayComponent?: React.FC<React.ComponentProps<typeof Day>>;
|
|
browsingDate: Dayjs;
|
|
weekStart: number;
|
|
month: string | null;
|
|
nextMonthButton: () => void;
|
|
customClassName?: {
|
|
datePickerDate?: string;
|
|
datePickerDateActive?: string;
|
|
};
|
|
scrollToTimeSlots?: () => void;
|
|
isBookingInPast: boolean;
|
|
periodData: PeriodData;
|
|
isCompact?: boolean;
|
|
}) => {
|
|
const slotsViewOnSmallScreen = useSlotsViewOnSmallScreen();
|
|
const layout = useBookerStoreContext((state) => state.layout);
|
|
const isMobile = layout === "mobile";
|
|
|
|
const includedDates = getAvailableDatesInMonth({
|
|
browsingDate: browsingDate.toDate(),
|
|
minDate,
|
|
includedDates: props.includedDates,
|
|
});
|
|
|
|
const today = dayjs();
|
|
const firstDayOfMonth = browsingDate.startOf("month");
|
|
const isSecondWeekOver = today.isAfter(firstDayOfMonth.add(2, "week"));
|
|
let days: (Dayjs | null)[] = [];
|
|
|
|
const getPadding = (day: number) =>
|
|
(browsingDate.set("date", day).day() - weekStart + 7) % 7;
|
|
const totalDays = daysInMonth(browsingDate);
|
|
|
|
const showNextMonthDays = isSecondWeekOver && !isCompact;
|
|
|
|
// Only apply end-of-month logic for main monthly view (not compact sidebar)
|
|
if (showNextMonthDays) {
|
|
const startDay = 8;
|
|
const pad = getPadding(startDay);
|
|
days = Array(pad).fill(null);
|
|
|
|
for (let day = startDay; day <= totalDays; day++) {
|
|
days.push(browsingDate.set("date", day));
|
|
}
|
|
|
|
const remainingInRow = days.length % 7;
|
|
const extraDays = (remainingInRow > 0 ? 7 - remainingInRow : 0) + 7;
|
|
const nextMonth = browsingDate.add(1, "month");
|
|
|
|
// Add days starting from day 1 of next month
|
|
for (let i = 0; i < extraDays; i++) {
|
|
days.push(nextMonth.set("date", 1 + i));
|
|
}
|
|
} else {
|
|
// Traditional calendar grid logic for compact sidebar or early in month
|
|
const pad = getPadding(1);
|
|
days = Array(pad).fill(null);
|
|
|
|
for (let day = 1; day <= totalDays; day++) {
|
|
days.push(browsingDate.set("date", day));
|
|
}
|
|
}
|
|
|
|
const [selectedDatesAndTimes] = useBookerStoreContext(
|
|
(state) => [state.selectedDatesAndTimes],
|
|
shallow
|
|
);
|
|
|
|
const isActive = (day: dayjs.Dayjs) => {
|
|
// for selecting a range of dates
|
|
if (Array.isArray(selected)) {
|
|
return (
|
|
Array.isArray(selected) &&
|
|
selected?.some((e) => yyyymmdd(e) === yyyymmdd(day))
|
|
);
|
|
}
|
|
|
|
if (selected && yyyymmdd(selected) === yyyymmdd(day)) {
|
|
return true;
|
|
}
|
|
|
|
// for selecting multiple dates for an event
|
|
if (
|
|
eventSlug &&
|
|
selectedDatesAndTimes &&
|
|
selectedDatesAndTimes[eventSlug as string] &&
|
|
Object.keys(selectedDatesAndTimes[eventSlug as string]).length > 0
|
|
) {
|
|
return Object.keys(selectedDatesAndTimes[eventSlug as string]).some(
|
|
(date) => {
|
|
return yyyymmdd(dayjs(date)) === yyyymmdd(day);
|
|
}
|
|
);
|
|
}
|
|
|
|
return false;
|
|
};
|
|
|
|
const daysToRenderForTheMonth = days.map((day) => {
|
|
if (!day) return { day: null, disabled: true };
|
|
|
|
const dateKey = yyyymmdd(day);
|
|
const daySlots = slots?.[dateKey] || [];
|
|
const oooInfo = daySlots.find((slot) => slot.away) || null;
|
|
|
|
const isNextMonth = day.month() !== browsingDate.month();
|
|
const isFirstDayOfNextMonth =
|
|
isSecondWeekOver && !isCompact && isNextMonth && day.date() === 1;
|
|
|
|
const included = includedDates?.includes(dateKey);
|
|
const excluded = excludedDates.includes(dateKey);
|
|
|
|
const hasAvailableSlots = daySlots.some((slot) => !slot.away);
|
|
const isOOOAllDay =
|
|
daySlots.length > 0 && daySlots.every((slot) => slot.away);
|
|
const away = isOOOAllDay;
|
|
|
|
// OOO dates are selectable only if there's a redirect user OR the note is public
|
|
const oooIsSelectable = oooInfo?.toUser || oooInfo?.showNotePublicly;
|
|
const disabled = away
|
|
? !oooIsSelectable
|
|
: isNextMonth
|
|
? !hasAvailableSlots
|
|
: !included || excluded;
|
|
|
|
return {
|
|
day,
|
|
disabled,
|
|
away,
|
|
emoji: oooInfo?.emoji,
|
|
isFirstDayOfNextMonth,
|
|
};
|
|
});
|
|
|
|
/**
|
|
* Takes care of selecting a valid date in the month if the selected date is not available in the month
|
|
*/
|
|
|
|
const useHandleInitialDateSelection = () => {
|
|
// Don't auto-select date when slots view on small screen is enabled on mobile
|
|
if (slotsViewOnSmallScreen) {
|
|
return;
|
|
}
|
|
|
|
// Let's not do something for now in case of multiple selected dates as behaviour is unclear and it's not needed at the moment
|
|
if (selected instanceof Array) {
|
|
return;
|
|
}
|
|
const firstAvailableDateOfTheMonth = daysToRenderForTheMonth.find(
|
|
(day) => !day.disabled
|
|
)?.day;
|
|
const isSelectedDateAvailable = selected
|
|
? daysToRenderForTheMonth.some(({ day, disabled }) => {
|
|
if (day && yyyymmdd(day) === yyyymmdd(selected) && !disabled)
|
|
return true;
|
|
})
|
|
: false;
|
|
if (!isSelectedDateAvailable && firstAvailableDateOfTheMonth) {
|
|
// If selected date not available in the month, select the first available date of the month
|
|
const shouldOmitUpdatingParams = selected?.isValid() ? false : true; // In case a date is selected and it is not available, then we have to change search params
|
|
props.onChange(firstAvailableDateOfTheMonth, shouldOmitUpdatingParams);
|
|
}
|
|
if (isSelectedDateAvailable) {
|
|
props.onChange(dayjs(selected), true);
|
|
}
|
|
if (!firstAvailableDateOfTheMonth) {
|
|
props.onChange(null);
|
|
}
|
|
};
|
|
|
|
useEffect(useHandleInitialDateSelection);
|
|
|
|
return (
|
|
<>
|
|
{daysToRenderForTheMonth.map(
|
|
({ day, disabled, away, emoji, isFirstDayOfNextMonth }, idx) => (
|
|
<div
|
|
key={day === null ? `e-${idx}` : `day-${day.format()}`}
|
|
className="relative w-full pt-[100%]"
|
|
>
|
|
{day === null ? (
|
|
<div key={`e-${idx}`} />
|
|
) : props.isLoading ? (
|
|
<button
|
|
className="bg-cal-muted text-muted absolute bottom-0 left-0 right-0 top-0 mx-auto flex w-full items-center justify-center rounded-sm border-transparent text-center font-medium opacity-90 transition"
|
|
key={`e-${idx}`}
|
|
disabled
|
|
>
|
|
<SkeletonText className="h-8 w-9" />
|
|
</button>
|
|
) : (
|
|
<DayComponent
|
|
customClassName={{
|
|
dayContainer: customClassName?.datePickerDate,
|
|
dayActive: customClassName?.datePickerDateActive,
|
|
}}
|
|
date={day}
|
|
onClick={() => {
|
|
props.onChange(day);
|
|
props?.scrollToTimeSlots?.();
|
|
}}
|
|
disabled={disabled}
|
|
active={isActive(day)}
|
|
away={away}
|
|
emoji={emoji}
|
|
showMonthTooltip={
|
|
showNextMonthDays &&
|
|
!disabled &&
|
|
day.month() !== browsingDate.month()
|
|
}
|
|
isFirstDayOfNextMonth={isFirstDayOfNextMonth}
|
|
/>
|
|
)}
|
|
</div>
|
|
)
|
|
)}
|
|
{!props.isLoading &&
|
|
!isBookingInPast &&
|
|
includedDates &&
|
|
includedDates?.length === 0 &&
|
|
showNoAvailabilityDialog && (
|
|
<NoAvailabilityDialog
|
|
month={month}
|
|
nextMonthButton={nextMonthButton}
|
|
browsingDate={browsingDate}
|
|
periodData={periodData}
|
|
/>
|
|
)}
|
|
</>
|
|
);
|
|
};
|
|
|
|
const DatePicker = ({
|
|
weekStart = 0,
|
|
className,
|
|
locale,
|
|
selected,
|
|
onMonthChange,
|
|
slots,
|
|
customClassNames,
|
|
includedDates,
|
|
periodData = {
|
|
periodStartDate: null,
|
|
periodEndDate: null,
|
|
periodCountCalendarDays: null,
|
|
periodDays: null,
|
|
periodType: "UNLIMITED",
|
|
},
|
|
isCompact,
|
|
showNoAvailabilityDialog,
|
|
...passThroughProps
|
|
}: DatePickerProps &
|
|
Partial<React.ComponentProps<typeof Days>> & {
|
|
customClassNames?: {
|
|
datePickerTitle?: string;
|
|
datePickerDays?: string;
|
|
datePickersDates?: string;
|
|
datePickerDatesActive?: string;
|
|
datePickerToggle?: string;
|
|
};
|
|
scrollToTimeSlots?: () => void;
|
|
}) => {
|
|
const minDate = passThroughProps.minDate;
|
|
const rawBrowsingDate =
|
|
passThroughProps.browsingDate || dayjs().startOf("month");
|
|
const browsingDate =
|
|
minDate && rawBrowsingDate.valueOf() < minDate.valueOf()
|
|
? dayjs(minDate)
|
|
: rawBrowsingDate;
|
|
|
|
const { i18n, t } = useLocale();
|
|
const bookingData = useBookerStoreContext((state) => state.bookingData);
|
|
const isBookingInPast = bookingData
|
|
? new Date(bookingData.endTime) < new Date()
|
|
: false;
|
|
const changeMonth = (newMonth: number) => {
|
|
if (onMonthChange) {
|
|
onMonthChange(browsingDate.add(newMonth, "month"));
|
|
}
|
|
};
|
|
const month = browsingDate
|
|
? new Intl.DateTimeFormat(i18n.language, { month: "long" }).format(
|
|
new Date(browsingDate.year(), browsingDate.month())
|
|
)
|
|
: null;
|
|
|
|
return (
|
|
<div className={className}>
|
|
<div className="mb-1 flex items-center justify-between text-xl">
|
|
<span className="text-default w-1/2 text-base">
|
|
{browsingDate ? (
|
|
<time
|
|
dateTime={browsingDate.format("YYYY-MM")}
|
|
data-testid="selected-month-label"
|
|
>
|
|
<strong
|
|
className={classNames(
|
|
`text-emphasis font-semibold`,
|
|
customClassNames?.datePickerTitle
|
|
)}
|
|
>
|
|
{month}
|
|
</strong>{" "}
|
|
<span
|
|
className={classNames(
|
|
`text-subtle font-medium`,
|
|
customClassNames?.datePickerTitle
|
|
)}
|
|
>
|
|
{browsingDate.format("YYYY")}
|
|
</span>
|
|
</time>
|
|
) : (
|
|
<SkeletonText className="h-8 w-24" />
|
|
)}
|
|
</span>
|
|
<div className="text-emphasis">
|
|
<div className="flex">
|
|
<Button
|
|
className={classNames(
|
|
`group p-1 opacity-70 transition hover:opacity-100 rtl:rotate-180`,
|
|
!browsingDate.isAfter(dayjs()) &&
|
|
`disabled:text-bookinglighter hover:bg-background hover:opacity-70`,
|
|
customClassNames?.datePickerToggle
|
|
)}
|
|
onClick={() => changeMonth(-1)}
|
|
disabled={!browsingDate.isAfter(dayjs())}
|
|
data-testid="decrementMonth"
|
|
color="minimal"
|
|
variant="icon"
|
|
StartIcon="chevron-left"
|
|
aria-label={t("view_previous_month")}
|
|
/>
|
|
<Button
|
|
className={classNames(
|
|
`group p-1 opacity-70 transition hover:opacity-100 rtl:rotate-180`,
|
|
`${customClassNames?.datePickerToggle}`
|
|
)}
|
|
onClick={() => changeMonth(+1)}
|
|
data-testid="incrementMonth"
|
|
color="minimal"
|
|
variant="icon"
|
|
StartIcon="chevron-right"
|
|
aria-label={t("view_next_month")}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div className="border-subtle mb-2 grid grid-cols-7 gap-4 border-b border-t text-center md:mb-0 md:border-0">
|
|
{weekdayNames(locale, weekStart, "short").map((weekDay) => (
|
|
<div
|
|
key={weekDay}
|
|
className={classNames(
|
|
`text-emphasis my-4 text-xs font-medium uppercase tracking-widest`,
|
|
customClassNames?.datePickerDays
|
|
)}
|
|
>
|
|
{weekDay}
|
|
</div>
|
|
))}
|
|
</div>
|
|
<div className="relative grid grid-cols-7 grid-rows-6 gap-1 text-center">
|
|
<Days
|
|
customClassName={{
|
|
datePickerDate: customClassNames?.datePickersDates,
|
|
datePickerDateActive: customClassNames?.datePickerDatesActive,
|
|
}}
|
|
weekStart={weekStart}
|
|
selected={selected}
|
|
{...passThroughProps}
|
|
browsingDate={browsingDate}
|
|
month={month}
|
|
nextMonthButton={() => changeMonth(+1)}
|
|
slots={slots}
|
|
includedDates={includedDates}
|
|
isBookingInPast={isBookingInPast}
|
|
periodData={periodData}
|
|
isCompact={isCompact}
|
|
showNoAvailabilityDialog={showNoAvailabilityDialog}
|
|
/>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export { DatePicker, Day };
|
|
export default DatePicker;
|