From 1467af253368b5ed67ff2c261e5ed058a92e6e61 Mon Sep 17 00:00:00 2001 From: Jean-Franck Ballet Date: Mon, 22 Sep 2025 17:45:38 +0200 Subject: [PATCH] feat(rangeCalendar): allow to customise the behavior when the pointer is released outside of the component --- .../calendar/src/useRangeCalendar.ts | 18 +++++++++++++----- packages/@react-stately/calendar/src/types.ts | 4 +++- .../calendar/src/useRangeCalendarState.ts | 6 +++++- packages/@react-types/calendar/src/index.d.ts | 18 +++++++++++++++--- 4 files changed, 36 insertions(+), 10 deletions(-) diff --git a/packages/@react-aria/calendar/src/useRangeCalendar.ts b/packages/@react-aria/calendar/src/useRangeCalendar.ts index 87695d4268d..fdecdde3d1f 100644 --- a/packages/@react-aria/calendar/src/useRangeCalendar.ts +++ b/packages/@react-aria/calendar/src/useRangeCalendar.ts @@ -22,7 +22,8 @@ import {useRef} from 'react'; * A range calendar displays one or more date grids and allows users to select a contiguous range of dates. */ export function useRangeCalendar(props: AriaRangeCalendarProps, state: RangeCalendarState, ref: RefObject): CalendarAria { - let res = useCalendarBase(props, state); + let {pointerUpOutsideAction = 'select', ...otherProps} = props; + let res = useCalendarBase(otherProps, state); // We need to ignore virtual pointer events from VoiceOver due to these bugs. // https://bugs.webkit.org/show_bug.cgi?id=222627 @@ -36,7 +37,13 @@ export function useRangeCalendar(props: AriaRangeCalendarPr isVirtualClick.current = e.width === 0 && e.height === 0; }); - // Stop range selection when pressing or releasing a pointer outside the calendar body, + const pointerUpOutsideActionMapping = { + clear: () => state.clearSelection(), + reset: () => state.setAnchorDate(null), + select: () => state.selectFocusedDate() + }; + + // Execute method corresponding to `pointerUpOutsideAction` when pressing or releasing a pointer outside the calendar body, // except when pressing the next or previous buttons to switch months. let endDragging = (e: PointerEvent) => { if (isVirtualClick.current) { @@ -55,19 +62,20 @@ export function useRangeCalendar(props: AriaRangeCalendarPr ref.current.contains(document.activeElement) && (!ref.current.contains(target) || !target.closest('button, [role="button"]')) ) { - state.selectFocusedDate(); + pointerUpOutsideActionMapping[pointerUpOutsideAction](); } }; useEvent(windowRef, 'pointerup', endDragging); - // Also stop range selection on blur, e.g. tabbing away from the calendar. + // Also execute method corresponding to `pointerUpOutsideAction` on blur, + // e.g. tabbing away from the calendar. res.calendarProps.onBlur = e => { if (!ref.current) { return; } if ((!e.relatedTarget || !ref.current.contains(e.relatedTarget)) && state.anchorDate) { - state.selectFocusedDate(); + pointerUpOutsideActionMapping[pointerUpOutsideAction](); } }; diff --git a/packages/@react-stately/calendar/src/types.ts b/packages/@react-stately/calendar/src/types.ts index 4fbf3c3fe60..fd1c27ee583 100644 --- a/packages/@react-stately/calendar/src/types.ts +++ b/packages/@react-stately/calendar/src/types.ts @@ -122,5 +122,7 @@ export interface RangeCalendarState extends CalendarStateBase { /** Whether the user is currently dragging over the calendar. */ readonly isDragging: boolean, /** Sets whether the user is dragging over the calendar. */ - setDragging(isDragging: boolean): void + setDragging(isDragging: boolean): void, + /** Clears the current selection. */ + clearSelection(): void } diff --git a/packages/@react-stately/calendar/src/useRangeCalendarState.ts b/packages/@react-stately/calendar/src/useRangeCalendarState.ts index e57623fe57c..c68896cb9e4 100644 --- a/packages/@react-stately/calendar/src/useRangeCalendarState.ts +++ b/packages/@react-stately/calendar/src/useRangeCalendarState.ts @@ -191,7 +191,11 @@ export function useRangeCalendarState(props: Ra return calendar.isInvalid(date) || isInvalid(date, availableRangeRef.current?.start, availableRangeRef.current?.end); }, isDragging, - setDragging + setDragging, + clearSelection() { + setAnchorDate(null); + setValue(null); + } }; } diff --git a/packages/@react-types/calendar/src/index.d.ts b/packages/@react-types/calendar/src/index.d.ts index 53df3c72ce1..813758d2f8d 100644 --- a/packages/@react-types/calendar/src/index.d.ts +++ b/packages/@react-types/calendar/src/index.d.ts @@ -67,8 +67,8 @@ export interface CalendarPropsBase { * The day that starts the week. */ firstDayOfWeek?: 'sun' | 'mon' | 'tue' | 'wed' | 'thu' | 'fri' | 'sat', - /** - * Determines the alignment of the visible months on initial render based on the current selection or current date if there is no selection. + /** + * Determines the alignment of the visible months on initial render based on the current selection or current date if there is no selection. * @default 'center' */ selectionAlignment?: 'start' | 'center' | 'end' @@ -86,7 +86,19 @@ export interface RangeCalendarProps extends CalendarPropsBa export interface AriaCalendarProps extends CalendarProps, DOMProps, AriaLabelingProps {} -export interface AriaRangeCalendarProps extends RangeCalendarProps, DOMProps, AriaLabelingProps {} +export interface AriaRangeCalendarProps extends RangeCalendarProps, DOMProps, AriaLabelingProps { + /** + * Controls the behavior when a pointer is released outside the calendar: + * + * - `clear`: clear the currently selected range of dates. + * + * - `reset`: reset the selection to the previously selected range of dates. + * + * - `select`: select the currently hovered range of dates. + * @default 'select' + */ + pointerUpOutsideAction?: 'clear' | 'reset' | 'select' +} export type PageBehavior = 'single' | 'visible';