@comicrelief/component-library 8.53.2 → 8.53.3

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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@comicrelief/component-library",
3
3
  "author": "Comic Relief Engineering Team",
4
- "version": "8.53.2",
4
+ "version": "8.53.3",
5
5
  "main": "dist/index.js",
6
6
  "license": "ISC",
7
7
  "jest": {
@@ -1,9 +1,230 @@
1
1
  const { test, expect } = require('@playwright/test');
2
2
 
3
+ function hexToRgb(hex) {
4
+ // Remove the '#' if it's included in the input
5
+ const hexValue = hex.replace(/^#/, '');
6
+
7
+ // Parse the hex values into separate R, G, and B values
8
+ const red = parseInt(hexValue.substring(0, 2), 16);
9
+ const green = parseInt(hexValue.substring(2, 4), 16);
10
+ const blue = parseInt(hexValue.substring(4, 6), 16);
11
+
12
+ return `rgb(${red}, ${green}, ${blue})`;
13
+ }
14
+
3
15
  test.describe('dynamic gallery component', () => {
4
- test('dynamic gallery', async ({ page }) => {
16
+ test('smoke test', async ({ page }) => {
5
17
  await page.goto('/#dynamicgallery');
6
18
 
7
19
  await page.close();
8
20
  });
21
+
22
+ test('gallery column props', async ({ page }) => {
23
+ await page.goto('/#!/DynamicGallery/5');
24
+
25
+ // expect three gallery columns
26
+ await expect(page.locator('.gallery-column')).toHaveCount(4);
27
+
28
+ await page.close();
29
+ });
30
+
31
+ test('responsive gallery columns', async ({ page }) => {
32
+ await page.goto('/#!/DynamicGallery/3');
33
+
34
+ // expect three gallery columns
35
+ await expect(page.locator('.gallery-column')).toHaveCount(3);
36
+
37
+ // resize the page to a small width
38
+ await page.setViewportSize({ width: 700, height: 1000 });
39
+
40
+ // expect one gallery column
41
+ await expect(page.locator('.gallery-column')).toHaveCount(2);
42
+
43
+ // resize the page to a medium width
44
+ await page.setViewportSize({ width: 320, height: 1000 });
45
+
46
+ // expect one gallery column
47
+ await expect(page.locator('.gallery-column')).toHaveCount(1);
48
+
49
+ await page.close();
50
+ });
51
+
52
+ test('chunk mode test', async ({ page }) => {
53
+ await page.goto('/#!/DynamicGallery/3');
54
+
55
+ // expect 25 images to be visible
56
+ await expect(page.locator('.gallery-node')).toHaveCount(25);
57
+
58
+ // find the "load more" button and click it
59
+ await page.locator('button:has-text("Show more")').click();
60
+
61
+ // expect 50 images to be visible
62
+ await expect(page.locator('.gallery-node')).toHaveCount(50);
63
+
64
+ // expect the "load more" button to be hidden
65
+ await expect(page.locator('button:has-text("Show more")')).toBeHidden();
66
+
67
+ await page.close();
68
+ });
69
+
70
+ test('non-chunk mode test', async ({ page }) => {
71
+ await page.goto('/#!/DynamicGallery/5');
72
+
73
+ // expect all 30 images to be visible
74
+ await expect(page.locator('.gallery-node')).toHaveCount(30);
75
+
76
+ // expect the "load more" button to be hidden
77
+ await expect(page.locator('button:has-text("Show more")')).toBeHidden();
78
+
79
+ await page.close();
80
+ });
81
+
82
+ test('gallery tabbing', async ({ page }) => {
83
+ await page.goto('/#!/DynamicGallery/3');
84
+
85
+ // focus the first gallery node
86
+ await page.locator('.gallery-node').first().focus();
87
+
88
+ await page.waitForTimeout(3000);
89
+
90
+ // first tab should focus the first node in the first column
91
+ const firstNode = page.locator('.gallery-column').first().locator('.gallery-node').first();
92
+ await firstNode.focus();
93
+ await expect(firstNode).toBeFocused();
94
+
95
+ // tab to the first node in the second column
96
+ await page.keyboard.press('Tab');
97
+ const secondNode = page.locator('.gallery-column').nth(1).locator('.gallery-node').first();
98
+ await expect(secondNode).toBeFocused();
99
+
100
+ // tab back to the first node in the first column
101
+ await page.keyboard.press('Shift+Tab');
102
+ await expect(firstNode).toBeFocused();
103
+
104
+ await page.close();
105
+ });
106
+
107
+ test('gallery tabbing should allow tabbing out of the gallery', async ({ page }) => {
108
+ await page.goto('/#!/DynamicGallery/5');
109
+
110
+ await page.waitForTimeout(3000);
111
+
112
+ // focus the first gallery node
113
+ const galleryNodes = await page.locator('.gallery-node').all();
114
+ await page.locator(`[data-node-index="${galleryNodes.length - 1}"]`).focus();
115
+
116
+ // press tab
117
+ await page.keyboard.press('Tab');
118
+ const galleryContainer = page.locator('.gallery-container');
119
+
120
+ // asset that the focus has moved outside the gallery
121
+ expect(
122
+ await galleryContainer.evaluate(
123
+ el => !el.contains(document.activeElement)
124
+ )
125
+ ).toBe(true);
126
+ await page.close();
127
+ });
128
+
129
+ test('custom page background and text colour', async ({ page }) => {
130
+ await page.goto('/#!/DynamicGallery/5');
131
+
132
+ const galleryContainer = page.locator('.gallery-container');
133
+
134
+ const backgroundColor = await galleryContainer.evaluate(el => window.getComputedStyle(el).getPropertyValue('background-color'));
135
+ expect(backgroundColor).toBe(hexToRgb('#0565D1'));
136
+
137
+ const textColor = await galleryContainer.evaluate(el => window.getComputedStyle(el).getPropertyValue('color'));
138
+ expect(textColor).toBe(hexToRgb('#FFFFFF'));
139
+
140
+ await page.close();
141
+ });
142
+
143
+ test('check lightbox mode', async ({ page }) => {
144
+ await page.goto('/#!/DynamicGallery/3');
145
+
146
+ // find the first gallery node
147
+ const galleryNode = page.locator('.gallery-node').first();
148
+
149
+ // click it
150
+ await galleryNode.click();
151
+
152
+ // expect the lightbox to be visible
153
+ await expect(page.locator('dialog')).toBeVisible();
154
+
155
+ await page.waitForTimeout(1000);
156
+ await page.keyboard.press('Escape');
157
+ await expect(page.locator('dialog')).toBeHidden();
158
+
159
+ // focus the gallery node and press enter
160
+ await galleryNode.focus();
161
+ await page.keyboard.press('Enter');
162
+
163
+ // expect the lightbox to be visible
164
+ await expect(page.locator('dialog')).toBeVisible();
165
+
166
+ await page.close();
167
+ });
168
+
169
+ test('lightbox navigation', async ({ page }) => {
170
+ await page.goto('/#!/DynamicGallery/3');
171
+
172
+ // find the first gallery node
173
+ const galleryNode = page.locator('.gallery-node').first();
174
+
175
+ // click it
176
+ await galleryNode.click();
177
+
178
+ // expect the lightbox and caption to be visible
179
+ await expect(page.locator('dialog')).toBeVisible();
180
+ await expect(page.getByText('Lightbox: This is the body for image 0')).toBeVisible();
181
+
182
+ await page.waitForTimeout(1000);
183
+
184
+ // navigate to the next image
185
+ await page.keyboard.press('ArrowRight');
186
+ await expect(page.getByText('Lightbox: This is the body for image 1')).toBeVisible();
187
+
188
+ await page.close();
189
+ });
190
+
191
+ test('lightbox pointer close', async ({ page }) => {
192
+ await page.goto('/#!/DynamicGallery/3');
193
+
194
+ // find the first gallery node
195
+ const galleryNode = page.locator('.gallery-node').first();
196
+
197
+ // click it
198
+ await galleryNode.click();
199
+
200
+ // click the close button
201
+ await page.locator('.close-button').click();
202
+
203
+ // expect the lightbox to be hidden
204
+ await expect(page.locator('dialog')).toBeHidden();
205
+
206
+ await page.close();
207
+ });
208
+
209
+ test('check non-lightbox mode', async ({ page }) => {
210
+ await page.goto('/#!/DynamicGallery/5');
211
+
212
+ // find the first gallery node
213
+ const galleryNode = page.locator('.gallery-node').first();
214
+
215
+ // click it
216
+ await galleryNode.click();
217
+
218
+ // expect the lightbox to be hidden
219
+ await expect(page.locator('dialog')).toBeHidden();
220
+
221
+ // focus the gallery node and press enter
222
+ await galleryNode.focus();
223
+ await page.keyboard.press('Enter');
224
+
225
+ // expect the lightbox to be hidden
226
+ await expect(page.locator('dialog')).toBeHidden();
227
+
228
+ await page.close();
229
+ });
9
230
  });
@@ -109,6 +109,7 @@ const Picture = ({
109
109
  {...rest}
110
110
  >
111
111
  <Image
112
+ key={image}
112
113
  alt={alt}
113
114
  height={height}
114
115
  width={width}
@@ -12,7 +12,8 @@ import Lightbox, { LightboxContext } from './_Lightbox';
12
12
  import {
13
13
  Container,
14
14
  EmptyMessage,
15
- ImageGrid
15
+ ImageGrid,
16
+ FocusTrap
16
17
  } from './DynamicGallery.style';
17
18
  import DynamicGalleryColumn from './_DynamicGalleryColumn';
18
19
  import { GalleryNodeType } from './_types';
@@ -58,8 +59,9 @@ const DynamicGallery = ({
58
59
  const { top, left } = node.getBoundingClientRect();
59
60
  return floor(top, -2) + Math.floor(left) / 1000;
60
61
  }, 'asc');
62
+
61
63
  sortedNodes.forEach((galleryNode, index) => {
62
- galleryNode.setAttribute('data-order', String(index));
64
+ galleryNode.setAttribute('data-tab-order', String(index));
63
65
  });
64
66
  }
65
67
  // create a throttled version of the updateTabOrder function
@@ -100,16 +102,30 @@ const DynamicGallery = ({
100
102
  const [selectedNode, setSelectedNode] = useState(null);
101
103
  const [focusedNode, setFocusedNode] = useState(null);
102
104
 
103
- // handle next/previous node events from the lightbox
105
+ // handle next/previous node events from the lightbox;
106
+ // slightly complicated because we need to use the data-tab-order attribute
107
+ // to navigate between nodes rather than the node index;
108
+ // this reflects the tab ordering in the DOM, rather than the order of the nodes in the array,
109
+ // because the dynamic image heights can confuse the normal order
104
110
  function handleNextNode(node) {
105
111
  const nodeIndex = nodes.indexOf(node);
106
- const nextNodeIndex = (nodeIndex + 1) % imageCount;
107
- setSelectedNode(nodes[nextNodeIndex]);
112
+ const nodeEl = containerRef.current.querySelector(`[data-node-index="${nodeIndex}"]`);
113
+ const tabOrder = nodeEl.getAttribute('data-tab-order');
114
+ const nextTabOrder = (+tabOrder + 1) % imageCount;
115
+ const nextNodeEl = containerRef.current.querySelector(`[data-tab-order="${nextTabOrder}"]`);
116
+ const nextNodeIndex = nextNodeEl.getAttribute('data-node-index');
117
+ const nextNode = nodes[nextNodeIndex];
118
+ setSelectedNode(nextNode);
108
119
  }
109
120
  function handlePreviousNode(node) {
110
121
  const nodeIndex = nodes.indexOf(node);
111
- const previousNodeIndex = (nodeIndex - 1 + imageCount) % imageCount;
112
- setSelectedNode(nodes[previousNodeIndex]);
122
+ const nodeEl = containerRef.current.querySelector(`[data-node-index="${nodeIndex}"]`);
123
+ const tabOrder = nodeEl.getAttribute('data-tab-order');
124
+ const previousTabOrder = (+tabOrder - 1 + imageCount) % imageCount;
125
+ const previousNodeEl = containerRef.current.querySelector(`[data-tab-order="${previousTabOrder}"]`);
126
+ const previousNodeIndex = previousNodeEl.getAttribute('data-node-index');
127
+ const previousNode = nodes[previousNodeIndex];
128
+ setSelectedNode(previousNode);
113
129
  }
114
130
 
115
131
  // handle keydown events,
@@ -139,7 +155,7 @@ const DynamicGallery = ({
139
155
  // - flex-column+order > no gaps but complex (https://mui.com/material-ui/react-masonry/)
140
156
  // - columns + custom tabbing > what we're doing here
141
157
  case 'Tab': {
142
- const nodeIndex = +event.target.dataset.order;
158
+ const nodeIndex = +event.target.dataset.tabOrder;
143
159
  if (Number.isNaN(nodeIndex)) return;
144
160
  const galleryContainer = event.target.closest('.gallery-container');
145
161
  if (!galleryContainer) return;
@@ -151,7 +167,7 @@ const DynamicGallery = ({
151
167
  newNodeIndex = nodeIndex - 1;
152
168
  if (newNodeIndex < 0) return;
153
169
  event.preventDefault();
154
- galleryContainer.querySelector(`[data-order="${newNodeIndex}"]`).focus();
170
+ galleryContainer.querySelector(`[data-tab-order="${newNodeIndex}"]`).focus();
155
171
  } else {
156
172
  // tab: move to the next image
157
173
  newNodeIndex = nodeIndex + 1;
@@ -166,7 +182,7 @@ const DynamicGallery = ({
166
182
  return;
167
183
  }
168
184
  event.preventDefault();
169
- galleryContainer.querySelector(`[data-order="${newNodeIndex}"]`).focus();
185
+ galleryContainer.querySelector(`[data-tab-order="${newNodeIndex}"]`).focus();
170
186
  }
171
187
  break;
172
188
  }
@@ -200,26 +216,33 @@ const DynamicGallery = ({
200
216
  {hasNodes
201
217
  && Array(columnCount)
202
218
  .fill(null)
203
- .map((column, columnIndex) => (
204
- <DynamicGalleryColumn
205
- // disabling the lint rule here
206
- // as we're chunking an array and have no unique keys
207
- // eslint-disable-next-line react/no-array-index-key
208
- key={columnIndex}
209
- columnIndex={columnIndex}
210
- columnCount={columnCount}
211
- nodes={nodes.slice(0, imageCount)}
212
- imageRatio={imageRatio}
213
- updateTabOrder={throttledUpdateTabOrder.current}
214
- focusOutlineColour={textColour}
215
- />
216
- ))}
219
+ .map((column, columnIndex) => {
220
+ // eslint prefers template literals for strings, but they break the compiler
221
+ // eslint-disable-next-line prefer-template
222
+ const key = String(columnIndex) + ':' + nodes.length;
223
+ return (
224
+ <DynamicGalleryColumn
225
+ // disabling the lint rule here
226
+ // as we're chunking an array and have no unique keys
227
+ // eslint-disable-next-line react/no-array-index-key
228
+ key={key}
229
+ columnIndex={columnIndex}
230
+ columnCount={columnCount}
231
+ nodes={nodes.slice(0, imageCount)}
232
+ imageRatio={imageRatio}
233
+ updateTabOrder={throttledUpdateTabOrder.current}
234
+ focusOutlineColour={textColour}
235
+ />
236
+ );
237
+ })}
217
238
 
218
239
  <EmptyMessage isEmpty={!hasNodes}>No images to display</EmptyMessage>
219
240
  </ImageGrid>
220
241
  <Lightbox />
221
242
  {/* eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex */}
222
- <div className="gallery-focus-trap" tabIndex={0} />
243
+ <FocusTrap className="gallery-focus-trap" tabIndex={0}>
244
+ <span>End of gallery</span>
245
+ </FocusTrap>
223
246
  </LightboxContext.Provider>
224
247
  {imageCount < nodes.length && <Button onClick={() => handleLoadMore()}>Show more</Button>}
225
248
  </Container>
@@ -18,7 +18,7 @@ import createMockGalleryNodes from './_utils';
18
18
  ```js
19
19
  const defaultData = require('../../../styleguide/data/data').defaultData;
20
20
  import createMockGalleryNodes from './_utils';
21
- <DynamicGallery gridWidth={4} nodes={createMockGalleryNodes(4)} loadingBehaviour="all" imageRatio="4:3" pageBackgroundColour="blue" textColour="white" paddingTop="6rem" paddingBottom="6rem" useLightbox={false} />;
21
+ <DynamicGallery gridWidth={4} nodes={createMockGalleryNodes(30)} loadingBehaviour="all" imageRatio="4:3" pageBackgroundColour="blue" textColour="white" paddingTop="6rem" paddingBottom="6rem" useLightbox={false} />;
22
22
  ```
23
23
 
24
24
  ### Gallery with max 5 columns
@@ -5,10 +5,15 @@ export const Container = styled.div`
5
5
  flex-direction: column;
6
6
  align-items: center;
7
7
  gap: 1rem;
8
+ position: relative;
8
9
  max-width: ${({ maxWidth }) => maxWidth};
9
10
  background: ${({ theme, pageBackgroundColour }) => theme.color(pageBackgroundColour)};
10
- ${({ paddingTop, paddingBottom }) => css`padding: ${paddingTop} 2rem ${paddingBottom};`}
11
+ padding: ${({ paddingTop, paddingBottom }) => `${paddingTop} 1rem ${paddingBottom}`};
11
12
  color: ${({ theme, textColour }) => theme.color(textColour)};
13
+
14
+ @media ${({ theme }) => theme.breakpoints2026('M')} {
15
+ gap: 1rem;
16
+ }
12
17
  `;
13
18
 
14
19
  export const ImageGrid = styled.div`
@@ -97,3 +102,20 @@ export const Details = styled.div`
97
102
  gap: 0.5rem;
98
103
  padding: 0 1rem;
99
104
  `;
105
+
106
+ export const ScreenReaderOnly = styled.span`
107
+ position: absolute;
108
+ width: 1px;
109
+ height: 1px;
110
+ margin: -1px;
111
+ border: 0;
112
+ padding: 0;
113
+ white-space: nowrap;
114
+ clip-path: inset(100%);
115
+ clip: rect(0 0 0 0);
116
+ overflow: hidden;
117
+ `;
118
+
119
+ export const FocusTrap = styled(ScreenReaderOnly)`
120
+ bottom: 0;
121
+ `;
@@ -91,10 +91,16 @@ export default function DynamicGalleryColumn({
91
91
  return (
92
92
  <Column ref={elRef} className="gallery-column">
93
93
  {nodes
94
- ?.filter((_, nodeIndex) => nodeIndex % columnCount === columnIndex)
95
94
  .map((node, nodeIndex) => {
95
+ // only render nodes that are in the current column;
96
+ // this lets us assign a unique index to each node
97
+ const columnNodeIndex = nodeIndex % columnCount;
98
+ if (columnNodeIndex !== columnIndex) return null;
99
+
96
100
  const bodyText = extractNodeText(node.gridBody);
97
- const key = String(nodeIndex) + bodyText;
101
+ // eslint prefers template literals for strings, but they break the compiler
102
+ // eslint-disable-next-line prefer-template
103
+ const key = String(nodeIndex) + ':' + bodyText + ':' + node.image;
98
104
  return (
99
105
  <NodeComponent
100
106
  key={key}
@@ -109,7 +115,6 @@ export default function DynamicGalleryColumn({
109
115
  >
110
116
  <ImageContainer
111
117
  className="gallery-node-image"
112
- // eslint prefers template literals for strings, but they break the compiler
113
118
  // eslint-disable-next-line prefer-template
114
119
  minHeight={String(minHeight) + 'px'}
115
120
  // eslint-disable-next-line prefer-template
@@ -15,9 +15,9 @@ import {
15
15
  LightboxImage,
16
16
  LightboxSpinner,
17
17
  NextButton,
18
- PreviousButton,
19
- ScreenReaderOnly
18
+ PreviousButton
20
19
  } from './_Lightbox.style';
20
+ import { ScreenReaderOnly } from './DynamicGallery.style';
21
21
  import ScrollFix from './_ScrollFix';
22
22
  import { extractNodeText } from './_utils';
23
23
 
@@ -68,6 +68,20 @@ const Lightbox = () => {
68
68
  const hasNode = Boolean(selectedNode);
69
69
  const dialogRef = useRef(null);
70
70
 
71
+ // handle interaction type
72
+ const interactionTypeRef = useRef('keyboard');
73
+
74
+ useEffect(() => {
75
+ function handlePointerDown() {
76
+ interactionTypeRef.current = 'pointer';
77
+ document.removeEventListener('pointerdown', handlePointerDown);
78
+ }
79
+ document.addEventListener('pointerdown', handlePointerDown);
80
+ return () => {
81
+ document.removeEventListener('pointerdown', handlePointerDown);
82
+ };
83
+ }, []);
84
+
71
85
  /**
72
86
  * handle keyboard events within the lightbox;
73
87
  * - trapped focus between UI elements
@@ -128,13 +142,15 @@ const Lightbox = () => {
128
142
  // handle focus management when dialog opens/closes
129
143
  useEffect(() => {
130
144
  if (hasNode) {
131
- // move focus to the first focusable element in the dialog when it opens
132
- setTimeout(() => {
133
- const focusableElements = getFocusableElements(dialogRef.current);
134
- if (focusableElements.length > 0) {
135
- focusableElements[0].focus();
145
+ // when the lightbox opens, optionally focus the close button,
146
+ // but only if the user is interacting via the keyboard;
147
+ // we check interaction type because although focus-visible should handle this,
148
+ // Safari on iOS always shows the focus ring
149
+ requestAnimationFrame(() => {
150
+ if (interactionTypeRef.current === 'keyboard') {
151
+ dialogRef.current.querySelector('.close-button').focus();
136
152
  }
137
- }, 0);
153
+ });
138
154
  } else {
139
155
  // restore focus to the previously focused element when lightbox closes
140
156
  focusedNode?.focus();
@@ -166,8 +182,8 @@ const Lightbox = () => {
166
182
  const scaleX = maxWidth / imageWidth;
167
183
  const scaleY = maxHeight / imageHeight;
168
184
  const scale = Math.min(scaleX, scaleY);
169
- const width = imageWidth * scale;
170
- const height = imageHeight * scale;
185
+ const width = Math.round(imageWidth * scale);
186
+ const height = Math.round(imageHeight * scale);
171
187
 
172
188
  // set the width and height on the image element, and make it visible
173
189
  setImageDimensions({ width: `${width}px`, height: `${height}px` });
@@ -201,8 +217,17 @@ const Lightbox = () => {
201
217
  height={imageDimensions.height}
202
218
  objectFit="contain"
203
219
  onLoad={event => onLoad(event)}
220
+ style={{ borderRadius: '0.6rem', overflow: 'hidden' }}
204
221
  />
205
222
  )}
223
+ <PreviousButton type="button" onClick={() => previousNode(selectedNode)}>
224
+ <ScreenReaderOnly>Previous</ScreenReaderOnly>
225
+ <Arrow direction="left" colour="black" size={16} />
226
+ </PreviousButton>
227
+ <NextButton type="button" onClick={() => nextNode(selectedNode)}>
228
+ <ScreenReaderOnly>Next</ScreenReaderOnly>
229
+ <Arrow direction="right" colour="black" size={16} />
230
+ </NextButton>
206
231
  </LightboxImage>
207
232
  <LightboxDetails id="lightboxDescription" aria-live="polite" aria-atomic="true">
208
233
  {selectedNode?.lightboxBody && (
@@ -216,18 +241,10 @@ const Lightbox = () => {
216
241
  </div>
217
242
  )}
218
243
  </LightboxDetails>
219
- <CloseButton type="button" onClick={() => setSelectedNode(null)}>
244
+ <CloseButton className="close-button" type="button" onClick={() => setSelectedNode(null)}>
220
245
  <ScreenReaderOnly>Close</ScreenReaderOnly>
221
246
  <Cross colour="black" size={16} />
222
247
  </CloseButton>
223
- <PreviousButton type="button" onClick={() => previousNode(selectedNode)}>
224
- <ScreenReaderOnly>Previous</ScreenReaderOnly>
225
- <Arrow direction="left" colour="black" size={16} />
226
- </PreviousButton>
227
- <NextButton type="button" onClick={() => nextNode(selectedNode)}>
228
- <ScreenReaderOnly>Next</ScreenReaderOnly>
229
- <Arrow direction="right" colour="black" size={16} />
230
- </NextButton>
231
248
  </LightboxContent>
232
249
  </Dialog>
233
250
  </Container>
@@ -54,8 +54,7 @@ export const LightboxImage = styled.div`
54
54
  justify-content: center;
55
55
  min-width: 128px;
56
56
  min-height: 32px;
57
- border-radius: 0.6rem;
58
- overflow: hidden;
57
+ width: 100%;
59
58
 
60
59
  & > div {
61
60
  display: flex;
@@ -83,6 +82,7 @@ export const LightboxDetails = styled.div`
83
82
  align-items: stretch;
84
83
  gap: 0.5rem;
85
84
  width: 100%;
85
+ max-width: 1024px;
86
86
  padding: 0 1rem;
87
87
  `;
88
88
 
@@ -100,16 +100,18 @@ export const NavButton = styled.button`
100
100
  z-index: 10;
101
101
 
102
102
  svg {
103
- transition: all 0.1s ease-out;
103
+ transition: fill 0.1s ease-out;
104
104
  }
105
105
 
106
- &:hover {
107
- svg {
108
- fill: ${({ theme }) => theme.color('red')};
106
+ @media (hover: hover) {
107
+ &:hover {
108
+ svg {
109
+ fill: ${({ theme }) => theme.color('red')};
110
+ }
109
111
  }
110
112
  }
111
113
 
112
- &:focus-visible {
114
+ :focus-visible {
113
115
  outline: 2px solid ${({ theme }) => theme.color('red')};
114
116
  }
115
117
  `;
@@ -120,40 +122,25 @@ export const CloseButton = styled(NavButton)`
120
122
  `;
121
123
 
122
124
  export const PreviousButton = styled(NavButton)`
123
- top: 30%;
125
+ top: 50%;
124
126
  left: 0;
125
- transform: translate(0, -50%);
126
- border-top-left-radius: 0;
127
- border-bottom-left-radius: 0;
127
+ transform: translate(-1rem, -50%);
128
128
 
129
129
  @media ${({ theme }) => theme.breakpoints2026('L')} {
130
130
  position: fixed;
131
+ transform: translate(1rem, -50%);
131
132
  top: 50%;
132
133
  }
133
134
  `;
134
135
 
135
136
  export const NextButton = styled(NavButton)`
136
- top: 30%;
137
+ top: 50%;
137
138
  right: 0;
138
- transform: translate(0, -50%);
139
- border-top-right-radius: 0;
140
- border-bottom-right-radius: 0;
139
+ transform: translate(1rem, -50%);
141
140
 
142
141
  @media ${({ theme }) => theme.breakpoints2026('L')} {
143
142
  position: fixed;
144
- top: 50%;
143
+ transform: translate(-1rem, -50%);
144
+ top: 50%;
145
145
  }
146
146
  `;
147
-
148
- export const ScreenReaderOnly = styled.span`
149
- position: absolute;
150
- width: 1px;
151
- height: 1px;
152
- margin: -1px;
153
- border: 0;
154
- padding: 0;
155
- white-space: nowrap;
156
- clip-path: inset(100%);
157
- clip: rect(0 0 0 0);
158
- overflow: hidden;
159
- `;