Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[BUG][EuiModal] Fix VoiceOver + Safari escaping focus trap #7564

Merged
merged 11 commits into from
Mar 12, 2024
5 changes: 5 additions & 0 deletions changelogs/upcoming/7564.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
**Accessibility**

- Updated `EuiModal` to set an `aria-modal` attribute and a default `dialog` role
- Updated `EuiConfirmModal` to set a default `alertdialog` role
- Fixed `EuiModal` and `EuiConfirmModal` to properly trap Safari+VoiceOver's virtual cursor
84 changes: 41 additions & 43 deletions src-docs/src/views/modal/confirm_modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
EuiConfirmModal,
EuiFlexGroup,
EuiFlexItem,
useGeneratedHtmlId,
} from '../../../../src';

export default () => {
Expand All @@ -17,48 +18,11 @@ export default () => {
const closeDestroyModal = () => setIsDestroyModalVisible(false);
const showDestroyModal = () => setIsDestroyModalVisible(true);

let modal;

if (isModalVisible) {
modal = (
<EuiConfirmModal
style={{ width: 600 }}
title="Update subscription to Platinum?"
onCancel={closeModal}
onConfirm={closeModal}
cancelButtonText="Cancel"
confirmButtonText="Update subscription"
defaultFocusedButton="confirm"
>
<p>
Your subscription and benefits increase immediately. If you change to
a lower subscription later, it will not take affect until the next
billing cycle.
</p>
</EuiConfirmModal>
);
}

let destroyModal;

if (isDestroyModalVisible) {
destroyModal = (
<EuiConfirmModal
title="Discard dashboard changes?"
onCancel={closeDestroyModal}
onConfirm={closeDestroyModal}
cancelButtonText="Keep editing"
confirmButtonText="Discard changes"
buttonColor="danger"
defaultFocusedButton="confirm"
>
<p>You will lose all unsaved changes made to this dashboard.</p>
</EuiConfirmModal>
);
}
const modalTitleId = useGeneratedHtmlId();
const destroyModalTitleId = useGeneratedHtmlId();

return (
<div>
<>
<EuiFlexGroup responsive={false} wrap gutterSize="xs">
<EuiFlexItem grow={false}>
<EuiButton onClick={showModal}>Show confirm modal</EuiButton>
Expand All @@ -69,8 +33,42 @@ export default () => {
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
{modal}
{destroyModal}
</div>

{isModalVisible && (
<EuiConfirmModal
aria-labelledby={modalTitleId}
style={{ width: 600 }}
title="Update subscription to Platinum?"
titleProps={{ id: modalTitleId }}
onCancel={closeModal}
onConfirm={closeModal}
cancelButtonText="Cancel"
confirmButtonText="Update subscription"
defaultFocusedButton="confirm"
>
<p>
Your subscription and benefits increase immediately. If you change
to a lower subscription later, it will not take affect until the
next billing cycle.
</p>
</EuiConfirmModal>
)}

{isDestroyModalVisible && (
<EuiConfirmModal
aria-labelledby={destroyModalTitleId}
title="Discard dashboard changes?"
titleProps={{ id: destroyModalTitleId }}
onCancel={closeDestroyModal}
onConfirm={closeDestroyModal}
cancelButtonText="Keep editing"
confirmButtonText="Discard changes"
buttonColor="danger"
defaultFocusedButton="confirm"
>
<p>You will lose all unsaved changes made to this dashboard.</p>
</EuiConfirmModal>
)}
</>
);
};
64 changes: 32 additions & 32 deletions src-docs/src/views/modal/confirm_modal_loading.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
EuiConfirmModal,
EuiFormRow,
EuiFieldText,
useGeneratedHtmlId,
} from '../../../../src';

export default () => {
Expand Down Expand Up @@ -36,40 +37,39 @@ export default () => {
setValue(e.target.value);
};

let modal;

if (isModalVisible) {
modal = (
<EuiConfirmModal
title="Delete the EUI repo?"
onCancel={closeModal}
onConfirm={() => {
closeModal();
window.alert('Shame on you!');
}}
confirmButtonText="Delete"
cancelButtonText="Cancel"
buttonColor="danger"
initialFocus="[name=delete]"
confirmButtonDisabled={value.toLowerCase() !== 'delete'}
isLoading={isLoading}
>
<EuiFormRow label="Type the word 'delete' to confirm">
<EuiFieldText
name="delete"
isLoading={isLoading}
value={value}
onChange={onChange}
/>
</EuiFormRow>
</EuiConfirmModal>
);
}
const modalTitleId = useGeneratedHtmlId();

return (
<div>
<>
<EuiButton onClick={showModal}>Show loading confirm modal</EuiButton>
{modal}
</div>

{isModalVisible && (
<EuiConfirmModal
aria-labelledby={modalTitleId}
title="Delete the EUI repo?"
titleProps={{ id: modalTitleId }}
onCancel={closeModal}
onConfirm={() => {
closeModal();
window.alert('Shame on you!');
}}
confirmButtonText="Delete"
cancelButtonText="Cancel"
buttonColor="danger"
initialFocus="[name=delete]"
confirmButtonDisabled={value.toLowerCase() !== 'delete'}
isLoading={isLoading}
>
<EuiFormRow label="Type the word 'delete' to confirm">
<EuiFieldText
name="delete"
isLoading={isLoading}
value={value}
onChange={onChange}
/>
</EuiFormRow>
</EuiConfirmModal>
)}
</>
);
};
62 changes: 31 additions & 31 deletions src-docs/src/views/modal/modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,31 +8,37 @@ import {
EuiModalHeader,
EuiModalHeaderTitle,
EuiCodeBlock,
} from '../../../../src/components';
import { EuiSpacer } from '../../../../src/components/spacer';
EuiSpacer,
useGeneratedHtmlId,
} from '../../../../src';

