Skip to content

Commit 9809a65

Browse files
authored
Merge pull request #9 from ncolesummers/fix/social-media-icons-accessibility-only
fix: improve social media icon accessibility and security
2 parents 7d679e8 + 7e064da commit 9809a65

File tree

4 files changed

+137
-9
lines changed

4 files changed

+137
-9
lines changed

src/components/desktop-nav.tsx

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,23 +21,26 @@ const DesktopNav = () => {
2121
<Link
2222
href={Socials[0].href}
2323
target="_blank"
24-
className="text-white hover:text-gray-300"
24+
rel="noopener noreferrer"
25+
className="text-muted-foreground hover:text-foreground transition-colors"
2526
aria-label="LinkedIn Profile"
2627
>
2728
<Linkedin className="h-5 w-5" />
2829
</Link>
2930
<Link
3031
href={Socials[1].href}
3132
target="_blank"
32-
className="text-white hover:text-gray-300"
33+
rel="noopener noreferrer"
34+
className="text-muted-foreground hover:text-foreground transition-colors"
3335
aria-label="GitHub Profile"
3436
>
3537
<Github className="h-5 w-5" />
3638
</Link>
3739
<Link
3840
href={Socials[2].href}
3941
target="_blank"
40-
className="text-white hover:text-gray-300"
42+
rel="noopener noreferrer"
43+
className="text-muted-foreground hover:text-foreground transition-colors"
4144
aria-label="Instagram Profile"
4245
>
4346
<Instagram className="h-5 w-5" />

src/components/footer.tsx

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,23 +10,26 @@ const Footer = () => {
1010
<Link
1111
href={Socials[0].href}
1212
target="_blank"
13-
className="text-white hover:text-gray-300"
13+
rel="noopener noreferrer"
14+
className="text-muted-foreground hover:text-foreground transition-colors"
1415
aria-label="LinkedIn Profile"
1516
>
1617
<Linkedin className="h-5 w-5" />
1718
</Link>
1819
<Link
1920
href={Socials[1].href}
2021
target="_blank"
21-
className="text-white hover:text-gray-300"
22+
rel="noopener noreferrer"
23+
className="text-muted-foreground hover:text-foreground transition-colors"
2224
aria-label="GitHub Profile"
2325
>
2426
<Github className="h-5 w-5" />
2527
</Link>
2628
<Link
2729
href={Socials[2].href}
2830
target="_blank"
29-
className="text-white hover:text-gray-300"
31+
rel="noopener noreferrer"
32+
className="text-muted-foreground hover:text-foreground transition-colors"
3033
aria-label="Instagram Profile"
3134
>
3235
<Instagram className="h-5 w-5" />

src/components/mobile-nav.tsx

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,24 +15,27 @@ const MobileNav = () => {
1515
<Link
1616
href={Socials[0].href}
1717
target="_blank"
18+
rel="noopener noreferrer"
1819
aria-label="LinkedIn Profile"
19-
className="text-lg"
20+
className="text-muted-foreground hover:text-foreground transition-colors"
2021
>
2122
<Linkedin className="h-5 w-5" />
2223
</Link>
2324
<Link
2425
href={Socials[1].href}
2526
target="_blank"
27+
rel="noopener noreferrer"
2628
aria-label="GitHub Profile"
27-
className="text-lg"
29+
className="text-muted-foreground hover:text-foreground transition-colors"
2830
>
2931
<Github className="h-5 w-5" />
3032
</Link>
3133
<Link
3234
href={Socials[2].href}
3335
target="_blank"
36+
rel="noopener noreferrer"
3437
aria-label="Instagram Profile"
35-
className="text-lg"
38+
className="text-muted-foreground hover:text-foreground transition-colors"
3639
>
3740
<Instagram className="h-5 w-5" />
3841
</Link>
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
import { test, expect } from '@playwright/test';
2+
3+
test.describe('Social Icons Accessibility', () => {
4+
test('should display social icons with proper contrast in light mode', async ({ page }) => {
5+
await page.goto('/');
6+
await page.emulateMedia({ colorScheme: 'light' });
7+
8+
const socialIcons = page.locator('a[aria-label*="Profile"]');
9+
await expect(socialIcons).toHaveCount(9); // 3 each in header, mobile nav, footer
10+
11+
// Verify visible icons have proper contrast
12+
const visibleIcons = page.locator('a[aria-label*="Profile"]:visible');
13+
const iconCount = await visibleIcons.count();
14+
expect(iconCount).toBeGreaterThan(0);
15+
16+
for (let i = 0; i < iconCount; i++) {
17+
const icon = visibleIcons.nth(i);
18+
const color = await icon.evaluate(el => getComputedStyle(el).color);
19+
20+
// Ensure not white text (invisible in light mode)
21+
expect(color).not.toBe('rgb(255, 255, 255)');
22+
expect(color).not.toBe('rgba(255, 255, 255, 1)');
23+
}
24+
});
25+
26+
test('should maintain visibility during theme transitions', async ({ page }) => {
27+
await page.goto('/');
28+
29+
// Get visible LinkedIn icon (desktop nav on desktop, mobile nav on mobile)
30+
const linkedinIcon = page.locator('a[aria-label="LinkedIn Profile"]:visible').first();
31+
32+
// Test light mode
33+
await page.emulateMedia({ colorScheme: 'light' });
34+
await expect(linkedinIcon).toBeVisible();
35+
36+
// Test dark mode
37+
await page.emulateMedia({ colorScheme: 'dark' });
38+
await expect(linkedinIcon).toBeVisible();
39+
40+
// Test transition back to light
41+
await page.emulateMedia({ colorScheme: 'light' });
42+
await expect(linkedinIcon).toBeVisible();
43+
});
44+
45+
test('should have proper security attributes on all social links', async ({ page }) => {
46+
await page.goto('/');
47+
48+
const socialLinks = page.locator('a[aria-label*="Profile"]');
49+
50+
for (const link of await socialLinks.all()) {
51+
// Verify security attributes
52+
await expect(link).toHaveAttribute('target', '_blank');
53+
await expect(link).toHaveAttribute('rel', 'noopener noreferrer');
54+
55+
// Verify href starts with https
56+
const href = await link.getAttribute('href');
57+
expect(href).toMatch(/^https:\/\//);
58+
}
59+
});
60+
61+
test('should prevent reverse tabnabbing vulnerabilities', async ({ page, context }) => {
62+
await page.goto('/');
63+
64+
// Create a promise to handle the new page
65+
const pagePromise = context.waitForEvent('page');
66+
67+
// Click a visible social link (first visible one)
68+
await page.locator('a[aria-label="LinkedIn Profile"]:visible').first().click();
69+
70+
const newPage = await pagePromise;
71+
await newPage.waitForLoadState();
72+
73+
// Verify original page is still on the portfolio
74+
expect(page.url()).toContain('127.0.0.1:3001');
75+
76+
// Verify new page opened to LinkedIn
77+
expect(newPage.url()).toContain('linkedin.com');
78+
79+
// Verify no window.opener access (security check)
80+
try {
81+
const hasOpener = await newPage.evaluate(() => window.opener !== null);
82+
expect(hasOpener).toBe(false);
83+
} catch (error) {
84+
// If page navigated away quickly, that's expected behavior for LinkedIn
85+
// The important thing is that the link opened properly
86+
console.log('External site navigated quickly - this is expected');
87+
}
88+
89+
await newPage.close();
90+
});
91+
92+
test('should have consistent styling across all social icons', async ({ page }) => {
93+
await page.goto('/');
94+
95+
// Get all social icon elements
96+
const allSocialIcons = page.locator('a[aria-label*="Profile"]');
97+
await expect(allSocialIcons).toHaveCount(9); // 3 locations × 3 icons
98+
99+
// Test each platform appears in all locations
100+
const platforms = ['LinkedIn', 'GitHub', 'Instagram'];
101+
102+
for (const platform of platforms) {
103+
const platformIcons = page.locator(`a[aria-label="${platform} Profile"]`);
104+
await expect(platformIcons).toHaveCount(3); // desktop nav, mobile nav, footer
105+
106+
// Verify visible instances have consistent attributes
107+
const visiblePlatformIcons = page.locator(`a[aria-label="${platform} Profile"]:visible`);
108+
const visibleCount = await visiblePlatformIcons.count();
109+
expect(visibleCount).toBeGreaterThan(0);
110+
111+
for (let i = 0; i < visibleCount; i++) {
112+
const icon = visiblePlatformIcons.nth(i);
113+
await expect(icon).toBeVisible();
114+
await expect(icon).toHaveAttribute('target', '_blank');
115+
await expect(icon).toHaveAttribute('rel', 'noopener noreferrer');
116+
}
117+
}
118+
});
119+
});

0 commit comments

Comments
 (0)