• 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

84.0
/src/components/timeline-vertical/timeline-point.tsx
1
import { TimelinePointModel } from '@models/TimelineVerticalModel'; // Assuming model path
2
import cls from 'classnames'; // Utility for conditionally joining classNames
1✔
3
import React, {
1✔
4
  memo, // Import memo for component optimization
5
  useEffect,
6
  useMemo,
7
  useRef,
8
  FunctionComponent, // Explicit import
9
  MouseEvent,
10
} from 'react';
11
import { useStableContext, useDynamicContext } from '../contexts'; // Context for global theme/settings
1✔
12
// Shape seems to be a shared styled component, potentially defined elsewhere
13
import { Shape } from '../timeline-elements/timeline-card/timeline-horizontal-card.styles';
1✔
14
import {
1✔
15
  TimelinePointContainer,
16
  TimelinePointWrapper,
17
} from './timeline-vertical-shape.styles'; // Associated styled components
18

19
/**
20
 * Renders the circular point or icon on the timeline line for a vertical item.
21
 * Handles click events, active state highlighting, custom icons, and appearance
22
 * based on global theme and configuration settings. It uses React.memo for
23
 * performance optimization.
24
 *
25
 * @param {TimelinePointModel} props - The properties for the TimelinePoint component.
26
 * @returns {React.ReactElement} The rendered TimelinePoint component.
27
 */
