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

Modal accessibility #4615

Merged
merged 14 commits into from
Feb 20, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions docs/src/__examples__/Modal/FIXED_FOOTER.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export default {
Example: () => {
return (
<OrbitProvider theme={defaultTheme} useId={React.useId}>
<Modal fixedFooter>
<Modal hasCloseButton={false} fixedFooter>
<ModalHeader
title="Enjoy a meal while you travel"
illustration={<Illustration name="Meal" size="small" />}
Expand Down Expand Up @@ -178,7 +178,6 @@ export default {
{ name: "hasCloseButton", type: "boolean", defaultValue: true },
{ name: "mobileHeader", type: "boolean", defaultValue: true },
{ name: "disableAnimation", type: "boolean", defaultValue: false },
{ name: "autoFocus", type: "boolean", defaultValue: true },
{ name: "preventOverlayClose", type: "boolean", defaultValue: false },
{
name: "size",
Expand Down
1 change: 1 addition & 0 deletions docs/src/__examples__/Modal/HEADER_FOOTER.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export default {
onClose={() => {
setShowModal(false);
}}
labelClose="Close"
>
<ModalHeader
title="Priority boarding"
Expand Down
5 changes: 5 additions & 0 deletions docs/src/__examples__/Modal/SIZES.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export default {
onClose={() => {
setShowModalExtraSmall(false);
}}
labelClose="Close"
>
<ModalSection>
Orbit is an open source design system for your next travel project.
Expand All @@ -36,6 +37,7 @@ export default {
onClose={() => {
setShowModalSmall(false);
}}
labelClose="Close"
>
<ModalSection>
Orbit is an open source design system for your next travel project.
Expand All @@ -47,6 +49,7 @@ export default {
onClose={() => {
setShowModalNormal(false);
}}
labelClose="Close"
>
<ModalSection>
Orbit is an open source design system for your next travel project.
Expand All @@ -59,6 +62,7 @@ export default {
onClose={() => {
setShowModalLarge(false);
}}
labelClose="Close"
>
<ModalSection>
Orbit is an open source design system for your next travel project.
Expand All @@ -71,6 +75,7 @@ export default {
onClose={() => {
setShowModalExtraLarge(false);
}}
labelClose="Close"
>
<ModalSection>
Orbit is an open source design system for your next travel project.
Expand Down
2 changes: 1 addition & 1 deletion docs/src/components/Navbar/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,7 @@ const Navbar = ({ location, docNavigation }: Props) => {
`)}
`}
>
<Modal fixedFooter onClose={() => setMenuOpen(false)}>
<Modal fixedFooter onClose={() => setMenuOpen(false)} labelClose="Close">
{docNavigation ? (
<>
<ModalHeader
Expand Down
7 changes: 1 addition & 6 deletions docs/src/components/Search/SearchModalUI.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -125,12 +125,7 @@ export default function SearchModalUI({
return (
<Portal>
<StyledModalWrapper>
<Modal
size="extraLarge"
// the search field will be auto focused
autoFocus={false}
onClose={onClose}
>
<Modal size="extraLarge" onClose={onClose} labelClose="Close">
<ModalHeader title={title} />
<ModalSection>
<StyledSearchWrapper>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
---
title: Accessibility
redirect_from:
- /components/modal/accessibility/
---

# Accessibility

## Modal

The Modal component has been designed with accessibility in mind.

To ease the keyboard navigation, when opening a modal, the focus is moved to the first focusable element inside the modal. It is also impossible to focus anything outside of the modal while it is open.
When closing the modal, the focus can be moved back to the element that triggered the modal automatically, if the prop `triggerRef` is defined with a ref to that element.

Besides that, assistive ARIA attributes are applied automatically to the modal and its children, to ensure that screen readers can announce the modal and its content correctly.

If you prefer, you can also provide those attributes manually, as described in the table below:

| Name | Type | Description |
| :-------------- | :------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| ariaLabelledby | `string` | The `id` of an element that serves as a label (title) for the modal. |
| ariaDescribedby | `string` | The `id` of an element that serves as a description for the modal. |
| ariaLabel | `string` | Text that labels the modal content. Think of it as the title of the modal. This should be used if `title` is not passed to `ModalHeader` and `ariaLabelledby` is undefined. |

All the props above are optional, but recommended to use to ensure the best experience for all users.

If you use a ModalHeader with `title` and `description` props defined, they are automatically assigned as `aria-labelledby` and `aria-describedby`, respectively.
However, if needed, you can overwrite the values by passing the corresponding props.

The `ariaLabelledby` and `ariaDescribedby` props can reference multiple ids, separated by a space.
The elements with those ids can be hidden, so that their text is only announced by screen readers.

Be mindful that all descriptive texts, even if invisible on the screen, should be translated and provide context for users of assistive technologies.
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,52 @@ With this guide, we aim to walk through all the breaking changes and how they ca

## Breaking changes

### Badge component removed the `border` prop

The `Badge` component removed the `border` prop, as it was not being used internally.

Please adapt your code to remove the `border` prop. No visual changes are expected.

**Before:**

```jsx
import Badge from "@kiwicom/orbit-components/lib/Badge";

<Badge border={false} />;
```

**Now:**

```jsx
import Badge from "@kiwicom/orbit-components/lib/Badge";

<Badge />;
```

### Modal component removed `autoFocus` prop

The `Modal` component removed the `autoFocus` prop, as the focus management is now handled automatically to comply with accessibility.

Therefore, this prop has no effect now. The first focusable element within the modal will be focused automatically when the modal is opened.

This prop had a default value of `true`, therefore, the default behavior remains the same.

**Before:**

```jsx
import Modal from "@kiwicom/orbit-components/lib/Modal";

<Modal autoFocus={false} />;
```

**Now:**

```jsx
import Modal from "@kiwicom/orbit-components/lib/Modal";

<Modal />;
```

### Required props in HorizontalScroll if `arrows` is true

The `HorizontalScroll` component now requires `arrowLeftAriaLabel` and `arrowRightAriaLabel` props if `arrows` is true.
Expand Down Expand Up @@ -50,34 +96,57 @@ intl.formatMessage({
});
```

To run the codemod, use the following command:

```bash
npx jscodeshift -t https://raw.githubusercontent.com/kiwicom/orbit/master/packages/orbit-components/transforms/horizontalscroll-aria-labels.js --parser=tsx --extensions=ts,tsx <pathToYourCode>
```

Make sure to run prettier after running the codemod, as it might introduce some formatting changes.

Feel free to customize the translations according to your needs, but please maintain the same translation key structure (`common.screenreader.*`) when requesting translations.

### Badge component removed the `border` prop
### Required prop in Modal if `onClose` is defined and `hasCloseButton` is explicitly false

The `Badge` component removed the `border` prop, as it was not being used internally.
The `Modal` component used to have a default value for the prop `labelClose`. We removed this default value, so now you need to provide it.
This prop is also now required if `onClose` is defined and `hasCloseButton` is **not** explicitly set to `false` (the default value of `hasCloseButton` prop is still `true`, as it has been).

Please adapt your code to remove the `border` prop. No visual changes are expected.
This `labelClose` prop provides the label for the close button that is applied to the `Modal` so that assistive technologies can announce it correctly, given that it renders a button with just an icon.

**Before:**

```jsx
import Badge from "@kiwicom/orbit-components/lib/Badge";
import Modal from "@kiwicom/orbit-components/lib/Modal";

<Badge border={false} />;
<Modal onClose={handleClose} />;
```

The `labelClose` prop has a default value of `"Close"`. Notice that the `hasCloseButton` prop is undefined, so by default it is set to `true`.

**Now:**

```jsx
import Badge from "@kiwicom/orbit-components/lib/Badge";
import Modal from "@kiwicom/orbit-components/lib/Modal";

<Badge />;
<Modal onClose={handleClose} labelClose="Close" />;
```

Make sure to provide translated strings for the `labelClose` prop.

To ease the migration, we provide a codemod to help you out, with some default generic translations.
The codemod will inject `intl.formatMessage` calls with the default translations, in this case:

```jsx
intl.formatMessage({
id: "orbit.button_close",
defaultMessage: "Close",
});
```

Feel free to customize the translations according to your needs, by eventually providing more context to the modal it refers to.

## Codemod

A codemod is available to help with the migration. It should target the new props that require (translated) strings.
Please note that the codemod **does not** guarantee a full migration, nor its complete correctness.
Manual checks are still necessary, especially if some props are being calculated conditionally or the components are imported with different names.

To run the codemod, use the following command:

```bash
npx jscodeshift -t https://raw.githubusercontent.com/kiwicom/orbit/master/packages/orbit-components/transforms/aria-labels.js --parser=tsx --extensions=ts,tsx <pathToYourCode>
```

Make sure to run prettier after running the codemod, as it might introduce some formatting changes.
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { Modal, ModalSection, ModalFooter, Button } from "@kiwicom/orbit-compone
export default function TestModalFooter() {
const [height, setHeight] = React.useState<"tall" | "short">("tall");
return (
<Modal fixedFooter autoFocus={false}>
<Modal fixedFooter>
<ModalSection dataTest="section">Content</ModalSection>
<ModalFooter dataTest="footer">
<div
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion packages/orbit-components/src/ErrorForms.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -392,7 +392,7 @@ export const RtlError: Story = {

export const WithModal: Story = {
render: ({ label, error, showMore }) => (
<Modal onClose={action("onClose")} fixedFooter>
<Modal onClose={action("onClose")} labelClose="Close" fixedFooter>
<ModalHeader title="Refund" description="Reservation number: 123456789" />
<ModalSection>
<Stack>
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 0 additions & 1 deletion packages/orbit-components/src/InputSelect/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -476,7 +476,6 @@ const InputSelect = React.forwardRef<HTMLInputElement, Props>(
}
}}
mobileHeader={false}
autoFocus
>
<ModalHeader className="!p-400 !mb-0">
{label && (
Expand Down
2 changes: 2 additions & 0 deletions packages/orbit-components/src/Itinerary/Itinerary.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -754,6 +754,7 @@ export const InsideModal: Story = {
ev.stopPropagation();
setIsOpenedModal(false);
}}
labelClose="Close"
>
<ModalSection>Hidden city info</ModalSection>
</Modal>
Expand Down Expand Up @@ -904,6 +905,7 @@ export const MultipleBanners: Story = {
ev.stopPropagation();
setIsOpenedModal(false);
}}
labelClose="Close"
>
<ModalSection>Throwaway ticketing info</ModalSection>
</Modal>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,7 @@ const ItinerarySegmentDetail = ({
className="flex w-full items-center overflow-hidden"
tabIndex={-1}
role="button"
onKeyDown={ev => {
ev.stopPropagation();
}}
onKeyDown={() => {}}
onClick={ev => {
if (isOverflowed && opened) ev.stopPropagation();
}}
Expand Down
18 changes: 14 additions & 4 deletions packages/orbit-components/src/Modal/Modal.ct-story.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ const content = "Lorem ispum dolor sit amet.";

export function ModalVisualDefaultStory({ size = SIZES.NORMAL, isMobileFullPage = false }) {
return (
<Modal onClose={() => {}} size={size} isMobileFullPage={isMobileFullPage}>
<Modal onClose={() => {}} size={size} isMobileFullPage={isMobileFullPage} labelClose="Close">
<ModalHeader
title="Normal header"
illustration={<Illustration name="AppKiwi" size="small" />}
Expand All @@ -36,7 +36,7 @@ export function ModalVisualDefaultStory({ size = SIZES.NORMAL, isMobileFullPage

export function ModalVisualMobileHeader() {
return (
<Modal onClose={() => {}} size={SIZES.NORMAL} mobileHeader>
<Modal onClose={() => {}} size={SIZES.NORMAL} mobileHeader labelClose="Close">
<ModalHeader
title="Suppressed header"
illustration={<Illustration name="AppKiwi" size="small" />}
Expand All @@ -59,7 +59,12 @@ export function ModalVisualMobileHeader() {

export function ModalVisualNoHeaderNoFooter({ isMobileFullPage = false }) {
return (
<Modal onClose={() => {}} size={SIZES.NORMAL} isMobileFullPage={isMobileFullPage}>
<Modal
onClose={() => {}}
size={SIZES.NORMAL}
isMobileFullPage={isMobileFullPage}
labelClose="Close"
>
<ModalSection>
<Text>No Header nor Footer modal</Text>
</ModalSection>
Expand All @@ -69,7 +74,12 @@ export function ModalVisualNoHeaderNoFooter({ isMobileFullPage = false }) {

export function ModalVisualHeaderOnly({ isMobileFullPage = false }) {
return (
<Modal onClose={() => {}} size={SIZES.NORMAL} isMobileFullPage={isMobileFullPage}>
<Modal
onClose={() => {}}
size={SIZES.NORMAL}
isMobileFullPage={isMobileFullPage}
labelClose="Close"
>
<ModalHeader
title="Normal header"
illustration={<Illustration name="AppKiwi" size="small" />}
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading