-
Couldn't load subscription status.
- Fork 1.3k
feat: Add CalendarCarousel component #9079
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
| {date => <CalendarCell date={date} />} | ||
| </CalendarGridBody> | ||
| </CalendarGrid> | ||
| <CalendarCarousel> (optional) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we show that this component is optional? I guess we don't do this anywhere else, but I'm not sure we have other optional components that accept children?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this could make sense for Virtualizer in some examples, e.g. Virtualized Select, to illustrate where the Virtualizer goes.
| {/* contain: 'inline-size' makes these extra pages not affect the width of the parent */} | ||
| <div inert style={{width: '100%', flexShrink: 0, contain: 'inline-size', scrollSnapAlign: 'start', scrollSnapStop: 'always'}}> | ||
| <CalendarGridContext.Provider value={{offset: {months: -state.visibleDuration.months!}}}> | ||
| {props.children} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note that this duplicates the children 3 times, and uses context to offset the months
| * can be keyboard navigated and selected by the user. | ||
| */ | ||
| export const CalendarGrid = /*#__PURE__*/ (forwardRef as forwardRefType)(function CalendarGrid(props: CalendarGridProps, ref: ForwardedRef<HTMLTableElement>) { | ||
| // Merge offset from context with props. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Weird? Otherwise I have to add a render prop function to the CalendarCarousel, pass in the offset, and make the user apply it manually.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We have precedence of doing something similar to this for layoutOptions, so I think fine 👍
|
Build successful! 🎉 |
|
Build successful! 🎉 |
## API Changes
react-aria-components/react-aria-components:CalendarState CalendarState {
focusNextDay: () => void
focusNextPage: () => void
focusNextRow: () => void
focusNextSection: (boolean) => void
focusPreviousDay: () => void
focusPreviousPage: () => void
focusPreviousRow: () => void
focusPreviousSection: (boolean) => void
focusSectionEnd: () => void
focusSectionStart: () => void
focusedDate: CalendarDate
getDatesInWeek: (number, CalendarDate) => Array<CalendarDate | null>
isCellDisabled: (CalendarDate) => boolean
isCellFocused: (CalendarDate) => boolean
isCellUnavailable: (CalendarDate) => boolean
isDisabled: boolean
isFocused: boolean
isInvalid: (CalendarDate) => boolean
isNextVisibleRangeInvalid: () => boolean
isPreviousVisibleRangeInvalid: () => boolean
isReadOnly: boolean
isSelected: (CalendarDate) => boolean
isValueInvalid: boolean
maxValue?: DateValue | null
minValue?: DateValue | null
selectDate: (CalendarDate) => void
selectFocusedDate: () => void
setFocused: (boolean) => void
- setFocusedDate: (CalendarDate) => void
+ setFocusedDate: (CalendarDate, 'start' | 'center' | 'end') => void
setValue: (CalendarDate | null) => void
timeZone: string
value: CalendarDate | null
+ visibleDuration: DateDuration
visibleRange: RangeValue<CalendarDate>
}/react-aria-components:RangeCalendarState RangeCalendarState <T extends DateValue = DateValue> {
anchorDate: CalendarDate | null
focusNextDay: () => void
focusNextPage: () => void
focusNextRow: () => void
focusNextSection: (boolean) => void
focusPreviousDay: () => void
focusPreviousPage: () => void
focusPreviousRow: () => void
focusPreviousSection: (boolean) => void
focusSectionEnd: () => void
focusSectionStart: () => void
focusedDate: CalendarDate
getDatesInWeek: (number, CalendarDate) => Array<CalendarDate | null>
highlightDate: (CalendarDate) => void
highlightedRange: RangeValue<CalendarDate> | null
isCellDisabled: (CalendarDate) => boolean
isCellFocused: (CalendarDate) => boolean
isCellUnavailable: (CalendarDate) => boolean
isDisabled: boolean
isDragging: boolean
isFocused: boolean
isInvalid: (CalendarDate) => boolean
isNextVisibleRangeInvalid: () => boolean
isPreviousVisibleRangeInvalid: () => boolean
isReadOnly: boolean
isSelected: (CalendarDate) => boolean
isValueInvalid: boolean
maxValue?: DateValue | null
minValue?: DateValue | null
selectDate: (CalendarDate) => void
selectFocusedDate: () => void
setAnchorDate: (CalendarDate | null) => void
setDragging: (boolean) => void
setFocused: (boolean) => void
- setFocusedDate: (CalendarDate) => void
+ setFocusedDate: (CalendarDate, 'start' | 'center' | 'end') => void
setValue: (RangeValue<DateValue> | null) => void
timeZone: string
value: RangeValue<DateValue> | null
+ visibleDuration: DateDuration
visibleRange: RangeValue<CalendarDate>
}/react-aria-components:CalendarCarousel+CalendarCarousel {
+ children: ReactNode
+ className?: string = 'react-aria-CalendarCarousel'
+ style?: CSSProperties
+}/react-aria-components:CalendarCarouselProps+CalendarCarouselProps {
+ children: ReactNode
+ className?: string = 'react-aria-CalendarCarousel'
+ style?: CSSProperties
+}@react-stately/calendar/@react-stately/calendar:CalendarState CalendarState {
focusNextDay: () => void
focusNextPage: () => void
focusNextRow: () => void
focusNextSection: (boolean) => void
focusPreviousDay: () => void
focusPreviousPage: () => void
focusPreviousRow: () => void
focusPreviousSection: (boolean) => void
focusSectionEnd: () => void
focusSectionStart: () => void
focusedDate: CalendarDate
getDatesInWeek: (number, CalendarDate) => Array<CalendarDate | null>
isCellDisabled: (CalendarDate) => boolean
isCellFocused: (CalendarDate) => boolean
isCellUnavailable: (CalendarDate) => boolean
isDisabled: boolean
isFocused: boolean
isInvalid: (CalendarDate) => boolean
isNextVisibleRangeInvalid: () => boolean
isPreviousVisibleRangeInvalid: () => boolean
isReadOnly: boolean
isSelected: (CalendarDate) => boolean
isValueInvalid: boolean
maxValue?: DateValue | null
minValue?: DateValue | null
selectDate: (CalendarDate) => void
selectFocusedDate: () => void
setFocused: (boolean) => void
- setFocusedDate: (CalendarDate) => void
+ setFocusedDate: (CalendarDate, 'start' | 'center' | 'end') => void
setValue: (CalendarDate | null) => void
timeZone: string
value: CalendarDate | null
+ visibleDuration: DateDuration
visibleRange: RangeValue<CalendarDate>
}/@react-stately/calendar:RangeCalendarState RangeCalendarState <T extends DateValue = DateValue> {
anchorDate: CalendarDate | null
focusNextDay: () => void
focusNextPage: () => void
focusNextRow: () => void
focusNextSection: (boolean) => void
focusPreviousDay: () => void
focusPreviousPage: () => void
focusPreviousRow: () => void
focusPreviousSection: (boolean) => void
focusSectionEnd: () => void
focusSectionStart: () => void
focusedDate: CalendarDate
getDatesInWeek: (number, CalendarDate) => Array<CalendarDate | null>
highlightDate: (CalendarDate) => void
highlightedRange: RangeValue<CalendarDate> | null
isCellDisabled: (CalendarDate) => boolean
isCellFocused: (CalendarDate) => boolean
isCellUnavailable: (CalendarDate) => boolean
isDisabled: boolean
isDragging: boolean
isFocused: boolean
isInvalid: (CalendarDate) => boolean
isNextVisibleRangeInvalid: () => boolean
isPreviousVisibleRangeInvalid: () => boolean
isReadOnly: boolean
isSelected: (CalendarDate) => boolean
isValueInvalid: boolean
maxValue?: DateValue | null
minValue?: DateValue | null
selectDate: (CalendarDate) => void
selectFocusedDate: () => void
setAnchorDate: (CalendarDate | null) => void
setDragging: (boolean) => void
setFocused: (boolean) => void
- setFocusedDate: (CalendarDate) => void
+ setFocusedDate: (CalendarDate, 'start' | 'center' | 'end') => void
setValue: (RangeValue<DateValue> | null) => void
timeZone: string
value: RangeValue<DateValue> | null
+ visibleDuration: DateDuration
visibleRange: RangeValue<CalendarDate>
} |
| clearTimeout(timeout.current); | ||
| timeout.current = setTimeout(() => { | ||
| let index = el.scrollLeft / el.offsetWidth; | ||
| if (Math.abs(Math.round(index) - index) < 0.01) { | ||
| dispatch({type: 'SCROLL_END', visibleMonths: state.visibleDuration.months!}); | ||
| } | ||
| }, 500); | ||
| }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We could probably work the same performance optimization here as in useScrollView, aka. rescheduling only when getting close to the end of the interval?
(Opening for team discussion)
This started as an idea for a docs example: a calendar where you could swipe to navigate between months like the native iOS calendar. It is possible to build today with our existing API: https://stackblitz.com/edit/rac-swipeable-calendar?file=src%2FCalendar.tsx. But then I thought: why not just add this as a built-in component?
The new
CalendarCarouselcomponent (name TBD) uses CSS scroll snapping to allow native swipe gestures between months. The current month is centered in the viewport, with one extra month on either side, along with a placeholder div that takes up a large number of pages.As you swipe, the months update and the placeholder shrinks giving the illusion of infinite pages. When you stop scrolling, the scroll position is re-centered and the placeholders become equal size again. This avoids changing the scroll positioning while the user is scrolling, which causes jumpiness. The placeholders are large to make it unlikely that the user will reach the end without stopping.
Questions