export default () => {
const [isModalVisible, setIsModalVisible] = useState(false);

const closeModal = () => setIsModalVisible(false);
const showModal = () => setIsModalVisible(true);

let modal;
const modalTitleId = useGeneratedHtmlId();

if (isModalVisible) {
modal = (
<EuiModal onClose={closeModal}>
<EuiModalHeader>
<EuiModalHeaderTitle>Modal title</EuiModalHeaderTitle>
</EuiModalHeader>
return (
<>
<EuiButton onClick={showModal}>Show modal</EuiButton>

{isModalVisible && (
<EuiModal aria-labelledby={modalTitleId} onClose={closeModal}>
<EuiModalHeader>
<EuiModalHeaderTitle id={modalTitleId}>
Modal title
</EuiModalHeaderTitle>
</EuiModalHeader>

<EuiModalBody>
This modal has the following setup:
<EuiSpacer />
<EuiCodeBlock language="html" isCopyable>
{`<EuiModal onClose={closeModal}>
<EuiModalBody>
This modal has the following setup:
<EuiSpacer />
<EuiCodeBlock language="html" isCopyable>
{`<EuiModal aria-labelledby={titleId} onClose={closeModal}>
<EuiModalHeader>
<EuiModalHeaderTitle><!-- Modal title --></EuiModalHeaderTitle>
<EuiModalHeaderTitle title={titleId}><!-- Modal title --></EuiModalHeaderTitle>
</EuiModalHeader>

<EuiModalBody>
Expand All @@ -45,22 +51,16 @@ export default () => {
</EuiButton>
</EuiModalFooter>
</EuiModal>`}
</EuiCodeBlock>
</EuiModalBody>
</EuiCodeBlock>
</EuiModalBody>

<EuiModalFooter>
<EuiButton onClick={closeModal} fill>
Close
</EuiButton>
</EuiModalFooter>
</EuiModal>
);
}

return (
<div>
<EuiButton onClick={showModal}>Show modal</EuiButton>
{modal}
</div>
<EuiModalFooter>
<EuiButton onClick={closeModal} fill>
Close
</EuiButton>
</EuiModalFooter>
</EuiModal>
)}
</>
);
};
16 changes: 10 additions & 6 deletions src-docs/src/views/modal/modal_example.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,9 @@ const confirmModalLoadingSource = require('!!raw-loader!./confirm_modal_loading'
import ModalWidth from './modal_width';
const modalWidthSource = require('!!raw-loader!./modal_width');

const modalSnippet = `<EuiModal onClose={closeModal}>
const modalSnippet = `<EuiModal aria-labelledby={titleId} onClose={closeModal}>
<EuiModalHeader>
<EuiModalHeaderTitle><!-- Modal title --></EuiModalHeaderTitle>
<EuiModalHeaderTitle id={titleId}><!-- Modal title --></EuiModalHeaderTitle>
</EuiModalHeader>

<EuiModalBody>
Expand All @@ -44,9 +44,9 @@ const modalSnippet = `<EuiModal onClose={closeModal}>
</EuiModalFooter>
</EuiModal>`;

const modalWidthSnippet = `<EuiModal style={{ width: 800 }} onClose={closeModal}>
const modalWidthSnippet = `<EuiModal style={{ width: 800 }} aria-labelledby={titleId} onClose={closeModal}>
<EuiModalHeader>
<EuiModalHeaderTitle><!-- Modal title --></EuiModalHeaderTitle>
<EuiModalHeaderTitle id={titleId}><!-- Modal title --></EuiModalHeaderTitle>
</EuiModalHeader>

<EuiModalBody>
Expand All @@ -58,9 +58,9 @@ const modalWidthSnippet = `<EuiModal style={{ width: 800 }} onClose={closeModal}
</EuiModalFooter>
</EuiModal>`;

const modalFormSnippet = `<EuiModal onClose={closeModal}>
const modalFormSnippet = `<EuiModal aria-labelledby={titleId} onClose={closeModal}>
<EuiModalHeader>
<EuiModalHeaderTitle><!-- Modal title --></EuiModalHeaderTitle>
<EuiModalHeaderTitle id={titleId><!-- Modal title --></EuiModalHeaderTitle>
</EuiModalHeader>

<EuiModalBody>
Expand All @@ -75,7 +75,9 @@ const modalFormSnippet = `<EuiModal onClose={closeModal}>

const confirmModalSnippet = [
`<EuiConfirmModal
aria-labelledby={titleId}
title={title}
titleProps={{ id: titleId }}
onCancel={closeModal}
onConfirm={closeModal}
cancelButtonText={cancelText}
Expand All @@ -95,7 +97,9 @@ const confirmModalSnippet = [

const confirmModalLoadingSnippet = [
`<EuiConfirmModal
aria-labelledby={titleId}
title={title}
id={{ id: titleId }}
onCancel={closeModal}
onConfirm={closeModal}
cancelButtonText={cancelText}
Expand Down
Loading
Loading