@samitouri / QOSami-HFS / commits / d9893d06

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