Skip to content

Commit cdd0e17

Browse files
authored
Merge pull request #1035 from lumapps/fix/DSW-48-lightbox-a11y
fix(lightbox): fix dialog accessibility
2 parents 8150af1 + 817a718 commit cdd0e17

File tree

7 files changed

+125
-58
lines changed

7 files changed

+125
-58
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1111

1212
- Tooltip: fixed tooltip closing when mouse is hovering the tooltip text.
1313
- Tooltip: fixed close on Escape key pressed (not only when anchor is focused).
14+
- Lightbox: fixed aria dialog accessibility (reworked role, labelling and default focus element).
15+
- Lightbox: document accessibility concerns.
1416

1517
## [3.6.0][] - 2023-12-05
1618

packages/lumx-react/package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,9 +44,9 @@
4444
"@types/classnames": "^2.2.9",
4545
"@types/jest": "^29.2.1",
4646
"@types/lodash": "^4.14.149",
47-
"@types/react": "^16.9.11",
48-
"@types/react-dom": "^16.9.4",
49-
"@types/react-is": "^16.7.1",
47+
"@types/react": "^17.0.2",
48+
"@types/react-dom": "^17.0.2",
49+
"@types/react-is": "^17.0.2",
5050
"autoprefixer": "^9.7.4",
5151
"babel-jest": "29.1.2",
5252
"babel-loader": "^8.0.6",

packages/lumx-react/src/components/lightbox/Lightbox.stories.tsx

Lines changed: 61 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
/* eslint-disable react-hooks/rules-of-hooks,react/display-name */
22
import React from 'react';
3-
import { ImageBlock, Alignment, Lightbox, Button } from '@lumx/react';
3+
import { ImageBlock, Alignment, Lightbox, Button, Slideshow, SlideshowItem } from '@lumx/react';
44
import { useBooleanState } from '@lumx/react/hooks/useBooleanState';
5-
import { LANDSCAPE_IMAGES } from '@lumx/react/stories/controls/image';
5+
import { LANDSCAPE_IMAGES, LANDSCAPE_IMAGES_ALT } from '@lumx/react/stories/controls/image';
66

