A11y: Fix various fastpass accessibility issues (#41154)

This commit is contained in:
kay delaney
2021-11-02 15:27:07 +00:00
committed by GitHub
parent 14ddb2939c
commit 3c5003373c
19 changed files with 104 additions and 31 deletions

View File

@@ -6,14 +6,16 @@ interface Props {
value: OrgRole;
disabled?: boolean;
'aria-label'?: string;
inputId?: string;
onChange: (role: OrgRole) => void;
}
const options = Object.keys(OrgRole).map((key) => ({ label: key, value: key }));
export const OrgRolePicker: FC<Props> = ({ value, onChange, 'aria-label': ariaLabel, ...restProps }) => (
export const OrgRolePicker: FC<Props> = ({ value, onChange, 'aria-label': ariaLabel, inputId, ...restProps }) => (
<Select
menuShouldPortal
inputId={inputId}
value={value}
options={options}
onChange={(val) => onChange(val.value as OrgRole)}

View File

@@ -45,15 +45,15 @@ const UserCreatePage: React.FC<UserCreatePageProps> = ({ navModel }) => {
invalid={!!errors.name}
error={errors.name ? 'Name is required' : undefined}
>
<Input {...register('name', { required: true })} />
<Input id="name-input" {...register('name', { required: true })} />
</Field>
<Field label="Email">
<Input {...register('email')} />
<Input id="email-input" {...register('email')} />
</Field>
<Field label="Username">
<Input {...register('login')} />
<Input id="username-input" {...register('login')} />
</Field>
<Field
label="Password"
@@ -62,6 +62,7 @@ const UserCreatePage: React.FC<UserCreatePageProps> = ({ navModel }) => {
error={errors.password ? 'Password is required and must contain at least 4 characters' : undefined}
>
<Input
id="password-input"
{...register('password', {
validate: (value) => value.trim() !== '' && value.length >= 4,
})}

View File

@@ -151,12 +151,15 @@ class UnThemedOrgRow extends PureComponent<OrgRowProps, OrgRowState> {
const canChangeRole = contextSrv.hasPermission(AccessControlAction.OrgUsersRoleUpdate);
const canRemoveFromOrg = contextSrv.hasPermission(AccessControlAction.OrgUsersRemove);
const inputId = `${org.name}-input`;
return (
<tr>
<td className={labelClass}>{org.name}</td>
<td className={labelClass}>
<label htmlFor={inputId}>{org.name}</label>
</td>
{isChangingRole ? (
<td>
<OrgRolePicker value={currentRole} onChange={this.onOrgRoleChange} />
<OrgRolePicker inputId={inputId} value={currentRole} onChange={this.onOrgRoleChange} />
</td>
) : (
<td className="width-25">{org.role}</td>
@@ -257,10 +260,10 @@ export class AddToOrgModal extends PureComponent<AddToOrgModalProps, AddToOrgMod
onDismiss={this.onCancel}
>
<Field label="Organization">
<OrgPicker onSelected={this.onOrgSelect} />
<OrgPicker inputId="new-org-input" onSelected={this.onOrgSelect} />
</Field>
<Field label="Role">
<OrgRolePicker value={role} onChange={this.onOrgRoleChange} />
<OrgRolePicker inputId="new-org-role-input" value={role} onChange={this.onOrgRoleChange} />
</Field>
<Modal.ButtonRow>
<HorizontalGroup spacing="md" justify="center">

View File

@@ -275,12 +275,16 @@ export class UserProfileRow extends PureComponent<UserProfileRowProps, UserProfi
return <LockedRow label={label} value={value} lockMessage={lockMessage} />;
}
const inputId = `${label}-input`;
return (
<tr>
<td className={labelClass}>{label}</td>
<td className={labelClass}>
<label htmlFor={inputId}>{label}</label>
</td>
<td className="width-25" colSpan={2}>
{this.state.editing ? (
<Input
id={inputId}
type={inputType}
defaultValue={value}
onBlur={this.onInputBlur}

View File

@@ -11,7 +11,7 @@ import {
} from 'app/plugins/datasource/alertmanager/types';
import { configureStore } from 'app/store/configureStore';
import { typeAsJestMock } from 'test/helpers/typeAsJestMock';
import { byRole, byTestId, byText } from 'testing-library-selector';
import { byLabelText, byRole, byTestId, byText } from 'testing-library-selector';
import AmRoutes from './AmRoutes';
import { fetchAlertManagerConfig, fetchStatus, updateAlertManagerConfig } from './api/alertmanager';
import { mockDataSource, MockDataSourceSrv, someCloudAlertManagerConfig, someCloudAlertManagerStatus } from './mocks';
@@ -76,8 +76,8 @@ const ui = {
editButton: byRole('button', { name: 'Edit' }),
saveButton: byRole('button', { name: 'Save' }),
editRouteButton: byTestId('edit-route'),
deleteRouteButton: byTestId('delete-route'),
editRouteButton: byLabelText('Edit route'),
deleteRouteButton: byLabelText('Delete route'),
newPolicyButton: byRole('button', { name: /New policy/ }),
receiverSelect: byTestId('am-receiver-select'),

View File

@@ -14,7 +14,12 @@ export const CollapseToggle: FC<Props> = ({ isCollapsed, onToggle, className, te
const styles = useStyles(getStyles);
return (
<button className={cx(styles.expandButton, className)} onClick={() => onToggle(!isCollapsed)} {...restOfProps}>
<button
aria-label={`${isCollapsed ? 'Expand' : 'Collapse'} alert group`}
className={cx(styles.expandButton, className)}
onClick={() => onToggle(!isCollapsed)}
{...restOfProps}
>
<Icon size={size} name={isCollapsed ? 'angle-right' : 'angle-down'} />
{text}
</button>

View File

@@ -96,6 +96,7 @@ export const DynamicTable = <T extends object>({
{isExpandable && (
<div className={cx(styles.cell, styles.expandCell)}>
<IconButton
aria-label={`${isItemExpanded ? 'Collapse' : 'Expand'} row`}
size="xl"
data-testid="collapse-toggle"
className={styles.expandButton}

View File

@@ -42,11 +42,12 @@ export const AmRootRouteForm: FC<AmRootRouteFormProps> = ({
<InputControl
render={({ field: { onChange, ref, ...field } }) => (
<Select
menuShouldPortal
aria-label="Default contact point"
{...field}
className={styles.input}
onChange={(value) => onChange(mapSelectValueToString(value))}
options={receivers}
menuShouldPortal
/>
)}
control={control}
@@ -72,6 +73,7 @@ export const AmRootRouteForm: FC<AmRootRouteFormProps> = ({
<InputControl
render={({ field: { onChange, ref, ...field } }) => (
<MultiSelect
aria-label="Group by"
menuShouldPortal
{...field}
allowCustomValue
@@ -129,6 +131,7 @@ export const AmRootRouteForm: FC<AmRootRouteFormProps> = ({
className={styles.input}
onChange={(value) => onChange(mapSelectValueToString(value))}
options={timeOptions}
aria-label="Group wait type"
/>
)}
control={control}
@@ -169,6 +172,7 @@ export const AmRootRouteForm: FC<AmRootRouteFormProps> = ({
className={styles.input}
onChange={(value) => onChange(mapSelectValueToString(value))}
options={timeOptions}
aria-label="Group interval type"
/>
)}
control={control}
@@ -205,6 +209,7 @@ export const AmRootRouteForm: FC<AmRootRouteFormProps> = ({
menuPlacement="top"
onChange={(value) => onChange(mapSelectValueToString(value))}
options={timeOptions}
aria-label="Repeat interval type"
/>
)}
control={control}

View File

@@ -79,6 +79,7 @@ export const AmRoutesExpandedForm: FC<AmRoutesExpandedFormProps> = ({ onCancel,
className={styles.matchersOperator}
onChange={(value) => onChange(value?.value)}
options={matcherFieldOptions}
aria-label="Operator"
/>
)}
defaultValue={field.operator}
@@ -127,11 +128,12 @@ export const AmRoutesExpandedForm: FC<AmRoutesExpandedFormProps> = ({ onCancel,
<InputControl
render={({ field: { onChange, ref, ...field } }) => (
<Select
menuShouldPortal
aria-label="Contact point"
{...field}
className={formStyles.input}
onChange={(value) => onChange(mapSelectValueToString(value))}
options={receivers}
menuShouldPortal
/>
)}
control={control}
@@ -139,10 +141,11 @@ export const AmRoutesExpandedForm: FC<AmRoutesExpandedFormProps> = ({ onCancel,
/>
</Field>
<Field label="Continue matching subsequent sibling nodes">
<Switch {...register('continue')} />
<Switch id="continue-toggle" {...register('continue')} />
</Field>
<Field label="Override grouping">
<Switch
id="override-grouping-toggle"
value={overrideGrouping}
onChange={() => setOverrideGrouping((overrideGrouping) => !overrideGrouping)}
/>
@@ -152,6 +155,7 @@ export const AmRoutesExpandedForm: FC<AmRoutesExpandedFormProps> = ({ onCancel,
<InputControl
render={({ field: { onChange, ref, ...field } }) => (
<MultiSelect
aria-label="Group by"
menuShouldPortal
{...field}
allowCustomValue
@@ -173,6 +177,7 @@ export const AmRoutesExpandedForm: FC<AmRoutesExpandedFormProps> = ({ onCancel,
)}
<Field label="Override general timings">
<Switch
id="override-timings-toggle"
value={overrideTimings}
onChange={() => setOverrideTimings((overrideTimings) => !overrideTimings)}
/>
@@ -189,7 +194,13 @@ export const AmRoutesExpandedForm: FC<AmRoutesExpandedFormProps> = ({ onCancel,
<div className={cx(formStyles.container, formStyles.timingContainer)}>
<InputControl
render={({ field, fieldState: { invalid } }) => (
<Input {...field} className={formStyles.smallInput} invalid={invalid} placeholder="Time" />
<Input
{...field}
className={formStyles.smallInput}
invalid={invalid}
placeholder="Time"
aria-label="Group wait value"
/>
)}
control={control}
name="groupWaitValue"
@@ -205,6 +216,7 @@ export const AmRoutesExpandedForm: FC<AmRoutesExpandedFormProps> = ({ onCancel,
className={formStyles.input}
onChange={(value) => onChange(mapSelectValueToString(value))}
options={timeOptions}
aria-label="Group wait type"
/>
)}
control={control}
@@ -223,7 +235,13 @@ export const AmRoutesExpandedForm: FC<AmRoutesExpandedFormProps> = ({ onCancel,
<div className={cx(formStyles.container, formStyles.timingContainer)}>
<InputControl
render={({ field, fieldState: { invalid } }) => (
<Input {...field} className={formStyles.smallInput} invalid={invalid} placeholder="Time" />
<Input
{...field}
className={formStyles.smallInput}
invalid={invalid}
placeholder="Time"
aria-label="Group interval value"
/>
)}
control={control}
name="groupIntervalValue"
@@ -239,6 +257,7 @@ export const AmRoutesExpandedForm: FC<AmRoutesExpandedFormProps> = ({ onCancel,
className={formStyles.input}
onChange={(value) => onChange(mapSelectValueToString(value))}
options={timeOptions}
aria-label="Group interval type"
/>
)}
control={control}
@@ -257,7 +276,13 @@ export const AmRoutesExpandedForm: FC<AmRoutesExpandedFormProps> = ({ onCancel,
<div className={cx(formStyles.container, formStyles.timingContainer)}>
<InputControl
render={({ field, fieldState: { invalid } }) => (
<Input {...field} className={formStyles.smallInput} invalid={invalid} placeholder="Time" />
<Input
{...field}
className={formStyles.smallInput}
invalid={invalid}
placeholder="Time"
aria-label="Repeat interval value"
/>
)}
control={control}
name="repeatIntervalValue"
@@ -274,6 +299,7 @@ export const AmRoutesExpandedForm: FC<AmRoutesExpandedFormProps> = ({ onCancel,
menuPlacement="top"
onChange={(value) => onChange(mapSelectValueToString(value))}
options={timeOptions}
aria-label="Repeat interval type"
/>
)}
control={control}

View File

@@ -76,7 +76,7 @@ export const AmRoutesTable: FC<AmRoutesTableProps> = ({
return (
<HorizontalGroup>
<Button
data-testid="edit-route"
aria-label="Edit route"
icon="pen"
onClick={expandWithCustomContent}
size="sm"
@@ -86,7 +86,7 @@ export const AmRoutesTable: FC<AmRoutesTableProps> = ({
Edit
</Button>
<IconButton
data-testid="delete-route"
aria-label="Delete route"
name="trash-alt"
onClick={() => {
const newRoutes = [...routes];

View File

@@ -72,17 +72,19 @@ export function ChannelSubForm<R extends ChannelValues>({
const mandatoryOptions = notifier?.options.filter((o) => o.required);
const optionalOptions = notifier?.options.filter((o) => !o.required);
const contactPointTypeInputId = `contact-point-type-${pathPrefix}`;
return (
<div className={styles.wrapper} data-testid="item-container">
<div className={styles.topRow}>
<div>
<Field label="Contact point type" data-testid={`${pathPrefix}type`}>
<Field label="Contact point type" htmlFor={contactPointTypeInputId} data-testid={`${pathPrefix}type`}>
<InputControl
name={name('type')}
defaultValue={defaultValues.type}
render={({ field: { ref, onChange, ...field } }) => (
<Select
disabled={readOnly}
inputId={contactPointTypeInputId}
menuShouldPortal
{...field}
width={37}

View File

@@ -89,6 +89,7 @@ export const AlertTypeStep: FC<Props> = ({ editingExistingRule }) => {
render={({ field: { onChange, ref, ...field } }) => (
<Select
menuShouldPortal
aria-label="Rule type"
{...field}
options={alertTypeOptions}
onChange={(v: SelectableValue) => onChange(v?.value)}

View File

@@ -10,9 +10,10 @@ interface Props {
value?: string;
width?: number;
className?: string;
'aria-label'?: string;
}
export const AnnotationKeyInput: FC<Props> = ({ value, existingKeys, ...rest }) => {
export const AnnotationKeyInput: FC<Props> = ({ value, existingKeys, 'aria-label': ariaLabel, ...rest }) => {
const annotationOptions = useMemo(
(): SelectableValue[] =>
Object.values(Annotation)
@@ -23,6 +24,7 @@ export const AnnotationKeyInput: FC<Props> = ({ value, existingKeys, ...rest })
return (
<SelectWithAdd
aria-label={ariaLabel}
value={value}
options={annotationOptions}
custom={!!value && !(Object.values(Annotation) as string[]).includes(value)}

View File

@@ -42,7 +42,12 @@ const AnnotationsField: FC = () => {
<InputControl
name={`annotations[${index}].key`}
render={({ field: { ref, ...field } }) => (
<AnnotationKeyInput {...field} existingKeys={existingKeys(index)} width={18} />
<AnnotationKeyInput
{...field}
aria-label={`Annotation detail ${index + 1}`}
existingKeys={existingKeys(index)}
width={18}
/>
)}
control={control}
rules={{ required: { value: !!annotations[index]?.value, message: 'Required.' } }}

View File

@@ -48,6 +48,7 @@ export const ConditionField: FC = () => {
render={({ field: { onChange, ref, ...field } }) => (
<Select
menuShouldPortal
aria-label="Condition"
{...field}
width={42}
options={options}

View File

@@ -51,12 +51,19 @@ export const GrafanaConditionsStep: FC = () => {
formState: { errors },
} = useFormContext<RuleFormValues>();
const evaluateEveryId = 'eval-every-input';
const evaluateForId = 'eval-for-input';
return (
<RuleEditorSection stepNo={3} title="Define alert conditions">
<ConditionField />
<Field label="Evaluate">
<div className={styles.flexRow}>
<InlineLabel width={16} tooltip="How often the alert will be evaluated to see if it fires">
<InlineLabel
htmlFor={evaluateEveryId}
width={16}
tooltip="How often the alert will be evaluated to see if it fires"
>
Evaluate every
</InlineLabel>
<Field
@@ -65,9 +72,10 @@ export const GrafanaConditionsStep: FC = () => {
invalid={!!errors.evaluateEvery?.message}
validationMessageHorizontalOverflow={true}
>
<Input width={8} {...register('evaluateEvery', evaluateEveryValidationOptions)} />
<Input id={evaluateEveryId} width={8} {...register('evaluateEvery', evaluateEveryValidationOptions)} />
</Field>
<InlineLabel
htmlFor={evaluateForId}
width={7}
tooltip='Once condition is breached, alert will go into pending state. If it is pending for longer than the "for" value, it will become a firing alert.'
>
@@ -79,7 +87,7 @@ export const GrafanaConditionsStep: FC = () => {
invalid={!!errors.evaluateFor?.message}
validationMessageHorizontalOverflow={true}
>
<Input width={8} {...register('evaluateFor', forValidationOptions)} />
<Input id={evaluateForId} width={8} {...register('evaluateFor', forValidationOptions)} />
</Field>
</div>
</Field>
@@ -92,11 +100,12 @@ export const GrafanaConditionsStep: FC = () => {
/>
{showErrorHandling && (
<>
<Field label="Alert state if no data or all values are null">
<Field htmlFor="no-data-state-input" label="Alert state if no data or all values are null">
<InputControl
render={({ field: { onChange, ref, ...field } }) => (
<GrafanaAlertStatePicker
{...field}
inputId="no-data-state-input"
width={42}
includeNoData={true}
onChange={(value) => onChange(value?.value)}
@@ -105,11 +114,12 @@ export const GrafanaConditionsStep: FC = () => {
name="noDataState"
/>
</Field>
<Field label="Alert state if execution error or timeout">
<Field htmlFor="exec-err-state-input" label="Alert state if execution error or timeout">
<InputControl
render={({ field: { onChange, ref, ...field } }) => (
<GrafanaAlertStatePicker
{...field}
inputId="exec-err-state-input"
width={42}
includeNoData={false}
onChange={(value) => onChange(value?.value)}

View File

@@ -13,6 +13,7 @@ interface Props {
onCustomChange?: (custom: boolean) => void;
width?: number;
disabled?: boolean;
'aria-label'?: string;
}
export const SelectWithAdd: FC<Props> = ({
@@ -26,6 +27,7 @@ export const SelectWithAdd: FC<Props> = ({
onCustomChange,
disabled = false,
addLabel = '+ Add new',
'aria-label': ariaLabel,
}) => {
const [isCustom, setIsCustom] = useState(custom);
@@ -43,6 +45,7 @@ export const SelectWithAdd: FC<Props> = ({
if (isCustom) {
return (
<Input
aria-label={ariaLabel}
width={width}
autoFocus={!custom}
value={value || ''}
@@ -56,6 +59,7 @@ export const SelectWithAdd: FC<Props> = ({
return (
<Select
menuShouldPortal
aria-label={ariaLabel}
width={width}
options={_options}
value={value}

View File

@@ -29,12 +29,13 @@ export const ActionIcon: FC<Props> = ({
}) => {
const iconEl = <Icon className={cx(useStyles(getStyle), className)} onClick={onClick} name={icon} {...rest} />;
const ariaLabel = typeof tooltip === 'string' ? tooltip : undefined;
return (
<Tooltip content={tooltip} placement={tooltipPlacement}>
{(() => {
if (to) {
return (
<Link to={to} target={target}>
<Link aria-label={ariaLabel} to={to} target={target}>
{iconEl}
</Link>
);

View File

@@ -52,7 +52,7 @@ export function RuleDetailsDataSources(props: Props): JSX.Element | null {
<div key={name}>
{icon && (
<>
<img className={styles.dataSourceIcon} src={icon} />{' '}
<img alt={`${name} datasource logo`} className={styles.dataSourceIcon} src={icon} />{' '}
</>
)}
{name}