Skip to content

Commit ec54a5f

Browse files
yashgoyal0110kasyaarkid15r
authored
Truncated text component (#1117)
* commit * Update pnpm-lock.yaml * Update vite.config.ts * Update package.json * truncate text component * commit * commit * commit * commit * commit * commit * commit * commit * commit * commit * fixed e2e and improved truncate component * fixed pre-commit * comit * comit * commit * fixed unit tests * fixed unit test * commit * Update code --------- Co-authored-by: Kate Golovanova <kate@kgthreads.com> Co-authored-by: Arkadii Yakovets <2201626+arkid15r@users.noreply.github.com> Co-authored-by: Arkadii Yakovets <arkadii.yakovets@owasp.org>
1 parent b3b9dea commit ec54a5f

File tree

7 files changed

+106
-20
lines changed

7 files changed

+106
-20
lines changed

frontend/__tests__/e2e/pages/Home.spec.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,4 +84,15 @@ test.describe('Home Page', () => {
8484
await expect(page.getByText('Feb 27 — 28, 2025')).toBeVisible()
8585
await page.getByRole('button', { name: 'Event 1' }).click()
8686
})
87+
88+
test('should have truncated text with overflow for all relevant elements', async ({ page }) => {
89+
const truncatedElements = await page.locator('span.truncate').all()
90+
expect(truncatedElements.length).toBeGreaterThan(0)
91+
92+
for (const element of truncatedElements) {
93+
await expect(element).toHaveCSS('overflow', 'hidden')
94+
await expect(element).toHaveCSS('text-overflow', 'ellipsis')
95+
await expect(element).toHaveCSS('white-space', 'nowrap')
96+
}
97+
})
8798
})
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { render, screen } from 'wrappers/testUtil'
2+
import { TruncatedText } from 'components/TruncatedText'
3+
4+
describe('TruncatedText Component', () => {
5+
const longText = 'This is very long text that should be truncated for display.'
6+
7+
test('renders full text when it fits within the container', () => {
8+
render(<TruncatedText text="Short text" className="w-auto" />)
9+
const textElement = screen.getByText('Short text')
10+
expect(textElement).toBeInTheDocument()
11+
expect(textElement).toHaveAttribute('title', 'Short text')
12+
})
13+
14+
test('truncates text when it exceeds container width', () => {
15+
render(
16+
<div style={{ width: '100px' }}>
17+
<TruncatedText text={longText} />
18+
</div>
19+
)
20+
const textElement = screen.getByText(longText)
21+
expect(textElement).toHaveClass('truncate')
22+
expect(textElement).toHaveClass('text-ellipsis')
23+
})
24+
25+
test('title attribute is always present', () => {
26+
render(<TruncatedText text={longText} />)
27+
const textElement = screen.getByText(longText)
28+
expect(textElement).toHaveAttribute('title', longText)
29+
})
30+
})

frontend/jest.setup.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ if (!global.structuredClone) {
1313
global.structuredClone = (val) => JSON.parse(JSON.stringify(val))
1414
}
1515

