Skip to content

Commit 45ce1e6

Browse files
committed
feat(Modal): add ariaLabel, ariaLabelledby and ariaDescribedby
These props can be used to enhance accessibility
1 parent f1d010d commit 45ce1e6

File tree

6 files changed

+38
-9
lines changed

6 files changed

+38
-9
lines changed

packages/orbit-components/src/Modal/Modal.stories.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -415,7 +415,7 @@ export const WithItinerary: Story = {
415415

416416
return (
417417
<Container>
418-
<Modal onClose={onClose}>
418+
<Modal ariaLabel="Itinerary from Prague to Frankfurt" onClose={onClose}>
419419
<ModalSection>
420420
<Itinerary>
421421
<ItineraryStatus type="success" label="This part is new">

packages/orbit-components/src/Modal/ModalContext.tsx

+3
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ export interface Props {
44
readonly setDimensions?: () => void;
55
readonly decideFixedFooter?: () => void;
66
readonly setHasModalTitle?: React.Dispatch<React.SetStateAction<boolean>>;
7+
readonly setHasModalDescription?: React.Dispatch<React.SetStateAction<boolean>>;
78
readonly setHasModalSection?: () => void;
89
readonly removeHasModalSection?: () => void;
910
readonly callContextFunctions?: () => void;
@@ -15,10 +16,12 @@ export interface Props {
1516
readonly isInsideModal?: boolean;
1617
readonly closable?: boolean;
1718
readonly titleID?: string;
19+
readonly descriptionID?: string;
1820
}
1921

2022
export const ModalContext = React.createContext<Props>({
2123
setHasModalTitle: () => {},
24+
setHasModalDescription: () => {},
2225
setHasModalSection: () => {},
2326
removeHasModalSection: () => {},
2427
setFooterHeight: () => {},

packages/orbit-components/src/Modal/ModalHeader/index.tsx

+16-3
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,14 @@ const ModalHeader = ({
4848
title,
4949
dataTest,
5050
}: Props) => {
51-
const { setHasModalTitle, hasMobileHeader, isMobileFullPage, titleID } =
52-
React.useContext(ModalContext);
51+
const {
52+
setHasModalTitle,
53+
setHasModalDescription,
54+
hasMobileHeader,
55+
isMobileFullPage,
56+
titleID,
57+
descriptionID,
58+
} = React.useContext(ModalContext);
5359

5460
useModalContextFunctions();
5561

@@ -60,6 +66,13 @@ const ModalHeader = ({
6066
};
6167
}, [title, setHasModalTitle]);
6268

69+
React.useEffect(() => {
70+
if (description) setHasModalDescription?.(true);
71+
return () => {
72+
setHasModalDescription?.(false);
73+
};
74+
}, [description, setHasModalDescription]);
75+
6376
const hasHeader = Boolean(title || description);
6477

6578
return (
@@ -82,7 +95,7 @@ const ModalHeader = ({
8295
)}
8396
{description && (
8497
<div className="mt-200">
85-
<Text size="large" as="div">
98+
<Text size="large" as="div" id={descriptionID}>
8699
{description}
87100
</Text>
88101
</div>

packages/orbit-components/src/Modal/README.md

+3
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,9 @@ Table below contains all types of the props available in the Modal component.
4040
| mobileHeader | `boolean` | `true` | If `false` the ModalHeader will not have MobileHeader and CloseContainer. |
4141
| labelClose | `string` | `Close` | The label for the close button. |
4242
| onScroll | `event => void \| Promise` | | Function for handling `onScroll` event. [See Functional specs](#functional-specs). |
43+
| ariaLabelledby | `string` | | The `aria-labelledby` attribute of the Modal. It should be used if `title` is not defined on the ModalHeader. |
44+
| ariaDescribedby | `string` | | The `aria-describedby` attribute of the Modal. It should be used if `description` is not defined on the ModalHeader. |
45+
| ariaLabel | `string` | | The `aria-label` attribute of the Modal. It should be used if `title` is not defined on the ModalHeader and `ariaLabelledby` is undefined. |
4346

4447
### Modal enum
4548

packages/orbit-components/src/Modal/index.tsx

+12-5
Original file line numberDiff line numberDiff line change
@@ -60,13 +60,17 @@ const Modal = React.forwardRef<Instance, Props>(
6060
id,
6161
labelClose = "Close",
6262
lockScrolling = true,
63+
ariaLabel,
64+
ariaLabelledby,
65+
ariaDescribedby,
6366
}: Props,
6467
ref,
6568
) => {
6669
const [loaded, setLoaded] = React.useState<boolean>(false);
6770
const [scrolled, setScrolled] = React.useState<boolean>(false);
6871
const [fullyScrolled, setFullyScrolled] = React.useState<boolean>(false);
6972
const [hasModalTitle, setHasModalTitle] = React.useState<boolean>(false);
73+
const [hasModalDescription, setHasModalDescription] = React.useState<boolean>(false);
7074
const [hasModalSection, setHasModalSection] = React.useState<boolean>(false);
7175
const [clickedModalBody, setClickedModalBody] = React.useState<boolean>(false);
7276
const [fixedClose, setFixedClose] = React.useState<boolean>(false);
@@ -79,6 +83,7 @@ const Modal = React.forwardRef<Instance, Props>(
7983
const modalContent = React.useRef<HTMLElement | null>(null);
8084
const modalBody = React.useRef<HTMLElement | null>(null);
8185
const modalTitleID = useRandomId();
86+
const modalDescriptionID = useRandomId();
8287
const theme = useTheme();
8388

8489
const { isLargeMobile } = useMediaQuery();
@@ -354,6 +359,7 @@ const Modal = React.forwardRef<Instance, Props>(
354359
const value = React.useMemo(
355360
() => ({
356361
setHasModalTitle,
362+
setHasModalDescription,
357363
setHasModalSection: () => setHasModalSection(true),
358364
removeHasModalSection: () => setHasModalSection(false),
359365
callContextFunctions,
@@ -364,6 +370,7 @@ const Modal = React.forwardRef<Instance, Props>(
364370
closable: Boolean(onClose),
365371
isInsideModal: true,
366372
titleID: modalTitleID,
373+
descriptionID: modalDescriptionID,
367374
}),
368375
[
369376
callContextFunctions,
@@ -372,6 +379,7 @@ const Modal = React.forwardRef<Instance, Props>(
372379
mobileHeader,
373380
onClose,
374381
modalTitleID,
382+
modalDescriptionID,
375383
],
376384
);
377385

@@ -403,19 +411,18 @@ const Modal = React.forwardRef<Instance, Props>(
403411
!isMobileFullPage && "bg-[black]/50",
404412
"lm:overflow-y-auto lm:p-1000 lm:bg-[black]/50",
405413
)}
406-
// eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex
407-
tabIndex={0}
414+
tabIndex={-1}
408415
onKeyDown={handleKeyDown}
409416
onScroll={handleScroll}
410417
onClick={handleClickOutside}
411418
data-test={dataTest}
412419
id={id}
413420
ref={modalBodyRef}
414421
role="dialog"
415-
// eslint-disable-next-line jsx-a11y/no-autofocus
416-
autoFocus={autoFocus}
417422
aria-modal="true"
418-
aria-labelledby={hasModalTitle ? modalTitleID : ""}
423+
aria-label={ariaLabel}
424+
aria-labelledby={ariaLabelledby || (hasModalTitle ? modalTitleID : undefined)}
425+
aria-describedby={ariaDescribedby || (hasModalDescription ? modalDescriptionID : undefined)}
419426
>
420427
<div
421428
className={cx(

packages/orbit-components/src/Modal/types.d.ts

+3
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@ export interface Props extends Common.Globals {
2525
readonly hasCloseButton?: boolean;
2626
readonly disableAnimation?: boolean;
2727
readonly labelClose?: string;
28+
readonly ariaLabel?: string;
29+
readonly ariaLabelledby?: string;
30+
readonly ariaDescribedby?: string;
2831
}
2932

3033
export interface Instance {

0 commit comments

Comments
 (0)