BrowseDashboards: Improve screen reader announcements (#75970)
* use aria-labelledby to describe the whole row * change * fix typo * i18n
This commit is contained in:
@@ -4,6 +4,7 @@ import React from 'react';
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
import { Checkbox, useStyles2 } from '@grafana/ui';
|
||||
import { t } from 'app/core/internationalization';
|
||||
|
||||
import { DashboardsTreeCellProps, SelectionState } from '../types';
|
||||
|
||||
@@ -32,6 +33,7 @@ export default function CheckboxCell({
|
||||
return (
|
||||
<Checkbox
|
||||
data-testid={selectors.pages.BrowseDashboards.table.checkbox(item.uid)}
|
||||
aria-label={t('browse-dashboards.dashboards-tree.select-checkbox', 'Select')}
|
||||
value={state === SelectionState.Selected}
|
||||
indeterminate={state === SelectionState.Mixed}
|
||||
onChange={(ev) => onItemSelectionChange?.(item, ev.currentTarget.checked)}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React from 'react';
|
||||
|
||||
import { Checkbox } from '@grafana/ui';
|
||||
import { t } from 'app/core/internationalization';
|
||||
|
||||
import { DashboardTreeHeaderProps, SelectionState } from '../types';
|
||||
|
||||
@@ -11,6 +12,7 @@ export default function CheckboxHeaderCell({ isSelected, onAllSelectionChange }:
|
||||
<Checkbox
|
||||
value={state === SelectionState.Selected}
|
||||
indeterminate={state === SelectionState.Mixed}
|
||||
aria-label={t('browse-dashboards.dashboards-tree.select-all-header-checkbox', 'Select all')}
|
||||
onChange={(ev) => {
|
||||
if (state === SelectionState.Mixed) {
|
||||
// Ensure clicking an indeterminate checkbox always clears the selection
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { css, cx } from '@emotion/css';
|
||||
import React, { useCallback, useEffect, useMemo, useRef } from 'react';
|
||||
import React, { useCallback, useEffect, useId, useMemo, useRef } from 'react';
|
||||
import { TableInstance, useTable } from 'react-table';
|
||||
import { FixedSizeList as List } from 'react-window';
|
||||
import InfiniteLoader from 'react-window-infinite-loader';
|
||||
@@ -17,6 +17,7 @@ import CheckboxHeaderCell from './CheckboxHeaderCell';
|
||||
import { NameCell } from './NameCell';
|
||||
import { TagsCell } from './TagsCell';
|
||||
import { useCustomFlexLayout } from './customFlexTableLayout';
|
||||
import { makeRowID } from './utils';
|
||||
|
||||
interface DashboardsTreeProps {
|
||||
items: DashboardsTreeItem[];
|
||||
@@ -47,6 +48,8 @@ export function DashboardsTree({
|
||||
requestLoadMore,
|
||||
canSelect = false,
|
||||
}: DashboardsTreeProps) {
|
||||
const treeID = useId();
|
||||
|
||||
const infiniteLoaderRef = useRef<InfiniteLoader>(null);
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
@@ -98,10 +101,11 @@ export function DashboardsTree({
|
||||
isSelected,
|
||||
onAllSelectionChange,
|
||||
onItemSelectionChange,
|
||||
treeID,
|
||||
}),
|
||||
// we need this to rerender if items changes
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[table, isSelected, onAllSelectionChange, onItemSelectionChange, items]
|
||||
[table, isSelected, onAllSelectionChange, onItemSelectionChange, items, treeID]
|
||||
);
|
||||
|
||||
const handleIsItemLoaded = useCallback(
|
||||
@@ -175,12 +179,13 @@ interface VirtualListRowProps {
|
||||
isSelected: DashboardsTreeCellProps['isSelected'];
|
||||
onAllSelectionChange: DashboardsTreeCellProps['onAllSelectionChange'];
|
||||
onItemSelectionChange: DashboardsTreeCellProps['onItemSelectionChange'];
|
||||
treeID: string;
|
||||
};
|
||||
}
|
||||
|
||||
function VirtualListRow({ index, style, data }: VirtualListRowProps) {
|
||||
const styles = useStyles2(getStyles);
|
||||
const { table, isSelected, onItemSelectionChange } = data;
|
||||
const { table, isSelected, onItemSelectionChange, treeID } = data;
|
||||
const { rows, prepareRow } = table;
|
||||
|
||||
const row = rows[index];
|
||||
@@ -190,6 +195,7 @@ function VirtualListRow({ index, style, data }: VirtualListRowProps) {
|
||||
<div
|
||||
{...row.getRowProps({ style })}
|
||||
className={cx(styles.row, styles.bodyRow)}
|
||||
aria-labelledby={makeRowID(treeID, row.original.item)}
|
||||
data-testid={selectors.pages.BrowseDashboards.table.row(
|
||||
'title' in row.original.item ? row.original.item.title : row.original.item.uid
|
||||
)}
|
||||
@@ -199,7 +205,7 @@ function VirtualListRow({ index, style, data }: VirtualListRowProps) {
|
||||
|
||||
return (
|
||||
<div key={key} {...cellProps} className={styles.cell}>
|
||||
{cell.render('Cell', { isSelected, onItemSelectionChange })}
|
||||
{cell.render('Cell', { isSelected, onItemSelectionChange, treeID })}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -1,26 +1,28 @@
|
||||
import { css } from '@emotion/css';
|
||||
import React from 'react';
|
||||
import Skeleton from 'react-loading-skeleton';
|
||||
import { CellProps } from 'react-table';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { reportInteraction } from '@grafana/runtime';
|
||||
import { Icon, IconButton, Link, Spinner, useStyles2, Text } from '@grafana/ui';
|
||||
import { getSvgSize } from '@grafana/ui/src/components/Icon/utils';
|
||||
import { t } from 'app/core/internationalization';
|
||||
import { getIconForKind } from 'app/features/search/service/utils';
|
||||
|
||||
import { Indent } from '../../../core/components/Indent/Indent';
|
||||
import { useChildrenByParentUIDState } from '../state';
|
||||
import { DashboardsTreeItem } from '../types';
|
||||
import { DashboardsTreeCellProps } from '../types';
|
||||
|
||||
import { makeRowID } from './utils';
|
||||
|
||||
const CHEVRON_SIZE = 'md';
|
||||
const ICON_SIZE = 'sm';
|
||||
|
||||
type NameCellProps = CellProps<DashboardsTreeItem, unknown> & {
|
||||
type NameCellProps = DashboardsTreeCellProps & {
|
||||
onFolderClick: (uid: string, newOpenState: boolean) => void;
|
||||
};
|
||||
|
||||
export function NameCell({ row: { original: data }, onFolderClick }: NameCellProps) {
|
||||
export function NameCell({ row: { original: data }, onFolderClick, treeID }: NameCellProps) {
|
||||
const styles = useStyles2(getStyles);
|
||||
const { item, level, isOpen } = data;
|
||||
const childrenByParentUID = useChildrenByParentUIDState();
|
||||
@@ -69,14 +71,24 @@ export function NameCell({ row: { original: data }, onFolderClick }: NameCellPro
|
||||
onFolderClick(item.uid, !isOpen);
|
||||
}}
|
||||
name={isOpen ? 'angle-down' : 'angle-right'}
|
||||
aria-label={isOpen ? `Collapse folder ${item.title}` : `Expand folder ${item.title}`}
|
||||
aria-label={
|
||||
isOpen
|
||||
? t('browse-dashboards.dashboards-tree.collapse-folder-button', 'Collapse folder {{title}}', {
|
||||
title: item.title,
|
||||
})
|
||||
: t('browse-dashboards.dashboards-tree.expand-folder-button', 'Expand folder {{title}}', {
|
||||
title: item.title,
|
||||
})
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<span className={styles.folderButtonSpacer} />
|
||||
)}
|
||||
|
||||
<div className={styles.iconNameContainer}>
|
||||
{isLoading ? <Spinner size={ICON_SIZE} /> : <Icon size={ICON_SIZE} name={iconName} />}
|
||||
<Text variant="body" truncate>
|
||||
|
||||
<Text variant="body" truncate id={treeID && makeRowID(treeID, item)}>
|
||||
{item.url ? (
|
||||
<Link
|
||||
onClick={() => {
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
import { DashboardViewItemWithUIItems } from '../types';
|
||||
|
||||
export function makeRowID(baseId: string, item: DashboardViewItemWithUIItems) {
|
||||
return baseId + item.uid;
|
||||
}
|
||||
@@ -48,6 +48,7 @@ interface RendererUserProps {
|
||||
isSelected?: (kind: DashboardViewItem | '$all') => SelectionState;
|
||||
onAllSelectionChange?: (newState: boolean) => void;
|
||||
onItemSelectionChange?: (item: DashboardViewItem, newState: boolean) => void;
|
||||
treeID?: string;
|
||||
}
|
||||
|
||||
export type DashboardsTreeColumn = Column<DashboardsTreeItem>;
|
||||
|
||||
@@ -55,7 +55,11 @@
|
||||
"total__other": ""
|
||||
},
|
||||
"dashboards-tree": {
|
||||
"collapse-folder-button": "",
|
||||
"expand-folder-button": "",
|
||||
"name-column": "",
|
||||
"select-all-header-checkbox": "",
|
||||
"select-checkbox": "",
|
||||
"tags-column": ""
|
||||
},
|
||||
"folder-actions-button": {
|
||||
|
||||
@@ -55,7 +55,11 @@
|
||||
"total__other": "{{count}} items"
|
||||
},
|
||||
"dashboards-tree": {
|
||||
"collapse-folder-button": "Collapse folder {{title}}",
|
||||
"expand-folder-button": "Expand folder {{title}}",
|
||||
"name-column": "Name",
|
||||
"select-all-header-checkbox": "Select all",
|
||||
"select-checkbox": "Select",
|
||||
"tags-column": "Tags"
|
||||
},
|
||||
"folder-actions-button": {
|
||||
|
||||
@@ -60,7 +60,11 @@
|
||||
"total__other": ""
|
||||
},
|
||||
"dashboards-tree": {
|
||||
"collapse-folder-button": "",
|
||||
"expand-folder-button": "",
|
||||
"name-column": "",
|
||||
"select-all-header-checkbox": "",
|
||||
"select-checkbox": "",
|
||||
"tags-column": ""
|
||||
},
|
||||
"folder-actions-button": {
|
||||
|
||||
@@ -60,7 +60,11 @@
|
||||
"total__other": ""
|
||||
},
|
||||
"dashboards-tree": {
|
||||
"collapse-folder-button": "",
|
||||
"expand-folder-button": "",
|
||||
"name-column": "",
|
||||
"select-all-header-checkbox": "",
|
||||
"select-checkbox": "",
|
||||
"tags-column": ""
|
||||
},
|
||||
"folder-actions-button": {
|
||||
|
||||
@@ -55,7 +55,11 @@
|
||||
"total__other": "{{count}} įŧęmş"
|
||||
},
|
||||
"dashboards-tree": {
|
||||
"collapse-folder-button": "Cőľľäpşę ƒőľđęř {{title}}",
|
||||
"expand-folder-button": "Ēχpäʼnđ ƒőľđęř {{title}}",
|
||||
"name-column": "Ńämę",
|
||||
"select-all-header-checkbox": "Ŝęľęčŧ äľľ",
|
||||
"select-checkbox": "Ŝęľęčŧ",
|
||||
"tags-column": "Ŧäģş"
|
||||
},
|
||||
"folder-actions-button": {
|
||||
|
||||
@@ -50,7 +50,11 @@
|
||||
"total__other": ""
|
||||
},
|
||||
"dashboards-tree": {
|
||||
"collapse-folder-button": "",
|
||||
"expand-folder-button": "",
|
||||
"name-column": "",
|
||||
"select-all-header-checkbox": "",
|
||||
"select-checkbox": "",
|
||||
"tags-column": ""
|
||||
},
|
||||
"folder-actions-button": {
|
||||
|
||||
Reference in New Issue
Block a user