Skip to content

Commit 8c39210

Browse files
Ryukemeisterdevin-ai-integration[bot]PeerRichhariombalhara
authored
fix: scroll issues in Embed (calcom#26583)
* 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>
1 parent 6c620e3 commit 8c39210

29 files changed

Lines changed: 1419 additions & 259 deletions

apps/web/modules/bookings/components/AvailableTimeSlots.tsx

Lines changed: 61 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
import { useCallback, useMemo, useRef } from "react";
22

33
import dayjs from "@calcom/dayjs";
4-
import { AvailableTimes, AvailableTimesSkeleton } from "@calcom/web/modules/bookings/components/AvailableTimes";
4+
import {
5+
AvailableTimes,
6+
AvailableTimesSkeleton,
7+
} from "@calcom/web/modules/bookings/components/AvailableTimes";
58
import { useBookerStoreContext } from "@calcom/features/bookings/Booker/BookerStoreProvider";
69
import type { IUseBookingLoadingStates } from "@calcom/features/bookings/Booker/components/hooks/useBookings";
710
import type { BookerEvent } from "@calcom/features/bookings/types";
@@ -25,7 +28,10 @@ type AvailableTimeSlotsProps = {
2528
seatsPerTimeSlot?: number | null;
2629
showAvailableSeatsCount?: boolean | null;
2730
event: {
28-
data?: Pick<BookerEvent, "length" | "bookingFields" | "price" | "currency" | "metadata"> | null;
31+
data?: Pick<
32+
BookerEvent,
33+
"length" | "bookingFields" | "price" | "currency" | "metadata"
34+
> | null;
2935
};
3036
customClassNames?: {
3137
availableTimeSlotsContainer?: string;
@@ -50,6 +56,7 @@ type AvailableTimeSlotsProps = {
5056
unavailableTimeSlots: string[];
5157
confirmButtonDisabled?: boolean;
5258
onAvailableTimeSlotSelect: (time: string) => void;
59+
hideAvailableTimesHeader?: boolean;
5360
};
5461

5562
/**
@@ -74,19 +81,23 @@ export const AvailableTimeSlots = ({
7481
confirmButtonDisabled,
7582
confirmStepClassNames,
7683
onAvailableTimeSlotSelect,
84+
hideAvailableTimesHeader = false,
7785
...props
7886
}: AvailableTimeSlotsProps) => {
7987
const selectedDate = useBookerStoreContext((state) => state.selectedDate);
8088

81-
const setSeatedEventData = useBookerStoreContext((state) => state.setSeatedEventData);
89+
const setSeatedEventData = useBookerStoreContext(
90+
(state) => state.setSeatedEventData
91+
);
8292
const date = selectedDate || dayjs().format("YYYY-MM-DD");
8393
const [layout] = useBookerStoreContext((state) => [state.layout]);
8494
const isColumnView = layout === BookerLayouts.COLUMN_VIEW;
8595
const containerRef = useRef<HTMLDivElement | null>(null);
86-
const { setTentativeSelectedTimeslots, tentativeSelectedTimeslots } = useBookerStoreContext((state) => ({
87-
setTentativeSelectedTimeslots: state.setTentativeSelectedTimeslots,
88-
tentativeSelectedTimeslots: state.tentativeSelectedTimeslots,
89-
}));
96+
const { setTentativeSelectedTimeslots, tentativeSelectedTimeslots } =
97+
useBookerStoreContext((state) => ({
98+
setTentativeSelectedTimeslots: state.setTentativeSelectedTimeslots,
99+
tentativeSelectedTimeslots: state.tentativeSelectedTimeslots,
100+
}));
90101

91102
const onTentativeTimeSelect = ({
92103
time,
@@ -124,13 +135,22 @@ export const AvailableTimeSlots = ({
124135
return [];
125136
}, [date, extraDays, nonEmptyScheduleDaysFromSelectedDate]);
126137

127-
const { slotsPerDay, toggleConfirmButton } = useSlotsForAvailableDates(dates, scheduleData?.slots);
138+
const { slotsPerDay, toggleConfirmButton } = useSlotsForAvailableDates(
139+
dates,
140+
scheduleData?.slots
141+
);
128142

129143
const overlayCalendarToggled =
130-
getQueryParam("overlayCalendar") === "true" || localStorage.getItem("overlayCalendarSwitchDefault");
144+
getQueryParam("overlayCalendar") === "true" ||
145+
localStorage.getItem("overlayCalendarSwitchDefault");
131146

132147
const onTimeSelect = useCallback(
133-
(time: string, attendees: number, seatsPerTimeSlot?: number | null, bookingUid?: string) => {
148+
(
149+
time: string,
150+
attendees: number,
151+
seatsPerTimeSlot?: number | null,
152+
bookingUid?: string
153+
) => {
134154
// Temporarily allow disabling it, till we are sure that it doesn't cause any significant load on the system
135155
if (PUBLIC_INVALIDATE_AVAILABLE_SLOTS_ON_BOOKING_FORM) {
136156
// Ensures that user has latest available slots when they are about to confirm the booking by filling up the details
@@ -181,12 +201,24 @@ export const AvailableTimeSlots = ({
181201
);
182202
}
183203
},
184-
[overlayCalendarToggled, onTimeSelect, seatsPerTimeSlot, skipConfirmStep, toggleConfirmButton]
204+
[
205+
overlayCalendarToggled,
206+
onTimeSelect,
207+
seatsPerTimeSlot,
208+
skipConfirmStep,
209+
toggleConfirmButton,
210+
]
185211
);
186212

187213
return (
188214
<>
189-
<div className={classNames(`flex`, `${customClassNames?.availableTimeSlotsContainer}`)}>
215+
<div
216+
className={classNames(
217+
`flex`,
218+
hideAvailableTimesHeader && "hidden",
219+
`${customClassNames?.availableTimeSlotsContainer}`
220+
)}
221+
>
190222
{isLoading ? (
191223
<div className="mb-3 h-8" />
192224
) : (
@@ -197,15 +229,19 @@ export const AvailableTimeSlots = ({
197229
return (
198230
<AvailableTimesHeader
199231
customClassNames={{
200-
availableTimeSlotsHeaderContainer: customClassNames?.availableTimeSlotsHeaderContainer,
201-
availableTimeSlotsTitle: customClassNames?.availableTimeSlotsTitle,
202-
availableTimeSlotsTimeFormatToggle: customClassNames?.availableTimeSlotsTimeFormatToggle,
232+
availableTimeSlotsHeaderContainer:
233+
customClassNames?.availableTimeSlotsHeaderContainer,
234+
availableTimeSlotsTitle:
235+
customClassNames?.availableTimeSlotsTitle,
236+
availableTimeSlotsTimeFormatToggle:
237+
customClassNames?.availableTimeSlotsTimeFormatToggle,
203238
}}
204239
key={slots.date}
205240
date={dayjs(slots.date)}
206241
showTimeFormatToggle={!isColumnView && !isOOODay}
207242
availableMonth={
208-
dayjs(selectedDate).format("MM") !== dayjs(slots.date).format("MM")
243+
dayjs(selectedDate).format("MM") !==
244+
dayjs(slots.date).format("MM")
209245
? dayjs(slots.date).format("MMM")
210246
: undefined
211247
}
@@ -221,13 +257,19 @@ export const AvailableTimeSlots = ({
221257
limitHeight && "no-scrollbar grow overflow-auto md:h-[400px]",
222258
!limitHeight && "flex h-full w-full flex-row gap-4",
223259
`${customClassNames?.availableTimeSlotsContainer}`
224-
)}>
260+
)}
261+
>
225262
{isLoading && // Shows exact amount of days as skeleton.
226-
Array.from({ length: 1 + (extraDays ?? 0) }).map((_, i) => <AvailableTimesSkeleton key={i} />)}
263+
Array.from({ length: 1 + (extraDays ?? 0) }).map((_, i) => (
264+
<AvailableTimesSkeleton key={i} />
265+
))}
227266
{!isLoading &&
228267
slotsPerDay.length > 0 &&
229268
slotsPerDay.map((slots) => (
230-
<div key={slots.date} className="no-scrollbar overflow-x-hidden! h-full w-full overflow-y-auto">
269+
<div
270+
key={slots.date}
271+
className="no-scrollbar overflow-x-hidden! h-full w-full overflow-y-auto"
272+
>
231273
<AvailableTimes
232274
className={customClassNames?.availableTimeSlotsContainer}
233275
customClassNames={customClassNames?.availableTimes}

apps/web/modules/bookings/components/Booker.test.tsx

Lines changed: 29 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,30 @@ import "@calcom/lib/__mocks__/logger";
1313
import React from "react";
1414
import { vi } from "vitest";
1515

16+
vi.mock("next/navigation", async (importOriginal) => {
17+
const actual = await importOriginal<typeof import("next/navigation")>();
18+
return {
19+
...actual,
20+
useRouter: () => ({
21+
push: vi.fn(),
22+
replace: vi.fn(),
23+
back: vi.fn(),
24+
forward: vi.fn(),
25+
refresh: vi.fn(),
26+
prefetch: vi.fn(),
27+
}),
28+
usePathname: () => "/test-path",
29+
useSearchParams: () => new URLSearchParams(),
30+
useParams: () => ({}),
31+
};
32+
});
33+
1634
import "@calcom/dayjs/__mocks__";
1735
import "@calcom/features/auth/Turnstile";
1836

19-
import { Booker } from "./Booker";
2037
import { render, screen } from "@calcom/features/bookings/Booker/__tests__/test-utils";
2138
import type { BookerProps, WrappedBookerProps } from "@calcom/features/bookings/Booker/types";
39+
import { Booker } from "./Booker";
2240

2341
vi.mock("framer-motion", async (importOriginal) => {
2442
const actual = (await importOriginal()) as any;
@@ -175,9 +193,12 @@ describe("Booker", () => {
175193
});
176194

177195
it("should render null when in loading state", () => {
178-
const { container } = render(<Booker {...defaultProps as unknown as BookerProps & WrappedBookerProps} />, {
179-
mockStore: { state: "loading" },
180-
});
196+
const { container } = render(
197+
<Booker {...(defaultProps as unknown as BookerProps & WrappedBookerProps)} />,
198+
{
199+
mockStore: { state: "loading" },
200+
}
201+
);
181202
expect(container).toBeEmptyDOMElement();
182203
});
183204

@@ -194,7 +215,7 @@ describe("Booker", () => {
194215
},
195216
};
196217

197-
render(<Booker {...propsWithDryRun as unknown as BookerProps & WrappedBookerProps} />, {
218+
render(<Booker {...(propsWithDryRun as unknown as BookerProps & WrappedBookerProps)} />, {
198219
mockStore: {
199220
state: "selecting_time",
200221
selectedDate: "2024-01-01",
@@ -218,7 +239,7 @@ describe("Booker", () => {
218239
},
219240
};
220241

221-
render(<Booker {...propsWithInvalidate as unknown as BookerProps & WrappedBookerProps} />, {
242+
render(<Booker {...(propsWithInvalidate as unknown as BookerProps & WrappedBookerProps)} />, {
222243
mockStore: { state: "booking" },
223244
});
224245
screen.logTestingPlaygroundURL();
@@ -240,11 +261,11 @@ describe("Booker", () => {
240261
},
241262
};
242263

243-
render(<Booker {...propsWithQuickChecks as unknown as BookerProps & WrappedBookerProps} />, {
264+
render(<Booker {...(propsWithQuickChecks as unknown as BookerProps & WrappedBookerProps)} />, {
244265
mockStore: { state: "booking" },
245266
});
246267
const bookEventForm = screen.getByTestId("book-event-form");
247268
await expect(bookEventForm).toHaveAttribute("data-unavailable", "true");
248269
});
249270
});
250-
});
271+
});

0 commit comments

Comments
 (0)