admin: a11y: better tooltips for menu and progress indicators
Massimo Melina committed
Feb 6, 2024 at 22:35 UTC
d9893d06df6a1672d4624a9d5cf57941599285aa
5 files changed
+41
-43
admin/src/InstalledPlugins.ts
+5
-8
@@ -1,8 +1,8 @@
1
// This file is part of HFS - Copyright 2021-2023, Massimo Melina <a@rejetto.com> - License https://www.gnu.org/licenses/gpl-3.0.txt
2
3
import { apiCall, useApiList } from './api'
4
-import { createElement as h, Fragment, ReactNode, useEffect } from 'react'
5
-import { Box, Link, Tooltip } from '@mui/material'
4
+import { createElement as h, Fragment, useEffect } from 'react'
5
+import { Box, Link } from '@mui/material'
6
import { DataTable } from './DataTable'
7
import { Delete, Error as ErrorIcon, PlayCircle, Settings, StopCircle, Upgrade } from '@mui/icons-material'
8
import { HTTP_FAILED_DEPENDENCY, prefix, with_, xlate } from './misc'
@@ -12,7 +12,7 @@ import { BoolField, Field, MultiSelectField, NumberField, SelectField, StringFie
12
import { ArrayField } from './ArrayField'
13
import FileField from './FileField'
14
import { PLUGIN_ERRORS } from './PluginsPage'
15
-import { Btn, IconBtn } from './mui'
15
+import { Btn, hTooltip, IconBtn } from './mui'
16
17
export default function InstalledPlugins({ updates }: { updates?: true }) {
18
const { list, updateEntry, error, updateList, initializing } = useApiList(updates ? 'get_plugin_updates' : 'get_plugins')
@@ -142,11 +142,8 @@ export function renderName({ row, value }: any) {
142
))
143
)
144
145
- function errorIcon(msg: ReactNode, warning=false) {
146
- return msg && h(Tooltip, {
147
- title: msg,
148
- children: h(ErrorIcon, { fontSize: 'small', color: warning ? 'warning' : 'error', sx: { ml: -.5, mr: .5 } })
149
- })
145
+ function errorIcon(msg: string, warning=false) {
146
+ return msg && hTooltip(msg, msg, h(ErrorIcon, { fontSize: 'small', color: warning ? 'warning' : 'error', sx: { ml: -.5, mr: .5 } }))
147
}
148
}
149
admin/src/LogsPage.ts
+4
-3
@@ -1,13 +1,14 @@
1
// This file is part of HFS - Copyright 2021-2023, Massimo Melina <a@rejetto.com> - License https://www.gnu.org/licenses/gpl-3.0.txt
2
3
import { createElement as h, Fragment, useMemo, useState } from 'react';
4
-import { Box, Tab, Tabs, Tooltip } from '@mui/material'
4
+import { Box, Tab, Tabs } from '@mui/material'
5
import { API_URL, useApiList } from './api'
6
import { DataTable } from './DataTable'
7
import { CFG, Dict, formatBytes, HTTP_UNAUTHORIZED, newDialog, prefix, shortenAgent, splitAt, tryJson, typedKeys, NBSP
8
} from '@hfs/shared'
9
import { logLabels } from './OptionsPage'
10
-import { NetmaskField, Flex, IconBtn, useBreakpoint, usePauseButton, useToggleButton, WildcardsSupported, Country } from './mui';
10
+import { NetmaskField, Flex, IconBtn, useBreakpoint, usePauseButton, useToggleButton, WildcardsSupported, Country,
11
+ hTooltip } from './mui';
12
import { GridColDef } from '@mui/x-data-grid'
13
import _ from 'lodash'
14
import { Settings, SmartToy } from '@mui/icons-material'
@@ -244,7 +245,7 @@ export function agentIcons(agent: string) {
245
win: UW + '0/0a/Unofficial_Windows_logo_variant_-_2002%E2%80%932012_%28Multicolored%29.svg',
246
apple: UW + '7/74/Apple_logo_dark_grey.svg', // grey works for both themes
247
})
247
- return h(Tooltip, { title: agent, children: h('span', { fontSize: '18px' }, browserIcon || short, ' ', osIcon) })
248
+ return hTooltip(agent, undefined, h('span', { fontSize: '18px' }, browserIcon || short, ' ', osIcon) )
249
250
function icon(k: string, map: Dict<string>) {
251
const src = map[k]
admin/src/MainMenu.ts
+8
-15
@@ -1,19 +1,9 @@
1
// This file is part of HFS - Copyright 2021-2023, Massimo Melina <a@rejetto.com> - License https://www.gnu.org/licenses/gpl-3.0.txt
2
3
import { createElement as h, FC } from 'react';
4
-import { List, ListItemButton, ListItemIcon, ListItemText, Box, Tooltip } from '@mui/material'
4
+import { List, ListItemButton, ListItemIcon, ListItemText, Box } from '@mui/material'
5
import {
6
- AccountTree,
7
- Extension,
8
- History,
9
- Home,
10
- Logout,
11
- ManageAccounts,
12
- Monitor,
13
- Public,
14
- Settings,
15
- Translate,
16
- Code,
6
+ AccountTree, Extension, History, Home, Logout, ManageAccounts, Monitor, Public, Settings, Translate, Code,
7
SvgIconComponent
8
} from '@mui/icons-material'
9
import _ from 'lodash'
@@ -32,6 +22,7 @@ import CustomHtmlPage from './CustomHtmlPage';
22
import InternetPage from './InternetPage'
23
import { replaceStringToReact } from './md'
24
import { useWindowSize } from 'usehooks-ts'
25
+import { hTooltip } from './mui'
26
27
interface MenuEntry {
28
path: string
@@ -75,7 +66,7 @@ export default function Menu({ onSelect, itemTitle }: { onSelect: ()=>void, item
66
h(Box, { fontSize: 'small' }, replaceStringToReact(VERSION||'', /-/, () => h('br'))),
67
short && h('img', { src: logo, style: { height: '2.5em' } }),
68
),
78
- mainMenu.map((it, idx) => h(Tooltip, { key: it.path, title: itemTitle(idx), placement: 'right', children:
69
+ mainMenu.map((it, idx) => hTooltip( itemTitle(idx), getMenuLabel(it) + ' ' + itemTitle(idx),
70
h(ListItemButton, {
71
to: it.path,
72
component: NavLink,
@@ -84,9 +75,11 @@ export default function Menu({ onSelect, itemTitle }: { onSelect: ()=>void, item
75
style: ({ isActive }) => isActive ? { textDecoration: 'underline' } : {},
76
children: undefined, // shut up ts
77
},
87
- it.icon && h(ListItemIcon, { sx:{ color: 'primary.contrastText', minWidth: 48 } }, h(it.icon)),
78
+ it.icon && h(ListItemIcon, { sx: { color: 'primary.contrastText', minWidth: 48 } }, h(it.icon)),
79
h(ListItemText, { sx: { whiteSpace: 'nowrap' }, primary: getMenuLabel(it) })
89
- ) }) ),
80
+ ),
81
+ { key: it.path, placement: 'right' }
82
+ )),
83
!short && h(Box, { sx: { flex: 1, opacity: .7, background: `url(${logo}) no-repeat bottom`, backgroundSize: 'contain', margin: 2 } }),
84
)
85
)
admin/src/MonitorPage.ts
+4
-4
@@ -4,10 +4,10 @@ import _ from "lodash"
4
import { createElement as h, useMemo, Fragment } from "react"
5
import { apiCall, useApiEvents, useApiEx, useApiList } from "./api"
6
import { LinkOff, Lock, Block, FolderZip, Upload, Download } from '@mui/icons-material'
7
-import { Box, Chip, ChipProps, Tooltip } from '@mui/material'
7
+import { Box, Chip, ChipProps } from '@mui/material'
8
import { DataTable } from './DataTable'
9
import { formatBytes, ipForUrl, manipulateConfig, CFG, formatSpeed, with_, createDurationFormatter } from "./misc"
10
-import { IconBtn, IconProgress, iconTooltip, usePauseButton, useBreakpoint, Country } from './mui'
10
+import { IconBtn, IconProgress, iconTooltip, usePauseButton, useBreakpoint, Country, hTooltip } from './mui'
11
import { Field, SelectField } from '@hfs/mui-grid-form'
12
import { StandardCSSProperties } from '@mui/system/styleFunctionSx/StandardCssProperties'
13
import { agentIcons } from './LogsPage'
@@ -70,7 +70,7 @@ function MoreInfo() {
70
}
71
if (!label)
72
label = _.capitalize(k.replaceAll('_', ' '))
73
- return h(Tooltip, { title: renderedTitle, children: h(Chip, {
73
+ return hTooltip(renderedTitle, undefined, h(Chip, {
74
variant: 'filled',
75
color,
76
label: h(Fragment, {},
@@ -78,7 +78,7 @@ function MoreInfo() {
78
': ',
79
h('span', { style:{ display: 'inline-block', minWidth } }, v),
80
),
81
- }) })
81
+ }) )
82
}
83
84
function port(v: any): ReturnType<Render> {
admin/src/mui.ts
+20
-13
@@ -3,7 +3,7 @@
3
4
import { PauseCircle, PlayCircle, Refresh, SvgIconComponent } from '@mui/icons-material'
5
import { SxProps } from '@mui/system'
6
-import { createElement as h, FC, forwardRef, Fragment, ReactNode, useCallback, useState } from 'react'
6
+import { createElement as h, FC, forwardRef, Fragment, ReactElement, ReactNode, useCallback, useState } from 'react'
7
import { Box, BoxProps, Breakpoint, ButtonProps, CircularProgress, IconButton, IconButtonProps, Link, LinkProps,
8
Tooltip, TooltipProps, useMediaQuery } from '@mui/material'
9
import { formatPerc, isIpLan, isIpLocalHost, prefix, WIKI_URL } from '../../src/cross'
@@ -48,19 +48,20 @@ export function IconProgress({ icon, progress, offset, addTitle, sx }: IconProgr
48
size: 32,
49
sx: { position: 'absolute' },
50
}),
51
- h(Tooltip, {
52
- title: h(Fragment, {},
51
+ hTooltip(
52
+ h(Fragment, {},
53
_.isNumber(progress) ? formatPerc(progress) : "Size unknown",
54
addTitle && h('div', {}, addTitle)
55
),
56
- children: h(CircularProgress, {
56
+ '',
57
+ h(CircularProgress, {
58
color: 'success',
59
value: (offset || 1e-7) * 100,
60
variant: 'determinate',
61
size: 32,
62
sx: { display: 'flex', ...sx }, // workaround: without this the element has 0 width when the space is crammy (monitor/file)
63
}),
63
- })
64
+ )
65
)
66
}
67
@@ -140,7 +141,7 @@ export const IconBtn = forwardRef(({ title, icon, onClick, disabled, progress, l
141
if (disabled)
142
ret = h('span', { role: 'button', 'aria-label': title + ', disabled' }, ret)
143
if (title)
143
- ret = h(Tooltip, { title, ...tooltipProps, children: ret })
144
+ ret = hTooltip(title, undefined, ret, tooltipProps)
145
return ret
146
})
147
@@ -199,7 +200,7 @@ export const Btn = forwardRef(({ icon, title, onClick, disabled, progress, link,
200
if (disabled)
201
ret = h('span', { role: 'button', 'aria-label': title + ', disabled' }, ret)
202
if (title)
202
- ret = h(Tooltip, { title, ...tooltipProps, children: ret })
203
+ ret = hTooltip(title, undefined, ret, tooltipProps)
204
return ret
205
})
206
@@ -209,7 +210,7 @@ function execDoneMessage(msg: boolean | string | undefined) {
210
}
211
212
export function iconTooltip(icon: SvgIconComponent, tooltip: ReactNode, sx?: SxProps, props?: SvgIconProps) {
212
- return h(Tooltip, { title: tooltip, children: h(icon, { sx, ...props }) })
213
+ return hTooltip(tooltip, undefined, h(icon, { sx, ...props }) )
214
}
215
216
export function InLink(props:any) {
@@ -263,9 +264,8 @@ export function Country({ code, ip, def, long, short }: { code: string, ip?: str
264
const { data } = useBatch(code === undefined && good && ip2countryBatch, ip, { delay: 100 }) // query if necessary
265
code ||= data || ''
266
const country = code && _.find(COUNTRIES, { code })
266
- return !country ? h(Fragment, {}, def) : h(Tooltip, {
267
- title: long ? undefined : country.name,
268
- children: h('span', {},
267
+ return !country ? h(Fragment, {}, def)
268
+ : hTooltip(long ? undefined : country.name, undefined, h('span', {},
269
h('img', {
270
className: 'flag icon-w-text',
271
src: `flags/${code.toLowerCase()}.png`,
@@ -273,11 +273,18 @@ export function Country({ code, ip, def, long, short }: { code: string, ip?: str
273
...long && { 'aria-hidden': true },
274
}),
275
long ? country.name + prefix(' (', short && code, ')') : code
276
- )
277
- })
276
+ ) )
277
}
278
279
async function ip2countryBatch(ips: string[]) {
280
const res = await apiCall('ip_country', { ips })
281
return res.codes as string[]
282
}
283
+
284
+// force you to think of aria when adding a tooltip
285
+export function hTooltip(title: ReactNode, ariaLabel: string | undefined, children: ReactElement, props?: Omit<TooltipProps, 'title' | 'children'> & { key?: any }) {
286
+ return h(Tooltip, { title, children,
287
+ ...ariaLabel === '' ? { 'aria-hidden': true } : { 'aria-label': ariaLabel },
288
+ ...props
289
+ })
290
+}
\ No newline at end of file