[Dashboard] Modal a11y (#93332)

* Fix a11y on dashboard confirm modals by adding EuiFocusTrap & EuiOutsideClickDetector
This commit is contained in:
Devon Thomson 2021-03-08 14:19:18 -05:00 committed by GitHub
parent 8a1d7f5b99
commit 08f3c6cba7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 280 additions and 212 deletions

View file

@ -19,6 +19,8 @@ import {
EuiRadio,
EuiSpacer,
EuiText,
EuiFocusTrap,
EuiOutsideClickDetector,
} from '@elastic/eui';
import { DashboardCopyToCapabilities } from './copy_to_dashboard_action';
import { DashboardPicker } from '../../services/presentation_util';
@ -66,74 +68,94 @@ export function CopyToDashboardModal({
});
}, [dashboardOption, embeddable, selectedDashboard, stateTransfer, closeModal]);
const titleId = 'copyToDashboardTitle';
const descriptionId = 'copyToDashboardDescription';
return (
<PresentationUtilContext>
<EuiModalHeader>
<EuiModalHeaderTitle>{dashboardCopyToDashboardAction.getDisplayName()}</EuiModalHeaderTitle>
</EuiModalHeader>
<EuiModalBody>
<>
<EuiText>
<p>{dashboardCopyToDashboardAction.getDescription()}</p>
</EuiText>
<EuiSpacer />
<EuiFormRow hasChildLabel={false}>
<EuiPanel color="subdued" hasShadow={false} data-test-subj="add-to-dashboard-options">
<div>
{capabilities.canEditExisting && (
<>
<EuiRadio
checked={dashboardOption === 'existing'}
data-test-subj="add-to-existing-dashboard-option"
id="existing-dashboard-option"
name="dashboard-option"
label={dashboardCopyToDashboardAction.getExistingDashboardOption()}
onChange={() => setDashboardOption('existing')}
/>
<div className="savAddDashboard__searchDashboards">
<DashboardPicker
isDisabled={dashboardOption !== 'existing'}
idsToOmit={dashboardId ? [dashboardId] : undefined}
onChange={(dashboard) => setSelectedDashboard(dashboard)}
/>
</div>
<EuiSpacer size="s" />
</>
)}
{capabilities.canCreateNew && (
<>
<EuiRadio
checked={dashboardOption === 'new'}
data-test-subj="add-to-new-dashboard-option"
id="new-dashboard-option"
name="dashboard-option"
disabled={!dashboardId}
label={dashboardCopyToDashboardAction.getNewDashboardOption()}
onChange={() => setDashboardOption('new')}
/>
<EuiSpacer size="s" />
</>
)}
</div>
</EuiPanel>
</EuiFormRow>
</>
</EuiModalBody>
<EuiModalFooter>
<EuiButtonEmpty data-test-subj="cancelCopyToButton" onClick={() => closeModal()}>
{dashboardCopyToDashboardAction.getCancelButtonName()}
</EuiButtonEmpty>
<EuiButton
fill
data-test-subj="confirmCopyToButton"
onClick={onSubmit}
disabled={dashboardOption === 'existing' && !selectedDashboard}
<EuiFocusTrap clickOutsideDisables={true}>
<EuiOutsideClickDetector onOutsideClick={closeModal}>
<div
role="dialog"
aria-modal="true"
aria-labelledby={titleId}
aria-describedby={descriptionId}
>
{dashboardCopyToDashboardAction.getAcceptButtonName()}
</EuiButton>
</EuiModalFooter>
</PresentationUtilContext>
<PresentationUtilContext>
<EuiModalHeader>
<EuiModalHeaderTitle>
<h2 id={titleId}>{dashboardCopyToDashboardAction.getDisplayName()}</h2>
</EuiModalHeaderTitle>
</EuiModalHeader>
<EuiModalBody>
<>
<EuiText>
<p id={descriptionId}>{dashboardCopyToDashboardAction.getDescription()}</p>
</EuiText>
<EuiSpacer />
<EuiFormRow hasChildLabel={false}>
<EuiPanel
color="subdued"
hasShadow={false}
data-test-subj="add-to-dashboard-options"
>
<div>
{capabilities.canEditExisting && (
<>
<EuiRadio
checked={dashboardOption === 'existing'}
data-test-subj="add-to-existing-dashboard-option"
id="existing-dashboard-option"
name="dashboard-option"
label={dashboardCopyToDashboardAction.getExistingDashboardOption()}
onChange={() => setDashboardOption('existing')}
/>
<div className="savAddDashboard__searchDashboards">
<DashboardPicker
isDisabled={dashboardOption !== 'existing'}
idsToOmit={dashboardId ? [dashboardId] : undefined}
onChange={(dashboard) => setSelectedDashboard(dashboard)}
/>
</div>
<EuiSpacer size="s" />
</>
)}
{capabilities.canCreateNew && (
<>
<EuiRadio
checked={dashboardOption === 'new'}
data-test-subj="add-to-new-dashboard-option"
id="new-dashboard-option"
name="dashboard-option"
disabled={!dashboardId}
label={dashboardCopyToDashboardAction.getNewDashboardOption()}
onChange={() => setDashboardOption('new')}
/>
<EuiSpacer size="s" />
</>
)}
</div>
</EuiPanel>
</EuiFormRow>
</>
</EuiModalBody>
<EuiModalFooter>
<EuiButtonEmpty data-test-subj="cancelCopyToButton" onClick={() => closeModal()}>
{dashboardCopyToDashboardAction.getCancelButtonName()}
</EuiButtonEmpty>
<EuiButton
fill
data-test-subj="confirmCopyToButton"
onClick={onSubmit}
disabled={dashboardOption === 'existing' && !selectedDashboard}
>
{dashboardCopyToDashboardAction.getAcceptButtonName()}
</EuiButton>
</EuiModalFooter>
</PresentationUtilContext>
</div>
</EuiOutsideClickDetector>
</EuiFocusTrap>
);
}

