mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
A11y: Fix fastpass issues for dashboard management pages (#39940)
* A11y: Fix fastpass issues for dashboard pages See #39429
This commit is contained in:
parent
3131388084
commit
1ce750471f
@ -3,6 +3,7 @@
|
||||
*/
|
||||
export interface SelectableValue<T = any> {
|
||||
label?: string;
|
||||
ariaLabel?: string;
|
||||
value?: T;
|
||||
imgUrl?: string;
|
||||
icon?: string;
|
||||
|
@ -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}
|
||||
|
@ -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}
|
||||
|
@ -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} />}
|
||||
/>
|
||||
|
@ -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}
|
||||
|
@ -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>
|
||||
|
@ -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),
|
||||
|
@ -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)};
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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">
|
||||
|
@ -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)}
|
||||
|
@ -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();
|
||||
|
@ -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';
|
||||
|
@ -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}>
|
||||
|
@ -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>
|
||||
|
@ -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;
|
||||
}
|
||||
`,
|
||||
};
|
||||
});
|
||||
|
@ -53,6 +53,7 @@ export const SectionHeader: FC<SectionHeaderProps> = ({
|
||||
editable={editable}
|
||||
checked={section.checked}
|
||||
onClick={handleCheckboxClick}
|
||||
aria-label="Select folder"
|
||||
/>
|
||||
|
||||
<div className={styles.icon}>
|
||||
|
Loading…
Reference in New Issue
Block a user