Skip to content

Commit a458219

Browse files
authored
feat: comprehensive accessibility audit report and improvements (#470)
- Added a detailed accessibility audit report for the daily.dev documentation site, highlighting strengths and areas for improvement. - Implemented accessible update notifications in the service worker registration. - Enhanced semantic HTML structure and ARIA attributes across various components for better screen reader support. - Improved color contrast and line height in CSS for better readability and accessibility. - Updated SVG icons with appropriate titles and roles for improved accessibility.
1 parent 32e998b commit a458219

17 files changed

+182
-50
lines changed

src/components/homepage/homeNavBoxes.js

Lines changed: 25 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -120,26 +120,41 @@ function FeatureItem({url, text}){
120120

121121

122122
function Feature({title, icon, items }) {
123+
const altTexts = {
124+
'Getting Started': 'Navigate to getting started guides and tutorials',
125+
'Setting up your feed': 'Customize and configure your content feed',
126+
'Key features': 'Explore daily.dev core features and functionality',
127+
'Your profile': 'Manage your profile settings and activity',
128+
'Squads': 'Join and manage developer community squads',
129+
'Plus': 'Discover premium features and subscriptions',
130+
'Monetization (beta)': 'Learn about monetization and earning features',
131+
'Customization': 'Personalize your daily.dev experience',
132+
'For content creators': 'Resources for content creators and publishers',
133+
'For OSS contributors': 'Open source contribution guidelines and resources'
134+
};
135+
123136
return (
124-
<article className={clsx('col col--4')}>
137+
<article className={clsx('col col--4')} role="region" aria-labelledby={`feature-${title.replace(/\s+/g, '-').toLowerCase()}`}>
125138
<div className={styles.homecard}>
126139
<img
127140
src={icon}
128141
className={styles.homeIcon}
129142
loading="eager"
130143
decoding="sync"
131-
alt={`${title} icon`}
144+
alt={altTexts[title] || `${title} icon`}
132145
width="48"
133146
height="48"
134147
style={{ aspectRatio: '1/1' }}
135148
/>
136-
<h2>{title}</h2>
149+
<h2 id={`feature-${title.replace(/\s+/g, '-').toLowerCase()}`}>{title}</h2>
137150
<div className={styles.listContainer}>
138-
<ul>
139-
{items.map((props, idx) => (
140-
<FeatureItem key={idx} {...props} />
141-
))}
142-
</ul>
151+
<nav aria-label={`${title} navigation`}>
152+
<ul>
153+
{items.map((props, idx) => (
154+
<FeatureItem key={idx} {...props} />
155+
))}
156+
</ul>
157+
</nav>
143158
</div>
144159
</div>
145160
</article>
@@ -158,8 +173,8 @@ export default function HomepageFeatures() {
158173
}, []);
159174

160175
return (
161-
<section className={styles.features}>
162-
<ul className={styles.grid3col}>
176+
<section className={styles.features} aria-label="Daily.dev documentation sections">
177+
<ul className={styles.grid3col} role="list">
163178
{FeatureList.map((props, idx) => (
164179
<Feature key={idx} {...props} />
165180
))}

src/components/video-page/navBoxes.js

Lines changed: 38 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -98,15 +98,40 @@ function Feature({title, url, type, duration }) {
9898
// Prevent the URL from redirecting users
9999
e.preventDefault();
100100

101-
// Get the video ID
101+
// Get the video ID and title for better accessibility
102102
let id = link.getAttribute('data-youtube');
103+
let videoTitle = title || 'Video';
103104

104-
// Create the player
105+
// Create the player with improved accessibility
105106
let player = document.createElement('div');
106-
player.innerHTML = `<iframe width="560" height="315" src="https://www.youtube-nocookie.com/embed/${id}?autoplay=1" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>`;
107+
player.innerHTML = `<iframe
108+
width="560"
109+
height="315"
110+
src="https://www.youtube-nocookie.com/embed/${id}"
111+
title="${videoTitle}"
112+
frameborder="0"
113+
allow="accelerometer; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
114+
allowfullscreen
115+
role="application"
116+
aria-label="YouTube video player for ${videoTitle}">
117+
</iframe>`;
107118

108119
// Inject the player into the UI
109120
link.replaceWith(player);
121+
122+
// Focus the iframe after loading for keyboard users
123+
const iframe = player.querySelector('iframe');
124+
if (iframe) {
125+
iframe.focus();
126+
}
127+
}
128+
129+
function handleKeyPress(e) {
130+
// Support Enter and Space key activation
131+
if (e.key === 'Enter' || e.key === ' ') {
132+
e.preventDefault();
133+
replaceVideo(e);
134+
}
110135
}
111136

112137

@@ -116,9 +141,17 @@ function Feature({title, url, type, duration }) {
116141
<img src="img/logo.png" className={styles.vidIcon}></img>
117142
<h2>{title}</h2>
118143
<div className={styles.iframecontainer}>
119-
<div className={styles.youTubeOverlay} onClick={replaceVideo} data-youtube={url}>
144+
<div
145+
className={styles.youTubeOverlay}
146+
onClick={replaceVideo}
147+
onKeyDown={handleKeyPress}
148+
data-youtube={url}
149+
role="button"
150+
tabIndex="0"
151+
aria-label={`Play video: ${title}`}
152+
>
120153
<div className={styles.youTubeOverlayTime}>{duration}</div>
121-
<img className={styles.imgVid} width="340" height="180" alt="" src={"https://img.youtube.com/vi/" + url + "/0.jpg"}/>
154+
<img className={styles.imgVid} width="340" height="180" alt={`Video thumbnail for ${title}`} src={"https://img.youtube.com/vi/" + url + "/0.jpg"}/>
122155
</div>
123156
</div>
124157
{/* <div className={styles.iframecontainer}>

src/css/custom.css

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -345,8 +345,8 @@ img[src='/img/logo.png'] {
345345
--burger90: #722f1b;
346346

347347
--ifm-container-width-xl: 1600px;
348-
--ifm-line-height-base: 1.333;
349-
/* a11y - needs upping to at least 1.5 */
348+
--ifm-line-height-base: 1.5;
349+
/* Fixed: Improved line height for better readability and accessibility */
350350

351351
/* these font and margin calculations are a little over complicated,
352352
but allow for us to change the h6 size and keep the ratios set by the design team,
@@ -359,8 +359,7 @@ img[src='/img/logo.png'] {
359359
--dd-h4-ratio: 1.1333;
360360
--dd-h5-ratio: 1.1333;
361361
--dd-h6-size: 1.0625rem;
362-
--dd-h6-size: 0.9375rem;
363-
/* ally - this needs removing and setting to 1.0625 or higher so headings are at least minimum sizes */
362+
/* Fixed: Increased from 0.9375rem to meet minimum accessibility requirements */
364363

365364
/* font size calculated as ratio relative to h6 */
366365
--dd-h1-font-size: 4rem;
@@ -412,6 +411,7 @@ img[src='/img/logo.png'] {
412411
--ifm-link-color: var(--water60);
413412
--ifm-link-hover-color: var(--water20);
414413
--ifm-color-warning-dark: var(--cheese90);
414+
--ifm-color-danger: var(--bacon70); /* Improved contrast: 4.89:1 ratio */
415415

416416
/* menu */
417417
--ifm-menu-link-padding-vertical: 0.5rem;
@@ -433,9 +433,9 @@ html[data-theme='dark'] *:focus-visible {
433433
}
434434

435435
html *:focus-visible {
436-
outline: 2px solid var(--bluecheese60) !important;
437-
/* A11y contrast not high enough, suggest Blue Cheese 90
438-
#009FB3 but needs discussion with Tsahi */
436+
outline: 2px solid var(--bluecheese90) !important;
437+
outline-offset: 2px;
438+
/* Fixed: Using bluecheese90 for better contrast (3.27:1) */
439439
}
440440

441441
article {
@@ -632,6 +632,24 @@ div[class^='announcementBar_'] {
632632
}
633633
}
634634

635+
/* Reduced motion preferences for accessibility */
636+
@media (prefers-reduced-motion: reduce) {
637+
.hero__title,
638+
.navbar__link,
639+
.menu__link--sublist:after,
640+
.menu__link--sublist[aria-expanded="true"]:after,
641+
.menu__link--sublist[aria-expanded="false"]:after,
642+
summary:before,
643+
*,
644+
*::before,
645+
*::after {
646+
animation-duration: 0.01ms !important;
647+
animation-iteration-count: 1 !important;
648+
transition-duration: 0.01ms !important;
649+
scroll-behavior: auto !important;
650+
}
651+
}
652+
635653

636654
/* typography */
637655

src/registerSW.js

Lines changed: 68 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,69 @@
1+
// Create accessible update notification
2+
function showUpdateNotification() {
3+
const notification = document.createElement('div');
4+
notification.setAttribute('role', 'alert');
5+
notification.setAttribute('aria-live', 'polite');
6+
notification.style.cssText = `
7+
position: fixed;
8+
top: 20px;
9+
right: 20px;
10+
background: var(--ifm-color-primary);
11+
color: white;
12+
padding: 1rem;
13+
border-radius: 8px;
14+
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
15+
z-index: 9999;
16+
font-family: inherit;
17+
max-width: 300px;
18+
`;
19+
20+
notification.innerHTML = `
21+
<div>
22+
<p style="margin: 0 0 1rem 0; font-size: 0.9rem;">New version available. Would you like to reload to get the latest features?</p>
23+
<div style="display: flex; gap: 0.5rem;">
24+
<button id="sw-reload-btn" style="
25+
background: white;
26+
color: var(--ifm-color-primary);
27+
border: none;
28+
padding: 0.5rem 1rem;
29+
border-radius: 4px;
30+
cursor: pointer;
31+
font-size: 0.8rem;
32+
font-weight: 500;
33+
">Reload</button>
34+
<button id="sw-dismiss-btn" style="
35+
background: transparent;
36+
color: white;
37+
border: 1px solid white;
38+
padding: 0.5rem 1rem;
39+
border-radius: 4px;
40+
cursor: pointer;
41+
font-size: 0.8rem;
42+
font-weight: 500;
43+
">Later</button>
44+
</div>
45+
</div>
46+
`;
47+
48+
document.body.appendChild(notification);
49+
50+
// Add event listeners
51+
document.getElementById('sw-reload-btn').addEventListener('click', () => {
52+
window.location.reload();
53+
});
54+
55+
document.getElementById('sw-dismiss-btn').addEventListener('click', () => {
56+
notification.remove();
57+
});
58+
59+
// Auto-dismiss after 10 seconds
60+
setTimeout(() => {
61+
if (notification.parentNode) {
62+
notification.remove();
63+
}
64+
}, 10000);
65+
}
66+
167
// Register service worker with optimal timing
268
export default function registerSW() {
369
if ('serviceWorker' in navigator) {
@@ -13,10 +79,8 @@ export default function registerSW() {
1379
if (newWorker) {
1480
newWorker.addEventListener('statechange', () => {
1581
if (newWorker.state === 'installed' && navigator.serviceWorker.controller) {
16-
// New update available
17-
if (confirm('New version available. Reload to get the latest features?')) {
18-
window.location.reload();
19-
}
82+
// New update available - show accessible notification
83+
showUpdateNotification();
2084
}
2185
});
2286
}

static/img/icons/community.svg

Lines changed: 2 additions & 2 deletions
Loading

static/img/icons/content-creator.svg

Lines changed: 2 additions & 2 deletions
Loading

static/img/icons/cores.svg

Lines changed: 2 additions & 1 deletion
Loading

0 commit comments

Comments
 (0)