Skip to content

Commit aea9f8b

Browse files
authored
webui : improve accessibility for visually impaired people (#13551)
* webui : improve accessibility for visually impaired people * add a11y for extra contents * fix some labels being read twice * add skip to main content
1 parent 06c1e4a commit aea9f8b

File tree

10 files changed

+147
-48
lines changed

10 files changed

+147
-48
lines changed

tools/server/public/index.html.gz

590 Bytes
Binary file not shown.

tools/server/webui/src/App.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,13 +28,13 @@ function AppLayout() {
2828
return (
2929
<>
3030
<Sidebar />
31-
<div
31+
<main
3232
className="drawer-content grow flex flex-col h-screen w-screen mx-auto px-4 overflow-auto bg-base-100"
3333
id="main-scroll"
3434
>
3535
<Header />
3636
<Outlet />
37-
</div>
37+
</main>
3838
{
3939
<SettingDialog
4040
show={showSettings}

tools/server/webui/src/components/ChatInputExtraContextItem.tsx

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,16 +18,26 @@ export default function ChatInputExtraContextItem({
1818
if (!items) return null;
1919

2020
return (
21-
<div className="flex flex-row gap-4 overflow-x-auto py-2 px-1 mb-1">
21+
<div
22+
className="flex flex-row gap-4 overflow-x-auto py-2 px-1 mb-1"
23+
role="group"
24+
aria-description="Selected files"
25+
>
2226
{items.map((item, i) => (
2327
<div
2428
className="indicator"
2529
key={i}
2630
onClick={() => clickToShow && setShow(i)}
31+
tabIndex={0}
32+
aria-description={
33+
clickToShow ? `Click to show: ${item.name}` : undefined
34+
}
35+
role={clickToShow ? 'button' : 'menuitem'}
2736
>
2837
{removeItem && (
2938
<div className="indicator-item indicator-top">
3039
<button
40+
aria-label="Remove file"
3141
className="btn btn-neutral btn-sm w-4 h-4 p-0 rounded-full"
3242
onClick={() => removeItem(i)}
3343
>
@@ -46,13 +56,16 @@ export default function ChatInputExtraContextItem({
4656
<>
4757
<img
4858
src={item.base64Url}
49-
alt={item.name}
59+
alt={`Preview image for ${item.name}`}
5060
className="w-14 h-14 object-cover rounded-md"
5161
/>
5262
</>
5363
) : (
5464
<>
55-
<div className="w-14 h-14 flex items-center justify-center">
65+
<div
66+
className="w-14 h-14 flex items-center justify-center"
67+
aria-description="Document icon"
68+
>
5669
<DocumentTextIcon className="h-8 w-14 text-base-content/50" />
5770
</div>
5871

@@ -66,16 +79,25 @@ export default function ChatInputExtraContextItem({
6679
))}
6780

6881
{showingItem && (
69-
<dialog className="modal modal-open">
82+
<dialog
83+
className="modal modal-open"
84+
aria-description={`Preview ${showingItem.name}`}
85+
>
7086
<div className="modal-box">
7187
<div className="flex justify-between items-center mb-4">
7288
<b>{showingItem.name ?? 'Extra content'}</b>
73-
<button className="btn btn-ghost btn-sm">
89+
<button
90+
className="btn btn-ghost btn-sm"
91+
aria-label="Close preview dialog"
92+
>
7493
<XMarkIcon className="h-5 w-5" onClick={() => setShow(-1)} />
7594
</button>
7695
</div>
7796
{showingItem.type === 'imageFile' ? (
78-
<img src={showingItem.base64Url} alt={showingItem.name} />
97+
<img
98+
src={showingItem.base64Url}
99+
alt={`Preview image for ${showingItem.name}`}
100+
/>
79101
) : (
80102
<div className="overflow-x-auto">
81103
<pre className="whitespace-pre-wrap break-words text-sm">

tools/server/webui/src/components/ChatMessage.tsx

Lines changed: 29 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -83,13 +83,20 @@ export default function ChatMessage({
8383

8484
if (!viewingChat) return null;
8585

86+
const isUser = msg.role === 'user';
87+
8688
return (
87-
<div className="group" id={id}>
89+
<div
90+
className="group"
91+
id={id}
92+
role="group"
93+
aria-description={`Message from ${msg.role}`}
94+
>
8895
<div
8996
className={classNames({
9097
chat: true,
91-
'chat-start': msg.role !== 'user',
92-
'chat-end': msg.role === 'user',
98+
'chat-start': !isUser,
99+
'chat-end': isUser,
93100
})}
94101
>
95102
{msg.extra && msg.extra.length > 0 && (
@@ -99,7 +106,7 @@ export default function ChatMessage({
99106
<div
100107
className={classNames({
101108
'chat-bubble markdown': true,
102-
'chat-bubble bg-transparent': msg.role !== 'user',
109+
'chat-bubble bg-transparent': !isUser,
103110
})}
104111
>
105112
{/* textarea for editing message */}
@@ -142,7 +149,7 @@ export default function ChatMessage({
142149
) : (
143150
<>
144151
{/* render message as markdown */}
145-
<div dir="auto">
152+
<div dir="auto" tabIndex={0}>
146153
{thought && (
147154
<ThoughtProcess
148155
isThinking={!!isThinking && !!isPending}
@@ -196,13 +203,18 @@ export default function ChatMessage({
196203
})}
197204
>
198205
{siblingLeafNodeIds && siblingLeafNodeIds.length > 1 && (
199-
<div className="flex gap-1 items-center opacity-60 text-sm">
206+
<div
207+
className="flex gap-1 items-center opacity-60 text-sm"
208+
role="navigation"
209+
aria-description={`Message version ${siblingCurrIdx + 1} of ${siblingLeafNodeIds.length}`}
210+
>
200211
<button
201212
className={classNames({
202213
'btn btn-sm btn-ghost p-1': true,
203214
'opacity-20': !prevSibling,
204215
})}
205216
onClick={() => prevSibling && onChangeSibling(prevSibling)}
217+
aria-label="Previous message version"
206218
>
207219
<ChevronLeftIcon className="h-4 w-4" />
208220
</button>
@@ -215,6 +227,7 @@ export default function ChatMessage({
215227
'opacity-20': !nextSibling,
216228
})}
217229
onClick={() => nextSibling && onChangeSibling(nextSibling)}
230+
aria-label="Next message version"
218231
>
219232
<ChevronRightIcon className="h-4 w-4" />
220233
</button>
@@ -223,7 +236,7 @@ export default function ChatMessage({
223236
{/* user message */}
224237
{msg.role === 'user' && (
225238
<BtnWithTooltips
226-
className="btn-mini show-on-hover w-8 h-8"
239+
className="btn-mini w-8 h-8"
227240
onClick={() => setEditingContent(msg.content)}
228241
disabled={msg.content === null}
229242
tooltipsContent="Edit message"
@@ -236,7 +249,7 @@ export default function ChatMessage({
236249
<>
237250
{!isPending && (
238251
<BtnWithTooltips
239-
className="btn-mini show-on-hover w-8 h-8"
252+
className="btn-mini w-8 h-8"
240253
onClick={() => {
241254
if (msg.content !== null) {
242255
onRegenerateMessage(msg as Message);
@@ -250,10 +263,7 @@ export default function ChatMessage({
250263
)}
251264
</>
252265
)}
253-
<CopyButton
254-
className="btn-mini show-on-hover w-8 h-8"
255-
content={msg.content}
256-
/>
266+
<CopyButton className="btn-mini w-8 h-8" content={msg.content} />
257267
</div>
258268
)}
259269
</div>
@@ -271,6 +281,8 @@ function ThoughtProcess({
271281
}) {
272282
return (
273283
<div
284+
role="button"
285+
aria-label="Toggle thought process display"
274286
tabIndex={0}
275287
className={classNames({
276288
'collapse bg-none': true,
@@ -292,7 +304,11 @@ function ThoughtProcess({
292304
)}
293305
</div>
294306
</div>
295-
<div className="collapse-content text-base-content/70 text-sm p-1">
307+
<div
308+
className="collapse-content text-base-content/70 text-sm p-1"
309+
tabIndex={0}
310+
aria-description="Thought process content"
311+
>
296312
<div className="border-l-2 border-base-content/20 pl-4 mb-4">
297313
<MarkdownDisplay content={content} />
298314
</div>

tools/server/webui/src/components/ChatScreen.tsx

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -279,7 +279,11 @@ export default function ChatScreen() {
279279
function ServerInfo() {
280280
const { serverProps } = useAppContext();
281281
return (
282-
<div className="card card-sm shadow-sm border-1 border-base-content/20 text-base-content/70 mb-6">
282+
<div
283+
className="card card-sm shadow-sm border-1 border-base-content/20 text-base-content/70 mb-6"
284+
tabIndex={0}
285+
aria-description="Server information"
286+
>
283287
<div className="card-body">
284288
<b>Server Info</b>
285289
<p>
@@ -311,6 +315,8 @@ function ChatInput({
311315

312316
return (
313317
<div
318+
role="group"
319+
aria-label="Chat input"
314320
className={classNames({
315321
'flex items-end pt-8 pb-6 sticky bottom-0 bg-base-100': true,
316322
'opacity-50': isDrag, // simply visual feedback to inform user that the file will be accepted
@@ -400,13 +406,15 @@ function ChatInput({
400406
'btn w-8 h-8 p-0 rounded-full': true,
401407
'btn-disabled': isGenerating,
402408
})}
409+
aria-label="Upload file"
410+
tabIndex={0}
411+
role="button"
403412
>
404413
<PaperClipIcon className="h-5 w-5" />
405414
</label>
406415
<input
407416
id="file-upload"
408417
type="file"
409-
className="hidden"
410418
disabled={isGenerating}
411419
{...getInputProps()}
412420
hidden
@@ -422,6 +430,7 @@ function ChatInput({
422430
<button
423431
className="btn btn-primary w-8 h-8 p-0 rounded-full"
424432
onClick={onSend}
433+
aria-label="Send message"
425434
>
426435
<ArrowUpIcon className="h-5 w-5" />
427436
</button>

tools/server/webui/src/components/Header.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,12 @@ export default function Header() {
3838

3939
{/* action buttons (top right) */}
4040
<div className="flex items-center">
41-
<div className="tooltip tooltip-bottom" data-tip="Settings">
42-
<button className="btn" onClick={() => setShowSettings(true)}>
41+
<div
42+
className="tooltip tooltip-bottom"
43+
data-tip="Settings"
44+
onClick={() => setShowSettings(true)}
45+
>
46+
<button className="btn" aria-hidden={true}>
4347
{/* settings button */}
4448
<Cog8ToothIcon className="w-5 h-5" />
4549
</button>

tools/server/webui/src/components/SettingDialog.tsx

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -335,14 +335,22 @@ export default function SettingDialog({
335335
};
336336

337337
return (
338-
<dialog className={classNames({ modal: true, 'modal-open': show })}>
338+
<dialog
339+
className={classNames({ modal: true, 'modal-open': show })}
340+
aria-label="Settings dialog"
341+
>
339342
<div className="modal-box w-11/12 max-w-3xl">
340343
<h3 className="text-lg font-bold mb-6">Settings</h3>
341344
<div className="flex flex-col md:flex-row h-[calc(90vh-12rem)]">
342345
{/* Left panel, showing sections - Desktop version */}
343-
<div className="hidden md:flex flex-col items-stretch pr-4 mr-4 border-r-2 border-base-200">
346+
<div
347+
className="hidden md:flex flex-col items-stretch pr-4 mr-4 border-r-2 border-base-200"
348+
role="complementary"
349+
aria-description="Settings sections"
350+
tabIndex={0}
351+
>
344352
{SETTING_SECTIONS.map((section, idx) => (
345-
<div
353+
<button
346354
key={idx}
347355
className={classNames({
348356
'btn btn-ghost justify-start font-normal w-44 mb-1': true,
@@ -352,12 +360,16 @@ export default function SettingDialog({
352360
dir="auto"
353361
>
354362
{section.title}
355-
</div>
363+
</button>
356364
))}
357365
</div>
358366

359367
{/* Left panel, showing sections - Mobile version */}
360-
<div className="md:hidden flex flex-row gap-2 mb-4">
368+
{/* This menu is skipped on a11y, otherwise it's repeated the desktop version */}
369+
<div
370+
className="md:hidden flex flex-row gap-2 mb-4"
371+
aria-disabled={true}
372+
>
361373
<details className="dropdown">
362374
<summary className="btn bt-sm w-full m-1">
363375
{SETTING_SECTIONS[sectionIdx].title}

0 commit comments

Comments
 (0)