Skip to content

Commit 1752f11

Browse files
committed
A11y enhancements
1 parent 5906ce4 commit 1752f11

8 files changed

Lines changed: 83 additions & 16 deletions

File tree

src/app/globals.css

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,3 +236,16 @@ a {
236236
color: inherit;
237237
text-decoration: none;
238238
}
239+
240+
/* Visually hidden but available to screen readers */
241+
.sr-only {
242+
position: absolute;
243+
width: 1px;
244+
height: 1px;
245+
padding: 0;
246+
margin: -1px;
247+
overflow: hidden;
248+
clip: rect(0, 0, 0, 0);
249+
white-space: nowrap;
250+
border-width: 0;
251+
}

src/app/layout.module.css

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
border-radius: 4px;
1111
}
1212

13-
.skipLink:focus {
13+
.skipLink:focus-visible {
1414
top: 0;
1515
outline: 2px solid white;
1616
outline-offset: 2px;

src/app/page.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,10 +121,14 @@ export default function Home() {
121121
}, []);
122122

123123
const handleBurnComplete = useCallback(() => {
124+
const prefersReducedMotion = window.matchMedia(
125+
'(prefers-reduced-motion: reduce)',
126+
).matches;
127+
124128
window.scrollTo({
125129
top: 0,
126130
left: 0,
127-
behavior: 'smooth',
131+
behavior: prefersReducedMotion ? 'auto' : 'smooth',
128132
});
129133

130134
window.location.reload();

src/components/chat-navigation/chat-navigation.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { MAIN_SITE_URL } from '@/config/common';
22
import styles from './chat-navigation.module.css';
3+
import NewTabLabel from '@/components/new-tab-label/new-tab-label';
34

45
/**
56
* ChatNavigation component displays help links at the bottom of the chat widget.
@@ -18,6 +19,7 @@ export default function ChatNavigation() {
1819
className={styles.link}
1920
>
2021
FAQs and Support
22+
<NewTabLabel />
2123
</a>
2224
<a
2325
href={`${MAIN_SITE_URL}/feedback`}
@@ -26,6 +28,7 @@ export default function ChatNavigation() {
2628
className={styles.link}
2729
>
2830
Send Feedback
31+
<NewTabLabel />
2932
</a>
3033
</div>
3134
</nav>

src/components/confirm-dialog/confirm-dialog.tsx

Lines changed: 47 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,15 @@ import {
1010
CONFIRM_DELETE_DATA_BUTTON_TEST_ID,
1111
} from '@/constants/test-ids';
1212

13+
const FOCUSABLE_SELECTORS = [
14+
'a[href]',
15+
'button:not([disabled])',
16+
'input:not([disabled])',
17+
'select:not([disabled])',
18+
'textarea:not([disabled])',
19+
'[tabindex]:not([tabindex="-1"])',
20+
].join(', ');
21+
1322
interface ConfirmDialogProps {
1423
isOpen: boolean;
1524
onConfirm: () => void;
@@ -39,34 +48,57 @@ export default function ConfirmDialog({
3948
}: ConfirmDialogProps) {
4049
const dialogRef = useRef<HTMLDivElement>(null);
4150
const confirmButtonRef = useRef<HTMLButtonElement>(null);
51+
const triggerRef = useRef<HTMLElement | null>(null);
4252

43-
// Focus the confirm button when dialog opens
53+
// Capture the trigger element, move focus into dialog on open,
54+
// and restore focus to the trigger on close.
4455
useEffect(() => {
45-
if (isOpen && confirmButtonRef.current) {
46-
confirmButtonRef.current.focus();
56+
if (isOpen) {
57+
triggerRef.current = document.activeElement as HTMLElement;
58+
confirmButtonRef.current?.focus();
59+
} else {
60+
triggerRef.current?.focus();
61+
triggerRef.current = null;
4762
}
4863
}, [isOpen]);
4964

50-
// Handle escape key
65+
// Focus trap + Escape key (only active while open)
5166
useEffect(() => {
52-
const handleEscape = (e: KeyboardEvent) => {
53-
if (e.key === 'Escape' && isOpen) {
67+
if (!isOpen) return;
68+
69+
const handleKeyDown = (e: KeyboardEvent) => {
70+
if (e.key === 'Escape') {
5471
onCancel();
72+
return;
73+
}
74+
75+
if (e.key !== 'Tab' || !dialogRef.current) return;
76+
77+
const focusable = Array.from(
78+
dialogRef.current.querySelectorAll<HTMLElement>(FOCUSABLE_SELECTORS),
79+
);
80+
81+
if (focusable.length === 0) return;
82+
83+
const first = focusable[0];
84+
const last = focusable[focusable.length - 1];
85+
86+
if (e.shiftKey && document.activeElement === first) {
87+
e.preventDefault();
88+
last.focus();
89+
} else if (!e.shiftKey && document.activeElement === last) {
90+
e.preventDefault();
91+
first.focus();
5592
}
5693
};
5794

58-
document.addEventListener('keydown', handleEscape);
59-
return () => document.removeEventListener('keydown', handleEscape);
95+
document.addEventListener('keydown', handleKeyDown);
96+
return () => document.removeEventListener('keydown', handleKeyDown);
6097
}, [isOpen, onCancel]);
6198

6299
// Prevent body scroll when dialog is open
63100
useEffect(() => {
64-
if (isOpen) {
65-
document.body.style.overflow = 'hidden';
66-
} else {
67-
document.body.style.overflow = '';
68-
}
69-
101+
document.body.style.overflow = isOpen ? 'hidden' : '';
70102
return () => {
71103
document.body.style.overflow = '';
72104
};
@@ -101,6 +133,7 @@ export default function ConfirmDialog({
101133
type="button"
102134
>
103135
<svg
136+
aria-hidden="true"
104137
width="24"
105138
height="24"
106139
viewBox="0 0 24 24"

src/components/consent-form/consent-form.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import { Button } from '@/components/button/button';
44
import { MAIN_SITE_URL } from '@/config/common';
5+
import NewTabLabel from '@/components/new-tab-label/new-tab-label';
56
import { LOAD_ZD_BUTTON_TEST_ID } from '@/constants/test-ids';
67
import { REDIRECT_DELAY_MS } from '@/constants/zendesk-timing';
78
import { DECLINE_BUTTON_TEXT, CONSENT_BUTTON_TEXT } from '@/config/zendesk';
@@ -27,6 +28,7 @@ export default function ConsentForm({ onContinue }: ConsentFormProps) {
2728
className={styles.link}
2829
>
2930
contact us
31+
<NewTabLabel />
3032
</a>
3133
. We try to answer questions within 2 business days.
3234
<span>

src/components/footer/footer.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import styles from './footer.module.css';
44
import { learnMoreLinks, otherResourcesLinks } from '@/constants/footerLinks';
55
import { MAIN_SITE_URL } from '@/config/common';
6+
import NewTabLabel from '@/components/new-tab-label/new-tab-label';
67

78
export default function Footer() {
89
const buildHref = (href: string): string => {
@@ -33,6 +34,7 @@ export default function Footer() {
3334
{link.badge && (
3435
<span className={styles.badge}>{link.badge}</span>
3536
)}
37+
<NewTabLabel />
3638
</a>
3739
))}
3840
</div>
@@ -50,6 +52,7 @@ export default function Footer() {
5052
rel={link.rel ?? undefined}
5153
>
5254
{link.text}
55+
<NewTabLabel />
5356
</a>
5457
))}
5558
</div>
@@ -69,6 +72,7 @@ export default function Footer() {
6972
href={`${MAIN_SITE_URL}/compare-privacy`}
7073
>
7174
choose DuckDuckGo over Chrome and other browsers
75+
<NewTabLabel />
7276
</a>{' '}
7377
to search and browse online. Our built-in search engine is like
7478
Google but never tracks your searches. And our browsing protections,
@@ -82,6 +86,7 @@ export default function Footer() {
8286
href={`${MAIN_SITE_URL}/duckduckgo-help-pages/company/how-duckduckgo-makes-money`}
8387
>
8488
privacy-respecting search ads
89+
<NewTabLabel />
8590
</a>
8691
, not by exploiting your data. Take back control of your personal
8792
information with the browser designed for data protection, not data
@@ -93,6 +98,7 @@ export default function Footer() {
9398
href={`${MAIN_SITE_URL}/mac`}
9499
>
95100
Mac
101+
<NewTabLabel />
96102
</a>
97103
,{' '}
98104
<a
@@ -102,6 +108,7 @@ export default function Footer() {
102108
href={`${MAIN_SITE_URL}/windows`}
103109
>
104110
Windows
111+
<NewTabLabel />
105112
</a>
106113
,{' '}
107114
<a
@@ -111,6 +118,7 @@ export default function Footer() {
111118
href="https://apps.apple.com/app/duckduckgo-privacy-browser/id663592361?platform=iphone"
112119
>
113120
iOS
121+
<NewTabLabel />
114122
</a>
115123
, and{' '}
116124
<a
@@ -120,6 +128,7 @@ export default function Footer() {
120128
href="https://play.google.com/store/apps/details?id=com.duckduckgo.mobile.android"
121129
>
122130
Android
131+
<NewTabLabel />
123132
</a>
124133
.
125134
</p>
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export default function NewTabLabel() {
2+
return <span className="sr-only">(opens in new tab)</span>;
3+
}

0 commit comments

Comments
 (0)