mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[Dashboard] Modal a11y (#93332)
* Fix a11y on dashboard confirm modals by adding EuiFocusTrap & EuiOutsideClickDetector
This commit is contained in:
parent
8a1d7f5b99
commit
08f3c6cba7
3 changed files with 280 additions and 212 deletions
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue