• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

prabhuignoto / react-chrono / #92

18 Jun 2025 10:08AM UTC coverage: 90.727% (+0.9%) from 89.791%
#92

push

web-flow
Minor cleanup and expanding test coverage (#548)

* refactor: rename project to React Chrono UI and update related files

* fix: update tsconfig to reference the correct entry file for React Chrono UI

* feat: enhance styles with vendor prefixes and improve cross-browser support

* Add tests for useNewScrollPosition hook and TimelineHorizontal component

- Implement comprehensive tests for the useNewScrollPosition hook covering horizontal, vertical, and edge cases.
- Create a new test file for the TimelineHorizontal component, ensuring it renders correctly and handles various props and states.
- Update snapshots for timeline control and vertical components to reflect recent changes in class names.
- Modify vitest configuration to include all test files in the src directory.

* refactor: simplify transform handling in timeline styles

* refactor: clean up test imports and remove unused styles in timeline components

1783 of 2112 branches covered (84.42%)

Branch coverage included in aggregate %.

670 of 674 new or added lines in 12 files covered. (99.41%)

400 existing lines in 29 files now uncovered.

10564 of 11497 relevant lines covered (91.88%)

10.09 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

88.37
/src/hooks/useTimelineNavigation.ts
1
import { useCallback, useRef, useMemo } from 'react';
1✔
2
import { findTimelineElement } from '../utils/timelineUtils';
1✔
3
import { TimelineMode } from '@models/TimelineModel';
4

5
type ExtendedTimelineMode = TimelineMode | 'HORIZONTAL_ALL';
6

7
interface UseTimelineNavigationProps {
8
  items: any[]; // Use any to avoid type conflicts
9
  mode: string;
10
  timelineId: string;
11
  hasFocus: boolean;
12
  flipLayout?: boolean;
13
  slideShowRunning?: boolean;
14
  onTimelineUpdated?: (index: number) => void;
15
  onNext?: () => void;
16
  onPrevious?: () => void;
17
  onFirst?: () => void;
18
  onLast?: () => void;
19
}
20

21
// Optimized scroll options for different modes
22
const SCROLL_OPTIONS = {
1✔
23
  HORIZONTAL: {
1✔
24
    behavior: 'smooth' as ScrollBehavior,
1✔
25
    block: 'nearest' as ScrollLogicalPosition,
1✔
26
    inline: 'center' as ScrollLogicalPosition,
1✔
27
  },
1✔
28
  VERTICAL: {
1✔
29
    behavior: 'smooth' as ScrollBehavior,
1✔
30
    block: 'center' as ScrollLogicalPosition,
1✔
31
    inline: 'center' as ScrollLogicalPosition,
1✔
32
  },
1✔
33
} as const;
1✔
34

35
export const useTimelineNavigation = ({
1✔
36
  items,
23✔
37
  mode,
23✔
38
  timelineId,
23✔
39
  hasFocus,
23✔
40
  flipLayout = false,
23✔
41
  slideShowRunning = false,
23✔
42
  onTimelineUpdated,
23✔
43
  onNext,
23✔
44
  onPrevious,
23✔
45
  onFirst,
23✔
46
  onLast,
23✔
47
}: UseTimelineNavigationProps) => {
23✔
48
  const activeItemIndex = useRef<number>(0);
23✔
49
  const callbacksRef = useRef({
23✔
50
    onTimelineUpdated,
23✔
51
    onNext,
23✔
52
    onPrevious,
23✔
53
    onFirst,
23✔
54
    onLast,
23✔
55
  });
23✔
56

57
  // Keep callbacks ref updated without triggering re-renders
58
  callbacksRef.current = {
23✔
59
    onTimelineUpdated,
23✔
60
    onNext,
23✔
61
    onPrevious,
23✔
62
    onFirst,
23✔
63
    onLast,
23✔
64
  };
23✔
65

66
  // Memoize items lookup map for O(1) access
67
  const itemsMap = useMemo(() => {
23✔
68
    const map = new Map<string, number>();
21✔
69
    items.forEach((item, index) => {
21✔
70
      if (item?.id) {
69✔
71
        map.set(item.id, index);
69✔
72
      }
69✔
73
    });
21✔
74
    return map;
21✔
75
  }, [items]);
23✔
76

77
  // Find target element in the DOM (memoized)
78
  const findTargetElement = useCallback(
23✔
79
    (itemId: string) => {
23✔
80
      if (mode === 'VERTICAL' || mode === 'VERTICAL_ALTERNATING') {
19✔
81
        // For vertical modes, directly search for the vertical-item-row
82
        // This is more reliable than using findTimelineElement and then looking for a parent
83
        const verticalItemRow = document.querySelector(`[data-testid="vertical-item-row"][data-item-id="${itemId}"]`);
15✔
84
        if (verticalItemRow) {
15✔
85
          return verticalItemRow as HTMLElement;
1✔
86
        }
1✔
87
        
88
        // Fallback: try to find the card content element and then get its parent row
89
        const cardContent = document.querySelector(`.timeline-card-content[data-item-id="${itemId}"]`);
14✔
90
        if (cardContent) {
14!
UNCOV
91
          const row = cardContent.closest('[data-testid="vertical-item-row"]');
×
UNCOV
92
          if (row) {
×
UNCOV
93
            return row as HTMLElement;
×
UNCOV
94
          }
×
UNCOV
95
        }
×
96
      }
15✔
97
      
98
      // Default behavior for horizontal modes or fallback
99
      return findTimelineElement(itemId, mode, timelineId);
18✔
100
    },
19✔
101
    [mode, timelineId],
23✔
102
  );
23✔
103

104
  // Optimized scroll function - matches timeline card content behavior
105
  const scrollToElement = useCallback(
23✔
106
    (element: HTMLElement, mode: string) => {
23✔
107
      if (!element) return;
2!
108
      
109
      // Ensure we handle the scroll in the next animation frame for smoother transitions
110
      requestAnimationFrame(() => {
2✔
111
        const isVerticalMode = mode === 'VERTICAL' || mode === 'VERTICAL_ALTERNATING';
2✔
112
        
113
        // Check if scrollIntoView is available (it may not be in test environments like JSDOM)
114
        if (typeof element.scrollIntoView === 'function') {
2✔
115
          if (isVerticalMode) {
1!
116
            // For vertical modes, ensure we fully center the element in the viewport
UNCOV
117
            element.scrollIntoView({
×
UNCOV
118
              behavior: 'smooth',
×
UNCOV
119
              block: 'center', // Always center vertically
×
UNCOV
120
              inline: 'nearest' // Nearest horizontal positioning
×
UNCOV
121
            });
×
122
            
123
            // Add a second scroll with a slight delay to ensure proper centering
124
            // This addresses issues with complex layouts and varying element heights
UNCOV
125
            setTimeout(() => {
×
UNCOV
126
              if (typeof element.scrollIntoView === 'function') {
×
UNCOV
127
                element.scrollIntoView({
×
UNCOV
128
                  behavior: 'smooth',
×
UNCOV
129
                  block: 'center',
×
UNCOV
130
                  inline: 'nearest'
×
UNCOV
131
                });
×
UNCOV
132
              }
×
UNCOV
133
            }, 50);
×
134
          } else {
1✔
135
            // In horizontal mode, use horizontal centering
136
            element.scrollIntoView(SCROLL_OPTIONS.HORIZONTAL);
1✔
137
          }
1✔
138
        }
1✔
139
      });
2✔
140
    },
2✔
141
    [],
23✔
142
  );
23✔
143

144
  // Update timeline position (optimized)
145
  const updateTimelinePosition = useCallback(
23✔
146
    (targetIndex: number, isSlideShow?: boolean) => {
23✔
147
      activeItemIndex.current = targetIndex;
5✔
148

149
      const updateIndex =
5✔
150
        isSlideShow && targetIndex < items.length - 1
5✔
151
          ? targetIndex + 1
1✔
152
          : targetIndex;
4✔
153

154
      callbacksRef.current.onTimelineUpdated?.(updateIndex);
5✔
155
    },
5✔
156
    [items.length],
23✔
157
  );
23✔
158

159
  // Handle timeline item click (significantly optimized)
160
  const handleTimelineItemClick = useCallback(
23✔
161
    (itemId?: string, isSlideShow?: boolean) => {
23✔
162
      if (!itemId) return;
6!
163

164
      // Use memoized map for O(1) lookup
165
      const targetIndex = itemsMap.get(itemId);
6✔
166
      if (targetIndex === undefined) return;
6✔
167

168
      // Update timeline position
169
      updateTimelinePosition(targetIndex, isSlideShow);
5✔
170

171
      // Skip scrolling in horizontal mode when slideshow is running to prevent toolbar hiding
172
      if (mode === 'HORIZONTAL' && slideShowRunning) return;
6✔
173

174
      // For vertical modes, directly find and scroll to the vertical item row
175
      if (mode === 'VERTICAL' || mode === 'VERTICAL_ALTERNATING') {
6✔
176
        const targetElement = findTargetElement(itemId);
3✔
177
        if (targetElement) {
3!
UNCOV
178
          scrollToElement(targetElement, mode);
×
UNCOV
179
        }
×
180
      } else {
6✔
181
        // For horizontal modes, use the original approach
182
        const timelinePointElement = document.getElementById(
1✔
183
          `timeline-${mode.toLowerCase()}-item-${itemId}`
1✔
184
        );
1✔
185
        
186
        if (timelinePointElement) {
1✔
187
          scrollToElement(timelinePointElement, mode);
1✔
188
        } else {
1!
UNCOV
189
          const targetElement = findTargetElement(itemId);
×
UNCOV
190
          if (targetElement) scrollToElement(targetElement, mode);
×
UNCOV
191
        }
×
192
      }
1✔
193
    },
6✔
194
    [itemsMap, updateTimelinePosition, findTargetElement, mode, scrollToElement, slideShowRunning],
23✔
195
  );
23✔
196

197
  // Handler for item elapsed (used in slideshow)
198
  const handleTimelineItemElapsed = useCallback(
23✔
199
    (itemId?: string) => handleTimelineItemClick(itemId, true),
23✔
200
    [handleTimelineItemClick],
23✔
201
  );
23✔
202

203
  // Navigation handlers (optimized with bounds checking and focus behavior)
204
  const handleNext = useCallback(() => {
23✔
205
    if (!hasFocus) return;
7✔
206
    
207
    const newIndex = Math.min(activeItemIndex.current + 1, items.length - 1);
6✔
208
    if (newIndex !== activeItemIndex.current) {
6✔
209
      activeItemIndex.current = newIndex;
6✔
210
      callbacksRef.current.onNext?.();
6✔
211
      
212
      // Trigger the same focus behavior as clicking
213
      const targetItem = items[newIndex];
6✔
214
      if (targetItem?.id) {
6✔
215
        // Find and scroll to the target element
216
        const targetElement = findTargetElement(targetItem.id);
6✔
217
        if (targetElement) scrollToElement(targetElement, mode);
6!
218
      }
6✔
219
    }
6✔
220
  }, [hasFocus, items, findTargetElement, mode, scrollToElement]);
23✔
221

222
  const handlePrevious = useCallback(() => {
23✔
223
    if (!hasFocus) return;
5✔
224
    
225
    const newIndex = Math.max(activeItemIndex.current - 1, 0);
4✔
226
    if (newIndex !== activeItemIndex.current) {
4✔
227
      activeItemIndex.current = newIndex;
4✔
228
      callbacksRef.current.onPrevious?.();
4✔
229
      
230
      // Trigger the same focus behavior as clicking
231
      const targetItem = items[newIndex];
4✔
232
      if (targetItem?.id) {
4✔
233
        // Find and scroll to the target element
234
        const targetElement = findTargetElement(targetItem.id);
4✔
235
        if (targetElement) scrollToElement(targetElement, mode);
4!
236
      }
4✔
237
    }
4✔
238
  }, [hasFocus, items, findTargetElement, mode, scrollToElement]);
23✔
239

240
  const handleFirst = useCallback(() => {
23✔
241
    if (!hasFocus) return;
3✔
242
    if (activeItemIndex.current !== 0) {
2✔
243
      activeItemIndex.current = 0;
2✔
244
      callbacksRef.current.onFirst?.();
2✔
245
      
246
      // Trigger the same focus behavior as clicking
247
      const targetItem = items[0];
2✔
248
      if (targetItem?.id) {
2✔
249
        // Find and scroll to the target element
250
        const targetElement = findTargetElement(targetItem.id);
2✔
251
        if (targetElement) scrollToElement(targetElement, mode);
2!
252
      }
2✔
253
    }
2✔
254
  }, [hasFocus, items, findTargetElement, mode, scrollToElement]);
23✔
255

256
  const handleLast = useCallback(() => {
23✔
257
    if (!hasFocus) return;
5✔
258
    const lastIndex = items.length - 1;
4✔
259
    if (activeItemIndex.current !== lastIndex) {
4✔
260
      activeItemIndex.current = lastIndex;
4✔
261
      callbacksRef.current.onLast?.();
4✔
262
      
263
      // Trigger the same focus behavior as clicking
264
      const targetItem = items[lastIndex];
4✔
265
      if (targetItem?.id) {
4✔
266
        // Find and scroll to the target element
267
        const targetElement = findTargetElement(targetItem.id);
4✔
268
        if (targetElement) scrollToElement(targetElement, mode);
4✔
269
      }
4✔
270
    }
4✔
271
  }, [hasFocus, items, findTargetElement, mode, scrollToElement]);
23✔
272

273
  // Keyboard navigation (optimized with key mapping)
274
  const handleKeySelection = useCallback(
23✔
275
    (event: React.KeyboardEvent<HTMLDivElement>) => {
23✔
276
      if (!hasFocus) return; // Add hasFocus check here
9!
277
      
278
      const { key } = event;
9✔
279

280
      // Common handlers
281
      switch (key) {
9✔
282
        case 'Home':
9✔
283
          event.preventDefault();
1✔
284
          handleFirst();
1✔
285
          return;
1✔
286
        case 'End':
9✔
287
          event.preventDefault();
1✔
288
          handleLast();
1✔
289
          return;
1✔
290
      }
9✔
291

292
      // Mode-specific handlers
293
      if (mode === 'HORIZONTAL') {
9✔
294
        if (key === 'ArrowRight') {
4✔
295
          event.preventDefault();
2✔
296
          flipLayout ? handlePrevious() : handleNext();
2✔
297
        } else if (key === 'ArrowLeft') {
2✔
298
          event.preventDefault();
2✔
299
          flipLayout ? handleNext() : handlePrevious();
2✔
300
        }
2✔
301
      } else if (mode === 'VERTICAL' || mode === 'VERTICAL_ALTERNATING') {
9!
302
        if (key === 'ArrowDown') {
3✔
303
          event.preventDefault();
2✔
304
          handleNext();
2✔
305
        } else if (key === 'ArrowUp') {
3✔
306
          event.preventDefault();
1✔
307
          handlePrevious();
1✔
308
        }
1✔
309
      }
3✔
310
    },
9✔
311
    [mode, flipLayout, hasFocus, handleNext, handlePrevious, handleFirst, handleLast],
23✔
312
  );
23✔
313

314
  return {
23✔
315
    activeItemIndex,
23✔
316
    handleTimelineItemClick,
23✔
317
    handleTimelineItemElapsed,
23✔
318
    handleNext,
23✔
319
    handlePrevious,
23✔
320
    handleFirst,
23✔
321
    handleLast,
23✔
322
    handleKeySelection,
23✔
323
  };
23✔
324
};
23✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc