Azure Monitor: Fastpass Fixes -- Add trap focus for modals in grafana/ui and other small a11y fixes for Azure Monitor. (#41449) (#42248)

(cherry picked from commit fc8d93e231)

Co-authored-by: Sarah Zinger <sarahzinger@users.noreply.github.com>
This commit is contained in:
Grot (@grafanabot) 2021-11-24 12:34:24 -05:00 committed by GitHub
parent 527de60b71
commit 97397fb8f9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 43 additions and 676 deletions

View File

@ -7,6 +7,7 @@ import { getModalStyles } from './getModalStyles';
import { ModalHeader } from './ModalHeader'; import { ModalHeader } from './ModalHeader';
import { IconButton } from '../IconButton/IconButton'; import { IconButton } from '../IconButton/IconButton';
import { HorizontalGroup } from '../Layout/Layout'; import { HorizontalGroup } from '../Layout/Layout';
import { FocusScope } from '@react-aria/focus';
export interface Props { export interface Props {
/** @deprecated no longer used */ /** @deprecated no longer used */
@ -75,16 +76,18 @@ export function Modal(props: PropsWithChildren<Props>) {
className={styles.modalBackdrop} className={styles.modalBackdrop}
onClick={onClickBackdrop || (closeOnBackdropClick ? onDismiss : undefined)} onClick={onClickBackdrop || (closeOnBackdropClick ? onDismiss : undefined)}
/> />
<div className={cx(styles.modal, className)}> <FocusScope contain autoFocus restoreFocus>
<div className={headerClass}> <div className={cx(styles.modal, className)}>
{typeof title === 'string' && <DefaultModalHeader {...props} title={title} />} <div className={headerClass}>
{typeof title !== 'string' && title} {typeof title === 'string' && <DefaultModalHeader {...props} title={title} />}
<div className={styles.modalHeaderClose}> {typeof title !== 'string' && title}
<IconButton aria-label="Close dialogue" surface="header" name="times" size="xl" onClick={onDismiss} /> <div className={styles.modalHeaderClose}>
<IconButton aria-label="Close dialogue" surface="header" name="times" size="xl" onClick={onDismiss} />
</div>
</div> </div>
<div className={cx(styles.modalContent, contentClassName)}>{children}</div>
</div> </div>
<div className={cx(styles.modalContent, contentClassName)}>{children}</div> </FocusScope>
</div>
</Portal> </Portal>
); );
} }

View File

