• 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

90.0
/src/components/timeline-vertical/timeline-vertical-item.tsx
1
import { VerticalItemModel } from '@models/TimelineVerticalModel';
2
import cls from 'classnames';
1✔
3
import {
1✔
4
  useCallback,
5
  useContext,
6
  useMemo,
7
  useRef,
8
  FunctionComponent,
9
  ReactElement,
10
} from 'react';
11
import { GlobalContext } from '../GlobalContext';
1✔
12
import TimelineCard from '../timeline-elements/timeline-card-content/timeline-card-content';
1✔
13
import TimelineItemTitle from '../timeline-elements/timeline-item-title/timeline-card-title';
1✔
14
import { TimelinePoint } from './timeline-point';
1✔
15
import {
1✔
16
  TimelineCardContentWrapper,
17
  TimelineTitleWrapper,
18
  VerticalItemWrapper,
19
} from './timeline-vertical.styles';
20

21
/**
22
 * Represents a single item (row) in the vertical timeline.
23
 * It coordinates the display of the title, the central point/icon,
24
 * and the main content card based on the provided props and global context.
25
 *
26
 * @param {VerticalItemModel} props - The properties for the VerticalItem component.
27
 * @returns {JSX.Element} The rendered VerticalItem component.
28
 */
29
const VerticalItem: FunctionComponent<VerticalItemModel> = (
1✔
30
  props: VerticalItemModel,
26✔
31
): ReactElement => {
26✔
32
  // Ref to the main list item element for calculating position/dimensions
33
  const contentRef = useRef<HTMLLIElement>(null);
26✔
34

35
  // Destructure all props for cleaner access
36
  const {
26✔
37
    active, // Is this item currently highlighted/active?
26✔
38
    alternateCards, // Layout mode: alternate card sides?
26✔
39
    cardDetailedText, // Detailed text content for the card
26✔
40
    cardSubtitle, // Subtitle/main content for the card
26✔
41
    cardTitle, // Title specifically for the card (distinct from the timeline item title)
26✔
42
    url, // URL for navigation, potentially used by the card
26✔
43
    className, // CSS class for the side ('left' or 'right')
26✔
44
    contentDetailsChildren, // Custom React nodes for card details area
26✔
45
    iconChild, // Custom React node for the timeline point/icon
26✔
46
    hasFocus, // Does the card content have focus (e.g., for slideshow)?
26✔
47
    id, // Unique ID for the item
26✔
48
    media, // Media element (image/video) for the card
26✔
49
    onActive, // Callback when item becomes active (e.g., scrolled into view)
26✔
50
    onClick, // Callback when the item (or point) is clicked
26✔
51
    onElapsed, // Callback when media finishes playing (if applicable)
26✔
52
    slideShowRunning, // Is a slideshow currently active?
26✔
53
    title, // Title for the timeline item itself (often a date or heading)
26✔
54
    visible, // Is this item currently visible in the viewport?
26✔
55
    timelineContent, // Custom React nodes for the main timeline content area
26✔
56
    items, // Data for nested items (if any)
26✔
57
    isNested, // Is this item part of a nested structure?
26✔
58
    nestedCardHeight, // Specific height for nested cards
26✔
59
  } = props;
26✔
60

61
  // Access global settings and theme from context
62
  const {
26✔
63
    cardHeight, // Default card height
26✔
64
    mode, // Timeline mode (VERTICAL, VERTICAL_ALTERNATING)
26✔
65
    flipLayout, // Reverse layout order (title/point/card)?
26✔
66
    timelinePointDimension, // Size of the timeline point
26✔
67
    lineWidth, // Width of the timeline central line
26✔
68
    disableClickOnCircle, // Prevent clicks on the timeline point?
26✔
69
    cardLess, // Mode without cards, only points/titles
26✔
70
    theme, // Theme object
26✔
71
    classNames, // Custom class names for sub-elements
26✔
72
    textOverlay, // Style where text overlays media
26✔
73
    mediaHeight, // Height for media elements
26✔
74
    disableInteraction, // Disable all user interactions?
26✔
75
    isMobile, // Is the view currently mobile?
26✔
76
  } = useContext(GlobalContext);
26✔
77

78
  /**
79
   * Callback handler triggered by the TimelinePoint when it becomes active.
80
   * Calculates the item's position and notifies the parent.
81
   * @param {number} offset - Vertical offset within the point element itself.
82
   */
83
  const handleOnActive = useCallback(
26✔
84
    (offset: number) => {
26✔
85
      if (contentRef.current && onActive) {
×
86
        const { offsetTop, clientHeight } = contentRef.current;
×
87
        // Call the parent's onActive with calculated position data
88
        onActive(offsetTop + offset, offsetTop, clientHeight);
×
89
      }
×
90
    },
×
91
    [onActive], // Dependency: only recreate if onActive changes
26✔
92
  );
26✔
93

94
  /**
95
   * Handler for the "Read More" action within the card.
96
   * Uses a short timeout to likely ensure the DOM has updated (card expanded)
97
   * before recalculating the active position.
98
   */
99
  const handleShowMore = useCallback(() => {
26✔
100
    // Use timeout to defer execution, allowing potential layout shifts to settle
101
    setTimeout(() => {
×
102
      handleOnActive(0); // Recalculate position after content change
×
103
    }, 100); // Small delay (adjust if needed)
×
104
  }, [handleOnActive]); // Dependency: handleOnActive
26✔
105

106
  /**
107
   * Memoized Timeline Item Title component.
108
   * Avoids re-rendering the title if its specific props haven't changed.
109
   */
110
  const Title = useMemo(() => {
26✔
111
    return (
22✔
112
      <TimelineTitleWrapper
22✔
113
        className={className} // 'left' or 'right'
22✔
114
        $alternateCards={alternateCards} // Pass prop to styled-component
22✔
115
        mode={mode}
22✔
116
        $hide={!title} // Hide wrapper if no title text
22✔
117
        // Flip title position only in non-alternating vertical mode
118
        $flip={!alternateCards && flipLayout}
22✔
119
      >
120
        <TimelineItemTitle
22✔
121
          title={title as string}
22✔
122
          active={active && !disableInteraction} // Highlight if active and interaction enabled
22!
123
          theme={theme}
22✔
124
          // Align text based on layout mode
125
          align={flipLayout && !alternateCards ? 'left' : 'right'}
22!
126
          classString={classNames?.title} // Optional custom class
22✔
127
        />
22✔
128
      </TimelineTitleWrapper>
22✔
129
    );
130
  }, [
26✔
131
    active,
26✔
132
    title,
26✔
133
    className,
26✔
134
    alternateCards,
26✔
135
    mode,
26✔
136
    flipLayout,
26✔
137
    theme,
26✔
138
    classNames?.title, // Correct dependency
26✔
139
    disableInteraction, // Added dependency
26✔
140
  ]);
26✔
141

142
  /**
143
   * Memoized CSS classes for the main VerticalItemWrapper.
144
   * Includes the side ('left'/'right'), visibility, and base class.
145
   */
146
  const verticalItemClass = useMemo(
26✔
147
    () =>
26✔
148
      cls(
22✔
149
        'vertical-item-row', // Base class
22✔
150
        { [className]: !!className }, // Add 'left' or 'right' if className is present
22✔
151
        { visible: visible }, // Add 'visible' class if visible prop is true
22✔
152
      ),
22✔
153
    [className, visible],
26✔
154
  );
26✔
155

156
  /**
157
   * Memoized CSS classes for the TimelineCardContentWrapper.
158
   */
159
  const contentClass = useMemo(
26✔
160
    () =>
26✔
161
      cls(
22✔
162
        'card-content-wrapper', // Base class
22✔
163
        { [className]: !!className }, // Add 'left' or 'right'
22✔
164
        { visible: visible }, // Add 'visible' class
22✔
165
      ),
22✔
166
    [className, visible],
26✔
167
  );
26✔
168

169
  /**
170
   * Memoized Timeline Point component.
171
   * Avoids re-rendering the point if its specific props haven't changed.
172
   */
173
  const TimelinePointMemo = useMemo(
26✔
174
    () => (
26✔
175
      <TimelinePoint
22✔
176
        active={active}
22✔
177
        alternateCards={alternateCards}
22✔
178
        className={className} // 'left' or 'right'
22✔
179
        id={id}
22✔
180
        mode={mode}
22✔
181
        onActive={handleOnActive} // Pass down the memoized handler
22✔
182
        onClick={onClick}
22✔
183
        slideShowRunning={slideShowRunning}
22✔
184
        iconChild={iconChild} // Custom icon
22✔
185
        timelinePointDimension={timelinePointDimension}
22✔
186
        lineWidth={lineWidth}
22✔
187
        disableClickOnCircle={disableClickOnCircle}
22✔
188
        cardLess={cardLess}
22✔
189
        isMobile={isMobile}
22✔
190
      />
22✔
191
    ),
192
    [
26✔
193
      // Comprehensive dependency list for memoization
194
      active,
26✔
195
      alternateCards,
26✔
196
      className,
26✔
197
      id,
26✔
198
      mode,
26✔
199
      handleOnActive, // Use the memoized callback
26✔
200
      onClick,
26✔
201
      slideShowRunning,
26✔
202
      iconChild,
26✔
203
      timelinePointDimension,
26✔
204
      lineWidth,
26✔
205
      disableClickOnCircle,
26✔
206
      cardLess,
26✔
207
      isMobile,
26✔
208
    ],
26✔
209
  );
26✔
210

211
  /**
212
   * Determines if the title section should be rendered.
213
   * Titles are typically hidden for nested items or on mobile for space.
214
   */
215
  const canShowTitle = useMemo(() => {
26✔
216
    // Show title only if it's NOT nested AND NOT mobile view
217
    return !isNested && !isMobile;
22✔
218
  }, [isNested, isMobile]);
26✔
219

220
  // Get a readable title for screen readers
221
  const accessibleTitle = useMemo(() => {
26✔
222
    if (typeof title === 'string') {
22✔
223
      return title;
22✔
224
    } else if (typeof cardTitle === 'string') {
22!
225
      return cardTitle;
×
226
    } else {
×
227
      return 'Timeline item';
×
228
    }
×
229
  }, [title, cardTitle]);
26✔
230

231
  // Render the complete timeline item structure
232
  return (
26✔
233
    <VerticalItemWrapper
26✔
234
      as="li"
26✔
235
      $alternateCards={alternateCards}
26✔
236
      $cardHeight={isNested ? nestedCardHeight : cardHeight}
26!
237
      $cardLess={cardLess}
26✔
238
      $isNested={isNested}
26✔
239
      className={verticalItemClass}
26✔
240
      data-testid="vertical-item-row"
26✔
241
      data-item-id={id}
26✔
242
      key={id}
26✔
243
      ref={contentRef}
26✔
244
      theme={theme}
26✔
245
      aria-current={active ? 'true' : undefined}
26!
246
      aria-label={accessibleTitle}
26✔
247
    >
248
      {/* Conditionally render the Title */}
249
      {canShowTitle ? Title : null}
26!
250

251
      {/* Wrapper for the card content */}
252
      <TimelineCardContentWrapper
26✔
253
        // --- Props passed to styled-component ---
254
        $alternateCards={alternateCards}
26✔
255
        $noTitle={!title} // Adjust width if title is absent
26✔
256
        // Flip content position only in non-alternating vertical mode
257
        $flip={!alternateCards && flipLayout}
26✔
258
        // Use media height if text overlay is active, otherwise card height
259
        height={textOverlay ? mediaHeight : cardHeight}
26!
260
        $isMobile={isMobile}
26✔
261
        // --- Standard React props ---
262
        className={contentClass} // Apply memoized classes
26✔
263
      >
264
        {/* Conditionally render the TimelineCard (only if not cardLess mode) */}
265
        {!cardLess ? (
26✔
266
          <TimelineCard
26✔
267
            active={active}
26✔
268
            branchDir={className} // Pass 'left' or 'right'
26✔
269
            content={cardSubtitle}
26✔
270
            customContent={contentDetailsChildren}
26✔
271
            detailedText={cardDetailedText as string | string[]}
26✔
272
            hasFocus={hasFocus}
26✔
273
            id={id}
26✔
274
            media={media}
26✔
275
            onClick={onClick}
26✔
276
            onElapsed={onElapsed}
26✔
277
            onShowMore={handleShowMore} // Pass down the memoized handler
26✔
278
            slideShowActive={slideShowRunning}
26✔
279
            theme={theme}
26✔
280
            url={url}
26✔
281
            // Flip card content only in non-alternating vertical mode
282
            flip={!alternateCards && flipLayout}
26✔
283
            timelineContent={timelineContent}
26✔
284
            items={items} // Pass nested items data
26✔
285
            isNested={isNested}
26✔
286
            nestedCardHeight={nestedCardHeight}
26✔
287
            title={cardTitle as string} // Card-specific title
26✔
288
            cardTitle={title as string} // Item title (might be redundant if cardTitle is used)
26✔
289
          />
26!
UNCOV
290
        ) : null}
×
291
      </TimelineCardContentWrapper>
26✔
292

293
      {/* Conditionally render the Timeline Point (hidden for nested items) */}
294
      {!isNested ? TimelinePointMemo : null}
26!
295
    </VerticalItemWrapper>
26✔
296
  );
297
};
26✔
298

299
// Set display name for React DevTools
300
VerticalItem.displayName = 'VerticalItem';
1✔
301

302
export default VerticalItem;
1✔
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