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

CBIIT / bento-c3dc-frontend / 20352582803

18 Dec 2025 09:59PM UTC coverage: 0.175% (-0.005%) from 0.18%
20352582803

Pull #439

github

web-flow
Merge 47a384ecb into 1e27c5885
Pull Request #439: C3dc 2011

6 of 3492 branches covered (0.17%)

Branch coverage included in aggregate %.

0 of 184 new or added lines in 4 files covered. (0.0%)

47 existing lines in 4 files now uncovered.

10 of 5648 relevant lines covered (0.18%)

0.09 hits per line

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

0.0
/src/components/EllipsisText/EllipsisText.js
1
import React, { useRef, useState, useLayoutEffect } from 'react';
×
2
import { withStyles } from '@material-ui/core';
×
3

4
/**
5
 * EllipsisText Component
6
 * Displays text with ellipsis when it overflows. Supports two modes:
7
 * - 'middle': Shows beginning and end with ellipsis in the middle
8
 * - 'end': Standard end truncation (uses CSS text-overflow: ellipsis)
9
 *
10
 * @param {string} text - The text to display
11
 * @param {string} className - Optional CSS class name
12
 * @param {object} classes - Material-UI classes object
13
 * @param {function} onTruncate - Optional callback called with true when text is truncated, false when not
14
 * @param {string} mode - Ellipsis mode: 'middle' or 'end' (default: 'middle')
15
 *
16
 * Example (middle mode):
17
 * "My Very Long Cohort Name That Exceeds...For Display Copy (3)"
18
 *
19
 * Example (end mode):
20
 * "My Very Long Cohort Name That Exceeds For Display..."
21
 */