@ -1,8 +1,6 @@
import React from 'react'; import React from 'react';
import { shallow } from 'enzyme'; import { render, screen, waitFor } from '@testing-library/react';
import AzureCredentialsForm, { Props } from './AzureCredentialsForm'; import AzureCredentialsForm, { Props } from './AzureCredentialsForm';
import { LegacyForms, Button } from '@grafana/ui';
const { Input, Select } = LegacyForms;
const setup = (propsFunc?: (props: Props) => Props) => { const setup = (propsFunc?: (props: Props) => Props) => {
let props: Props = { let props: Props = {
@ -22,24 +20,25 @@ const setup = (propsFunc?: (props: Props) => Props) => {
{ value: 'chinaazuremonitor', label: 'Azure China' }, { value: 'chinaazuremonitor', label: 'Azure China' },
], ],
onCredentialsChange: jest.fn(), onCredentialsChange: jest.fn(),
getSubscriptions: jest.fn(), getSubscriptions: jest.fn(() => Promise.resolve([])),
}; };
if (propsFunc) { if (propsFunc) {
props = propsFunc(props); props = propsFunc(props);
} }
return shallow(<AzureCredentialsForm {...props} />); return render(<AzureCredentialsForm {...props} />);
}; };
describe('Render', () => { describe('Render', () => {
it('should render component', () => { it('should render component', () => {
const wrapper = setup(); setup();
expect(wrapper).toMatchSnapshot();
expect(screen.getByText('Azure Cloud')).toBeInTheDocument();
}); });
it('should disable azure monitor secret input', () => { it('should disable azure monitor secret input', async () => {
const wrapper = setup((props) => ({ setup((props) => ({
...props, ...props,
credentials: { credentials: {
authType: 'clientsecret', authType: 'clientsecret',
@ -49,11 +48,12 @@ describe('Render', () => {
clientSecret: Symbol(), clientSecret: Symbol(),
}, },
})); }));
expect(wrapper).toMatchSnapshot(); await waitFor(() => screen.getByTestId('client-secret'));
expect(screen.getByTestId('client-secret')).toBeDisabled();
}); });
it('should enable azure monitor load subscriptions button', () => { it('should enable azure monitor load subscriptions button', async () => {
const wrapper = setup((props) => ({ setup((props) => ({
...props, ...props,
credentials: { credentials: {
authType: 'clientsecret', authType: 'clientsecret',
@ -63,46 +63,34 @@ describe('Render', () => {
clientSecret: 'e7f3f661-a933-4b3f-8176-51c4f982ec48', clientSecret: 'e7f3f661-a933-4b3f-8176-51c4f982ec48',
}, },
})); }));
expect(wrapper).toMatchSnapshot(); await waitFor(() => expect(screen.getByText('Load Subscriptions')).toBeInTheDocument());
}); });
describe('when disabled', () => { describe('when disabled', () => {
it('should disable inputs', () => { it('should disable inputs', async () => {
const wrapper = setup((props) => ({ setup((props) => ({
...props, ...props,
disabled: true, disabled: true,
})); }));
const inputs = wrapper.find(Input);
expect(inputs.length).toBeGreaterThan(1); await waitFor(() => screen.getByLabelText('Azure Cloud'));
inputs.forEach((input) => { expect(screen.getByLabelText('Azure Cloud')).toBeDisabled();
expect(input.prop('disabled')).toBe(true);
});
}); });
it('should remove buttons', () => { it('should remove buttons', async () => {
const wrapper = setup((props) => ({ setup((props) => ({
...props, ...props,
disabled: true, disabled: true,
})); }));
expect(wrapper.find(Button).exists()).toBe(false); await waitFor(() => expect(screen.queryByText('Load Subscriptions')).not.toBeInTheDocument());
}); });
it('should disable cloud selector', () => { it('should render children components', () => {
const wrapper = setup((props) => ({ setup((props) => ({
...props,
disabled: true,
}));
const selects = wrapper.find(Select);
selects.forEach((s) => expect(s.prop('isDisabled')).toBe(true));
});
it('should render a children component', () => {
const wrapper = setup((props) => ({
...props, ...props,
children: <button>click me</button>, children: <button>click me</button>,
})); }));
const button = wrapper.find('button'); expect(screen.getByText('click me')).toBeInTheDocument();
expect(button.text()).toBe('click me');
}); });
}); });
}); });

View File

@ -1,9 +1,9 @@
import React, { ChangeEvent, FunctionComponent, useEffect, useReducer, useState } from 'react'; import React, { ChangeEvent, FunctionComponent, useEffect, useReducer, useState } from 'react';
import { SelectableValue } from '@grafana/data'; import { SelectableValue } from '@grafana/data';
import { InlineFormLabel, LegacyForms, Button } from '@grafana/ui'; import { InlineFormLabel, LegacyForms, Button, Select } from '@grafana/ui';
import { AzureAuthType, AzureCredentials } from '../types'; import { AzureAuthType, AzureCredentials } from '../types';
import { isCredentialsComplete } from '../credentials'; import { isCredentialsComplete } from '../credentials';
const { Select, Input } = LegacyForms; const { Input } = LegacyForms;
export interface Props { export interface Props {
managedIdentityEnabled: boolean; managedIdentityEnabled: boolean;
@ -162,7 +162,7 @@ export const AzureCredentialsForm: FunctionComponent<Props> = (props: Props) =>
value={authTypeOptions.find((opt) => opt.value === credentials.authType)} value={authTypeOptions.find((opt) => opt.value === credentials.authType)}
options={authTypeOptions} options={authTypeOptions}
onChange={onAuthTypeChange} onChange={onAuthTypeChange}
isDisabled={disabled} disabled={disabled}
/> />
</div> </div>
</div> </div>
@ -176,12 +176,13 @@ export const AzureCredentialsForm: FunctionComponent<Props> = (props: Props) =>
Azure Cloud Azure Cloud
</InlineFormLabel> </InlineFormLabel>
<Select <Select
aria-label="Azure Cloud"
menuShouldPortal menuShouldPortal
className="width-15" className="width-15"
value={azureCloudOptions.find((opt) => opt.value === credentials.azureCloud)} value={azureCloudOptions.find((opt) => opt.value === credentials.azureCloud)}
options={azureCloudOptions} options={azureCloudOptions}
onChange={onAzureCloudChange} onChange={onAzureCloudChange}
isDisabled={disabled} disabled={disabled}
/> />
</div> </div>
</div> </div>
@ -219,7 +220,7 @@ export const AzureCredentialsForm: FunctionComponent<Props> = (props: Props) =>
<div className="gf-form-inline"> <div className="gf-form-inline">
<div className="gf-form"> <div className="gf-form">
<InlineFormLabel className="width-12">Client Secret</InlineFormLabel> <InlineFormLabel className="width-12">Client Secret</InlineFormLabel>
<Input className="width-25" placeholder="configured" disabled={true} /> <Input data-testid="client-secret" className="width-25" placeholder="configured" disabled={true} />
</div> </div>
<div className="gf-form"> <div className="gf-form">
<div className="max-width-30 gf-form-inline"> <div className="max-width-30 gf-form-inline">
@ -254,6 +255,7 @@ export const AzureCredentialsForm: FunctionComponent<Props> = (props: Props) =>
<InlineFormLabel className="width-12">Default Subscription</InlineFormLabel> <InlineFormLabel className="width-12">Default Subscription</InlineFormLabel>
<div className="width-30"> <div className="width-30">
<Select <Select
aria-label="Default Subscription"
menuShouldPortal menuShouldPortal
value={ value={
credentials.defaultSubscriptionId credentials.defaultSubscriptionId
@ -262,7 +264,7 @@ export const AzureCredentialsForm: FunctionComponent<Props> = (props: Props) =>
} }
options={subscriptions} options={subscriptions}
onChange={onSubscriptionChange} onChange={onSubscriptionChange}
isDisabled={disabled} disabled={disabled}
/> />
</div> </div>
</div> </div>

View File

@ -1,626 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Render should disable azure monitor secret input 1`] = `
<div
className="gf-form-group"
>
<div
className="gf-form-inline"
>
<div
className="gf-form"
>
<FormLabel
className="width-12"
tooltip="Choose an Azure Cloud"
>
Azure Cloud
</FormLabel>
<Select
allowCustomValue={false}
autoFocus={false}
backspaceRemovesValue={true}
className="width-15"
components={
Object {
"Group": [Function],
"IndicatorsContainer": [Function],
"MenuList": [Function],
"Option": [Function],
"SingleValue": [Function],
}
}
isClearable={false}
isDisabled={false}
isLoading={false}
isMulti={false}
isSearchable={true}
maxMenuHeight={300}
menuShouldPortal={true}
onChange={[Function]}
openMenuOnFocus={false}
options={
Array [
Object {
"label": "Azure",
"value": "azuremonitor",
},
Object {
"label": "Azure US Government",
"value": "govazuremonitor",
},
Object {
"label": "Azure Germany",
"value": "germanyazuremonitor",
},
Object {
"label": "Azure China",
"value": "chinaazuremonitor",
},
]
}
tabSelectsValue={true}
value={
Object {
"label": "Azure",
"value": "azuremonitor",
}
}
/>
</div>
</div>
<div
className="gf-form-inline"
>
<div
className="gf-form"
>
<FormLabel
className="width-12"
>
Directory (tenant) ID
</FormLabel>
<div
className="width-15"
>
<Input
className="width-30"
onChange={[Function]}
placeholder="XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX"
value="e7f3f661-a933-3h3f-0294-31c4f962ec48"
/>
</div>
</div>
</div>
<div
className="gf-form-inline"
>
<div
className="gf-form"
>
<FormLabel
className="width-12"
>
Application (client) ID
</FormLabel>
<div
className="width-15"
>
<Input
className="width-30"
onChange={[Function]}
placeholder="XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX"
value="34509fad-c0r9-45df-9e25-f1ee34af6900"
/>
</div>
</div>
</div>
<div
className="gf-form-inline"
>
<div
className="gf-form"
>
<FormLabel
className="width-12"
>
Client Secret
</FormLabel>
<Input
className="width-25"
disabled={true}
placeholder="configured"
/>
</div>
<div
className="gf-form"
>
<div
className="max-width-30 gf-form-inline"
>
<Button
onClick={[Function]}
type="button"
variant="secondary"
>
reset
</Button>
</div>
</div>
</div>
<div
className="gf-form-inline"
>
<div
className="gf-form"
>
<FormLabel
className="width-12"
>
Default Subscription
</FormLabel>
<div
className="width-30"
>
<Select
allowCustomValue={false}
autoFocus={false}
backspaceRemovesValue={true}
className=""
components={
Object {
"Group": [Function],
"IndicatorsContainer": [Function],
"MenuList": [Function],
"Option": [Function],
"SingleValue": [Function],
}
}
isClearable={false}
isDisabled={false}
isLoading={false}
isMulti={false}
isSearchable={true}
maxMenuHeight={300}
menuShouldPortal={true}
onChange={[Function]}
openMenuOnFocus={false}
options={Array []}
tabSelectsValue={true}
/>
</div>
</div>
</div>
<div
className="gf-form-inline"
>
<div
className="gf-form"
>
<div
className="max-width-30 gf-form-inline"
>
<Button
disabled={false}
onClick={[Function]}
size="sm"
type="button"
variant="secondary"
>
Load Subscriptions
</Button>
</div>
</div>
</div>
</div>
`;
exports[`Render should enable azure monitor load subscriptions button 1`] = `
<div
className="gf-form-group"
>
<div
className="gf-form-inline"
>
<div
className="gf-form"
>
<FormLabel
className="width-12"
tooltip="Choose an Azure Cloud"
>
Azure Cloud
</FormLabel>
<Select
allowCustomValue={false}
autoFocus={false}
backspaceRemovesValue={true}
className="width-15"
components={
Object {
"Group": [Function],
"IndicatorsContainer": [Function],
"MenuList": [Function],
"Option": [Function],
"SingleValue": [Function],
}
}
isClearable={false}
isDisabled={false}
isLoading={false}
isMulti={false}
isSearchable={true}
maxMenuHeight={300}
menuShouldPortal={true}
onChange={[Function]}
openMenuOnFocus={false}
options={
Array [
Object {
"label": "Azure",
"value": "azuremonitor",
},
Object {
"label": "Azure US Government",
"value": "govazuremonitor",
},
Object {
"label": "Azure Germany",
"value": "germanyazuremonitor",
},
Object {
"label": "Azure China",
"value": "chinaazuremonitor",
},
]
}
tabSelectsValue={true}
value={
Object {
"label": "Azure",
"value": "azuremonitor",
}
}
/>
</div>
</div>
<div
className="gf-form-inline"
>
<div
className="gf-form"
>
<FormLabel
className="width-12"
>
Directory (tenant) ID
</FormLabel>
<div
className="width-15"
>
<Input
className="width-30"
onChange={[Function]}
placeholder="XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX"
value="e7f3f661-a933-3h3f-0294-31c4f962ec48"
/>
</div>
</div>
</div>
<div
className="gf-form-inline"
>
<div
className="gf-form"
>
<FormLabel
className="width-12"
>
Application (client) ID
</FormLabel>
<div
className="width-15"
>
<Input
className="width-30"
onChange={[Function]}
placeholder="XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX"
value="34509fad-c0r9-45df-9e25-f1ee34af6900"
/>
</div>
</div>
</div>
<div
className="gf-form-inline"
>
<div
className="gf-form"
>
<FormLabel
className="width-12"
>
Client Secret
</FormLabel>
<div
className="width-15"
>
<Input
className="width-30"
onChange={[Function]}
placeholder="XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX"
value="e7f3f661-a933-4b3f-8176-51c4f982ec48"
/>
</div>
</div>
</div>
<div
className="gf-form-inline"
>
<div
className="gf-form"
>
<FormLabel
className="width-12"
>
Default Subscription
</FormLabel>
<div
className="width-30"
>
<Select
allowCustomValue={false}
autoFocus={false}
backspaceRemovesValue={true}
className=""
components={
Object {
"Group": [Function],
"IndicatorsContainer": [Function],
"MenuList": [Function],
"Option": [Function],
"SingleValue": [Function],
}
}
isClearable={false}
isDisabled={false}
isLoading={false}
isMulti={false}
isSearchable={true}
maxMenuHeight={300}
menuShouldPortal={true}
onChange={[Function]}
openMenuOnFocus={false}
options={Array []}
tabSelectsValue={true}
/>
</div>
</div>
</div>
<div
className="gf-form-inline"
>
<div
className="gf-form"
>
<div
className="max-width-30 gf-form-inline"
>
<Button
disabled={false}
onClick={[Function]}
size="sm"
type="button"
variant="secondary"
>
Load Subscriptions
</Button>
</div>
</div>
</div>
</div>
`;
exports[`Render should render component 1`] = `
<div
className="gf-form-group"
>
<div
className="gf-form-inline"
>
<div
className="gf-form"
>
<FormLabel
className="width-12"
tooltip="Choose an Azure Cloud"
>
Azure Cloud
</FormLabel>
<Select
allowCustomValue={false}
autoFocus={false}
backspaceRemovesValue={true}
className="width-15"
components={
Object {
"Group": [Function],
"IndicatorsContainer": [Function],
"MenuList": [Function],
"Option": [Function],
"SingleValue": [Function],
}
}
isClearable={false}
isDisabled={false}
isLoading={false}
isMulti={false}
isSearchable={true}
maxMenuHeight={300}
menuShouldPortal={true}
onChange={[Function]}
openMenuOnFocus={false}
options={
Array [
Object {
"label": "Azure",
"value": "azuremonitor",
},
Object {
"label": "Azure US Government",
"value": "govazuremonitor",
},
Object {
"label": "Azure Germany",
"value": "germanyazuremonitor",
},
Object {
"label": "Azure China",
"value": "chinaazuremonitor",
},
]
}
tabSelectsValue={true}
value={
Object {
"label": "Azure",
"value": "azuremonitor",
}
}
/>
</div>
</div>
<div
className="gf-form-inline"
>
<div
className="gf-form"
>
<FormLabel
className="width-12"
>
Directory (tenant) ID
</FormLabel>
<div
className="width-15"
>
<Input
className="width-30"
onChange={[Function]}
placeholder="XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX"
value="e7f3f661-a933-3h3f-0294-31c4f962ec48"
/>
</div>
</div>
</div>
<div
className="gf-form-inline"
>
<div
className="gf-form"
>
<FormLabel
className="width-12"
>
Application (client) ID
</FormLabel>
<div
className="width-15"
>
<Input
className="width-30"
onChange={[Function]}
placeholder="XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX"
value="34509fad-c0r9-45df-9e25-f1ee34af6900"
/>
</div>
</div>
</div>
<div
className="gf-form-inline"
>
<div
className="gf-form"
>
<FormLabel
className="width-12"
>
Client Secret
</FormLabel>
<div
className="width-15"
>
<Input
className="width-30"
onChange={[Function]}
placeholder="XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX"
value=""
/>
</div>
</div>
</div>
<div
className="gf-form-inline"
>
<div
className="gf-form"
>
<FormLabel
className="width-12"
>
Default Subscription
</FormLabel>
<div
className="width-30"
>
<Select
allowCustomValue={false}
autoFocus={false}
backspaceRemovesValue={true}
className=""
components={
Object {
"Group": [Function],
"IndicatorsContainer": [Function],
"MenuList": [Function],
"Option": [Function],
"SingleValue": [Function],
}
}
isClearable={false}
isDisabled={false}
isLoading={false}
isMulti={false}
isSearchable={true}
maxMenuHeight={300}
menuShouldPortal={true}
onChange={[Function]}
openMenuOnFocus={false}
options={Array []}
tabSelectsValue={true}
/>
</div>
</div>
</div>
<div
className="gf-form-inline"
>
<div
className="gf-form"
>
<div
className="max-width-30 gf-form-inline"
>
<Button
disabled={true}
onClick={[Function]}
size="sm"
type="button"
variant="secondary"
>
Load Subscriptions
</Button>
</div>
</div>
</div>
</div>
`;