@comicrelief/component-library 8.52.0 → 8.52.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/Molecules/CTA/CTAMultiCard/__snapshots__/CTAMultiCard.test.js.snap +12 -12
- package/dist/components/Molecules/CTA/shared/CTACard.style.js +1 -1
- package/dist/components/Organisms/DynamicGallery/DynamicGallery.js +9 -3
- package/dist/components/Organisms/DynamicGallery/DynamicGallery.style.js +13 -16
- package/dist/components/Organisms/DynamicGallery/_DynamicGalleryColumn.js +37 -28
- package/dist/components/Organisms/DynamicGallery/_Lightbox.js +14 -18
- package/dist/components/Organisms/DynamicGallery/__snapshots__/DynamicGallery.test.js.snap +64 -111
- package/dist/components/Organisms/DynamicGallery/_types.js +4 -3
- package/dist/components/Organisms/DynamicGallery/_utils.js +29 -3
- package/package.json +1 -1
- package/src/components/Molecules/CTA/CTAMultiCard/__snapshots__/CTAMultiCard.test.js.snap +12 -12
- package/src/components/Molecules/CTA/shared/CTACard.style.js +1 -1
- package/src/components/Organisms/DynamicGallery/DynamicGallery.js +8 -2
- package/src/components/Organisms/DynamicGallery/DynamicGallery.style.js +7 -15
- package/src/components/Organisms/DynamicGallery/_DynamicGalleryColumn.js +49 -41
- package/src/components/Organisms/DynamicGallery/_Lightbox.js +21 -26
- package/src/components/Organisms/DynamicGallery/__snapshots__/DynamicGallery.test.js.snap +64 -111
- package/src/components/Organisms/DynamicGallery/_types.js +4 -3
- package/src/components/Organisms/DynamicGallery/_utils.js +40 -3
|
@@ -98,6 +98,7 @@ const DynamicGallery = ({
|
|
|
98
98
|
|
|
99
99
|
// handle selected gallery node
|
|
100
100
|
const [selectedNode, setSelectedNode] = useState(null);
|
|
101
|
+
const [focusedNode, setFocusedNode] = useState(null);
|
|
101
102
|
|
|
102
103
|
// handle next/previous node events from the lightbox
|
|
103
104
|
function handleNextNode(node) {
|
|
@@ -122,6 +123,8 @@ const DynamicGallery = ({
|
|
|
122
123
|
const nodeIndex = +event.target.dataset.nodeIndex;
|
|
123
124
|
if (Number.isNaN(nodeIndex)) return;
|
|
124
125
|
setSelectedNode(nodes[nodeIndex]);
|
|
126
|
+
// also store the focused node for focus restoration when the lightbox closes
|
|
127
|
+
setFocusedNode(event.target.closest('.gallery-node'));
|
|
125
128
|
}
|
|
126
129
|
break;
|
|
127
130
|
}
|
|
@@ -188,7 +191,9 @@ const DynamicGallery = ({
|
|
|
188
191
|
selectedNode,
|
|
189
192
|
setSelectedNode,
|
|
190
193
|
nextNode: handleNextNode,
|
|
191
|
-
previousNode: handlePreviousNode
|
|
194
|
+
previousNode: handlePreviousNode,
|
|
195
|
+
focusedNode,
|
|
196
|
+
setFocusedNode
|
|
192
197
|
}}
|
|
193
198
|
>
|
|
194
199
|
<ImageGrid className="gallery-grid" onKeyDown={event => handleKeyDown(event)}>
|
|
@@ -206,6 +211,7 @@ const DynamicGallery = ({
|
|
|
206
211
|
nodes={nodes.slice(0, imageCount)}
|
|
207
212
|
imageRatio={imageRatio}
|
|
208
213
|
updateTabOrder={throttledUpdateTabOrder.current}
|
|
214
|
+
focusOutlineColour={textColour}
|
|
209
215
|
/>
|
|
210
216
|
))}
|
|
211
217
|
|
|
@@ -215,7 +221,7 @@ const DynamicGallery = ({
|
|
|
215
221
|
{/* eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex */}
|
|
216
222
|
<div className="gallery-focus-trap" tabIndex={0} />
|
|
217
223
|
</LightboxContext.Provider>
|
|
218
|
-
{imageCount < nodes.length && <Button onClick={() => handleLoadMore()}>
|
|
224
|
+
{imageCount < nodes.length && <Button onClick={() => handleLoadMore()}>Show more</Button>}
|
|
219
225
|
</Container>
|
|
220
226
|
);
|
|
221
227
|
};
|
|
@@ -39,12 +39,18 @@ export const EmptyMessage = styled.div`
|
|
|
39
39
|
const GalleryNodeBase = css`
|
|
40
40
|
display: flex;
|
|
41
41
|
flex-direction: column;
|
|
42
|
-
gap: 0.
|
|
42
|
+
gap: 0.9rem;
|
|
43
43
|
padding: 0;
|
|
44
44
|
margin: 0;
|
|
45
45
|
background: none;
|
|
46
46
|
border: none;
|
|
47
47
|
text-align: left;
|
|
48
|
+
|
|
49
|
+
&:focus-visible {
|
|
50
|
+
outline: 2px solid ${({ focusOutlineColour }) => focusOutlineColour};
|
|
51
|
+
outline-offset: 0.5rem;
|
|
52
|
+
border-radius: 1rem;
|
|
53
|
+
}
|
|
48
54
|
`;
|
|
49
55
|
|
|
50
56
|
export const GalleryNode = styled.div`
|
|
@@ -60,10 +66,6 @@ export const InteractiveGalleryNode = styled.button`
|
|
|
60
66
|
transition: all 0.1s ease-out;
|
|
61
67
|
}
|
|
62
68
|
|
|
63
|
-
&:focus-visible {
|
|
64
|
-
outline: 2px solid #000000;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
69
|
& > div:first-child {
|
|
68
70
|
&:hover {
|
|
69
71
|
box-shadow: 0px 3px 10px 0px rgba(0, 0, 0, 0.4);
|
|
@@ -95,13 +97,3 @@ export const Details = styled.div`
|
|
|
95
97
|
gap: 0.5rem;
|
|
96
98
|
padding: 0 1rem;
|
|
97
99
|
`;
|
|
98
|
-
|
|
99
|
-
export const Title = styled.div`
|
|
100
|
-
&:first-child {
|
|
101
|
-
margin-bottom: 0;
|
|
102
|
-
}
|
|
103
|
-
`;
|
|
104
|
-
|
|
105
|
-
export const Caption = styled.div`
|
|
106
|
-
line-height: 1;
|
|
107
|
-
`;
|
|
@@ -10,21 +10,21 @@ import React, {
|
|
|
10
10
|
import Picture from '../../Atoms/Picture/Picture';
|
|
11
11
|
import { LightboxContext } from './_Lightbox';
|
|
12
12
|
import {
|
|
13
|
-
Caption,
|
|
14
13
|
Column,
|
|
15
14
|
Details,
|
|
16
15
|
GalleryNode,
|
|
17
16
|
ImageContainer,
|
|
18
|
-
InteractiveGalleryNode
|
|
19
|
-
Title
|
|
17
|
+
InteractiveGalleryNode
|
|
20
18
|
} from './DynamicGallery.style';
|
|
21
19
|
import { GalleryNodeType } from './_types';
|
|
20
|
+
import { extractNodeText } from './_utils';
|
|
22
21
|
|
|
23
22
|
/**
|
|
24
23
|
* a separate component to handle columns of images;
|
|
25
24
|
* this component handles aspect ratio calculations to enfore a min/max ratio for its images
|
|
26
25
|
*/
|
|
27
26
|
export default function DynamicGalleryColumn({
|
|
27
|
+
focusOutlineColour,
|
|
28
28
|
updateTabOrder,
|
|
29
29
|
nodes,
|
|
30
30
|
imageRatio,
|
|
@@ -92,45 +92,52 @@ export default function DynamicGalleryColumn({
|
|
|
92
92
|
<Column ref={elRef} className="gallery-column">
|
|
93
93
|
{nodes
|
|
94
94
|
?.filter((_, nodeIndex) => nodeIndex % columnCount === columnIndex)
|
|
95
|
-
.map((node, nodeIndex) =>
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
// eslint-disable-next-line prefer-template
|
|
110
|
-
maxHeight={String(maxHeight) + 'px'}
|
|
95
|
+
.map((node, nodeIndex) => {
|
|
96
|
+
const bodyText = extractNodeText(node.gridBody);
|
|
97
|
+
const key = String(nodeIndex) + bodyText;
|
|
98
|
+
return (
|
|
99
|
+
<NodeComponent
|
|
100
|
+
key={key}
|
|
101
|
+
className="gallery-node"
|
|
102
|
+
caption={bodyText}
|
|
103
|
+
aria-label={bodyText}
|
|
104
|
+
title={bodyText}
|
|
105
|
+
data-node-index={nodeIndex}
|
|
106
|
+
focusOutlineColour={focusOutlineColour}
|
|
107
|
+
onPointerUp={useLightbox ? () => handlePointerUp(node) : undefined}
|
|
108
|
+
tabIndex={0}
|
|
111
109
|
>
|
|
112
|
-
<
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
110
|
+
<ImageContainer
|
|
111
|
+
className="gallery-node-image"
|
|
112
|
+
// eslint prefers template literals for strings, but they break the compiler
|
|
113
|
+
// eslint-disable-next-line prefer-template
|
|
114
|
+
minHeight={String(minHeight) + 'px'}
|
|
115
|
+
// eslint-disable-next-line prefer-template
|
|
116
|
+
maxHeight={String(maxHeight) + 'px'}
|
|
117
|
+
>
|
|
118
|
+
<Picture
|
|
119
|
+
image={node.image}
|
|
120
|
+
objectFit="cover"
|
|
121
|
+
alt={bodyText}
|
|
122
|
+
// animate image in on load
|
|
123
|
+
onLoad={event => {
|
|
124
|
+
event.target
|
|
125
|
+
.closest('.gallery-node-image')
|
|
126
|
+
.querySelector('img')
|
|
127
|
+
.style.setProperty('opacity', '1');
|
|
122
128
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
129
|
+
// update tab order once the image has loaded
|
|
130
|
+
updateTabOrder();
|
|
131
|
+
}}
|
|
132
|
+
/>
|
|
133
|
+
</ImageContainer>
|
|
134
|
+
<Details>
|
|
135
|
+
{node.gridBody && <div>{node.gridBody}</div>}
|
|
136
|
+
{node.gridCaption && <div>{node.gridCaption}</div>}
|
|
137
|
+
</Details>
|
|
138
|
+
</NodeComponent>
|
|
139
|
+
);
|
|
140
|
+
})}
|
|
134
141
|
</Column>
|
|
135
142
|
);
|
|
136
143
|
}
|
|
@@ -140,5 +147,6 @@ DynamicGalleryColumn.propTypes = {
|
|
|
140
147
|
imageRatio: PropTypes.oneOf(['dynamic', '4:3']),
|
|
141
148
|
columnIndex: PropTypes.number,
|
|
142
149
|
columnCount: PropTypes.number,
|
|
143
|
-
updateTabOrder: PropTypes.func
|
|
150
|
+
updateTabOrder: PropTypes.func,
|
|
151
|
+
focusOutlineColour: PropTypes.string
|
|
144
152
|
};
|
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
ScreenReaderOnly
|
|
20
20
|
} from './_Lightbox.style';
|
|
21
21
|
import ScrollFix from './_ScrollFix';
|
|
22
|
+
import { extractNodeText } from './_utils';
|
|
22
23
|
|
|
23
24
|
/**
|
|
24
25
|
* lightbox context:
|
|
@@ -59,12 +60,13 @@ const Lightbox = () => {
|
|
|
59
60
|
selectedNode,
|
|
60
61
|
setSelectedNode,
|
|
61
62
|
nextNode,
|
|
62
|
-
previousNode
|
|
63
|
+
previousNode,
|
|
64
|
+
focusedNode,
|
|
65
|
+
setFocusedNode
|
|
63
66
|
} = useContext(LightboxContext);
|
|
64
67
|
|
|
65
68
|
const hasNode = Boolean(selectedNode);
|
|
66
69
|
const dialogRef = useRef(null);
|
|
67
|
-
const previousFocusRef = useRef(null);
|
|
68
70
|
|
|
69
71
|
/**
|
|
70
72
|
* handle keyboard events within the lightbox;
|
|
@@ -125,30 +127,20 @@ const Lightbox = () => {
|
|
|
125
127
|
|
|
126
128
|
// handle focus management when dialog opens/closes
|
|
127
129
|
useEffect(() => {
|
|
128
|
-
// when the lightbox is opened, store the previously focused element
|
|
129
|
-
// and move focus to the first focusable element in the dialog
|
|
130
130
|
if (hasNode) {
|
|
131
|
-
//
|
|
132
|
-
previousFocusRef.current = document.activeElement;
|
|
133
|
-
// move focus to the first focusable element in the dialog
|
|
131
|
+
// move focus to the first focusable element in the dialog when it opens
|
|
134
132
|
setTimeout(() => {
|
|
135
133
|
const focusableElements = getFocusableElements(dialogRef.current);
|
|
136
134
|
if (focusableElements.length > 0) {
|
|
137
135
|
focusableElements[0].focus();
|
|
138
136
|
}
|
|
139
137
|
}, 0);
|
|
140
|
-
|
|
138
|
+
} else {
|
|
139
|
+
// restore focus to the previously focused element when lightbox closes
|
|
140
|
+
focusedNode?.focus();
|
|
141
|
+
setFocusedNode(null);
|
|
141
142
|
}
|
|
142
|
-
|
|
143
|
-
// when the lightbox is closed, restore focus to the previously focused element
|
|
144
|
-
if (
|
|
145
|
-
previousFocusRef.current
|
|
146
|
-
&& typeof previousFocusRef.current.focus === 'function'
|
|
147
|
-
) {
|
|
148
|
-
previousFocusRef.current.focus();
|
|
149
|
-
previousFocusRef.current = null;
|
|
150
|
-
}
|
|
151
|
-
}, [hasNode]);
|
|
143
|
+
}, [hasNode, focusedNode, setFocusedNode]);
|
|
152
144
|
|
|
153
145
|
/**
|
|
154
146
|
* close the lightbox when the backdrop is clicked
|
|
@@ -182,9 +174,13 @@ const Lightbox = () => {
|
|
|
182
174
|
target.style.opacity = '1';
|
|
183
175
|
}
|
|
184
176
|
|
|
177
|
+
const bodyText = extractNodeText(selectedNode?.lightboxBody);
|
|
178
|
+
|
|
185
179
|
return (
|
|
186
180
|
<Container isOpen={hasNode}>
|
|
187
|
-
<Backdrop
|
|
181
|
+
<Backdrop
|
|
182
|
+
onPointerUp={() => handleBackdropClick()}
|
|
183
|
+
/>
|
|
188
184
|
<Dialog
|
|
189
185
|
ref={dialogRef}
|
|
190
186
|
aria-labelledby="lightboxTitle"
|
|
@@ -199,7 +195,7 @@ const Lightbox = () => {
|
|
|
199
195
|
{hasNode && (
|
|
200
196
|
<Picture
|
|
201
197
|
key={selectedNode?.image}
|
|
202
|
-
alt={
|
|
198
|
+
alt={bodyText}
|
|
203
199
|
image={selectedNode?.image}
|
|
204
200
|
width={imageDimensions.width}
|
|
205
201
|
height={imageDimensions.height}
|
|
@@ -209,15 +205,14 @@ const Lightbox = () => {
|
|
|
209
205
|
)}
|
|
210
206
|
</LightboxImage>
|
|
211
207
|
<LightboxDetails id="lightboxDescription" aria-live="polite" aria-atomic="true">
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
{selectedNode?.caption}
|
|
208
|
+
{selectedNode?.lightboxBody && (
|
|
209
|
+
<div id="lightboxTitle">
|
|
210
|
+
{selectedNode.lightboxBody}
|
|
216
211
|
</div>
|
|
217
212
|
)}
|
|
218
|
-
{selectedNode?.
|
|
213
|
+
{selectedNode?.lightboxCaption && (
|
|
219
214
|
<div>
|
|
220
|
-
{selectedNode
|
|
215
|
+
{selectedNode?.lightboxCaption}
|
|
221
216
|
</div>
|
|
222
217
|
)}
|
|
223
218
|
</LightboxDetails>
|