View file

@ -9,10 +9,12 @@
import {
EuiButton,
EuiButtonEmpty,
EuiFocusTrap,
EuiModalBody,
EuiModalFooter,
EuiModalHeader,
EuiModalHeaderTitle,
EuiOutsideClickDetector,
EuiText,
EUI_MODAL_CANCEL_BUTTON,
} from '@elastic/eui';
@ -45,49 +47,64 @@ export const confirmDiscardUnsavedChanges = (overlays: OverlayStart, discardCall
export const confirmDiscardOrKeepUnsavedChanges = (
overlays: OverlayStart
): Promise<DiscardOrKeepSelection> => {
const titleId = 'confirmDiscardOrKeepTitle';
const descriptionId = 'confirmDiscardOrKeepDescription';
return new Promise((resolve) => {
const session = overlays.openModal(
toMountPoint(
<>
<EuiModalHeader data-test-subj="dashboardDiscardConfirm">
<EuiModalHeaderTitle>
{leaveEditModeConfirmStrings.getLeaveEditModeTitle()}
</EuiModalHeaderTitle>
</EuiModalHeader>
<EuiFocusTrap clickOutsideDisables={true} initialFocus={'.discardConfirmKeepButton'}>
<EuiOutsideClickDetector onOutsideClick={() => session.close()}>
<div
role="dialog"
aria-modal="true"
aria-labelledby={titleId}
aria-describedby={descriptionId}
>
<EuiModalHeader data-test-subj="dashboardDiscardConfirm">
<EuiModalHeaderTitle>
<h2 id={titleId}>{leaveEditModeConfirmStrings.getLeaveEditModeTitle()}</h2>
</EuiModalHeaderTitle>
</EuiModalHeader>
<EuiModalBody>
<EuiText>{leaveEditModeConfirmStrings.getLeaveEditModeSubtitle()}</EuiText>
</EuiModalBody>
<EuiModalBody>
<EuiText>
<p id={descriptionId}>{leaveEditModeConfirmStrings.getLeaveEditModeSubtitle()}</p>
</EuiText>
</EuiModalBody>
<EuiModalFooter>
<EuiButtonEmpty
data-test-subj="dashboardDiscardConfirmCancel"
onClick={() => session.close()}
>
{leaveEditModeConfirmStrings.getLeaveEditModeCancelButtonText()}
</EuiButtonEmpty>
<EuiButtonEmpty
color="danger"
data-test-subj="dashboardDiscardConfirmDiscard"
onClick={() => {
session.close();
resolve('discard');
}}
>
{leaveEditModeConfirmStrings.getLeaveEditModeDiscardButtonText()}
</EuiButtonEmpty>
<EuiButton
fill
data-test-subj="dashboardDiscardConfirmKeep"
onClick={() => {
session.close();
resolve('keep');
}}
>
{leaveEditModeConfirmStrings.getLeaveEditModeKeepChangesText()}
</EuiButton>
</EuiModalFooter>
</>
<EuiModalFooter>
<EuiButtonEmpty
data-test-subj="dashboardDiscardConfirmCancel"
onClick={() => session.close()}
>
{leaveEditModeConfirmStrings.getLeaveEditModeCancelButtonText()}
</EuiButtonEmpty>
<EuiButtonEmpty
color="danger"
data-test-subj="dashboardDiscardConfirmDiscard"
onClick={() => {
session.close();
resolve('discard');
}}
>
{leaveEditModeConfirmStrings.getLeaveEditModeDiscardButtonText()}
</EuiButtonEmpty>
<EuiButton
fill
data-test-subj="dashboardDiscardConfirmKeep"
className="discardConfirmKeepButton"
onClick={() => {
session.close();
resolve('keep');
}}
>
{leaveEditModeConfirmStrings.getLeaveEditModeKeepChangesText()}
</EuiButton>
</EuiModalFooter>
</div>
</EuiOutsideClickDetector>
</EuiFocusTrap>
),
{
'data-test-subj': 'dashboardDiscardConfirmModal',
@ -102,46 +119,66 @@ export const confirmCreateWithUnsaved = (
startBlankCallback: () => void,
contineCallback: () => void
) => {
const titleId = 'confirmDiscardOrKeepTitle';
const descriptionId = 'confirmDiscardOrKeepDescription';
const session = overlays.openModal(
toMountPoint(
<>
<EuiModalHeader data-test-subj="dashboardCreateConfirm">
<EuiModalHeaderTitle>{createConfirmStrings.getCreateTitle()}</EuiModalHeaderTitle>
</EuiModalHeader>
<EuiFocusTrap
clickOutsideDisables={true}
initialFocus={'.dashboardCreateConfirmContinueButton'}
>
<EuiOutsideClickDetector onOutsideClick={() => session.close()}>
<div
role="dialog"
aria-modal="true"
aria-labelledby={titleId}
aria-describedby={descriptionId}
>
<EuiModalHeader data-test-subj="dashboardCreateConfirm">
<EuiModalHeaderTitle>
<h2 id={titleId}>{createConfirmStrings.getCreateTitle()}</h2>
</EuiModalHeaderTitle>
</EuiModalHeader>
<EuiModalBody>
<EuiText>{createConfirmStrings.getCreateSubtitle()}</EuiText>
</EuiModalBody>
<EuiModalBody>
<EuiText>
<p id={descriptionId}>{createConfirmStrings.getCreateSubtitle()}</p>
</EuiText>
</EuiModalBody>
<EuiModalFooter>
<EuiButtonEmpty
data-test-subj="dashboardCreateConfirmCancel"
onClick={() => session.close()}
>
{createConfirmStrings.getCancelButtonText()}
</EuiButtonEmpty>
<EuiButtonEmpty
color="danger"
data-test-subj="dashboardCreateConfirmStartOver"
onClick={() => {
startBlankCallback();
session.close();
}}
>
{createConfirmStrings.getStartOverButtonText()}
</EuiButtonEmpty>
<EuiButton
fill
data-test-subj="dashboardCreateConfirmContinue"
onClick={() => {
contineCallback();
session.close();
}}
>
{createConfirmStrings.getContinueButtonText()}
</EuiButton>
</EuiModalFooter>
</>
<EuiModalFooter>
<EuiButtonEmpty
data-test-subj="dashboardCreateConfirmCancel"
onClick={() => session.close()}
>
{createConfirmStrings.getCancelButtonText()}
</EuiButtonEmpty>
<EuiButtonEmpty
color="danger"
data-test-subj="dashboardCreateConfirmStartOver"
onClick={() => {
startBlankCallback();
session.close();
}}
>
{createConfirmStrings.getStartOverButtonText()}
</EuiButtonEmpty>
<EuiButton
fill
data-test-subj="dashboardCreateConfirmContinue"
className="dashboardCreateConfirmContinueButton"
onClick={() => {
contineCallback();
session.close();
}}
>
{createConfirmStrings.getContinueButtonText()}
</EuiButton>
</EuiModalFooter>
</div>
</EuiOutsideClickDetector>
</EuiFocusTrap>
),
{
'data-test-subj': 'dashboardCreateConfirmModal',

View file

@ -18,6 +18,8 @@ import {
EuiModalFooter,
EuiModalBody,
EuiModalHeaderTitle,
EuiFocusTrap,
EuiOutsideClickDetector,
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n';
@ -64,82 +66,89 @@ export class CustomizePanelModal extends Component<CustomizePanelProps, State> {
};
public render() {
const titleId = 'customizePanelModalTitle';
return (
<React.Fragment>
<EuiModalHeader>
<EuiModalHeaderTitle data-test-subj="customizePanelTitle">
Customize panel
</EuiModalHeaderTitle>
</EuiModalHeader>
<EuiFocusTrap clickOutsideDisables={true} initialFocus={'.panelTitleInputText'}>
<EuiOutsideClickDetector onOutsideClick={this.props.cancel}>
<div role="dialog" aria-modal="true" aria-labelledby={titleId}>
<EuiModalHeader>
<EuiModalHeaderTitle data-test-subj="customizePanelTitle">
<h2 id={titleId}>Customize panel</h2>
</EuiModalHeaderTitle>
</EuiModalHeader>
<EuiModalBody>
<EuiFormRow>
<EuiSwitch
checked={!this.state.hideTitle}
data-test-subj="customizePanelHideTitle"
id="hideTitle"
label={
<FormattedMessage
defaultMessage="Show panel title"
id="embeddableApi.customizePanel.modal.showTitle"
<EuiModalBody>
<EuiFormRow>
<EuiSwitch
checked={!this.state.hideTitle}
data-test-subj="customizePanelHideTitle"
id="hideTitle"
label={
<FormattedMessage
defaultMessage="Show panel title"
id="embeddableApi.customizePanel.modal.showTitle"
/>
}
onChange={this.onHideTitleToggle}
/>
}
onChange={this.onHideTitleToggle}
/>
</EuiFormRow>
<EuiFormRow
label={i18n.translate(
'embeddableApi.customizePanel.modal.optionsMenuForm.panelTitleFormRowLabel',
{
defaultMessage: 'Panel title',
}
)}
>
<EuiFieldText
id="panelTitleInput"
data-test-subj="customEmbeddablePanelTitleInput"
name="min"
type="text"
disabled={this.state.hideTitle}
value={this.state.title || ''}
onChange={(e) => this.setState({ title: e.target.value })}
aria-label={i18n.translate(
'embeddableApi.customizePanel.modal.optionsMenuForm.panelTitleInputAriaLabel',
{
defaultMessage: 'Enter a custom title for your panel',
}
)}
append={
<EuiButtonEmpty
data-test-subj="resetCustomEmbeddablePanelTitle"
onClick={this.reset}
</EuiFormRow>
<EuiFormRow
label={i18n.translate(
'embeddableApi.customizePanel.modal.optionsMenuForm.panelTitleFormRowLabel',
{
defaultMessage: 'Panel title',
}
)}
>
<EuiFieldText
id="panelTitleInput"
className="panelTitleInputText"
data-test-subj="customEmbeddablePanelTitleInput"
name="min"
type="text"
disabled={this.state.hideTitle}
>
<FormattedMessage
id="embeddableApi.customizePanel.modal.optionsMenuForm.resetCustomDashboardButtonLabel"
defaultMessage="Reset"
/>
</EuiButtonEmpty>
}
/>
</EuiFormRow>
</EuiModalBody>
<EuiModalFooter>
<EuiButtonEmpty onClick={() => this.props.cancel()}>
<FormattedMessage
id="embeddableApi.customizePanel.modal.cancel"
defaultMessage="Cancel"
/>
</EuiButtonEmpty>
value={this.state.title || ''}
onChange={(e) => this.setState({ title: e.target.value })}
aria-label={i18n.translate(
'embeddableApi.customizePanel.modal.optionsMenuForm.panelTitleInputAriaLabel',
{
defaultMessage: 'Enter a custom title for your panel',
}
)}
append={
<EuiButtonEmpty
data-test-subj="resetCustomEmbeddablePanelTitle"
onClick={this.reset}
disabled={this.state.hideTitle}
>
<FormattedMessage
id="embeddableApi.customizePanel.modal.optionsMenuForm.resetCustomDashboardButtonLabel"
defaultMessage="Reset"
/>
</EuiButtonEmpty>
}
/>
</EuiFormRow>
</EuiModalBody>
<EuiModalFooter>
<EuiButtonEmpty onClick={() => this.props.cancel()}>
<FormattedMessage
id="embeddableApi.customizePanel.modal.cancel"
defaultMessage="Cancel"
/>
</EuiButtonEmpty>
<EuiButton data-test-subj="saveNewTitleButton" onClick={this.save} fill>
<FormattedMessage
id="embeddableApi.customizePanel.modal.saveButtonTitle"
defaultMessage="Save"
/>
</EuiButton>
</EuiModalFooter>
</React.Fragment>
<EuiButton data-test-subj="saveNewTitleButton" onClick={this.save} fill>
<FormattedMessage
id="embeddableApi.customizePanel.modal.saveButtonTitle"
defaultMessage="Save"
/>
</EuiButton>
</EuiModalFooter>
</div>
</EuiOutsideClickDetector>
</EuiFocusTrap>
);
}
}