From 2997d8ac915416a8c75b40f78f13fe02f00264e1 Mon Sep 17 00:00:00 2001 From: anton-patrushev Date: Thu, 27 Nov 2025 07:49:02 +0100 Subject: [PATCH 01/10] feat: resources view with scroll-by-day support - snap each resource - has some bugs with working/unavailable hours --- .../src/CalendarBody.tsx | 11 +- .../src/CalendarContainer.tsx | 129 ++++++++++++++++-- .../src/CalendarHeader.tsx | 3 + .../src/components/BodyResourceItem.tsx | 43 ++++-- .../src/components/Resource/ResourceBoard.tsx | 2 +- .../Resource/ResourceContainers.tsx | 49 ++++++- .../components/Resource/ResourceListView.tsx | 106 +++++++++++--- .../src/context/CalendarProvider.tsx | 4 + .../src/context/EventsProvider.tsx | 19 ++- 9 files changed, 308 insertions(+), 58 deletions(-) diff --git a/packages/react-native-calendar-kit/src/CalendarBody.tsx b/packages/react-native-calendar-kit/src/CalendarBody.tsx index 91d3264e..8918ce5c 100644 --- a/packages/react-native-calendar-kit/src/CalendarBody.tsx +++ b/packages/react-native-calendar-kit/src/CalendarBody.tsx @@ -100,6 +100,8 @@ const CalendarBody: React.FC = ({ resourcePerPage, resourcePagingEnabled, linkedScrollGroup, + dateResourceItems, + handleResourceScrollOffsetChange, } = useCalendar(); const { onTouchStart, onWheel } = linkedScrollGroup.addAndGet( ScrollType.calendarGrid, @@ -195,9 +197,11 @@ const CalendarBody: React.FC = ({ const _renderResourceItem = useCallback( (item: { items: ResourceItem[]; index: number }) => { - return ; + // In dual-axis mode, get the date for this specific item + const dateUnix = dateResourceItems?.[item.index]?.date; + return ; }, - [] + [dateResourceItems] ); const value = useMemo( @@ -360,6 +364,7 @@ const CalendarBody: React.FC = ({ = ({ scrollEnabled={allowHorizontalSwipe} onTouchStart={onTouchStart} onWheel={onWheel} + onScrollOffsetChange={handleResourceScrollOffsetChange} + initialOffset={initialOffset} /> ) : ( daysToShow - ? daysToShow - : initialNumberOfDays; + const numberOfDays = + isResourceMode && !enableResourceScroll + ? 1 + : initialNumberOfDays > daysToShow + ? daysToShow + : initialNumberOfDays; const isSingleDay = numberOfDays === 1; const columns = isSingleDay ? 1 : daysToShow; @@ -276,7 +277,14 @@ const CalendarContainer: React.ForwardRefRenderFunction< } const nearestIndex = nearestDate.index; if (isSingleDay || scrollByDay) { - const colWidth = isSingleDay ? calendarGridWidth : columnWidth; + let colWidth = isSingleDay ? calendarGridWidth : columnWidth; + + // For resource mode with enableResourceScroll, calculate day width + if (isResourceMode && enableResourceScroll && resources) { + const resourceWidth = calendarGridWidth / resourcePerPage; + colWidth = resources.length * resourceWidth; + } + return nearestIndex * colWidth; } @@ -290,12 +298,14 @@ const CalendarContainer: React.ForwardRefRenderFunction< isSingleDay, scrollByDay, visibleDateUnix, + isResourceMode, + enableResourceScroll, + resources, + resourcePerPage, ]); const offsetY = useSharedValue(0); - const offsetX = useSharedValue( - isResourceMode && enableResourceScroll ? 0 : initialOffset - ); + const offsetX = useSharedValue(initialOffset); const linkedScrollGroup = useLinkedScrollGroup(offsetX); const scrollVisibleHeightAnim = useSharedValue(0); const timeIntervalHeight = useSharedValue(initialTimeIntervalHeight); @@ -730,12 +740,97 @@ const CalendarContainer: React.ForwardRefRenderFunction< useImperativeHandle(ref, () => calendarMethods, [calendarMethods]); useEffect(() => { - if (enableResourceScroll && isResourceMode) { - offsetX.value = 0; - } else { - offsetX.value = initialOffset; + offsetX.value = initialOffset; + }, [initialOffset, offsetX]); + + const dateResourceItems = useMemo(() => { + if (!enableResourceScroll || !isResourceMode || !resources) { + return undefined; + } + + const visibleDatesArray = calendarData.visibleDatesArray; + const items = visibleDatesArray.flatMap((date) => + resources.map((resource) => ({ date, resource })) + ); + + return items; + }, [enableResourceScroll, isResourceMode, resources, calendarData]); + + const daySnapOffsets = useMemo(() => { + if (!enableResourceScroll || !isResourceMode || !resources) { + return undefined; + } + + const visibleDatesArray = calendarData.visibleDatesArray; + const resourceWidth = calendarGridWidth / resourcePerPage; + const dayWidth = resources.length * resourceWidth; + + const offsets = visibleDatesArray.map((_, i) => i * dayWidth); + + return offsets; + }, [ + enableResourceScroll, + isResourceMode, + resources, + calendarData, + calendarGridWidth, + resourcePerPage, + ]); + + const handleResourceScrollOffsetChange = useLatestCallback( + (scrollOffset: number) => { + if ( + !enableResourceScroll || + !isResourceMode || + !resources || + !dateResourceItems + ) { + return; + } + + const resourceWidth = calendarGridWidth / resourcePerPage; + const viewportStart = scrollOffset; + const viewportEnd = scrollOffset + calendarGridWidth; + + // Calculate which resource items are visible + // Use Math.floor for start, but subtract 0.5 from end to avoid counting items at exact boundary + const startItemIndex = Math.floor(viewportStart / resourceWidth); + const endItemIndex = Math.floor((viewportEnd - 0.5) / resourceWidth); + + // Count visible resources per day + const dayCounts = new Map(); + for ( + let i = startItemIndex; + i <= Math.min(endItemIndex, dateResourceItems.length - 1); + i++ + ) { + const item = dateResourceItems[i]; + if (item) { + const count = dayCounts.get(item.date) || 0; + dayCounts.set(item.date, count + 1); + } + } + + // Select the latest date that has at least one visible resource + const visibleDates = Array.from(dayCounts.keys()).sort((a, b) => a - b); + const activeDayUnix = + visibleDates.length > 0 + ? visibleDates[visibleDates.length - 1] + : visibleDateUnix.current; + + if (activeDayUnix && activeDayUnix !== visibleDateUnix.current) { + visibleDateUnix.current = activeDayUnix; + visibleDateUnixAnim.value = activeDayUnix; + visibleDateRef.current?.updateVisibleDate(activeDayUnix); + + const dateObj = forceUpdateZone(activeDayUnix, timeZone); + const newDate = dateTimeToISOString(dateObj); + + onDateChanged?.(newDate); + onChange?.(newDate); + } } - }, [enableResourceScroll, initialOffset, isResourceMode, offsetX]); + ); const snapToInterval = numberOfDays > 1 && scrollByDay && !isResourceMode @@ -798,6 +893,9 @@ const CalendarContainer: React.ForwardRefRenderFunction< resourcePerPage, resourcePagingEnabled, linkedScrollGroup, + dateResourceItems, + daySnapOffsets, + handleResourceScrollOffsetChange, }), [ calendarLayout, @@ -852,6 +950,9 @@ const CalendarContainer: React.ForwardRefRenderFunction< resourcePerPage, resourcePagingEnabled, linkedScrollGroup, + dateResourceItems, + daySnapOffsets, + handleResourceScrollOffsetChange, ] ); diff --git a/packages/react-native-calendar-kit/src/CalendarHeader.tsx b/packages/react-native-calendar-kit/src/CalendarHeader.tsx index b3e564f6..28388274 100644 --- a/packages/react-native-calendar-kit/src/CalendarHeader.tsx +++ b/packages/react-native-calendar-kit/src/CalendarHeader.tsx @@ -71,6 +71,7 @@ const CalendarHeader: React.FC = ({ resourcePerPage, resourcePagingEnabled, linkedScrollGroup, + dateResourceItems, } = useCalendar(); const { onTouchStart, onWheel } = linkedScrollGroup.addAndGet( ScrollType.dayBar, @@ -390,6 +391,7 @@ const CalendarHeader: React.FC = ({ = ({ scrollEnabled={allowHorizontalSwipe} onTouchStart={onTouchStart} onWheel={onWheel} + initialOffset={initialOffset} /> ) : ( { - const { spaceFromTop, hourWidth, timelineHeight, spaceFromBottom } = +const BodyResourceItem = ({ resources, dateUnix }: BodyResourceItemProps) => { + const { spaceFromTop, hourWidth, timelineHeight, spaceFromBottom, calendarData } = useBody(); - const visibleDateUnix = useDateChangedListener(); + const globalVisibleDateUnix = useDateChangedListener(); + + // Use provided dateUnix (dual-axis mode) or global visibleDateUnix (regular resource mode) + const visibleDateUnix = dateUnix ?? globalVisibleDateUnix; + + // Build visibleDates for prev, current, next days + const visibleDates = useMemo(() => { + const visibleDatesArray = calendarData.visibleDatesArray; + const currentIndex = visibleDatesArray.indexOf(visibleDateUnix); + + const data: Record = {}; + let diffDays = 1; + + // Show prev, current, next days + for (let i = -1; i <= 1; i++) { + const index = currentIndex + i; + if (index >= 0 && index < visibleDatesArray.length) { + const unix = visibleDatesArray[index]; + data[unix] = { + unix, + diffDays, + }; + diffDays += 1; + } + } + + return data; + }, [visibleDateUnix, calendarData.visibleDatesArray]); const height = useDerivedValue(() => { return timelineHeight.value - spaceFromTop - spaceFromBottom; @@ -44,12 +72,7 @@ const BodyResourceItem = ({ resources }: BodyResourceItemProps) => { ]}> diff --git a/packages/react-native-calendar-kit/src/components/Resource/ResourceBoard.tsx b/packages/react-native-calendar-kit/src/components/Resource/ResourceBoard.tsx index 94e003c1..5f6211fa 100644 --- a/packages/react-native-calendar-kit/src/components/Resource/ResourceBoard.tsx +++ b/packages/react-native-calendar-kit/src/components/Resource/ResourceBoard.tsx @@ -163,7 +163,7 @@ const ResourceBoard = ({ resources }: ResourceBoardProps) => { {_renderHorizontalLines} - {resources.length > 1 && _renderVerticalLines} + {!!resources?.length && _renderVerticalLines} ); }; diff --git a/packages/react-native-calendar-kit/src/components/Resource/ResourceContainers.tsx b/packages/react-native-calendar-kit/src/components/Resource/ResourceContainers.tsx index 421ef475..57198853 100644 --- a/packages/react-native-calendar-kit/src/components/Resource/ResourceContainers.tsx +++ b/packages/react-native-calendar-kit/src/components/Resource/ResourceContainers.tsx @@ -1,9 +1,11 @@ import React, { useMemo } from 'react'; import { View } from 'react-native'; import { ResourceItem } from '../../types'; +import { DateResourceItem } from './ResourceListView'; interface ResourceContainerProps { resources?: ResourceItem[]; + items?: DateResourceItem[]; renderItem: (item: { items: ResourceItem[]; index: number; @@ -18,6 +20,7 @@ interface ResourceContainerProps { export const ResourceContainer = React.memo( ({ resources, + items, renderItem, itemSize, visibleRange, @@ -26,14 +29,47 @@ export const ResourceContainer = React.memo( getItemPosition, }: ResourceContainerProps) => { const renderItems = useMemo(() => { - const items: React.ReactNode[] = []; + const renderedItems: React.ReactNode[] = []; + const firstVisiblePosition = getItemPosition(visibleRange.start); + + // Dual-axis mode: items provided (date+resource pairs) + // Each item is rendered individually, one resource per position + if (items) { + for ( + let itemIndex = visibleRange.start; + itemIndex <= Math.min(visibleRange.end, items.length - 1); + itemIndex++ + ) { + const item = items[itemIndex]; + const absolutePosition = getItemPosition(itemIndex); + const relativePosition = absolutePosition - firstVisiblePosition; + const key = `item-${itemIndex}`; + + renderedItems.push( + + {renderItem({ + items: [item.resource], + index: itemIndex, + })} + + ); + } + return renderedItems; + } + + // Regular mode: resources grouped into pages if (!resources) { - return items; + return renderedItems; } - const firstVisiblePosition = getItemPosition(visibleRange.start); const pageCount = Math.ceil(resources.length / resourcePerPage); - for ( let pageIndex = visibleRange.start; pageIndex <= Math.min(visibleRange.end, pageCount - 1); @@ -51,7 +87,7 @@ export const ResourceContainer = React.memo( const relativePosition = absolutePosition - firstVisiblePosition; const key = `page-${pageIndex}`; - items.push( + renderedItems.push( ; width: number; height: number; onScroll?: (event: NativeSyntheticEvent) => void; resources?: ResourceItem[]; + items?: DateResourceItem[]; resourcePerPage: number; drawDistance?: number; renderItem: (item: { @@ -45,6 +51,9 @@ export interface ResourceListViewProps { onTouchStart?: (event: GestureResponderEvent) => void; scrollEventThrottle?: number; onWheel?: (event: WheelEvent) => void; + snapToOffsets?: number[]; + onScrollOffsetChange?: (offset: number) => void; + initialOffset?: number; } export interface ResourceListViewRef { @@ -60,6 +69,7 @@ const ResourceListView = forwardRef( height, onScroll, resources, + items, resourcePerPage, drawDistance = width * 2, renderItem, @@ -69,17 +79,31 @@ const ResourceListView = forwardRef( onTouchStart, scrollEventThrottle = 16, onWheel, + snapToOffsets, + onScrollOffsetChange, + initialOffset = 0, }, ref ) => { const [viewportWidth, setViewportWidth] = useState(0); - const [scrollOffset, setScrollOffset] = useState(0); + const [scrollOffset, setScrollOffset] = useState(initialOffset); const scrollTimeoutRef = useRef(null); + const scrollViewRef = useRef(null); - const count = Math.ceil((resources?.length ?? 0) / resourcePerPage); + // Dual-axis mode (items provided): each item is a separate resource + // Regular mode (resources only): items grouped into pages + const isDualAxisMode = !!items; - const totalSize = (resources?.length ?? 0) * (width / resourcePerPage); - const snapToInterval = width / resourcePerPage; + const itemsLength = items?.length ?? resources?.length ?? 0; + const count = isDualAxisMode + ? itemsLength + : Math.ceil(itemsLength / resourcePerPage); + + const itemWidth = width / resourcePerPage; + const totalSize = isDualAxisMode + ? itemsLength * itemWidth + : itemsLength * itemWidth; + const snapToInterval = itemWidth; const visibleRange = useMemo(() => { if (viewportWidth === 0 || count === 0) { @@ -89,22 +113,38 @@ const ResourceListView = forwardRef( const buffer = drawDistance; const scrollStart = Math.max(0, scrollOffset - buffer); const scrollEnd = scrollOffset + viewportWidth + buffer; - const startIndex = Math.max(0, Math.floor(scrollStart / width)); - const endIndex = Math.min(count - 1, Math.floor(scrollEnd / width)); + + // In dual-axis mode, calculate range based on individual item width + // In regular mode, calculate based on page width + const positionWidth = isDualAxisMode ? itemWidth : width; + const startIndex = Math.max(0, Math.floor(scrollStart / positionWidth)); + const endIndex = Math.min(count - 1, Math.floor(scrollEnd / positionWidth)); return { start: startIndex, end: endIndex }; - }, [count, scrollOffset, viewportWidth, drawDistance, width]); + }, [ + count, + scrollOffset, + viewportWidth, + drawDistance, + width, + isDualAxisMode, + itemWidth, + ]); const animScrollRef = useAnimatedRef(); const scrollOffsetAnim = useScrollViewOffset(animScrollRef); - const throttledSetScrollOffset = useCallback((offset: number) => { - if (scrollTimeoutRef.current) { - clearTimeout(scrollTimeoutRef.current); - } - scrollTimeoutRef.current = setTimeout(() => { - setScrollOffset(offset); - }, 16); - }, []); + const throttledSetScrollOffset = useCallback( + (offset: number) => { + if (scrollTimeoutRef.current) { + clearTimeout(scrollTimeoutRef.current); + } + scrollTimeoutRef.current = setTimeout(() => { + setScrollOffset(offset); + onScrollOffsetChange?.(offset); + }, 16); + }, + [onScrollOffsetChange] + ); useAnimatedReaction( () => scrollOffsetAnim.value, @@ -121,6 +161,17 @@ const ResourceListView = forwardRef( }; }, []); + useEffect(() => { + if (initialOffset > 0 && scrollViewRef.current) { + setTimeout(() => { + scrollViewRef.current?.scrollTo({ + x: initialOffset, + animated: false, + }); + }, 0); + } + }, [initialOffset]); + const handleLayout = useCallback((event: LayoutChangeEvent) => { const { width: viewWidth } = event.nativeEvent.layout; setViewportWidth(viewWidth); @@ -128,15 +179,18 @@ const ResourceListView = forwardRef( const getItemPosition = useCallback( (index: number) => { - return index * width; + // In dual-axis mode, position each item individually + // In regular mode, position pages + return isDualAxisMode ? index * itemWidth : index * width; }, - [width] + [isDualAxisMode, itemWidth, width] ); return ( { if (node) { + scrollViewRef.current = node as any; if (typeof ref === 'function') { ref(node as any); } else if (ref) { @@ -150,9 +204,18 @@ const ResourceListView = forwardRef( onLayout={handleLayout} showsHorizontalScrollIndicator={false} showsVerticalScrollIndicator={false} - snapToInterval={pagingEnabled ? undefined : snapToInterval} - pagingEnabled={pagingEnabled} - disableIntervalMomentum={!pagingEnabled} + snapToOffsets={snapToOffsets} + snapToInterval={ + snapToOffsets + ? undefined + : isDualAxisMode + ? snapToInterval + : pagingEnabled + ? undefined + : snapToInterval + } + pagingEnabled={isDualAxisMode ? false : pagingEnabled} + disableIntervalMomentum={isDualAxisMode ? true : !pagingEnabled} scrollEnabled={scrollEnabled} scrollEventThrottle={scrollEventThrottle} onTouchStart={onTouchStart} @@ -161,8 +224,9 @@ const ResourceListView = forwardRef( void; } export const CalendarContext = React.createContext< diff --git a/packages/react-native-calendar-kit/src/context/EventsProvider.tsx b/packages/react-native-calendar-kit/src/context/EventsProvider.tsx index e1ad54ab..92635c54 100644 --- a/packages/react-native-calendar-kit/src/context/EventsProvider.tsx +++ b/packages/react-native-calendar-kit/src/context/EventsProvider.tsx @@ -302,10 +302,21 @@ export const useRegularEvents = ( const selectorByDate = useCallback( (state: EventsState) => { const data: PackedEvent[] = []; - const totalDays = numberOfDays === 1 ? 1 : 7; - for (let i = 0; i < totalDays; i++) { - const dateUnix = parseDateTime(date).plus({ days: i }).toMillis(); - if (visibleDays[dateUnix]) { + const visibleDaysKeys = Object.keys(visibleDays); + + // If we have explicit visible days, use them directly + if (visibleDaysKeys.length > 0) { + visibleDaysKeys.forEach((dateUnixStr) => { + const events = state.regularEvents[Number(dateUnixStr)]; + if (events) { + data.push(...events); + } + }); + } else { + // Fallback to original sequential logic + const totalDays = numberOfDays === 1 ? 1 : 7; + for (let i = 0; i < totalDays; i++) { + const dateUnix = parseDateTime(date).plus({ days: i }).toMillis(); const events = state.regularEvents[dateUnix]; if (events) { data.push(...events); From 94e9b1f0e896ffa3fa544d0f59578500a02bc714 Mon Sep 17 00:00:00 2001 From: anton-patrushev Date: Thu, 27 Nov 2025 11:39:40 +0100 Subject: [PATCH 02/10] feat: re-implement snapping for resources view mode - If all resources fit in one page, only snap at the start of the day - Snap at every resource position that keeps all visible resources within the current day - Last valid snap position is where the last resource of the day is at the right edge --- .../src/CalendarBody.tsx | 2 + .../src/CalendarContainer.tsx | 21 +++++++++- .../src/components/BodyResourceItem.tsx | 2 +- .../src/components/Resource/ResourceBoard.tsx | 5 ++- .../components/Resource/ResourceListView.tsx | 2 +- .../Resource/UnavailableHoursByResource.tsx | 40 ++++++++++++------- 6 files changed, 52 insertions(+), 20 deletions(-) diff --git a/packages/react-native-calendar-kit/src/CalendarBody.tsx b/packages/react-native-calendar-kit/src/CalendarBody.tsx index 8918ce5c..8a4bb12b 100644 --- a/packages/react-native-calendar-kit/src/CalendarBody.tsx +++ b/packages/react-native-calendar-kit/src/CalendarBody.tsx @@ -101,6 +101,7 @@ const CalendarBody: React.FC = ({ resourcePagingEnabled, linkedScrollGroup, dateResourceItems, + daySnapOffsets, handleResourceScrollOffsetChange, } = useCalendar(); const { onTouchStart, onWheel } = linkedScrollGroup.addAndGet( @@ -374,6 +375,7 @@ const CalendarBody: React.FC = ({ scrollEnabled={allowHorizontalSwipe} onTouchStart={onTouchStart} onWheel={onWheel} + snapToOffsets={daySnapOffsets} onScrollOffsetChange={handleResourceScrollOffsetChange} initialOffset={initialOffset} /> diff --git a/packages/react-native-calendar-kit/src/CalendarContainer.tsx b/packages/react-native-calendar-kit/src/CalendarContainer.tsx index a1423d36..5789dc5b 100644 --- a/packages/react-native-calendar-kit/src/CalendarContainer.tsx +++ b/packages/react-native-calendar-kit/src/CalendarContainer.tsx @@ -763,9 +763,26 @@ const CalendarContainer: React.ForwardRefRenderFunction< const visibleDatesArray = calendarData.visibleDatesArray; const resourceWidth = calendarGridWidth / resourcePerPage; - const dayWidth = resources.length * resourceWidth; + const resourcesCount = resources.length; - const offsets = visibleDatesArray.map((_, i) => i * dayWidth); + const offsets: number[] = []; + + visibleDatesArray.forEach((_, dayIndex) => { + const dayStartOffset = dayIndex * resourcesCount * resourceWidth; + + if (resourcesCount <= resourcePerPage) { + // If all resources fit in one page, only snap at the start of the day + offsets.push(dayStartOffset); + } else { + // Snap at every resource position that keeps all visible resources within the current day + // Last valid snap position is where the last resource of the day is at the right edge + const maxResourceStartIndex = resourcesCount - resourcePerPage; + + for (let i = 0; i <= maxResourceStartIndex; i++) { + offsets.push(dayStartOffset + i * resourceWidth); + } + } + }); return offsets; }, [ diff --git a/packages/react-native-calendar-kit/src/components/BodyResourceItem.tsx b/packages/react-native-calendar-kit/src/components/BodyResourceItem.tsx index cd53d7ff..49322631 100644 --- a/packages/react-native-calendar-kit/src/components/BodyResourceItem.tsx +++ b/packages/react-native-calendar-kit/src/components/BodyResourceItem.tsx @@ -59,7 +59,7 @@ const BodyResourceItem = ({ resources, dateUnix }: BodyResourceItemProps) => { return ( - + ; } -const ResourceBoard = ({ resources }: ResourceBoardProps) => { +const ResourceBoard = ({ resources, visibleDates }: ResourceBoardProps) => { const colors = useTheme((state) => state.colors); const { @@ -160,7 +161,7 @@ const ResourceBoard = ({ resources }: ResourceBoardProps) => { !onLongPressBackground } /> - + {_renderHorizontalLines} {!!resources?.length && _renderVerticalLines} diff --git a/packages/react-native-calendar-kit/src/components/Resource/ResourceListView.tsx b/packages/react-native-calendar-kit/src/components/Resource/ResourceListView.tsx index fd73b6b6..08667b2a 100644 --- a/packages/react-native-calendar-kit/src/components/Resource/ResourceListView.tsx +++ b/packages/react-native-calendar-kit/src/components/Resource/ResourceListView.tsx @@ -215,7 +215,7 @@ const ResourceListView = forwardRef( : snapToInterval } pagingEnabled={isDualAxisMode ? false : pagingEnabled} - disableIntervalMomentum={isDualAxisMode ? true : !pagingEnabled} + disableIntervalMomentum={snapToOffsets ? true : isDualAxisMode ? true : !pagingEnabled} scrollEnabled={scrollEnabled} scrollEventThrottle={scrollEventThrottle} onTouchStart={onTouchStart} diff --git a/packages/react-native-calendar-kit/src/components/Resource/UnavailableHoursByResource.tsx b/packages/react-native-calendar-kit/src/components/Resource/UnavailableHoursByResource.tsx index 773686e9..569af865 100644 --- a/packages/react-native-calendar-kit/src/components/Resource/UnavailableHoursByResource.tsx +++ b/packages/react-native-calendar-kit/src/components/Resource/UnavailableHoursByResource.tsx @@ -1,6 +1,4 @@ -import React from 'react'; -import { useUnavailableHoursByDate } from '../../context/UnavailableHoursProvider'; -import { useDateChangedListener } from '../../context/VisibleDateProvider'; +import React, { useCallback } from 'react'; import { ResourceItem } from '../../types'; import { UnavailableHoursByDate } from '../TimelineBoard/UnavailableHours'; import { StyleSheet, View } from 'react-native'; @@ -8,27 +6,41 @@ import { useBody } from '../../context/BodyContext'; interface UnavailableHoursByResourceProps { resources: ResourceItem[]; + visibleDates: Record; } const UnavailableHoursByResource = ({ resources, + visibleDates, }: UnavailableHoursByResourceProps) => { - const visibleDateUnix = useDateChangedListener(); - const unavailableHours = useUnavailableHoursByDate(visibleDateUnix); const { enableResourceScroll, resourcePerPage } = useBody(); - if (!unavailableHours) { - return null; - } + const _renderColumn = useCallback( + (currentUnix: string) => { + const dateInfo = visibleDates[currentUnix]; + + if (!dateInfo) { + return null; + } + + return ( + + ); + }, + [visibleDates, resources, enableResourceScroll, resourcePerPage] + ); return ( - + {Object.keys(visibleDates).map(_renderColumn)} ); }; From 3965281c8c487e336c9832a21737f665b976de7d Mon Sep 17 00:00:00 2001 From: anton-patrushev Date: Fri, 28 Nov 2025 07:49:17 +0100 Subject: [PATCH 03/10] fix: for unavailable hours and events jumps --- .../src/components/BodyResourceItem.tsx | 2 +- .../src/components/Resource/UnavailableHoursByResource.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/react-native-calendar-kit/src/components/BodyResourceItem.tsx b/packages/react-native-calendar-kit/src/components/BodyResourceItem.tsx index 49322631..c31ad132 100644 --- a/packages/react-native-calendar-kit/src/components/BodyResourceItem.tsx +++ b/packages/react-native-calendar-kit/src/components/BodyResourceItem.tsx @@ -31,7 +31,7 @@ const BodyResourceItem = ({ resources, dateUnix }: BodyResourceItemProps) => { const currentIndex = visibleDatesArray.indexOf(visibleDateUnix); const data: Record = {}; - let diffDays = 1; + let diffDays = 0; // Show prev, current, next days for (let i = -1; i <= 1; i++) { diff --git a/packages/react-native-calendar-kit/src/components/Resource/UnavailableHoursByResource.tsx b/packages/react-native-calendar-kit/src/components/Resource/UnavailableHoursByResource.tsx index 569af865..ba48bad9 100644 --- a/packages/react-native-calendar-kit/src/components/Resource/UnavailableHoursByResource.tsx +++ b/packages/react-native-calendar-kit/src/components/Resource/UnavailableHoursByResource.tsx @@ -25,7 +25,7 @@ const UnavailableHoursByResource = ({ return ( r.id).join('-')}`} currentUnix={Number(currentUnix)} visibleDateIndex={dateInfo.diffDays} resources={resources} From 7bae661be6c555aa2ace4327deca29ea7945f2cb Mon Sep 17 00:00:00 2001 From: anton-patrushev Date: Fri, 28 Nov 2025 22:57:30 +0100 Subject: [PATCH 04/10] fix: drag-n-drop to create/edit and make sure events are properly rendered in both resources and regular view modes --- .../src/components/BodyItem.tsx | 4 +- .../src/components/BodyResourceItem.tsx | 45 +++------ .../src/components/DraggableEvent.tsx | 4 +- .../src/components/DraggingEvent.tsx | 9 +- .../src/components/EventItem.tsx | 84 +++++++++++----- .../src/components/Resource/ResourceBoard.tsx | 20 ++-- .../src/context/DragEventProvider.tsx | 98 ++++++++++++++----- 7 files changed, 168 insertions(+), 96 deletions(-) diff --git a/packages/react-native-calendar-kit/src/components/BodyItem.tsx b/packages/react-native-calendar-kit/src/components/BodyItem.tsx index bb15aeab..acb808fc 100644 --- a/packages/react-native-calendar-kit/src/components/BodyItem.tsx +++ b/packages/react-native-calendar-kit/src/components/BodyItem.tsx @@ -41,15 +41,13 @@ const BodyItem = ({ const visibleDates = useMemo(() => { const data: Record = {}; - let diffDays = 1; for (let i = 0; i < columns; i++) { const currentUnix = calendarData.visibleDatesArray[pageIndex + i]; if (currentUnix) { data[currentUnix] = { unix: currentUnix, - diffDays, + diffDays: i, // Use i directly: 0 for first column, 1 for second, etc. }; - diffDays += 1; } } diff --git a/packages/react-native-calendar-kit/src/components/BodyResourceItem.tsx b/packages/react-native-calendar-kit/src/components/BodyResourceItem.tsx index c31ad132..b79d2cf1 100644 --- a/packages/react-native-calendar-kit/src/components/BodyResourceItem.tsx +++ b/packages/react-native-calendar-kit/src/components/BodyResourceItem.tsx @@ -14,40 +14,21 @@ import ResourceBoard from './Resource/ResourceBoard'; interface BodyResourceItemProps { resources: ResourceItem[]; - dateUnix?: number; } -const BodyResourceItem = ({ resources, dateUnix }: BodyResourceItemProps) => { - const { spaceFromTop, hourWidth, timelineHeight, spaceFromBottom, calendarData } = - useBody(); - const globalVisibleDateUnix = useDateChangedListener(); +const BodyResourceItem = ({ resources }: BodyResourceItemProps) => { + const { spaceFromTop, timelineHeight, spaceFromBottom } = useBody(); + const visibleDateUnix = useDateChangedListener(); - // Use provided dateUnix (dual-axis mode) or global visibleDateUnix (regular resource mode) - const visibleDateUnix = dateUnix ?? globalVisibleDateUnix; - - // Build visibleDates for prev, current, next days - const visibleDates = useMemo(() => { - const visibleDatesArray = calendarData.visibleDatesArray; - const currentIndex = visibleDatesArray.indexOf(visibleDateUnix); - - const data: Record = {}; - let diffDays = 0; - - // Show prev, current, next days - for (let i = -1; i <= 1; i++) { - const index = currentIndex + i; - if (index >= 0 && index < visibleDatesArray.length) { - const unix = visibleDatesArray[index]; - data[unix] = { - unix, - diffDays, - }; - diffDays += 1; - } - } - - return data; - }, [visibleDateUnix, calendarData.visibleDatesArray]); + const visibleDates = useMemo( + () => ({ + [visibleDateUnix]: { + diffDays: 0, + unix: visibleDateUnix, + }, + }), + [visibleDateUnix] + ); const height = useDerivedValue(() => { return timelineHeight.value - spaceFromTop - spaceFromBottom; @@ -65,7 +46,7 @@ const BodyResourceItem = ({ resources, dateUnix }: BodyResourceItemProps) => { style={[ styles.content, { - left: resources ? 0 : Math.max(0, hourWidth - 1), + left: 0, top: EXTRA_HEIGHT + spaceFromTop, }, animView, diff --git a/packages/react-native-calendar-kit/src/components/DraggableEvent.tsx b/packages/react-native-calendar-kit/src/components/DraggableEvent.tsx index 7f9c982a..79a44484 100644 --- a/packages/react-native-calendar-kit/src/components/DraggableEvent.tsx +++ b/packages/react-native-calendar-kit/src/components/DraggableEvent.tsx @@ -84,8 +84,8 @@ export const DraggableEvent: FC = ({ ); }, [resources, selectedEvent?.resourceId]); const left = useMemo(() => { - const diffDays = visibleDates[startUnix]?.diffDays ?? 1; - return (diffDays - 1) * columnWidth; + const diffDays = visibleDates[startUnix]?.diffDays ?? 0; + return diffDays * columnWidth; }, [visibleDates, startUnix, columnWidth]); const top = useDerivedValue(() => { diff --git a/packages/react-native-calendar-kit/src/components/DraggingEvent.tsx b/packages/react-native-calendar-kit/src/components/DraggingEvent.tsx index 7ce7378d..559cb3e1 100644 --- a/packages/react-native-calendar-kit/src/components/DraggingEvent.tsx +++ b/packages/react-native-calendar-kit/src/components/DraggingEvent.tsx @@ -8,7 +8,6 @@ import Animated, { useAnimatedStyle, useDerivedValue, useSharedValue, - withTiming, } from 'react-native-reanimated'; import { useBody } from '../context/BodyContext'; import { useDragEvent } from '../context/DragEventProvider'; @@ -84,7 +83,7 @@ export const DraggingEvent: FC = ({ ); const nearestVisibleIndex = calendarData.visibleDates[nearestVisibleUnix]?.index; - if (!nearestVisibleIndex) { + if (nearestVisibleIndex === undefined) { return 0; } currentIndex = nearestVisibleIndex; @@ -99,7 +98,7 @@ export const DraggingEvent: FC = ({ ); const nearestVisibleIndex = calendarData.visibleDates[nearestVisibleUnix]?.index; - if (!nearestVisibleIndex) { + if (nearestVisibleIndex === undefined) { return 0; } startIndex = nearestVisibleIndex; @@ -127,7 +126,8 @@ export const DraggingEvent: FC = ({ (dayUnix) => { if (dayUnix !== -1) { const dayIndex = getDayIndex(dayUnix); - internalDayIndex.value = withTiming(dayIndex, { duration: 100 }); + // Update immediately without animation to avoid position lag after drag ends + internalDayIndex.value = dayIndex; } } ); @@ -139,6 +139,7 @@ export const DraggingEvent: FC = ({ const animView = useAnimatedStyle(() => { const startX = resourceIndex.value * eventWidth; const dIndex = enableResourceScroll ? 0 : internalDayIndex.value; + return { top: (dragStartMinutes.value - start) * minuteHeight.value, height: dragDuration.value * minuteHeight.value, diff --git a/packages/react-native-calendar-kit/src/components/EventItem.tsx b/packages/react-native-calendar-kit/src/components/EventItem.tsx index 22c267a2..6e54ce65 100644 --- a/packages/react-native-calendar-kit/src/components/EventItem.tsx +++ b/packages/react-native-calendar-kit/src/components/EventItem.tsx @@ -81,30 +81,46 @@ const EventItem: FC = ({ newStart = 0; } - let diffDays = Math.floor( - (eventStartUnix - startUnix) / MILLISECONDS_IN_DAY - ); + // Get the event's day start (not the event time, but the start of that day) + const eventDayStart = parseDateTime(eventStartUnix) + .startOf('day') + .toMillis(); - if (eventStartUnix < startUnix) { - for ( - let dayUnix = eventStartUnix; - dayUnix < startUnix; - dayUnix = parseDateTime(dayUnix).plus({ days: 1 }).toMillis() - ) { - const dayStartUnix = parseDateTime(dayUnix).startOf('day').toMillis(); - if (!visibleDates[dayStartUnix]) { - diffDays++; - } - } + // Use the diffDays from visibleDates if available, otherwise calculate it + let diffDays = 0; + if (visibleDates[eventDayStart]) { + diffDays = visibleDates[eventDayStart].diffDays; } else { - for ( - let dayUnix = startUnix; - dayUnix < eventStartUnix; - dayUnix = parseDateTime(dayUnix).plus({ days: 1 }).toMillis() - ) { - const dayStartUnix = parseDateTime(dayUnix).startOf('day').toMillis(); - if (!visibleDates[dayStartUnix]) { - diffDays--; + // Fallback: calculate based on day difference + const referenceDayStart = parseDateTime(startUnix) + .startOf('day') + .toMillis(); + diffDays = Math.floor( + (eventDayStart - referenceDayStart) / MILLISECONDS_IN_DAY + ); + + // Adjust for hidden days + if (eventStartUnix < startUnix) { + for ( + let dayUnix = eventStartUnix; + dayUnix < startUnix; + dayUnix = parseDateTime(dayUnix).plus({ days: 1 }).toMillis() + ) { + const dayStartUnix = parseDateTime(dayUnix).startOf('day').toMillis(); + if (!visibleDates[dayStartUnix]) { + diffDays++; + } + } + } else { + for ( + let dayUnix = startUnix; + dayUnix < eventStartUnix; + dayUnix = parseDateTime(dayUnix).plus({ days: 1 }).toMillis() + ) { + const dayStartUnix = parseDateTime(dayUnix).startOf('day').toMillis(); + if (!visibleDates[dayStartUnix]) { + diffDays--; + } } } } @@ -124,6 +140,12 @@ const EventItem: FC = ({ visibleDates, ]); + // Calculate childColumns based on mode: + // - Dual-axis resource mode: totalResources === 1, use resourcePerPage + // - Grouped resource mode: totalResources > 1, use resourcePerPage + // - Regular resource mode (no scroll): use totalResources + // - Week view (no resources): use 1 (diffDays handles day positioning) + const isDualAxisMode = enableResourceScroll && totalResources === 1; const childColumns = enableResourceScroll ? resourcePerPage : totalResources && totalResources > 0 @@ -161,17 +183,26 @@ const EventItem: FC = ({ const eventWidth = widthPercent * availableWidth; const eventPosX = useMemo(() => { const colWidth = columnWidth / childColumns; - const startOffset = resourceIndex - ? (enableResourceScroll + + // In dual-axis mode, each container has only 1 resource, so position is always 0 + // In grouped mode, calculate visual column position + let startOffset = 0; + if (!isDualAxisMode) { + const visualColumn = + resourceIndex !== undefined && enableResourceScroll ? resourceIndex % resourcePerPage - : resourceIndex) * colWidth - : 0; + : resourceIndex || 0; + startOffset = visualColumn * colWidth; + } + let left = data.diffDays * colWidth + startOffset; + if (xOffsetPercentage) { left += availableWidth * (xOffsetPercentage / 100); } else if (columnSpan && index) { left += (eventWidth + overlapEventsSpacing) * (index / columnSpan); } + return left; }, [ availableWidth, @@ -182,6 +213,7 @@ const EventItem: FC = ({ enableResourceScroll, eventWidth, index, + isDualAxisMode, overlapEventsSpacing, resourceIndex, resourcePerPage, diff --git a/packages/react-native-calendar-kit/src/components/Resource/ResourceBoard.tsx b/packages/react-native-calendar-kit/src/components/Resource/ResourceBoard.tsx index 3dfa2720..d8e20b2f 100644 --- a/packages/react-native-calendar-kit/src/components/Resource/ResourceBoard.tsx +++ b/packages/react-native-calendar-kit/src/components/Resource/ResourceBoard.tsx @@ -57,9 +57,13 @@ const ResourceBoard = ({ resources, visibleDates }: ResourceBoardProps) => { dateTime: dateTimeToISOString(dateObj), }; if (resources) { - const colWidth = columnWidth / resourcePerPage; - const resourceIdx = Math.floor(event.nativeEvent.locationX / colWidth); - newProps.resourceId = resources[resourceIdx]?.id; + if (resources.length === 1) { + newProps.resourceId = resources[0]?.id; + } else { + const colWidth = columnWidth / resourcePerPage; + const resourceIdx = Math.floor(event.nativeEvent.locationX / colWidth); + newProps.resourceId = resources[resourceIdx]?.id; + } } onPressBackground?.(newProps, event); }; @@ -76,9 +80,13 @@ const ResourceBoard = ({ resources, visibleDates }: ResourceBoardProps) => { dateTime: dateString, }; if (resources) { - const colWidth = columnWidth / resourcePerPage; - const resourceIdx = Math.floor(event.nativeEvent.locationX / colWidth); - newProps.resourceId = resources[resourceIdx]?.id; + if (resources.length === 1) { + newProps.resourceId = resources[0]?.id; + } else { + const colWidth = columnWidth / resourcePerPage; + const resourceIdx = Math.floor(event.nativeEvent.locationX / colWidth); + newProps.resourceId = resources[resourceIdx]?.id; + } } onLongPressBackground?.(newProps, event); if (triggerDragCreateEvent) { diff --git a/packages/react-native-calendar-kit/src/context/DragEventProvider.tsx b/packages/react-native-calendar-kit/src/context/DragEventProvider.tsx index c82051de..6f4e4a4b 100644 --- a/packages/react-native-calendar-kit/src/context/DragEventProvider.tsx +++ b/packages/react-native-calendar-kit/src/context/DragEventProvider.tsx @@ -225,16 +225,32 @@ const DragEventProvider: FC< let resourceId = draggingEvent?.resourceId; if (resources?.length) { - const totalResources = enableResourceScroll - ? resourcePerPage - : resources.length; - const width = columnWidth / totalResources; - const startIndex = enableResourceScroll - ? Math.round(offsetX.value / width) - : 0; - const resourceColumn = Math.floor((dragX.value - hourWidth) / width); - const resourceIndex = startIndex + resourceColumn; - resourceId = resources[resourceIndex]?.id; + if (enableResourceScroll) { + const totalResources = resources.length; + const resourceWidth = columnWidth / resourcePerPage; + + // offsetX tracks position across all date-resource items + const firstVisibleItemIndex = Math.floor(offsetX.value / resourceWidth); + const firstVisibleResourceInDay = + firstVisibleItemIndex % totalResources; + + // Calculate which resource column the dragX is in + const resourceColumn = Math.floor( + (dragX.value - hourWidth) / resourceWidth + ); + let resourceIndex = firstVisibleResourceInDay + resourceColumn; + + // Handle wrap-around + if (resourceIndex >= totalResources) { + resourceIndex = resourceIndex % totalResources; + } + + resourceId = resources[resourceIndex]?.id; + } else { + const width = columnWidth / resources.length; + const resourceColumn = Math.floor((dragX.value - hourWidth) / width); + resourceId = resources[resourceColumn]?.id; + } } return { newStartUnix, newEndUnix, resourceId }; @@ -718,12 +734,22 @@ const DragEventProvider: FC< (resource) => resource.id === initialDrag.resourceId ); if (resourceIndex !== -1) { + const totalResources = resources.length; const resourceWidth = columnWidth / resourcePerPage; - const currentResourceIndex = Math.round( + + // offsetX tracks position across all date-resource items + const firstVisibleItemIndex = Math.floor( offsetX.value / resourceWidth ); - const diff = resourceIndex - currentResourceIndex; - dragX.value = diff * resourceWidth + hourWidth + 1; + const firstVisibleResourceInDay = + firstVisibleItemIndex % totalResources; + + let resourceVisualIndex = resourceIndex - firstVisibleResourceInDay; + if (resourceVisualIndex < 0) { + resourceVisualIndex += totalResources; + } + + dragX.value = hourWidth + resourceVisualIndex * resourceWidth + 1; } } else if ( initialDrag.resourceIndex !== undefined && @@ -829,12 +855,22 @@ const DragEventProvider: FC< (resource) => resource.id === event.resourceId ) ?? -1; if (resourceIndex !== -1) { + const totalResources = resources?.length ?? 0; const resourceWidth = columnWidth / resourcePerPage; - const currentResourceIndex = Math.round( + + // offsetX tracks position across all date-resource items + const firstVisibleItemIndex = Math.floor( offsetX.value / resourceWidth ); - const diff = resourceIndex - currentResourceIndex; - newDragX = diff * resourceWidth + hourWidth + 1; + const firstVisibleResourceInDay = + firstVisibleItemIndex % totalResources; + + let resourceVisualIndex = resourceIndex - firstVisibleResourceInDay; + if (resourceVisualIndex < 0) { + resourceVisualIndex += totalResources; + } + + newDragX = hourWidth + resourceVisualIndex * resourceWidth + 1; } } dragX.value = newDragX; @@ -913,8 +949,6 @@ const DragEventProvider: FC< const triggerDragCreateEvent = useCallback( (props: DateOrDateTime, event?: GestureResponderEvent) => { - console.log(props); - if (!event?.nativeEvent?.locationX) { return; } @@ -922,9 +956,6 @@ const DragEventProvider: FC< let newDragX = event.nativeEvent.locationX + hourWidth; if (enableResourceScroll) { - const currentResourceIndex = Math.round( - offsetX.value / (columnWidth / resourcePerPage) - ); const selectedResourceIndex = resources?.findIndex( (resource) => resource.id === props.resourceId @@ -932,9 +963,29 @@ const DragEventProvider: FC< if (selectedResourceIndex === -1) { return; } - const diff = selectedResourceIndex - currentResourceIndex; - newDragX = diff * (columnWidth / resourcePerPage) + hourWidth + 1; + const totalResources = resources?.length ?? 0; + const resourceWidth = columnWidth / resourcePerPage; + + // offsetX tracks position across all date-resource items + // Calculate which resource column is at the left edge of viewport + const firstVisibleItemIndex = Math.floor(offsetX.value / resourceWidth); + + // The item index within a day (modulo by total resources per day) + const firstVisibleResourceInDay = + firstVisibleItemIndex % totalResources; + + // Calculate visual position of selected resource relative to viewport + let resourceVisualIndex = + selectedResourceIndex - firstVisibleResourceInDay; + + // Handle wrap-around if needed + if (resourceVisualIndex < 0) { + resourceVisualIndex += totalResources; + } + + newDragX = hourWidth + resourceVisualIndex * resourceWidth + 1; } + dragX.value = newDragX; const start = parseDateTime(props.dateTime, { zone: timeZone }); const startUnix = parseDateTime(start.toISODate()).toMillis(); @@ -949,6 +1000,7 @@ const DragEventProvider: FC< start: { dateTime: startISO }, end: { dateTime: endISO }, }); + if (onDragCreateEventStart) { onDragCreateEventStart({ start: { dateTime: startISO }, From 54cd6bd9636a3f11c2407c85a3649b2c73a34394 Mon Sep 17 00:00:00 2001 From: anton-patrushev Date: Fri, 28 Nov 2025 23:08:18 +0100 Subject: [PATCH 05/10] fix: improve rendering user UX & performance by rendering events for all visible days and resource columns - this avoids jumping UI --- .../src/components/BodyResourceItem.tsx | 15 +++++++++------ .../src/context/VisibleDateProvider.tsx | 2 +- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/packages/react-native-calendar-kit/src/components/BodyResourceItem.tsx b/packages/react-native-calendar-kit/src/components/BodyResourceItem.tsx index b79d2cf1..f08da5dc 100644 --- a/packages/react-native-calendar-kit/src/components/BodyResourceItem.tsx +++ b/packages/react-native-calendar-kit/src/components/BodyResourceItem.tsx @@ -14,20 +14,23 @@ import ResourceBoard from './Resource/ResourceBoard'; interface BodyResourceItemProps { resources: ResourceItem[]; + dateUnix?: number; } -const BodyResourceItem = ({ resources }: BodyResourceItemProps) => { +const BodyResourceItem = ({ resources, dateUnix }: BodyResourceItemProps) => { const { spaceFromTop, timelineHeight, spaceFromBottom } = useBody(); - const visibleDateUnix = useDateChangedListener(); + const globalVisibleDateUnix = useDateChangedListener(); + + const targetDateUnix = dateUnix ?? globalVisibleDateUnix; const visibleDates = useMemo( () => ({ - [visibleDateUnix]: { + [targetDateUnix]: { diffDays: 0, - unix: visibleDateUnix, + unix: targetDateUnix, }, }), - [visibleDateUnix] + [targetDateUnix] ); const height = useDerivedValue(() => { @@ -52,7 +55,7 @@ const BodyResourceItem = ({ resources }: BodyResourceItemProps) => { animView, ]}> diff --git a/packages/react-native-calendar-kit/src/context/VisibleDateProvider.tsx b/packages/react-native-calendar-kit/src/context/VisibleDateProvider.tsx index 98c10fb8..ad0db02f 100644 --- a/packages/react-native-calendar-kit/src/context/VisibleDateProvider.tsx +++ b/packages/react-native-calendar-kit/src/context/VisibleDateProvider.tsx @@ -36,7 +36,7 @@ const VisibleDateProvider: ForwardRefRenderFunction< useEffect(() => { const timeoutId = setTimeout(() => { setDebouncedDateUnix(visibleDateUnix); - }, 150); + }, 50); return () => clearTimeout(timeoutId); }, [visibleDateUnix]); From 9c58d575b61c01b46e5f31065b11946edb4a7fb1 Mon Sep 17 00:00:00 2001 From: anton-patrushev Date: Fri, 28 Nov 2025 23:24:57 +0100 Subject: [PATCH 06/10] fix: implement missing support for calendar ref methods - goToDate - goToNextPage - goToPrevPage - goToNextResource - goToPrevResource --- .../src/CalendarContainer.tsx | 62 ++++++++++++++++--- 1 file changed, 54 insertions(+), 8 deletions(-) diff --git a/packages/react-native-calendar-kit/src/CalendarContainer.tsx b/packages/react-native-calendar-kit/src/CalendarContainer.tsx index 5789dc5b..575ff322 100644 --- a/packages/react-native-calendar-kit/src/CalendarContainer.tsx +++ b/packages/react-native-calendar-kit/src/CalendarContainer.tsx @@ -342,7 +342,7 @@ const CalendarContainer: React.ForwardRefRenderFunction< offset = pageIndex * (columnWidth * columns); } - if (isResourceMode && enableResourceScroll) { + if (isResourceMode && enableResourceScroll && resources) { visibleDateUnix.current = nearestUnix; visibleDateUnixAnim.value = nearestUnix; visibleDateRef.current?.updateVisibleDate(nearestUnix); @@ -350,6 +350,20 @@ const CalendarContainer: React.ForwardRefRenderFunction< const newDate = dateTimeToISOString(dateObj); onDateChanged?.(newDate); onChange?.(newDate); + + // Calculate the scroll offset for the date + const resourceWidth = calendarGridWidth / resourcePerPage; + const dayOffset = visibleDayIndex * resources.length * resourceWidth; + + linkedScrollGroup.setActiveId(ScrollType.calendarGrid); + const animatedDate = + props?.animatedDate !== undefined ? props.animatedDate : true; + + runOnUI(() => { + offsetX.value = dayOffset; + scrollTo(dayBarListRef, dayOffset, 0, animatedDate); + scrollTo(gridListRef, dayOffset, 0, animatedDate); + })(); } else { const isScrollable = calendarListRef.current?.isScrollable( offset, @@ -418,7 +432,7 @@ const CalendarContainer: React.ForwardRefRenderFunction< } const nextDateUnix = visibleDatesArray[nextVisibleDayIndex]; - if (isResourceMode && enableResourceScroll && nextDateUnix) { + if (isResourceMode && enableResourceScroll && nextDateUnix && resources) { visibleDateUnix.current = nextDateUnix; visibleDateUnixAnim.value = nextDateUnix; visibleDateRef.current?.updateVisibleDate(nextDateUnix); @@ -426,6 +440,17 @@ const CalendarContainer: React.ForwardRefRenderFunction< const newDate = dateTimeToISOString(dateObj); onDateChanged?.(newDate); onChange?.(newDate); + + // Calculate the scroll offset for the new day + const resourceWidth = calendarGridWidth / resourcePerPage; + const dayOffset = nextVisibleDayIndex * resources.length * resourceWidth; + + linkedScrollGroup.setActiveId(ScrollType.calendarGrid); + runOnUI(() => { + offsetX.value = dayOffset; + scrollTo(dayBarListRef, dayOffset, 0, animated); + scrollTo(gridListRef, dayOffset, 0, animated); + })(); return; } @@ -476,7 +501,7 @@ const CalendarContainer: React.ForwardRefRenderFunction< numberOfDays ); const nextDateUnix = visibleDatesArray[nextVisibleDayIndex]; - if (isResourceMode && enableResourceScroll && nextDateUnix) { + if (isResourceMode && enableResourceScroll && nextDateUnix && resources) { visibleDateUnix.current = nextDateUnix; visibleDateUnixAnim.value = nextDateUnix; visibleDateRef.current?.updateVisibleDate(nextDateUnix); @@ -484,6 +509,17 @@ const CalendarContainer: React.ForwardRefRenderFunction< const newDate = dateTimeToISOString(dateObj); onDateChanged?.(newDate); onChange?.(newDate); + + // Calculate the scroll offset for the new day + const resourceWidth = calendarGridWidth / resourcePerPage; + const dayOffset = nextVisibleDayIndex * resources.length * resourceWidth; + + linkedScrollGroup.setActiveId(ScrollType.calendarGrid); + runOnUI(() => { + offsetX.value = dayOffset; + scrollTo(dayBarListRef, dayOffset, 0, animated); + scrollTo(gridListRef, dayOffset, 0, animated); + })(); return; } @@ -648,14 +684,19 @@ const CalendarContainer: React.ForwardRefRenderFunction< const goToNextResource = useLatestCallback( (animated?: boolean, resourceScrollType?: 'resource' | 'page') => { - const resourceWidth = columnWidth / resourcePerPage; + if (!isResourceMode || !enableResourceScroll) { + console.warn('Only available for resource mode (enableResourceScroll)'); + return; + } + + const resourceWidth = calendarGridWidth / resourcePerPage; let nextOffset = 0; let mode = resourcePagingEnabled ? 'page' : 'resource'; if (resourceScrollType) { mode = resourceScrollType; } if (mode === 'page') { - nextOffset = offsetX.value + columnWidth; + nextOffset = offsetX.value + calendarGridWidth; } else { nextOffset = offsetX.value + resourceWidth; } @@ -677,19 +718,24 @@ const CalendarContainer: React.ForwardRefRenderFunction< const goToPrevResource = useLatestCallback( (animated?: boolean, resourceScrollType?: 'resource' | 'page') => { - const resourceWidth = columnWidth / resourcePerPage; + if (!isResourceMode || !enableResourceScroll) { + console.warn('Only available for resource mode (enableResourceScroll)'); + return; + } + + const resourceWidth = calendarGridWidth / resourcePerPage; let nextOffset = 0; let mode = resourcePagingEnabled ? 'page' : 'resource'; if (resourceScrollType) { mode = resourceScrollType; } if (mode === 'page') { - nextOffset = offsetX.value - columnWidth; + nextOffset = offsetX.value - calendarGridWidth; } else { nextOffset = offsetX.value - resourceWidth; } if (nextOffset < 0) { - return; + nextOffset = 0; } linkedScrollGroup.setActiveId(ScrollType.calendarGrid); From f330ee709a6a741d67644400d16231db4ec3ebee Mon Sep 17 00:00:00 2001 From: anton-patrushev Date: Fri, 28 Nov 2025 23:27:19 +0100 Subject: [PATCH 07/10] feat: adjuts total resources in example app from 50 to 10 --- apps/example/app/(drawer)/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/example/app/(drawer)/index.tsx b/apps/example/app/(drawer)/index.tsx index 28ccd181..76994c4a 100644 --- a/apps/example/app/(drawer)/index.tsx +++ b/apps/example/app/(drawer)/index.tsx @@ -344,7 +344,7 @@ const allDayEvents: EventItem[] = [ }, ]; -const TOTAL_RESOURCES = 50; +const TOTAL_RESOURCES = 10; const generateEvents = () => { return new Array(500) From 36323ff31047cc1ec4444dd703f0760e8068a068 Mon Sep 17 00:00:00 2001 From: anton-patrushev Date: Fri, 28 Nov 2025 23:54:39 +0100 Subject: [PATCH 08/10] fix: tiny ui bug to align body and header vertical lines --- .../src/components/Resource/ResourceBoard.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/react-native-calendar-kit/src/components/Resource/ResourceBoard.tsx b/packages/react-native-calendar-kit/src/components/Resource/ResourceBoard.tsx index d8e20b2f..359bde1b 100644 --- a/packages/react-native-calendar-kit/src/components/Resource/ResourceBoard.tsx +++ b/packages/react-native-calendar-kit/src/components/Resource/ResourceBoard.tsx @@ -169,7 +169,10 @@ const ResourceBoard = ({ resources, visibleDates }: ResourceBoardProps) => { !onLongPressBackground } /> - + {_renderHorizontalLines} {!!resources?.length && _renderVerticalLines} @@ -182,6 +185,7 @@ export default memo(ResourceBoard); const styles = StyleSheet.create({ container: { flex: 1, + marginLeft: -0.5, }, calendarGrid: { width: '100%' }, touchable: { flex: 1 }, From a1a6ee661e24bc31990227bbee758165dee977c4 Mon Sep 17 00:00:00 2001 From: anton-patrushev Date: Sat, 29 Nov 2025 00:44:34 +0100 Subject: [PATCH 09/10] fix: implement a drag-n-drop support for resources view - add drag to create support - add drag to edti support --- .../src/context/DragEventProvider.tsx | 124 ++++++++++++++---- 1 file changed, 100 insertions(+), 24 deletions(-) diff --git a/packages/react-native-calendar-kit/src/context/DragEventProvider.tsx b/packages/react-native-calendar-kit/src/context/DragEventProvider.tsx index 6f4e4a4b..350ef4ef 100644 --- a/packages/react-native-calendar-kit/src/context/DragEventProvider.tsx +++ b/packages/react-native-calendar-kit/src/context/DragEventProvider.tsx @@ -153,8 +153,8 @@ const DragEventProvider: FC< dayBarListRef, enableResourceScroll, resourcePerPage, - resourcePagingEnabled, linkedScrollGroup, + daySnapOffsets, } = useCalendar(); const { onDragSelectedEventStart, @@ -534,46 +534,94 @@ const DragEventProvider: FC< return; } - const resourceWidth = columnWidth / resourcePerPage; - const totalResources = resources?.length ?? 0; - const maxOffset = (totalResources - resourcePerPage) * resourceWidth; - const shouldCancel = isNextPage - ? offsetX.value === maxOffset - : offsetX.value === 0; - - if (shouldCancel) { + if (!daySnapOffsets || daySnapOffsets.length === 0 || !resources) { return; } const scrollInterval = () => { const scrollTargetDiff = Math.abs(scrollTargetX.value - offsetX.value); const hasScrolledToTarget = scrollTargetDiff < 2; + if (!hasScrolledToTarget) { return; } - let nextOffset = 0; - const reverse = isNextPage ? 1 : -1; - if (resourcePagingEnabled) { - nextOffset = offsetX.value + columnWidth * reverse; - } else { - nextOffset = offsetX.value + resourceWidth * reverse; + // Find current snap offset index + const currentOffset = offsetX.value; + let currentSnapIndex = daySnapOffsets.findIndex( + (offset) => Math.abs(offset - currentOffset) < 2 + ); + + // If not at a snap point, find the nearest one + if (currentSnapIndex === -1) { + currentSnapIndex = daySnapOffsets.reduce((nearestIdx, offset, idx) => { + const currentNearest = daySnapOffsets[nearestIdx]; + return Math.abs(offset - currentOffset) < + Math.abs(currentNearest - currentOffset) + ? idx + : nearestIdx; + }, 0); } - const isCancel = isNextPage ? nextOffset > maxOffset : nextOffset < 0; - if (isCancel) { + // Calculate next snap index + const nextSnapIndex = isNextPage + ? currentSnapIndex + 1 + : currentSnapIndex - 1; + + // Check if next snap index is valid + if (nextSnapIndex < 0 || nextSnapIndex >= daySnapOffsets.length) { clearInterval(autoHScrollTimer.current); autoHScrollTimer.current = undefined; return; } + const nextOffset = daySnapOffsets[nextSnapIndex]; + if (nextOffset === undefined) { + clearInterval(autoHScrollTimer.current); + autoHScrollTimer.current = undefined; + return; + } + + // Determine if we're transitioning to a new day + const resourceWidth = calendarGridWidth / resourcePerPage; + const totalResources = resources.length; + const currentItemIndex = Math.floor(currentOffset / resourceWidth); + const nextItemIndex = Math.floor(nextOffset / resourceWidth); + const currentDayIndex = Math.floor(currentItemIndex / totalResources); + const nextDayIndex = Math.floor(nextItemIndex / totalResources); + linkedScrollGroup.setActiveId(ScrollType.calendarGrid); - runOnUI(() => { - scrollTargetX.value = nextOffset; - scrollTo(dayBarListRef, nextOffset, 0, true); - scrollTo(gridListRef, nextOffset, 0, true); - offsetX.value = nextOffset; - })(); + + if (currentDayIndex !== nextDayIndex) { + // Day transition - update visible date and dragStartUnix + const visibleDates = calendarData.visibleDatesArray; + const nextDateUnix = visibleDates[nextDayIndex]; + + if (!nextDateUnix) { + clearInterval(autoHScrollTimer.current); + autoHScrollTimer.current = undefined; + return; + } + + triggerDateChanged.current = nextDateUnix; + + runOnUI(() => { + scrollTargetX.value = nextOffset; + scrollTo(dayBarListRef, nextOffset, 0, true); + scrollTo(gridListRef, nextOffset, 0, true); + dragStartUnix.value = nextDateUnix; + roundedDragStartUnix.value = nextDateUnix; + offsetX.value = nextOffset; + })(); + } else { + // Same day - just scroll + runOnUI(() => { + scrollTargetX.value = nextOffset; + scrollTo(dayBarListRef, nextOffset, 0, true); + scrollTo(gridListRef, nextOffset, 0, true); + offsetX.value = nextOffset; + })(); + } }; autoHScrollTimer.current = setInterval( @@ -593,7 +641,32 @@ const DragEventProvider: FC< dragSelectedType.value !== 'bottom' ) { const isAtLeftEdge = curX <= hourWidth - 10; - const width = columnWidth * numberOfDays + hourWidth; + const width = enableResourceScroll + ? calendarGridWidth + hourWidth + : columnWidth * numberOfDays + hourWidth; + const isAtRightEdge = width - curX <= 24; + const isStartAutoScroll = isAtLeftEdge || isAtRightEdge; + + if (isStartAutoScroll) { + if (enableResourceScroll) { + runOnJS(_startAutoResourceScroll)(isAtRightEdge); + } else { + runOnJS(_startAutoHScroll)(isAtRightEdge); + } + } else { + runOnJS(_stopAutoHScroll)(); + } + } else if ( + isDraggingAnim.value && + isDraggingCreateAnim.value && + curX !== prevX && + curX !== -1 + ) { + // For drag-to-create, always allow horizontal auto-scroll regardless of dragSelectedType + const isAtLeftEdge = curX <= hourWidth - 10; + const width = enableResourceScroll + ? calendarGridWidth + hourWidth + : columnWidth * numberOfDays + hourWidth; const isAtRightEdge = width - curX <= 24; const isStartAutoScroll = isAtLeftEdge || isAtRightEdge; @@ -616,6 +689,9 @@ const DragEventProvider: FC< columnWidth, calendarData, enableResourceScroll, + daySnapOffsets, + resources, + resourcePerPage, ] ); From 7df36eee976c944085568197c09cb858a6969a24 Mon Sep 17 00:00:00 2001 From: anton-patrushev Date: Mon, 1 Dec 2025 15:56:44 +0100 Subject: [PATCH 10/10] fix: the bug with NowIndicator shown by a few 1 day off --- .../react-native-calendar-kit/src/components/NowIndicator.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-native-calendar-kit/src/components/NowIndicator.tsx b/packages/react-native-calendar-kit/src/components/NowIndicator.tsx index a74055f2..789aa3fa 100644 --- a/packages/react-native-calendar-kit/src/components/NowIndicator.tsx +++ b/packages/react-native-calendar-kit/src/components/NowIndicator.tsx @@ -91,7 +91,7 @@ const NowIndicator: FC<{ return ( );