28
const TimelinePoint: FunctionComponent<TimelinePointModel> = memo(
1✔
29
  (props: TimelinePointModel) => {
1✔
30
    const {
23✔
31
      className, // 'left' or 'right' - passed to styled components
23✔
32
      id, // Unique ID of the timeline item
23✔
33
      onClick, // Callback function when the point is clicked
23✔
34
      active, // Is this point currently active/highlighted?
23✔
35
      onActive, // Callback function when the point should trigger the 'active' state calculation
23✔
36
      slideShowRunning, // Is a slideshow currently active? (disables onClick)
23✔
37
      iconChild, // Custom React node to display inside the point (replaces default shape)
23✔
38
      timelinePointDimension, // Size (width/height) of the point
23✔
39
      lineWidth, // Width of the timeline line connecting points
23✔
40
      disableClickOnCircle, // Should clicks on the point be ignored?
23✔
41
      cardLess, // Is the timeline in 'cardLess' mode?
23✔
42
      isMobile, // Is the view currently mobile?
23✔
43
    } = props;
23✔
44

45
    // Ref to the button element representing the point
46
    const circleRef = useRef<HTMLButtonElement>(null);
23✔
47

48
    // Access context settings
49
    const {
23✔
50
      staticDefaults: {
23✔
51
        focusActiveItemOnLoad,
23✔
52
        timelinePointShape,
23✔
53
        disableTimelinePoint,
23✔
54
      },
23✔
55
      memoizedButtonTexts: buttonTexts, // Custom button text labels
23✔
56
    } = useStableContext();
23✔
57

58
    const {
23✔
59
      memoizedTheme: theme, // Theme object (primary color, etc.)
23✔
60
    } = useDynamicContext();
23✔
61

62
    // Ref to track if this is the component's first render cycle
63
    const isFirstRender = useRef(true);
23✔
64

65
    /**
66
     * Determines if the onActive callback should be invoked based on the active state
67
     * and whether it's the initial render (controlled by focusActiveItemOnLoad).
68
     */
69
    const canInvokeOnActive = useMemo(() => {
23✔
70
      // If focusing on load is enabled, invoke if active.
71
      if (focusActiveItemOnLoad) {
23!
UNCOV
72
        return active;
×
UNCOV
73
      }
×
74
      // Otherwise, invoke only if active AND it's not the first render.
75
      else {
23✔
76
        return active && !isFirstRender.current;
23!
77
      }
23✔
78
    }, [active, focusActiveItemOnLoad]); // Dependencies: active state and global setting
23✔
79

80
    /**
81
     * Effect to call the onActive callback when conditions are met.
82
     * This usually happens when an item scrolls into view or is programmatically activated.
83
     */
84
    useEffect(() => {
23✔
85
      if (canInvokeOnActive && onActive && circleRef.current) {
23!
86
        // Call the parent's onActive handler with the point's offsetTop
UNCOV
87
        onActive(circleRef.current.offsetTop);
×
UNCOV
88
      }
×
89
      // Intentionally excluding onActive from dependencies if it's stable,
90
      // otherwise, include it if it might change. Usually, it's stable.
91
    }, [canInvokeOnActive, active]); // Re-run when activation condition or active state changes
23✔
92

93
    /**
94
     * Memoized CSS classes for the inner Shape component.
95
     * Applies 'active' class and 'using-icon' if a custom icon is provided.
96
     */
97
    const circleClass = useMemo(
23✔
98
      () =>
23✔
99
        cls({
23✔
100
          active: active, // Apply 'active' class if the point is active
23✔
101
          'using-icon': !!iconChild, // Apply class if a custom icon is used
23✔
102
        }),
23✔
103
      [active, iconChild], // Dependencies: active state and presence of iconChild
23✔
104
    );
23✔
105

106
    /**
107
     * Memoized click handler props for the TimelinePointContainer button.
108
     * Only adds onClick if clicks are enabled and slideshow isn't running.
109
     */
110
    const clickHandlerProps = useMemo(() => {
23✔
111
      // Return empty object (no click handler) if clicks are disabled
112
      if (disableClickOnCircle) {
23!
UNCOV
113
        return {};
×
114
      }
×
115

116
      // Return props containing the onClick handler
117
      return {
23✔
118
        onClick: (ev: MouseEvent) => {
23✔
119
          ev.stopPropagation(); // Prevent event bubbling up
×
120
          // Call the provided onClick handler if it exists, passing the item ID
UNCOV
121
          if (id && onClick && !slideShowRunning) {
×
UNCOV
122
            onClick(id);
×
UNCOV
123
          }
×
UNCOV
124
        },
×
125
      };
23✔
126
    }, [id, onClick, slideShowRunning, disableClickOnCircle]); // Dependencies for the click logic
23✔
127

128
    /**
129
     * Effect to update the isFirstRender flag after the initial render is complete.
130
     */
131
    useEffect(() => {
23✔
132
      // This effect runs only once after the initial mount
133
      if (isFirstRender.current) {
23✔
134
        isFirstRender.current = false;
23✔
135
      }
23✔
136
    }, []); // Empty dependency array ensures it runs only once
23✔
137

138
    // Create an accessible label for the timeline point
139
    const timelinePointLabel = useMemo(() => {
23✔
140
      return (
23✔
141
        buttonTexts?.timelinePoint ??
23!
UNCOV
142
        (active ? 'Active timeline point' : 'Timeline point')
×
143
      );
144
    }, [active, buttonTexts]);
23✔
145

146
    // Render the timeline point structure
147
    return (
23✔
148
      <TimelinePointWrapper
23✔
149
        // --- Props passed to styled-component ---
150
        width={lineWidth} // Controls the width of the connecting lines (via ::before/::after)
23✔
151
        bg={theme?.primary} // Background color for the connecting lines
23✔
152
        $cardLess={cardLess} // Pass cardLess state
23✔
153
        $isMobile={isMobile} // Pass mobile state
23✔
154
        // --- Standard React props ---
155
        className={className} // 'left' or 'right'
23✔
156
        data-testid="tree-leaf" // Test ID for the wrapper
23✔
157
      >
158
        {/* Container is a button for accessibility and click handling */}
159
        <TimelinePointContainer
23✔
160
          // --- Props passed to styled-component ---
161
          $hide={disableTimelinePoint} // Hide based on global setting
23✔
162
          // --- Standard React props ---
163
          className={`${className} timeline-vertical-circle`} // Combine classes
23✔
164
          {...clickHandlerProps} // Spread the memoized click handler props
23✔
165
          ref={circleRef} // Attach ref for position measurement
23✔
166
          data-testid="tree-leaf-click" // Test ID for the clickable element
23✔
167
          aria-label={timelinePointLabel} // Accessibility label
23✔
168
          aria-disabled={disableClickOnCircle ?? disableTimelinePoint} // Disable button if needed
23✔
169
          disabled={disableClickOnCircle || disableTimelinePoint} // Disable button if needed
23✔
170
          tabIndex={disableClickOnCircle || disableTimelinePoint ? -1 : 0} // Manage tab order
23!
171
        >
172
          {/* The visual shape (circle, square, or custom icon) */}
173
          <Shape
23✔
174
            // --- Props passed to styled-component ---
175
            theme={theme}
23✔
176
            dimension={timelinePointDimension} // Controls the size
23✔
177
            $timelinePointShape={timelinePointShape} // Controls the shape ('circle', 'square')
23✔
178
            // --- Standard React props ---
179
            className={circleClass} // Apply 'active' and 'using-icon' classes
23✔
180
            aria-hidden="true" // Hide from screen readers as it's decorative
23✔
181
          >
182
            {iconChild}
23✔
183
          </Shape>
23✔
184
        </TimelinePointContainer>
23✔
185
      </TimelinePointWrapper>
23✔
186
    ) as React.ReactElement;
187
  },
23✔
188
  // Use default shallow comparison for memoization.
189
  // The previous custom comparison (prev.active === next.active && prev.isMobile === next.isMobile)
190
  // was too restrictive and would prevent updates when other props like iconChild, theme, onClick, etc., changed.
191
  // Default shallow comparison is generally safer unless profiling reveals a specific need for a custom function.
192
);
1✔
193

194
// Set display name for React DevTools
195
TimelinePoint.displayName = 'TimelinePoint';
1✔
196

197
export { TimelinePoint }; // Export the component
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