22
const EllipsisText = ({ text, className, classes, onTruncate, mode = 'middle' }) => {
×
23
    const containerRef = useRef(null);
×
24
    const measureRef = useRef(null);
×
25
    const [displayText, setDisplayText] = useState(text);
×
26

27
    // Constants for middle ellipsis truncation
28
    const MIN_CHARS_PER_SIDE = 5; // Minimum characters to show on each side of ellipsis
×
29
    const MIN_TOTAL_CHARS = MIN_CHARS_PER_SIDE * 2; // Minimum total characters (start + end)
×
30

31
    useLayoutEffect(() => {
×
32
        if (!containerRef.current || !text) {
×
33
            setDisplayText(text);
×
34
            return;
×
35
        }
36

37
        const container = containerRef.current;
×
38

UNCOV
39
        const measureAndTruncate = () => {
×
40
            if (!container) return;
×
41

42
            // End mode: Simple overflow detection, CSS handles truncation
43
            if (mode === 'end') {
×
44
                const isOverflowing = container.scrollWidth > container.clientWidth;
×
UNCOV
45
                if (onTruncate) onTruncate(isOverflowing);
×
UNCOV
46
                setDisplayText(text);
×
UNCOV
47
                return;
×
48
            }
49

50
            // Middle mode: Custom truncation logic
UNCOV
51
            if (!measureRef.current) {
×
UNCOV
52
                setDisplayText(text);
×
53
                return;
×
54
            }
55

56
            const measureSpan = measureRef.current;
×
57
            const availableWidth = container.offsetWidth;
×
58

UNCOV
59
            if (availableWidth === 0) {
×
UNCOV
60
                setDisplayText(text);
×
UNCOV
61
                return;
×
62
            }
63

64
            // Measure full text width
UNCOV
65
            measureSpan.textContent = text;
×
66
            const fullWidth = measureSpan.offsetWidth;
×
67

68
            // If text fits, no truncation needed
69
            if (fullWidth <= availableWidth) {
×
UNCOV
70
                setDisplayText(text);
×
UNCOV
71
                if (onTruncate) onTruncate(false);
×
UNCOV
72
                return;
×
73
            }
74

75
            // Text needs truncation with middle ellipsis
76
            const ellipsis = '...';
×
UNCOV
77
            measureSpan.textContent = ellipsis;
×
78
            const ellipsisWidth = measureSpan.offsetWidth;
×
UNCOV
79
            const targetWidth = availableWidth - ellipsisWidth;
×
80

81
            let bestFit = '';
×
82

83
            // Use binary search to find the maximum number of characters that fit (O(log n) complexity)
84
            let left = MIN_TOTAL_CHARS;
×
85
            let right = text.length - MIN_CHARS_PER_SIDE;
×
86

UNCOV
87
            while (left <= right) {
×
88
                const totalChars = Math.floor((left + right) / 2);
×
89

90
                // Split as evenly as possible (50/50)
91
                const startLen = Math.floor(totalChars / 2);
×
92
                const endLen = totalChars - startLen;
×
93

UNCOV
94
                if (startLen < MIN_CHARS_PER_SIDE || endLen < MIN_CHARS_PER_SIDE) {
×
UNCOV
95
                    right = totalChars - 1;
×
96
                    continue;
×
97
                }
98

99
                const start = text.substring(0, startLen);
×
100
                const end = text.substring(text.length - endLen);
×
101

102
                measureSpan.textContent = start + end;
×
UNCOV
103
                const combinedWidth = measureSpan.offsetWidth;
×
104

105
                if (combinedWidth <= targetWidth) {
×
106
                    // This fits, try to fit more characters
UNCOV
107
                    bestFit = start + ellipsis + end;
×
108
                    left = totalChars + 1;
×
109
                } else {
110
                    // Too wide, try fewer characters
UNCOV
111
                    right = totalChars - 1;
×
112
                }
113
            }
114

UNCOV
115
            if (bestFit) {
×
UNCOV
116
                setDisplayText(bestFit);
×
117
                if (onTruncate) onTruncate(true);
×
118
            } else {
119
                // Fallback: just show first part with ellipsis
120
                let truncateLen = Math.floor(text.length * 0.5);
×
121
                while (truncateLen > 1) {
×
122
                    const truncated = text.substring(0, truncateLen) + ellipsis;
×
123
                    measureSpan.textContent = truncated;
×
124
                    if (measureSpan.offsetWidth <= availableWidth) {
×
UNCOV
125
                        setDisplayText(truncated);
×
126
                        if (onTruncate) onTruncate(true);
×
UNCOV
127
                        return;
×
128
                    }
129
                    truncateLen--;
×
130
                }
UNCOV
131
                setDisplayText(text.substring(0, 1) + ellipsis);
×
UNCOV
132
                if (onTruncate) onTruncate(true);
×
133
            }
134
        };
135

136
        // Use ResizeObserver to react to actual layout changes
UNCOV
137
        const resizeObserver = new ResizeObserver((entries) => {
×
138
            // Only measure when container has stable, non-zero dimensions
UNCOV
139
            const entry = entries[0];
×
UNCOV
140
            if (entry && entry.contentRect.width > 0) {
×
UNCOV
141
                measureAndTruncate();
×
142
            }
143
        });
144

UNCOV
145
        resizeObserver.observe(container);
×
146

147
        // Also run immediately in case container is already sized
UNCOV
148
        if (container.offsetWidth > 0) {
×
UNCOV
149
            measureAndTruncate();
×
150
        }
151

UNCOV
152
        return () => {
×
UNCOV
153
            resizeObserver.disconnect();
×
154
        };
155
    }, [text, mode, onTruncate, MIN_CHARS_PER_SIDE, MIN_TOTAL_CHARS]);
156

157
    // End mode: Simple span with CSS truncation
UNCOV
158
    if (mode === 'end') {
×
UNCOV
159
        return (
×
160
            <span
161
                ref={containerRef}
162
                className={`${classes.endContainer} ${className || ''}`}
×
163
            >
164
                {displayText}
165
            </span>
166
        );
167
    }
168

169
    // Middle mode: Custom truncation with measurement span
UNCOV
170
    return (
×
171
        <span ref={containerRef} className={`${classes.middleContainer} ${className || ''}`}>
×
172
            <span className={classes.measureSpan} ref={measureRef} />
173
            <span className={classes.displaySpan}>{displayText}</span>
174
        </span>
175
    );
176
};
177

178
const styles = () => ({
×
179
    endContainer: {
180
        overflow: 'hidden',
181
        textOverflow: 'ellipsis',
182
        whiteSpace: 'nowrap',
183
        display: 'inline-block',
184
        maxWidth: '100%',
185
    },
186
    middleContainer: {
187
        position: 'relative',
188
        display: 'inline-block',
189
        maxWidth: '100%',
190
    },
191
    measureSpan: {
192
        visibility: 'hidden',
193
        position: 'absolute',
194
        whiteSpace: 'nowrap',
195
    },
196
    displaySpan: {
197
        display: 'inline-block',
198
        maxWidth: '100%',
199
    },
200
});
201

UNCOV
202
const StyledEllipsisText = withStyles(styles)(EllipsisText);
×
203

204
// Export convenient named variants
UNCOV
205
export const MiddleEllipsisText = (props) => <StyledEllipsisText {...props} mode="middle" />;
×
UNCOV
206
export const EndEllipsisText = (props) => <StyledEllipsisText {...props} mode="end" />;
×
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

© 2026 Coveralls, Inc