A11y: Fix fastpass issues for dashboard management pages (#39940)

* A11y: Fix fastpass issues for dashboard pages
See #39429
This commit is contained in:
kay delaney 2021-10-06 11:58:18 +01:00 committed by GitHub
parent 3131388084
commit 1ce750471f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 71 additions and 36 deletions

View File

@ -3,6 +3,7 @@
*/
export interface SelectableValue<T = any> {
label?: string;
ariaLabel?: string;
value?: T;
imgUrl?: string;
icon?: string;

View File

@ -4,6 +4,7 @@ import { GrafanaTheme2 } from '@grafana/data';
import { css } from '@emotion/css';
import { getPropertiesForButtonSize } from '../commonStyles';
import { getFocusStyles, getMouseFocusStyles } from '../../../themes/mixins';
import { StringSelector } from '@grafana/e2e-selectors';
export type RadioButtonSize = 'sm' | 'md';
@ -16,6 +17,7 @@ export interface RadioButtonProps {
id: string;
onChange: () => void;
fullWidth?: boolean;
'aria-label'?: StringSelector;
}
export const RadioButton: React.FC<RadioButtonProps> = ({
@ -28,6 +30,7 @@ export const RadioButton: React.FC<RadioButtonProps> = ({
name = undefined,
description,
fullWidth,
'aria-label': ariaLabel,
}) => {
const theme = useTheme2();
const styles = getRadioButtonStyles(theme, size, fullWidth);
@ -42,6 +45,7 @@ export const RadioButton: React.FC<RadioButtonProps> = ({
id={id}
checked={active}
name={name}
aria-label={ariaLabel}
/>
<label className={styles.radioLabel} htmlFor={id} title={description}>
{children}

View File

@ -52,6 +52,7 @@ export function RadioButtonGroup<T>({
disabled={isItemDisabled || disabled}
active={value === o.value}
key={`o.label-${i}`}
aria-label={o.ariaLabel}
onChange={handleOnChange(o)}
id={`option-${o.value}-${id}`}
name={groupName.current}

View File

@ -34,6 +34,7 @@ export const SortPicker: FC<Props> = ({ onChange, value, placeholder, filter })
onChange={onChange}
value={selected ?? null}
options={options}
aria-label="Sort"
placeholder={placeholder ?? `Sort (Default ${DEFAULT_SORT.label})`}
prefix={<Icon name={(value?.includes('asc') ? 'sort-amount-up' : 'sort-amount-down') as IconName} />}
/>

View File

@ -22,6 +22,7 @@ interface Props {
isClearable?: boolean;
invalid?: boolean;
disabled?: boolean;
id?: string;
}
const getDashboards = (query = '') => {
@ -38,11 +39,20 @@ const getDashboards = (query = '') => {
/**
* @deprecated prefer using dashboard uid rather than id
*/
export const DashboardPickerByID: FC<Props> = ({ onChange, value, width, isClearable = false, invalid, disabled }) => {
export const DashboardPickerByID: FC<Props> = ({
onChange,
value,
width,
isClearable = false,
invalid,
disabled,
id,
}) => {
const debouncedSearch = debounce(getDashboards, 300);
return (
<AsyncSelect
inputId={id}
menuShouldPortal
width={width}
isClearable={isClearable}

View File

@ -41,7 +41,7 @@ export const PanelTypeCard: React.FC<Props> = ({
onClick={disabled ? undefined : onClick}
title={isCurrent ? 'Click again to close this section' : plugin.name}
>
<img className={styles.img} src={plugin.info.logos.small} />
<img className={styles.img} src={plugin.info.logos.small} alt={`${plugin.name} logo`} />
<div className={styles.itemContent}>
<div className={styles.name}>{title}</div>

View File

@ -57,6 +57,7 @@ export class NewDashboardsFolder extends PureComponent<Props> {
error={errors.folderName && errors.folderName.message}
>
<Input
id="folder-name-input"
{...register('folderName', {
required: 'Folder name is required.',
validate: async (v) => await this.validateFolderName(v),

View File

@ -88,7 +88,7 @@ function getStyles(theme: GrafanaTheme2) {
metaContainer: css`
display: flex;
align-items: center;
color: ${theme.colors.text.disabled};
color: ${theme.colors.text.secondary};
font-size: ${theme.typography.bodySmall.fontSize};
padding-top: ${theme.spacing(0.5)};

View File

@ -3,18 +3,7 @@ import { MapDispatchToProps, MapStateToProps } from 'react-redux';
import { css } from '@emotion/css';
import { AppEvents, GrafanaTheme2, NavModel } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import {
Button,
stylesFactory,
withTheme2,
Input,
TextArea,
Field,
Form,
Legend,
FileUpload,
Themeable2,
} from '@grafana/ui';
import { Button, stylesFactory, withTheme2, Input, TextArea, Field, Form, FileUpload, Themeable2 } from '@grafana/ui';
import Page from 'app/core/components/Page/Page';
import { connectWithCleanUp } from 'app/core/components/connectWithCleanUp';
import { ImportDashboardOverview } from './components/ImportDashboardOverview';
@ -94,11 +83,15 @@ class UnthemedDashboardImport extends PureComponent<Props> {
</FileUpload>
</div>
<div className={styles.option}>
<Legend>Import via grafana.com</Legend>
<Form onSubmit={this.getGcomDashboard} defaultValues={{ gcomDashboard: '' }}>
{({ register, errors }) => (
<Field invalid={!!errors.gcomDashboard} error={errors.gcomDashboard && errors.gcomDashboard.message}>
<Field
label="Import via grafana.com"
invalid={!!errors.gcomDashboard}
error={errors.gcomDashboard && errors.gcomDashboard.message}
>
<Input
id="url-input"
placeholder="Grafana.com dashboard URL or ID"
type="text"
{...register('gcomDashboard', {
@ -112,17 +105,21 @@ class UnthemedDashboardImport extends PureComponent<Props> {
</Form>
</div>
<div className={styles.option}>
<Legend>Import via panel json</Legend>
<Form onSubmit={this.getDashboardFromJson} defaultValues={{ dashboardJson: '' }}>
{({ register, errors }) => (
<>
<Field invalid={!!errors.dashboardJson} error={errors.dashboardJson && errors.dashboardJson.message}>
<Field
label="Import via panel json"
invalid={!!errors.dashboardJson}
error={errors.dashboardJson && errors.dashboardJson.message}
>
<TextArea
{...register('dashboardJson', {
required: 'Need a dashboard JSON model',
validate: validateDashboardJson,
})}
data-testid={selectors.components.DashboardImportPage.textarea}
id="dashboard-json-textarea"
rows={10}
/>
</Field>

View File

@ -52,7 +52,7 @@ export const PlaylistForm: FC<PlaylistFormProps> = ({ onSubmit, playlist }) => {
<h3 className="page-headering">Add dashboards</h3>
<Field label="Add by title">
<DashboardPickerByID onChange={addById} isClearable />
<DashboardPickerByID onChange={addById} id="dashboard-picker" isClearable />
</Field>
<Field label="Add by tag">

View File

@ -94,7 +94,7 @@ export const PlaylistPage: FC<PlaylistPageProps> = ({ navModel }) => {
{playlistToDelete && (
<ConfirmModal
title={playlistToDelete.name}
confirmText="delete"
confirmText="Delete"
body={`Are you sure you want to delete '${playlistToDelete.name}' playlist?`}
onConfirm={onDeletePlaylist}
isOpen={Boolean(playlistToDelete)}

View File

@ -8,8 +8,8 @@ import { SearchSrv } from 'app/core/services/search_srv';
import { DashboardQuery, SearchLayout } from '../types';
export const layoutOptions = [
{ value: SearchLayout.Folders, icon: 'folder' },
{ value: SearchLayout.List, icon: 'list-ul' },
{ value: SearchLayout.Folders, icon: 'folder', ariaLabel: 'View by folders' },
{ value: SearchLayout.List, icon: 'list-ul', ariaLabel: 'View as list' },
];
const searchSrv = new SearchSrv();

View File

@ -6,14 +6,17 @@ interface Props {
onClick?: React.MouseEventHandler<HTMLInputElement>;
className?: string;
editable?: boolean;
'aria-label'?: string;
}
export const SearchCheckbox: FC<Props> = memo(({ onClick, className, checked = false, editable = false }) => {
return editable ? (
<div onClick={onClick} className={className}>
<Checkbox value={checked} />
</div>
) : null;
});
export const SearchCheckbox: FC<Props> = memo(
({ onClick, className, checked = false, editable = false, 'aria-label': ariaLabel }) => {
return editable ? (
<div onClick={onClick} className={className}>
<Checkbox value={checked} aria-label={ariaLabel} />
</div>
) : null;
}
);
SearchCheckbox.displayName = 'SearchCheckbox';

View File

@ -56,7 +56,12 @@ export const SearchItem: FC<Props> = ({ item, editable, onToggleChecked, onTagSe
className={styles.container}
>
<Card.Figure align={'center'} className={styles.checkbox}>
<SearchCheckbox editable={editable} checked={item.checked} onClick={handleCheckboxClick} />
<SearchCheckbox
aria-label="Select dashboard"
editable={editable}
checked={item.checked}
onClick={handleCheckboxClick}
/>
</Card.Figure>
<Card.Meta separator={''}>
<span className={styles.metaContainer}>

View File

@ -69,9 +69,9 @@ export const SearchResults: FC<Props> = memo(
// The wrapper div is needed as the inner SearchItem has margin-bottom spacing
// And without this wrapper there is no room for that margin
return (
<div style={style}>
<li style={style}>
<SearchItem key={item.id} {...itemProps} item={item} />
</div>
</li>
);
}}
</FixedSizeList>

View File

@ -42,7 +42,11 @@ export const SearchResultsFilter: FC<Props> = ({
return (
<div className={styles.wrapper}>
{editable && <Checkbox value={allChecked} onChange={onToggleAllChecked} />}
{editable && (
<div className={styles.checkboxWrapper}>
<Checkbox aria-label="Select all" value={allChecked} onChange={onToggleAllChecked} />
</div>
)}
{showActions ? (
<HorizontalGroup spacing="md">
<Button disabled={!canMove} onClick={moveTo} icon="exchange-alt" variant="secondary">
@ -73,9 +77,10 @@ const getStyles = stylesFactory((theme: GrafanaTheme) => {
const { sm, md } = theme.spacing;
return {
wrapper: css`
height: 35px;
height: ${theme.height.md}px;
display: flex;
justify-content: space-between;
justify-content: flex-start;
gap: ${theme.spacing.md};
align-items: center;
margin-bottom: ${sm};
@ -84,5 +89,11 @@ const getStyles = stylesFactory((theme: GrafanaTheme) => {
margin: 0 ${md} 0 ${sm};
}
`,
checkboxWrapper: css`
label {
line-height: 1.2;
width: max-content;
}
`,
};
});

View File

@ -53,6 +53,7 @@ export const SectionHeader: FC<SectionHeaderProps> = ({
editable={editable}
checked={section.checked}
onClick={handleCheckboxClick}
aria-label="Select folder"
/>
<div className={styles.icon}>