@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.
@@ -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()}>Load more</Button>}
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.8rem;
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
- <NodeComponent
97
- key={String(nodeIndex) + node.title}
98
- className="gallery-node"
99
- title={node.title}
100
- aria-label={node.title}
101
- data-node-index={nodeIndex}
102
- onPointerUp={useLightbox ? () => handlePointerUp(node) : undefined}
103
- tabIndex={0}
104
- >
105
- <ImageContainer
106
- className="gallery-node-image"
107
- // eslint-disable-next-line prefer-template
108
- minHeight={String(minHeight) + 'px'}
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
- <Picture
113
- image={node.image}
114
- objectFit="cover"
115
- alt={node.title}
116
- // animate image in on load
117
- onLoad={event => {
118
- event.target
119
- .closest('.gallery-node-image')
120
- .querySelector('img')
121
- .style.setProperty('opacity', '1');
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
- // update tab order once the image has loaded
124
- updateTabOrder();
125
- }}
126
- />
127
- </ImageContainer>
128
- <Details>
129
- <Title>{node.title}</Title>
130
- {node.caption && <Caption>{node.caption}</Caption>}
131
- </Details>
132
- </NodeComponent>
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
- // store the previously focused element
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
- return;
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 onPointerUp={() => handleBackdropClick()} />
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={selectedNode?.title}
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
- <div id="lightboxTitle">{selectedNode?.title}</div>
213
- {selectedNode?.caption && (
214
- <div>
215
- {selectedNode?.caption}
208
+ {selectedNode?.lightboxBody && (
209
+ <div id="lightboxTitle">
210
+ {selectedNode.lightboxBody}
216
211
  </div>
217
212
  )}
218
- {selectedNode?.body && (
213
+ {selectedNode?.lightboxCaption && (
219
214
  <div>
220
- {selectedNode.body}
215
+ {selectedNode?.lightboxCaption}
221
216
  </div>
222
217
  )}
223
218
  </LightboxDetails>