Files
calendar/apps/web/modules/bookings/lib/bookingSheetKeyboardHandler.ts
T
Peer RichelsenGitHubDevin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>Dhairyashileunjae@cal.com <hey@eunjae.dev>Dhairyashil Shinde
60b6086221 feat: add keyboard shortcuts and tooltips to booking slideover buttons (#27428)
* feat: add keyboard shortcuts and tooltips to booking slideover buttons

Co-Authored-By: peer@cal.com <peer@cal.com>

* fix: remove duplicate useBookingLocation import and fix import ordering

- Removed duplicate import of useBookingLocation from non-existent
  @calcom/web/modules/bookings/hooks/useBookingLocation path
- Fixed import ordering to satisfy biome organizeImports rules
- Removed unnecessary code comment
- Original feature by @PeerRich via Devin AI

Co-Authored-By: unknown <>

* use same style arrows for both button and remove bydefault focus from them

* fix join button tooltip hover

* fix: disable keyboard shortcuts when overlays/dialogs are open on BookingDetailsSheet

Co-Authored-By: unknown <>

* fix: use focus-based detection instead of selector-based overlay detection for keyboard shortcuts

Co-Authored-By: unknown <>

* fix: allow keyboard shortcuts when focus is on sheet ancestors

Co-Authored-By: unknown <>

* fix: use capture phase for keyboard handler to prevent Enter from activating focused buttons

Co-Authored-By: unknown <>

* fix: allow shortcuts when focus is on page elements outside any Radix portal

Co-Authored-By: unknown <>

* fix: handle calendar event clicks in onInteractOutside to prevent sheet close/reopen

Co-Authored-By: unknown <>

* fix: stop arrow key propagation to prevent Radix dropdown from opening during booking navigation

Co-Authored-By: eunjae@cal.com <hey@eunjae.dev>

* fix: always stop arrow key propagation when sheet is active, even at first/last booking

Co-Authored-By: eunjae@cal.com <hey@eunjae.dev>

* refactor: extract keyboard handler into testable utility with tests

Co-Authored-By: eunjae@cal.com <hey@eunjae.dev>

* fix: resolve type errors in keyboard handler config and test mocks

Co-Authored-By: eunjae@cal.com <hey@eunjae.dev>

* test: add e2e tests for booking sheet keyboard shortcuts

Co-Authored-By: eunjae@cal.com <hey@eunjae.dev>

* fix: replace text= locators with data-testid selectors in e2e tests

Co-Authored-By: eunjae@cal.com <hey@eunjae.dev>

* made 3 fixes: Fix 1 — Stabilize handleNext/handlePrevious/handleClose with useCallback
In BookingDetailsSheet.tsx, all three handler functions were plain arrow functions recreated on every render, causing the useEffect to tear down and re-attach the document keydown listener unnecessarily. Wrapped all three in useCallback with proper dependency arrays (the Zustand store functions they call).
Fix 2 — data-booking-list-item verified (no change needed)
Confirmed that data-booking-list-item is rendered on BookingListItem.tsx and data-booking-calendar-event is rendered on Event.tsx. The onInteractOutside handler in the final merged state correctly checks both selectors. No code change required.
Fix 3 — Removed dead code from JoinMeetingButton
Reverted JoinMeetingButton back to a plain function component:
Removed forwardRef wrapping (no caller passes a ref)
Removed showTooltip prop (unused — tooltip is handled by the parent BookingDetailsSheet)
Removed ref prop from the inner Button
Removed unused Tod forwardRef imports

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Dhairyashil <dhairyashil10101010@gmail.com>
Co-authored-by: eunjae@cal.com <hey@eunjae.dev>
Co-authored-by: Dhairyashil Shinde <93669429+dhairyashiil@users.noreply.github.com>
2026-02-19 22:32:43 +05:30

72 lines
1.8 KiB
TypeScript

export interface BookingSheetKeyboardConfig {
isSheetActive: () => boolean;
canGoPrev: boolean | undefined;
canGoNext: boolean | undefined;
isTransitioning: boolean;
handlePrevious: () => void;
handleNext: () => void;
getJoinLink: () => HTMLAnchorElement | null | undefined;
}
export function isEditableTarget(target: EventTarget | null): boolean {
if (target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement) {
return true;
}
if (target instanceof HTMLElement) {
return target.contentEditable === "true";
}
return false;
}
export function checkSheetActive(
sheetContent: HTMLElement | null,
activeElement: Element | null
): boolean {
if (!activeElement || activeElement === document.body) {
return true;
}
if (!sheetContent) return false;
if (sheetContent.contains(activeElement)) {
return true;
}
return !activeElement.closest("[data-radix-portal]");
}
export function createBookingSheetKeydownHandler(config: BookingSheetKeyboardConfig) {
return (e: KeyboardEvent) => {
if (isEditableTarget(e.target)) {
return;
}
if (!config.isSheetActive()) {
return;
}
switch (e.key) {
case "ArrowUp":
e.preventDefault();
e.stopPropagation();
if (config.canGoPrev && !config.isTransitioning) {
config.handlePrevious();
}
break;
case "ArrowDown":
e.preventDefault();
e.stopPropagation();
if (config.canGoNext && !config.isTransitioning) {
config.handleNext();
}
break;
case "Enter": {
const joinLink = config.getJoinLink();
if (joinLink) {
e.preventDefault();
e.stopPropagation();
joinLink.click();
}
break;
}
}
};
}