Skip to content

Commit

Permalink
[BUG][EuiModal] Fix VoiceOver + Safari escaping focus trap (#7564)
Browse files Browse the repository at this point in the history
Co-authored-by: Cee Chen <constance.chen@elastic.co>
  • Loading branch information
1Copenut and cee-chen authored Mar 12, 2024
1 parent 2c68be5 commit f9ff62e
Show file tree
Hide file tree
Showing 11 changed files with 262 additions and 220 deletions.
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

0 comments on commit f9ff62e

Please sign in to comment.