77
export default {
88
title: 'LumX components/lightbox/Lightbox',
@@ -28,9 +28,17 @@ export default {
2828
/**
2929
* Base LightBox with image block
3030
*/
31-
export const ImageBlock_ = {
31+
export const Image = {
3232
args: {
33-
children: <ImageBlock align={Alignment.center} alt="" fillHeight image={LANDSCAPE_IMAGES.landscape1} />,
33+
'aria-label': 'Fullscreen image',
34+
children: (
35+
<ImageBlock
36+
align={Alignment.center}
37+
fillHeight
38+
image={LANDSCAPE_IMAGES.landscape1}
39+
alt={LANDSCAPE_IMAGES_ALT.landscape1}
40+
/>
41+
),
3442
},
3543
};
3644

@@ -39,7 +47,55 @@ export const ImageBlock_ = {
3947
*/
4048
export const WithCloseButton = {
4149
args: {
42-
...ImageBlock_.args,
50+
...Image.args,
4351
closeButtonProps: { label: 'Close' },
4452
},
4553
};
54+
55+
/**
56+
* Demo a LightBox containing an image slideshow
57+
*/
58+
export const ImageSlideshow = {
59+
args: {
60+
'aria-label': 'Fullscreen image slideshow',
61+
closeButtonProps: { label: 'Close' },
62+
children: (
63+
<Slideshow
64+
aria-label="Image slideshow"
65+
theme="dark"
66+
slideshowControlsProps={{
67+
nextButtonProps: { label: 'Next image' },
68+
previousButtonProps: { label: 'Previous image' },
69+
}}
70+
slideGroupLabel={(currentGroup, totalGroup) => `${currentGroup} of ${totalGroup}`}
71+
>
72+
<SlideshowItem>
73+
<ImageBlock
74+
align="center"
75+
fillHeight
76+
image={LANDSCAPE_IMAGES.landscape1}
77+
alt={LANDSCAPE_IMAGES_ALT.landscape1}
78+
/>
79+
</SlideshowItem>
80+
81+
<SlideshowItem>
82+
<ImageBlock
83+
align="center"
84+
fillHeight
85+
image={LANDSCAPE_IMAGES.landscape2}
86+
alt={LANDSCAPE_IMAGES_ALT.landscape2}
87+
/>
88+
</SlideshowItem>
89+
90+
<SlideshowItem>
91+
<ImageBlock
92+
align="center"
93+
fillHeight
94+
image={LANDSCAPE_IMAGES.landscape3}
95+
alt={LANDSCAPE_IMAGES_ALT.landscape3}
96+
/>
97+
</SlideshowItem>
98+
</Slideshow>
99+
),
100+
},
101+
};

packages/lumx-react/src/components/lightbox/Lightbox.tsx

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { forwardRef, RefObject, useRef, useEffect } from 'react';
1+
import React, { forwardRef, RefObject, useRef, useEffect, AriaAttributes } from 'react';
22

33
import classNames from 'classnames';
44
import { createPortal } from 'react-dom';
@@ -19,14 +19,16 @@ import { useTransitionVisibility } from '@lumx/react/hooks/useTransitionVisibili
1919
/**
2020
* Defines the props of the component.
2121
*/
22-
export interface LightboxProps extends GenericProps, HasTheme {
22+
export interface LightboxProps extends GenericProps, HasTheme, Pick<AriaAttributes, 'aria-label' | 'aria-labelledby'> {
2323
/** Props to pass to the close button (minus those already set by the Lightbox props). */
2424
closeButtonProps?: Pick<IconButtonProps, 'label'> &
2525
Omit<IconButtonProps, 'label' | 'onClick' | 'icon' | 'emphasis' | 'color'>;
2626
/** Whether the component is open or not. */
2727
isOpen?: boolean;
2828
/** Reference to the element that triggered modal opening to set focus on. */
2929
parentElement: RefObject<any>;
30+
/** Reference to the element that should get the focus when the lightbox opens. By default, the close button or the lightbox itself will take focus. */
31+
focusElement?: RefObject<HTMLElement>;
3032
/** Whether to keep the dialog open on clickaway or escape press. */
3133
preventAutoClose?: boolean;
3234
/** Z-axis position. */
@@ -54,13 +56,17 @@ const CLASSNAME = getRootClassName(COMPONENT_NAME);
5456
*/
5557
export const Lightbox: Comp<LightboxProps, HTMLDivElement> = forwardRef((props, ref) => {
5658
const {
57-
ariaLabel,
59+
'aria-labelledby': propAriaLabelledBy,
60+
ariaLabelledBy = propAriaLabelledBy,
61+
'aria-label': propAriaLabel,
62+
ariaLabel = propAriaLabel,
5863
children,
5964
className,
6065
closeButtonProps,
6166
isOpen,
6267
onClose,
6368
parentElement,
69+
focusElement,
6470
preventAutoClose,
6571
theme,
6672
zIndex,
@@ -75,6 +81,8 @@ export const Lightbox: Comp<LightboxProps, HTMLDivElement> = forwardRef((props,
7581
const childrenRef = useRef<any>(null);
7682
// eslint-disable-next-line react-hooks/rules-of-hooks
7783
const wrapperRef = useRef<HTMLDivElement>(null);
84+
// eslint-disable-next-line react-hooks/rules-of-hooks
85+
const closeButtonRef = useRef<HTMLButtonElement>(null);
7886

7987
// eslint-disable-next-line react-hooks/rules-of-hooks
8088
useDisableBodyScroll(isOpen && wrapperRef.current);
@@ -84,7 +92,12 @@ export const Lightbox: Comp<LightboxProps, HTMLDivElement> = forwardRef((props,
8492

8593
// Handle focus trap.
8694
// eslint-disable-next-line react-hooks/rules-of-hooks
87-
useFocusTrap(isOpen && wrapperRef.current, childrenRef.current?.firstChild);
95+
useFocusTrap(
96+
// Focus trap zone
97+
isOpen && wrapperRef.current,
98+
// Focus element (fallback on close button and then on the dialog)
99+
focusElement?.current || closeButtonRef.current || wrapperRef.current,
100+
);
88101

89102
// eslint-disable-next-line react-hooks/rules-of-hooks
90103
const previousOpen = useRef(isOpen);
@@ -116,7 +129,10 @@ export const Lightbox: Comp<LightboxProps, HTMLDivElement> = forwardRef((props,
116129
ref={mergeRefs(ref, wrapperRef)}
117130
{...forwardedProps}
118131
aria-label={ariaLabel}
132+
aria-labelledby={ariaLabelledBy}
119133
aria-modal="true"
134+
role="dialog"
135+
tabIndex={-1}
120136
className={classNames(
121137
className,
122138
handleBasicClasses({
@@ -131,6 +147,7 @@ export const Lightbox: Comp<LightboxProps, HTMLDivElement> = forwardRef((props,
131147
{closeButtonProps && (
132148
<IconButton
133149
{...closeButtonProps}
150+
ref={closeButtonRef}
134151
className={`${CLASSNAME}__close`}
135152
color={ColorPalette.light}
136153
emphasis={Emphasis.low}

packages/lumx-react/src/stories/controls/image.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,12 @@ export const SQUARE_IMAGES = { square1, square2 };
2222
export const SVG_IMAGES = { defaultSvg };
2323
export const EMPTY_IMAGES = { emptyImage };
2424
export const LANDSCAPE_IMAGES = { landscape1, landscape1s200, landscape2, landscape3 };
25+
export const LANDSCAPE_IMAGES_ALT: { [key in keyof typeof LANDSCAPE_IMAGES]: string } = {
26+
landscape1: 'A majestic snowy mountain range with a peak covered in glistening snow',
27+
landscape1s200: 'A majestic snowy mountain range with a peak covered in glistening snow',
28+
landscape2: 'A colorful rack of shirts displaying various hues and styles',
29+
landscape3: 'An open book resting on a table, ready to be explored and read',
30+
};
2531
export const PORTRAIT_IMAGES = { portrait1, portrait1s200, portrait2, portrait3 };
2632
export const IMAGES = {
2733
...LANDSCAPE_IMAGES,

packages/site-demo/content/product/components/lightbox/index.mdx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,18 @@
44

55
<DemoBlock demo="default" />
66

7+
### Accessibility concerns
8+
9+
This component uses the `dialog` accessible pattern, a few specific behaviors are applied:
10+
11+
- When opening, focus will be set either on the provided `focusElement` prop or on the close button (if defined) or on the dialog itself.
12+
Once the focus is set within, it will be trapped and stay within the lightbox until it is closed.
13+
14+
- A label must be defined using either `aria-label` or `aria-labelledby` props.
15+
Setting `aria-labelledby` is recommended as it target a visible label rather than adding an invisible `aria-label` on the dialog.
16+
17+
- When closing, the activating element should take focus. Make sure to fill the `parentElement` prop to make this work properly.
18+
719
### Properties
820

921
<PropTable component="Lightbox" />

yarn.lock

Lines changed: 20 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -6229,9 +6229,9 @@ __metadata:
62296229
"@types/classnames": ^2.2.9
62306230
"@types/jest": ^29.2.1
62316231
"@types/lodash": ^4.14.149
6232-
"@types/react": ^16.9.11
6233-
"@types/react-dom": ^16.9.4
6234-
"@types/react-is": ^16.7.1
6232+
"@types/react": ^17.0.2
6233+
"@types/react-dom": ^17.0.2
6234+
"@types/react-is": ^17.0.2
62356235
autoprefixer: ^9.7.4
62366236
babel-jest: 29.1.2
62376237
babel-loader: ^8.0.6
@@ -8501,21 +8501,12 @@ __metadata:
85018501
languageName: node
85028502
linkType: hard
85038503

8504-
"@types/react-dom@npm:<18.0.0":
8505-
version: 17.0.18
8506-
resolution: "@types/react-dom@npm:17.0.18"
8504+
"@types/react-dom@npm:<18.0.0, @types/react-dom@npm:^17.0.2":
8505+
version: 17.0.25
8506+
resolution: "@types/react-dom@npm:17.0.25"
85078507
dependencies:
85088508
"@types/react": ^17
8509-
checksum: b74525b1a13a0e27fe20859ff7a7e8f7e4581fb9d45ed1b6447ad1534d86f813818353c39d0df2e28f9d2b9be2e3af1908c244b2214a979393d19f217665e614
8510-
languageName: node
8511-
linkType: hard
8512-
8513-
"@types/react-dom@npm:^16.9.4":
8514-
version: 16.9.4
8515-
resolution: "@types/react-dom@npm:16.9.4"
8516-
dependencies:
8517-
"@types/react": "*"
8518-
checksum: 5261c5aeaa86a39a9bcf5997a391c9fb38a00dc4cb64fb025dfc2ec7664ce17621f8f5e79f612734ad7948972b86b096113edff64ecd5b4090d68c773f399239
8509+
checksum: d1e582682478e0848c8d54ea3e89d02047bac6d916266b85ce63731b06987575919653ea7159d98fda47ade3362b8c4d5796831549564b83088e7aa9ce8b60ed
85198510
languageName: node
85208511
linkType: hard
85218512

@@ -8528,44 +8519,34 @@ __metadata:
85288519
languageName: node
85298520
linkType: hard
85308521

8531-
"@types/react-is@npm:^16.7.1":
8532-
version: 16.7.1
8533-
resolution: "@types/react-is@npm:16.7.1"
8522+
"@types/react-is@npm:^17.0.2":
8523+
version: 17.0.7
8524+
resolution: "@types/react-is@npm:17.0.7"
85348525
dependencies:
8535-
"@types/react": "*"
8536-
checksum: 064b0bc6aaf98a87bd8c56f17407e429e675c06ca5fa523102b3f5fc4c53eac08fd27a493605f52e26891fae2dd65e03a475ef729d4ad5b9ae8104b187b3c4a9
8537-
languageName: node
8538-
linkType: hard
8539-
8540-
"@types/react@npm:*, @types/react@npm:^16.9.11":
8541-
version: 16.9.11
8542-
resolution: "@types/react@npm:16.9.11"
8543-
dependencies:
8544-
"@types/prop-types": "*"
8545-
csstype: ^2.2.0
8546-
checksum: 6e4b66a31d9b728f0da6ea64a733dd7403c695c42cd7ff8ca6474dbacc0c314d28c3ef05d8b091f9ffa9090516bca0ddcea5751ae9dbef268d225abe2956b28f
8526+
"@types/react": ^17
8527+
checksum: a8f11067795dbcf54a54d5fdc1977816be155fd04051e850f7c85dbbad83897f846dd3e474d56bd12a7055e0ae1825185f41c6f56342fd5cd31a08df3b3fbfff
85478528
languageName: node
85488529
linkType: hard
85498530

8550-
"@types/react@npm:>=16":
8551-
version: 18.2.9
8552-
resolution: "@types/react@npm:18.2.9"
8531+
"@types/react@npm:*, @types/react@npm:>=16":
8532+
version: 18.2.45
8533+
resolution: "@types/react@npm:18.2.45"
85538534
dependencies:
85548535
"@types/prop-types": "*"
85558536
"@types/scheduler": "*"
85568537
csstype: ^3.0.2
8557-
checksum: f155256171a2d701eb962a1d3aa2a1c9ee36d9dd4a4aecb911d29e50717aab1a76914aef25242665147c455b9e8d081d1a60275d13ca81075c148ebd6607414a
8538+
checksum: 40b256bdce67b026348022b4f8616a693afdad88cf493b77f7b4e6c5f4b0e4ba13a6068e690b9b94572920840ff30d501ea3d8518e1f21cc8fb8204d4b140c8a
85588539
languageName: node
85598540
linkType: hard
85608541

8561-
"@types/react@npm:^17":
8562-
version: 17.0.52
8563-
resolution: "@types/react@npm:17.0.52"
8542+
"@types/react@npm:^17, @types/react@npm:^17.0.2":
8543+
version: 17.0.73
8544+
resolution: "@types/react@npm:17.0.73"
85648545
dependencies:
85658546
"@types/prop-types": "*"
85668547
"@types/scheduler": "*"
85678548
csstype: ^3.0.2
8568-
checksum: a51b98dd87838d161278fdf9dd78e6a4ff8c018f406d6647f77963e144fb52a8beee40c89fd0e7e840eaeaa8bd9fe2f34519410540b1a52d43a6f8b4d2fbce33
8549+
checksum: 08107645acdd734c8ddb4d26f1b43dfa0d75f7a8d268eaacb897337e103eaa620fe8c3c6972dab9860aaa47bbee1da587cf06b11bb4e655588e38485daf48a6c
85698550
languageName: node
85708551
linkType: hard
85718552

@@ -14166,13 +14147,6 @@ __metadata:
1416614147
languageName: node
1416714148
linkType: hard
1416814149

14169-
"csstype@npm:^2.2.0":
14170-
version: 2.6.7
14171-
resolution: "csstype@npm:2.6.7"
14172-
checksum: 4495eba98af6cd9d9c61b4bf8aa1ac457d98455effec77f58f1a88842457cb0ef740152bbaa5d47e05b0b7a41dae64ef978e0e1a17065241b4e5914707e801eb
14173-
languageName: node
14174-
linkType: hard
14175-
1417614150
"csstype@npm:^3.0.2":
1417714151
version: 3.1.1
1417814152
resolution: "csstype@npm:3.1.1"

0 commit comments

Comments
 (0)