16-
// mock runAnimationFrameCallbacks function for testing
1716
beforeAll(() => {
1817
if (typeof window !== 'undefined') {
1918
jest.spyOn(window, 'requestAnimationFrame').mockImplementation((cb) => {
@@ -22,10 +21,16 @@ beforeAll(() => {
2221

2322
Object.defineProperty(window, 'runAnimationFrameCallbacks', {
2423
value: () => {},
25-
writable: true,
2624
configurable: true,
25+
writable: true,
2726
})
2827
}
28+
29+
global.ResizeObserver = class {
30+
disconnect() {}
31+
observe() {}
32+
unobserve() {}
33+
}
2934
})
3035

3136
beforeEach(() => {

frontend/src/components/ItemCardList.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { JSX } from 'react'
22
import { ProjectIssuesType, ProjectReleaseType } from 'types/project'
33
import { PullRequestsType } from 'types/user'
44
import SecondaryCard from './SecondaryCard'
5+
import { TruncatedText } from './TruncatedText'
56

67
const ItemCardList = ({
78
title,
@@ -44,7 +45,7 @@ const ItemCardList = ({
4445
href={item?.url}
4546
target="_blank"
4647
>
47-
{item.title || item.name}
48+
<TruncatedText text={item.title || item.name} />
4849
</a>
4950
</h3>
5051
</div>

frontend/src/components/RepositoriesCard.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import type React from 'react'
1313
import { useState } from 'react'
1414
import { useNavigate } from 'react-router-dom'
1515
import { RepositoriesCardProps } from 'types/project'
16+
import { TruncatedText } from './TruncatedText'
1617

1718
const RepositoriesCard: React.FC<RepositoriesCardProps> = ({ repositories }) => {
1819
const [showAllRepositories, setShowAllRepositories] = useState(false)
@@ -59,7 +60,7 @@ const RepositoryItem = ({ details }) => {
5960
onClick={handleClick}
6061
className="text-start font-semibold text-blue-500 hover:underline dark:text-blue-400"
6162
>
62-
{details?.name}
63+
<TruncatedText text={details?.name} />
6364
</button>
6465

6566
<div className="space-y-2 text-sm">
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { useRef, useEffect, useCallback } from 'react'
2+
3+
export const TruncatedText = ({ text, className = '' }: { text: string; className?: string }) => {
4+
const textRef = useRef<HTMLSpanElement>(null)
5+
6+
const checkTruncation = useCallback(() => {
7+
const element = textRef.current
8+
if (element) {
9+
element.title = text
10+
}
11+
}, [text])
12+
13+
useEffect(() => {
14+
checkTruncation()
15+
16+
const observer = new ResizeObserver(() => checkTruncation())
17+
if (textRef.current) {
18+
observer.observe(textRef.current)
19+
}
20+
21+
window.addEventListener('resize', checkTruncation)
22+
23+
return () => {
24+
observer.disconnect()
25+
window.removeEventListener('resize', checkTruncation)
26+
}
27+
}, [text, checkTruncation])
28+
29+
return (
30+
<span
31+
ref={textRef}
32+
className={`block overflow-hidden truncate text-ellipsis whitespace-nowrap ${className}`}
33+
>
34+
{text}
35+
</span>
36+
)
37+
}

frontend/src/pages/Home.tsx

Lines changed: 17 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import Modal from 'components/Modal'
2929
import MultiSearchBar from 'components/MultiSearch'
3030
import SecondaryCard from 'components/SecondaryCard'
3131
import TopContributors from 'components/ToggleContributors'
32+
import { TruncatedText } from 'components/TruncatedText'
3233
import { toaster } from 'components/ui/toaster'
3334

3435
export default function Home() {
@@ -135,16 +136,16 @@ export default function Home() {
135136
/>
136137
</div>
137138
</div>
138-
<SecondaryCard title="Upcoming Events">
139+
<SecondaryCard title="Upcoming Events" className="overflow-hidden">
139140
<div className="grid gap-4 sm:grid-cols-1 md:grid-cols-2 lg:grid-cols-3">
140141
{data.upcomingEvents.map((event: EventType, index: number) => (
141-
<div key={`card-${event.name}`}>
142+
<div key={`card-${event.name}`} className="overflow-hidden">
142143
<div className="rounded-lg bg-gray-200 p-4 dark:bg-gray-700">
143144
<button
144145
className="mb-2 w-full text-left text-lg font-semibold text-blue-500 hover:underline"
145146
onClick={() => setModalOpenIndex(index)}
146147
>
147-
<h3 className="truncate text-wrap md:text-nowrap">{event.name}</h3>
148+
<TruncatedText text={event.name} />
148149
</button>
149150
<div className="flex flex-col flex-wrap items-start text-sm text-gray-600 dark:text-gray-300 md:flex-row">
150151
<div className="mr-2 flex items-center">
@@ -173,13 +174,13 @@ export default function Home() {
173174
</div>
174175
</SecondaryCard>
175176
<div className="grid gap-4 md:grid-cols-2">
176-
<SecondaryCard title="New Chapters">
177+
<SecondaryCard title="New Chapters" className="overflow-hidden">
177178
<div className="space-y-4">
178179
{data.recentChapters.map((chapter) => (
179180
<div key={chapter.key} className="rounded-lg bg-gray-200 p-4 dark:bg-gray-700">
180181
<h3 className="mb-2 text-lg font-semibold">
181182
<a href={`/chapters/${chapter.key}`} className="hover:underline">
182-
{chapter.name}
183+
<TruncatedText text={chapter.name} />
183184
</a>
184185
</h3>
185186
<div className="flex flex-wrap items-center text-sm text-gray-600 dark:text-gray-300">
@@ -202,15 +203,15 @@ export default function Home() {
202203
))}
203204
</div>
204205
</SecondaryCard>
205-
<SecondaryCard title="New Projects">
206+
<SecondaryCard title="New Projects" className="overflow-hidden">
206207
<div className="space-y-4">
207208
{data.recentProjects.map((project) => (
208209
<div key={project.key} className="rounded-lg bg-gray-200 p-4 dark:bg-gray-700">
209-
<h3 className="mb-2 text-lg font-semibold">
210-
<a href={`/projects/${project.key}`} className="hover:underline">
211-
{project.name}
212-
</a>
213-
</h3>
210+
<a href={`/projects/${project.key}`} className="hover:underline">
211+
<h3 className="mb-2 truncate text-wrap text-lg font-semibold md:text-nowrap">
212+
<TruncatedText text={project.name} />
213+
</h3>
214+
</a>
214215
<div className="flex flex-wrap items-center text-sm text-gray-600 dark:text-gray-300">
215216
<div className="mr-4 flex items-center">
216217
<FontAwesomeIcon icon={faCalendar} className="mr-2 h-4 w-4" />
@@ -280,22 +281,22 @@ export default function Home() {
280281
)}
281282
/>
282283
</div>
283-
<SecondaryCard title="Recent News & Opinions">
284+
<SecondaryCard title="Recent News & Opinions" className="overflow-hidden">
284285
<div className="grid gap-4 sm:grid-cols-1 md:grid-cols-2">
285286
{data.recentPosts.map((post) => (
286287
<div
287288
key={post.title}
288-
className="rounded-lg bg-gray-200 p-4 dark:bg-gray-700"
289+
className="overflow-hidden rounded-lg bg-gray-200 p-4 dark:bg-gray-700"
289290
data-testid="post-container"
290291
>
291-
<h3 className="mb-1 truncate text-wrap text-lg font-semibold text-blue-500 md:text-nowrap">
292+
<h3 className="mb-1 text-lg font-semibold">
292293
<a
293294
href={post.url}
294-
className="hover:underline"
295+
className="text-blue-500 hover:underline"
295296
target="_blank"
296297
rel="noopener noreferrer"
297298
>
298-
{post.title}
299+
<TruncatedText text={post.title} />
299300
</a>
300301
</h3>
301302
<div className="mt-2 flex flex-col flex-wrap items-start text-sm text-gray-600 dark:text-gray-300 md:flex-row">

0 commit comments

Comments
 (0)