Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -5,24 +5,31 @@ import { useState } from "react";
import dayjs from "@calcom/dayjs";
import { Calendar } from "@calcom/features/calendars/weeklyview";
import type { CalendarEvent } from "@calcom/features/calendars/weeklyview/types/events";
import type { CalendarComponentProps } from "@calcom/features/calendars/weeklyview/types/state";
import type { CalendarComponentProps, Hours } from "@calcom/features/calendars/weeklyview/types/state";

const makeDate = (dayOffset: number, hour: number, minute: number = 0) => {
return dayjs("2025-01-06").add(dayOffset, "day").hour(hour).minute(minute).second(0).toDate();
};

const getBaseProps = (events: CalendarEvent[]): CalendarComponentProps => ({
const getBaseProps = ({
events,
startHour = 6,
}: {
events: CalendarEvent[];
startHour?: Hours;
}): CalendarComponentProps => ({
startDate: dayjs("2025-01-06").toDate(), // Monday
endDate: dayjs("2025-01-12").toDate(), // Sunday
events,
startHour: 6,
startHour,
endHour: 18,
gridCellsPerHour: 4,
timezone: "UTC",
showBackgroundPattern: false,
showBorder: false,
hideHeader: true,
borderColor: "subtle",
scrollToCurrentTime: false,
});

type Scenario = {
Expand All @@ -31,6 +38,7 @@ type Scenario = {
description: string;
expected: string;
events: CalendarEvent[];
startHour: Hours;
};

const scenarios: Scenario[] = [
Expand All @@ -39,7 +47,8 @@ const scenarios: Scenario[] = [
title: "Two Overlapping Events",
description: "Two events with overlapping time ranges on the same day",
expected:
"Second event should be offset 8% to the right, both 80% width. Hover should bring event to front.",
"First event 80% width at left edge (0%), second event 50% width aligned to right edge (49.5% offset). Events spread across full width for maximum visual distinction. Hover should bring event to front.",
startHour: 9,
events: [
{
id: 1,
Expand All @@ -62,7 +71,8 @@ const scenarios: Scenario[] = [
title: "Three Overlapping Events (Cascading)",
description: "Three events that overlap, creating a cascading effect",
expected:
"Events should cascade with offsets 0%, 8%, 16%. Z-index should increment. Hover brings any to top.",
"Events spread across full width with variable widths (55%, ~42%, 33%). Offsets: 0%, ~35%, 66.5% (last event aligned to right edge). Right edges evenly distributed for maximum scatter. Z-index should increment. Hover brings any to top.",
startHour: 9,
events: [
{
id: 3,
Expand Down Expand Up @@ -91,7 +101,8 @@ const scenarios: Scenario[] = [
id: "non-overlapping",
title: "Non-Overlapping Events",
description: "Events that don't overlap should not cascade",
expected: "Both events at 0% offset (separate groups), no cascade. Both should be 80% width.",
expected: "Both events at 0% offset (separate groups), no cascade. Both should be 100% width.",
startHour: 9,
events: [
{
id: 6,
Expand All @@ -113,7 +124,9 @@ const scenarios: Scenario[] = [
id: "same-start-time",
title: "Same Start Time, Different Durations",
description: "Multiple events starting at the same time with varying lengths",
expected: "Longest event first (base of cascade), shorter ones offset 8%, 16%. All start at 10:00.",
expected:
"Longest event first (base of cascade), spread across full width with variable widths (55%, ~42%, 33%). Last event aligned to right edge. All start at 10:00.",
startHour: 9,
events: [
{
id: 8,
Expand All @@ -139,16 +152,18 @@ const scenarios: Scenario[] = [
],
},
{
id: "chain-overlaps",
title: "Chain Overlaps (A→B→C)",
description: "Events where A overlaps B, and B overlaps C",
expected: "Single overlap group with cascading offsets 0%, 8%, 16%.",
id: "four-overlapping",
title: "Four Overlapping Events",
description: "Four events that overlap simultaneously",
expected:
"Events spread across full width with variable widths (40%, ~33%, ~28%, 25%). Last event aligned to right edge. Right edges evenly distributed for maximum scatter.",
startHour: 9,
events: [
{
id: 11,
title: "Event A",
start: makeDate(4, 10, 0),
end: makeDate(4, 11, 0),
end: makeDate(4, 12, 0),
options: { status: "ACCEPTED", color: "#3b82f6" },
},
{
Expand All @@ -162,9 +177,16 @@ const scenarios: Scenario[] = [
id: 13,
title: "Event C",
start: makeDate(4, 11, 0),
end: makeDate(4, 12, 0),
end: makeDate(4, 12, 30),
options: { status: "ACCEPTED", color: "#10b981" },
},
{
id: 50,
title: "Event D",
start: makeDate(4, 11, 15),
end: makeDate(4, 12, 15),
options: { status: "PENDING", color: "#8b5cf6" },
},
],
},
{
Expand All @@ -173,6 +195,7 @@ const scenarios: Scenario[] = [
description: "A very busy day with many overlapping events",
expected:
"Visually tight stack with multiple cascading levels. Right edge should not overflow. Hover should still work.",
startHour: 8,
events: [
{
id: 14,
Expand Down Expand Up @@ -327,7 +350,9 @@ const scenarios: Scenario[] = [
id: "touching-events",
title: "Touching Events (Edge Case)",
description: "Events that touch exactly at boundaries",
expected: "Separate groups; no cascade; both at 0% offset. Events touching at 11:00 should not overlap.",
expected:
"Separate groups; no cascade; both at 0% offset. Both should be 100% width. Events touching at 11:00 should not overlap.",
startHour: 9,
events: [
{
id: 25,
Expand All @@ -351,6 +376,7 @@ const scenarios: Scenario[] = [
description: "Events with different booking statuses",
expected:
"Visual styling should differ by status (ACCEPTED, PENDING, CANCELLED). Cascade should still work.",
startHour: 13,
events: [
{
id: 27,
Expand Down Expand Up @@ -381,6 +407,7 @@ const scenarios: Scenario[] = [
description: "Events with different durations to test layout logic (eventDuration > 30 changes flex-col)",
expected:
"Events ≤30min show horizontal layout (title and time inline). Events >30min show vertical layout (title and time stacked).",
startHour: 8,
events: [
{
id: 40,
Expand Down Expand Up @@ -465,7 +492,7 @@ function ScenarioCard({ scenario }: { scenario: Scenario }) {
</div>

<div className="h-[600px] overflow-hidden rounded border">
<Calendar {...getBaseProps(scenario.events)} />
<Calendar {...getBaseProps({ events: scenario.events, startHour: scenario.startHour })} />
</div>

<button
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ function CalendarInner(props: CalendarComponentProps) {
const showBackgroundPattern = useCalendarStore((state) => state.showBackgroundPattern);
const showBorder = useCalendarStore((state) => state.showBorder ?? true);
const borderColor = useCalendarStore((state) => state.borderColor ?? "default");
const scrollToCurrentTime = useCalendarStore((state) => state.scrollToCurrentTime ?? true);

const days = useMemo(() => getDaysBetweenDates(startDate, endDate), [startDate, endDate]);

Expand Down Expand Up @@ -74,7 +75,7 @@ function CalendarInner(props: CalendarComponentProps) {
borderColor={borderColor}
/>
<div className="relative flex flex-auto">
<CurrentTime timezone={timezone} />
<CurrentTime timezone={timezone} scrollToCurrentTime={scrollToCurrentTime} />
<div
className={classNames(
"bg-default dark:bg-muted ring-muted sticky left-0 z-10 w-16 flex-none ring-1",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ export function DateValues({ showBorder, borderColor, days, containerNavRef }: P
<div
ref={containerNavRef}
className={classNames(
"bg-default dark:bg-default sticky top-[var(--calendar-dates-sticky-offset,0px)] z-[80] flex-none border-b sm:pr-8",
"bg-default dark:bg-default sticky top-[var(--calendar-dates-sticky-offset,0px)] z-[80] flex-none border-b",
borderColor === "subtle" ? "border-b-subtle" : "border-b-default",
showBorder && (borderColor === "subtle" ? "border-r-subtle border-r" : "border-r-default border-r")
)}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,13 @@ function calculateMinutesFromStart(startHour: number, currentHour: number, curre
return currentMinuteOfDay - startMinute;
}

export function CurrentTime({ timezone }: { timezone: string }) {
export function CurrentTime({
timezone,
scrollToCurrentTime = true,
}: {
timezone: string;
scrollToCurrentTime?: boolean;
}) {
const { timeFormat } = useTimePreferences();
const currentTimeRef = useRef<HTMLDivElement>(null);
const [scrolledIntoView, setScrolledIntoView] = useState(false);
Expand All @@ -36,14 +42,14 @@ export function CurrentTime({ timezone }: { timezone: string }) {
const minutesFromStart = calculateMinutesFromStart(startHour, currentHour, currentMinute);
setCurrentTimePos(minutesFromStart);

if (!currentTimeRef.current || scrolledIntoView) return;
if (!scrollToCurrentTime || !currentTimeRef.current || scrolledIntoView) return;
// Within a small timeout so element has time to render.
setTimeout(() => {
// Doesn't seem to cause any issue. Put it under condition if needed
currentTimeRef?.current?.scrollIntoView({ block: "center" });
setScrolledIntoView(true);
}, 100);
}, [startHour, endHour, scrolledIntoView, timezone]);
}, [startHour, endHour, scrolledIntoView, timezone, scrollToCurrentTime]);

return (
<div
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export const SchedulerColumns = React.forwardRef<HTMLOListElement, Props>(functi
return (
<ol
ref={ref}
className="scheduler-grid-row-template col-start-1 col-end-2 row-start-1 grid auto-cols-auto text-[0px] sm:pr-8"
className="scheduler-grid-row-template col-start-1 col-end-2 row-start-1 grid auto-cols-auto text-[0px]"
style={{ marginTop: offsetHeight || "var(--gridDefaultSize)", zIndex }}
data-gridstopsperday={gridStopsPerDay}>
{children}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export const VerticalLines = ({ days, borderColor }: { days: dayjs.Dayjs[]; bord
return (
<div
className={classNames(
"pointer-events-none relative z-[60] col-start-1 col-end-2 row-start-1 grid auto-cols-auto grid-rows-1 divide-x sm:pr-8",
"pointer-events-none relative z-[60] col-start-1 col-end-2 row-start-1 grid auto-cols-auto grid-rows-1 divide-x",
borderColor === "subtle" ? "divide-subtle" : "divide-default"
)}
dir={direction}
Expand Down
Loading
Loading