diff --git a/frontend/__tests__/e2e/pages/Home.spec.ts b/frontend/__tests__/e2e/pages/Home.spec.ts
index 4f5aa5ffd9..33caeef0af 100644
--- a/frontend/__tests__/e2e/pages/Home.spec.ts
+++ b/frontend/__tests__/e2e/pages/Home.spec.ts
@@ -84,4 +84,15 @@ test.describe('Home Page', () => {
await expect(page.getByText('Feb 27 — 28, 2025')).toBeVisible()
await page.getByRole('button', { name: 'Event 1' }).click()
})
+
+ test('should have truncated text with overflow for all relevant elements', async ({ page }) => {
+ const truncatedElements = await page.locator('span.truncate').all()
+ expect(truncatedElements.length).toBeGreaterThan(0)
+
+ for (const element of truncatedElements) {
+ await expect(element).toHaveCSS('overflow', 'hidden')
+ await expect(element).toHaveCSS('text-overflow', 'ellipsis')
+ await expect(element).toHaveCSS('white-space', 'nowrap')
+ }
+ })
})
diff --git a/frontend/__tests__/unit/components/TruncatedText.test.tsx b/frontend/__tests__/unit/components/TruncatedText.test.tsx
new file mode 100644
index 0000000000..6c9a4559f6
--- /dev/null
+++ b/frontend/__tests__/unit/components/TruncatedText.test.tsx
@@ -0,0 +1,30 @@
+import { render, screen } from 'wrappers/testUtil'
+import { TruncatedText } from 'components/TruncatedText'
+
+describe('TruncatedText Component', () => {
+ const longText = 'This is very long text that should be truncated for display.'
+
+ test('renders full text when it fits within the container', () => {
+ render()
+ const textElement = screen.getByText('Short text')
+ expect(textElement).toBeInTheDocument()
+ expect(textElement).toHaveAttribute('title', 'Short text')
+ })
+
+ test('truncates text when it exceeds container width', () => {
+ render(
+
+
+
+ )
+ const textElement = screen.getByText(longText)
+ expect(textElement).toHaveClass('truncate')
+ expect(textElement).toHaveClass('text-ellipsis')
+ })
+
+ test('title attribute is always present', () => {
+ render()
+ const textElement = screen.getByText(longText)
+ expect(textElement).toHaveAttribute('title', longText)
+ })
+})
diff --git a/frontend/jest.setup.ts b/frontend/jest.setup.ts
index 247373e4c6..59e2e8d720 100644
--- a/frontend/jest.setup.ts
+++ b/frontend/jest.setup.ts
@@ -13,7 +13,6 @@ if (!global.structuredClone) {
global.structuredClone = (val) => JSON.parse(JSON.stringify(val))
}
-// mock runAnimationFrameCallbacks function for testing
beforeAll(() => {
if (typeof window !== 'undefined') {
jest.spyOn(window, 'requestAnimationFrame').mockImplementation((cb) => {
@@ -22,10 +21,16 @@ beforeAll(() => {
Object.defineProperty(window, 'runAnimationFrameCallbacks', {
value: () => {},
- writable: true,
configurable: true,
+ writable: true,
})
}
+
+ global.ResizeObserver = class {
+ disconnect() {}
+ observe() {}
+ unobserve() {}
+ }
})
beforeEach(() => {
diff --git a/frontend/src/components/ItemCardList.tsx b/frontend/src/components/ItemCardList.tsx
index 7dfc1e18ec..c4fd6ef3c6 100644
--- a/frontend/src/components/ItemCardList.tsx
+++ b/frontend/src/components/ItemCardList.tsx
@@ -2,6 +2,7 @@ import { JSX } from 'react'
import { ProjectIssuesType, ProjectReleaseType } from 'types/project'
import { PullRequestsType } from 'types/user'
import SecondaryCard from './SecondaryCard'
+import { TruncatedText } from './TruncatedText'
const ItemCardList = ({
title,
@@ -44,7 +45,7 @@ const ItemCardList = ({
href={item?.url}
target="_blank"
>
- {item.title || item.name}
+
diff --git a/frontend/src/components/RepositoriesCard.tsx b/frontend/src/components/RepositoriesCard.tsx
index 16d241d911..83edab903a 100644
--- a/frontend/src/components/RepositoriesCard.tsx
+++ b/frontend/src/components/RepositoriesCard.tsx
@@ -13,6 +13,7 @@ import type React from 'react'
import { useState } from 'react'
import { useNavigate } from 'react-router-dom'
import { RepositoriesCardProps } from 'types/project'
+import { TruncatedText } from './TruncatedText'
const RepositoriesCard: React.FC = ({ repositories }) => {
const [showAllRepositories, setShowAllRepositories] = useState(false)
@@ -59,7 +60,7 @@ const RepositoryItem = ({ details }) => {
onClick={handleClick}
className="text-start font-semibold text-blue-500 hover:underline dark:text-blue-400"
>
- {details?.name}
+
diff --git a/frontend/src/components/TruncatedText.tsx b/frontend/src/components/TruncatedText.tsx
new file mode 100644
index 0000000000..1cf1d60efc
--- /dev/null
+++ b/frontend/src/components/TruncatedText.tsx
@@ -0,0 +1,37 @@
+import { useRef, useEffect, useCallback } from 'react'
+
+export const TruncatedText = ({ text, className = '' }: { text: string; className?: string }) => {
+ const textRef = useRef(null)
+
+ const checkTruncation = useCallback(() => {
+ const element = textRef.current
+ if (element) {
+ element.title = text
+ }
+ }, [text])
+
+ useEffect(() => {
+ checkTruncation()
+
+ const observer = new ResizeObserver(() => checkTruncation())
+ if (textRef.current) {
+ observer.observe(textRef.current)
+ }
+
+ window.addEventListener('resize', checkTruncation)
+
+ return () => {
+ observer.disconnect()
+ window.removeEventListener('resize', checkTruncation)
+ }
+ }, [text, checkTruncation])
+
+ return (
+
+ {text}
+
+ )
+}
diff --git a/frontend/src/pages/Home.tsx b/frontend/src/pages/Home.tsx
index ca1b1b93a7..7b82432150 100644
--- a/frontend/src/pages/Home.tsx
+++ b/frontend/src/pages/Home.tsx
@@ -29,6 +29,7 @@ import Modal from 'components/Modal'
import MultiSearchBar from 'components/MultiSearch'
import SecondaryCard from 'components/SecondaryCard'
import TopContributors from 'components/ToggleContributors'
+import { TruncatedText } from 'components/TruncatedText'
import { toaster } from 'components/ui/toaster'
export default function Home() {
@@ -135,16 +136,16 @@ export default function Home() {
/>
-
+
{data.upcomingEvents.map((event: EventType, index: number) => (
-
+
@@ -173,13 +174,13 @@ export default function Home() {
-
+
{data.recentChapters.map((chapter) => (
@@ -202,15 +203,15 @@ export default function Home() {
))}
-
+
{data.recentProjects.map((project) => (
-
+
+
+
+
+
@@ -280,22 +281,22 @@ export default function Home() {
)}
/>
-
+
{data.recentPosts.map((post) => (