ZeenathHamza commited on
Commit
09bb0e2
·
verified ·
1 Parent(s): f64a3bf

Improve submission dialog keyboard accessibility

Browse files

Changed the submission dialog in directory.tsx so keyboard users can operate it more reliably:

Opening it moves focus to the close button.
Tab and Shift+Tab keep focus inside the dialog.
Escape closes it.
Closing it returns focus to the button that opened it.
After a successful submission, focus moves to the success heading.

Files changed (1) hide show
  1. src/app/directory.tsx +81 -10
src/app/directory.tsx CHANGED
@@ -68,6 +68,10 @@ export function Directory({ initialTools }: Props) {
68
  const [tools, setTools] = useState(initialTools);
69
  const [search, setSearch] = useState("");
70
  const searchRef = useRef<HTMLInputElement>(null);
 
 
 
 
71
  const [category, setCategory] = useState("All tools");
72
  const [sort, setSort] = useState("Featured");
73
  const [showForm, setShowForm] = useState(false);
@@ -76,6 +80,23 @@ export function Directory({ initialTools }: Props) {
76
  const [submitError, setSubmitError] = useState("");
77
  const [submitted, setSubmitted] = useState(false);
78
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
79
  useEffect(() => {
80
  function focusSearch(event: KeyboardEvent) {
81
  const activeElement = document.activeElement;
@@ -93,6 +114,50 @@ export function Directory({ initialTools }: Props) {
93
  return () => window.removeEventListener("keydown", focusSearch);
94
  }, []);
95
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
96
  const filteredTools = useMemo(() => {
97
  const query = search.trim().toLowerCase();
98
  const filtered = tools.filter((tool) => {
@@ -167,7 +232,7 @@ export function Directory({ initialTools }: Props) {
167
  </nav>
168
  <div className="header-actions">
169
  <ThemeToggle />
170
- <button className="button button-small button-primary header-submit" onClick={() => { setShowForm(true); setSubmitted(false); }}>
171
  <Plus size={16} aria-hidden="true" /> Submit a tool
172
  </button>
173
  </div>
@@ -187,7 +252,7 @@ export function Directory({ initialTools }: Props) {
187
  <a className="button button-primary hero-cta" href="#directory">
188
  Explore the directory <ArrowRight size={16} aria-hidden="true" />
189
  </a>
190
- <button className="button button-quiet" onClick={() => { setShowForm(true); setSubmitted(false); }}>
191
  <Plus size={16} aria-hidden="true" /> Add your project
192
  </button>
193
  </div>
@@ -313,7 +378,7 @@ export function Directory({ initialTools }: Props) {
313
  )}
314
  <div className="directory-foot">
315
  <span>Showing <strong>{filteredTools.length}</strong> of <strong>{tools.length}</strong> projects</span>
316
- <button className="button button-outline" onClick={() => { setShowForm(true); setSubmitted(false); }}>
317
  Share a project <ArrowUpRight size={15} />
318
  </button>
319
  </div>
@@ -326,7 +391,7 @@ export function Directory({ initialTools }: Props) {
326
  <h2>Good tools deserve to be found.</h2>
327
  <p>Submit an open-source project, share what works, and help the community find its next idea.</p>
328
  </div>
329
- <button className="button button-primary community-cta" onClick={() => { setShowForm(true); setSubmitted(false); }}>
330
  Submit a tool <ArrowUpRight size={16} />
331
  </button>
332
  </section>
@@ -344,20 +409,26 @@ export function Directory({ initialTools }: Props) {
344
  {showForm ? (
345
  <div
346
  className="modal-backdrop"
347
- onKeyDown={(event) => { if (event.key === "Escape") setShowForm(false); }}
348
- onMouseDown={(event) => { if (event.target === event.currentTarget) setShowForm(false); }}
349
  >
350
- <section className="submit-modal" role="dialog" aria-modal="true" aria-labelledby="modal-heading">
351
- <button className="modal-close" onClick={() => setShowForm(false)} aria-label="Close submission form">
 
 
 
 
 
 
 
352
  <X size={19} />
353
  </button>
354
  {submitted ? (
355
  <div className="success-panel">
356
  <div className="success-icon"><Check size={23} /></div>
357
  <div className="section-kicker">YOU’RE IN THE INDEX</div>
358
- <h2 id="modal-heading">Thanks for sharing.</h2>
359
  <p>Your project has been added to the community directory.</p>
360
- <button className="button button-primary" onClick={() => setShowForm(false)}>Back to exploring</button>
361
  </div>
362
  ) : (
363
  <>
 
68
  const [tools, setTools] = useState(initialTools);
69
  const [search, setSearch] = useState("");
70
  const searchRef = useRef<HTMLInputElement>(null);
71
+ const dialogRef = useRef<HTMLElement>(null);
72
+ const closeButtonRef = useRef<HTMLButtonElement>(null);
73
+ const submitTriggerRef = useRef<HTMLButtonElement | null>(null);
74
+ const successHeadingRef = useRef<HTMLHeadingElement>(null);
75
  const [category, setCategory] = useState("All tools");
76
  const [sort, setSort] = useState("Featured");
77
  const [showForm, setShowForm] = useState(false);
 
80
  const [submitError, setSubmitError] = useState("");
81
  const [submitted, setSubmitted] = useState(false);
82
 
83
+ useEffect(() => {
84
+ if (!showForm) return;
85
+
86
+ closeButtonRef.current?.focus();
87
+ return () => {
88
+ if (submitTriggerRef.current?.isConnected) {
89
+ submitTriggerRef.current.focus();
90
+ }
91
+ };
92
+ }, [showForm]);
93
+
94
+ useEffect(() => {
95
+ if (showForm && submitted) {
96
+ successHeadingRef.current?.focus();
97
+ }
98
+ }, [showForm, submitted]);
99
+
100
  useEffect(() => {
101
  function focusSearch(event: KeyboardEvent) {
102
  const activeElement = document.activeElement;
 
114
  return () => window.removeEventListener("keydown", focusSearch);
115
  }, []);
116
 
117
+ function openSubmitForm(event: React.MouseEvent<HTMLButtonElement>) {
118
+ submitTriggerRef.current = event.currentTarget;
119
+ setSubmitted(false);
120
+ setShowForm(true);
121
+ }
122
+
123
+ function closeSubmitForm() {
124
+ setShowForm(false);
125
+ }
126
+
127
+ function handleDialogKeyDown(event: React.KeyboardEvent<HTMLElement>) {
128
+ if (event.key === "Escape") {
129
+ event.preventDefault();
130
+ closeSubmitForm();
131
+ return;
132
+ }
133
+ if (event.key !== "Tab") return;
134
+
135
+ const dialog = dialogRef.current;
136
+ if (!dialog) return;
137
+
138
+ const focusable = Array.from(
139
+ dialog.querySelectorAll<HTMLElement>(
140
+ 'a[href], button:not([disabled]), input:not([disabled]), textarea:not([disabled]), select:not([disabled]), [tabindex]:not([tabindex="-1"])',
141
+ ),
142
+ ).filter((element) => element.getAttribute("aria-hidden") !== "true");
143
+
144
+ if (focusable.length === 0) {
145
+ event.preventDefault();
146
+ dialog.focus();
147
+ return;
148
+ }
149
+
150
+ const first = focusable[0];
151
+ const last = focusable[focusable.length - 1];
152
+ if (event.shiftKey && document.activeElement === first) {
153
+ event.preventDefault();
154
+ last.focus();
155
+ } else if (!event.shiftKey && document.activeElement === last) {
156
+ event.preventDefault();
157
+ first.focus();
158
+ }
159
+ }
160
+
161
  const filteredTools = useMemo(() => {
162
  const query = search.trim().toLowerCase();
163
  const filtered = tools.filter((tool) => {
 
232
  </nav>
233
  <div className="header-actions">
234
  <ThemeToggle />
235
+ <button className="button button-small button-primary header-submit" onClick={openSubmitForm}>
236
  <Plus size={16} aria-hidden="true" /> Submit a tool
237
  </button>
238
  </div>
 
252
  <a className="button button-primary hero-cta" href="#directory">
253
  Explore the directory <ArrowRight size={16} aria-hidden="true" />
254
  </a>
255
+ <button className="button button-quiet" onClick={openSubmitForm}>
256
  <Plus size={16} aria-hidden="true" /> Add your project
257
  </button>
258
  </div>
 
378
  )}
379
  <div className="directory-foot">
380
  <span>Showing <strong>{filteredTools.length}</strong> of <strong>{tools.length}</strong> projects</span>
381
+ <button className="button button-outline" onClick={openSubmitForm}>
382
  Share a project <ArrowUpRight size={15} />
383
  </button>
384
  </div>
 
391
  <h2>Good tools deserve to be found.</h2>
392
  <p>Submit an open-source project, share what works, and help the community find its next idea.</p>
393
  </div>
394
+ <button className="button button-primary community-cta" onClick={openSubmitForm}>
395
  Submit a tool <ArrowUpRight size={16} />
396
  </button>
397
  </section>
 
409
  {showForm ? (
410
  <div
411
  className="modal-backdrop"
412
+ onMouseDown={(event) => { if (event.target === event.currentTarget) closeSubmitForm(); }}
 
413
  >
414
+ <section
415
+ ref={dialogRef}
416
+ className="submit-modal"
417
+ role="dialog"
418
+ aria-modal="true"
419
+ aria-labelledby="modal-heading"
420
+ onKeyDown={handleDialogKeyDown}
421
+ >
422
+ <button ref={closeButtonRef} className="modal-close" onClick={closeSubmitForm} aria-label="Close submission form">
423
  <X size={19} />
424
  </button>
425
  {submitted ? (
426
  <div className="success-panel">
427
  <div className="success-icon"><Check size={23} /></div>
428
  <div className="section-kicker">YOU’RE IN THE INDEX</div>
429
+ <h2 id="modal-heading" ref={successHeadingRef} tabIndex={-1}>Thanks for sharing.</h2>
430
  <p>Your project has been added to the community directory.</p>
431
+ <button className="button button-primary" onClick={closeSubmitForm}>Back to exploring</button>
432
  </div>
433
  ) : (
434
  <>