@comicrelief/component-library 8.51.8 → 8.52.1
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/Atoms/Icons/Cross.js +40 -0
- package/dist/components/Atoms/Picture/Picture.js +3 -1
- 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 +218 -0
- package/dist/components/Organisms/DynamicGallery/DynamicGallery.md +30 -0
- package/dist/components/Organisms/DynamicGallery/DynamicGallery.style.js +97 -0
- package/dist/components/Organisms/DynamicGallery/DynamicGallery.test.js +33 -0
- package/dist/components/Organisms/DynamicGallery/_DynamicGalleryColumn.js +111 -0
- package/dist/components/Organisms/DynamicGallery/_Lightbox.js +218 -0
- package/dist/components/Organisms/DynamicGallery/_Lightbox.style.js +86 -0
- package/dist/components/Organisms/DynamicGallery/_ScrollFix.js +57 -0
- package/dist/components/Organisms/DynamicGallery/__snapshots__/DynamicGallery.test.js.snap +1113 -0
- package/dist/components/Organisms/DynamicGallery/_types.js +18 -0
- package/dist/components/Organisms/DynamicGallery/_utils.js +24 -0
- package/dist/index.js +8 -1
- package/dist/styleguide/assets/tall.jpg +0 -0
- package/dist/styleguide/assets/wide.jpg +0 -0
- package/package.json +1 -1
- package/playwright/components/organisms/dynamicGallery.spec.js +9 -0
- package/src/components/Atoms/Icons/Cross.js +37 -0
- package/src/components/Atoms/Picture/Picture.js +4 -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 +243 -0
- package/src/components/Organisms/DynamicGallery/DynamicGallery.md +30 -0
- package/src/components/Organisms/DynamicGallery/DynamicGallery.style.js +107 -0
- package/src/components/Organisms/DynamicGallery/DynamicGallery.test.js +34 -0
- package/src/components/Organisms/DynamicGallery/_DynamicGalleryColumn.js +144 -0
- package/src/components/Organisms/DynamicGallery/_Lightbox.js +242 -0
- package/src/components/Organisms/DynamicGallery/_Lightbox.style.js +159 -0
- package/src/components/Organisms/DynamicGallery/_ScrollFix.js +60 -0
- package/src/components/Organisms/DynamicGallery/__snapshots__/DynamicGallery.test.js.snap +1113 -0
- package/src/components/Organisms/DynamicGallery/_types.js +12 -0
- package/src/components/Organisms/DynamicGallery/_utils.js +28 -0
- package/src/index.js +1 -0
- package/src/styleguide/assets/tall.jpg +0 -0
- package/src/styleguide/assets/wide.jpg +0 -0
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
var _interopRequireWildcard = require("@babel/runtime/helpers/interopRequireWildcard").default;
|
|
4
|
+
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault").default;
|
|
5
|
+
Object.defineProperty(exports, "__esModule", {
|
|
6
|
+
value: true
|
|
7
|
+
});
|
|
8
|
+
exports.default = void 0;
|
|
9
|
+
var _react = _interopRequireDefault(require("react"));
|
|
10
|
+
var _styledComponents = _interopRequireWildcard(require("styled-components"));
|
|
11
|
+
const Icon = _styledComponents.default.svg.withConfig({
|
|
12
|
+
displayName: "Cross__Icon",
|
|
13
|
+
componentId: "sc-1px9lqa-0"
|
|
14
|
+
})(["fill:", ";"], _ref => {
|
|
15
|
+
let {
|
|
16
|
+
colour,
|
|
17
|
+
theme
|
|
18
|
+
} = _ref;
|
|
19
|
+
return theme.color(colour);
|
|
20
|
+
});
|
|
21
|
+
const Cross = _ref2 => {
|
|
22
|
+
let {
|
|
23
|
+
colour = 'black',
|
|
24
|
+
mobileColour = null,
|
|
25
|
+
theme,
|
|
26
|
+
size = 24,
|
|
27
|
+
...rest
|
|
28
|
+
} = _ref2;
|
|
29
|
+
return /*#__PURE__*/_react.default.createElement(Icon, Object.assign({
|
|
30
|
+
width: size,
|
|
31
|
+
height: size,
|
|
32
|
+
colour: colour,
|
|
33
|
+
mobileColour: mobileColour,
|
|
34
|
+
xmlns: "http://www.w3.org/2000/svg",
|
|
35
|
+
viewBox: "0 0 96 96"
|
|
36
|
+
}, rest), /*#__PURE__*/_react.default.createElement("polygon", {
|
|
37
|
+
points: "85.48 17.59 78.41 10.52 48 40.93 17.59 10.52 10.52 17.59 40.93 48 10.52 78.41 17.59 85.48 48 55.07 78.41 85.48 85.48 78.41 55.07 48 85.48 17.59"
|
|
38
|
+
}));
|
|
39
|
+
};
|
|
40
|
+
var _default = exports.default = (0, _styledComponents.withTheme)(Cross);
|
|
@@ -88,6 +88,7 @@ const Picture = _ref11 => {
|
|
|
88
88
|
isBackgroundImage = false,
|
|
89
89
|
smallBreakpointRowLayout = null,
|
|
90
90
|
mediumBreakpointRowLayout = null,
|
|
91
|
+
onLoad,
|
|
91
92
|
...rest
|
|
92
93
|
} = _ref11;
|
|
93
94
|
const document = typeof window !== 'undefined' ? window.document : null;
|
|
@@ -122,7 +123,8 @@ const Picture = _ref11 => {
|
|
|
122
123
|
objectFit: objectFit,
|
|
123
124
|
"data-src": image,
|
|
124
125
|
className: "lazyload",
|
|
125
|
-
objFitState: objFitState
|
|
126
|
+
objFitState: objFitState,
|
|
127
|
+
onLoad: onLoad
|
|
126
128
|
}));
|
|
127
129
|
}
|
|
128
130
|
return /*#__PURE__*/_react.default.createElement(Wrapper, Object.assign({
|
|
@@ -14,7 +14,7 @@ exports[`handles data structure correctly 1`] = `
|
|
|
14
14
|
className="CTAMultiCardstyle__CardsContainer-sc-gsdqzv-3 gAuYUa"
|
|
15
15
|
>
|
|
16
16
|
<div
|
|
17
|
-
className="CTACardstyle__CardWrapper-sc-si8xx1-5
|
|
17
|
+
className="CTACardstyle__CardWrapper-sc-si8xx1-5 hKmRek"
|
|
18
18
|
>
|
|
19
19
|
<a
|
|
20
20
|
className="CTACardstyle__CardLink-sc-si8xx1-4 eIrLL"
|
|
@@ -95,7 +95,7 @@ exports[`handles data structure correctly 1`] = `
|
|
|
95
95
|
</a>
|
|
96
96
|
</div>
|
|
97
97
|
<div
|
|
98
|
-
className="CTACardstyle__CardWrapper-sc-si8xx1-5
|
|
98
|
+
className="CTACardstyle__CardWrapper-sc-si8xx1-5 hKmRek"
|
|
99
99
|
>
|
|
100
100
|
<div
|
|
101
101
|
className="CTACardstyle__CardLink-sc-si8xx1-4 lbrljd"
|
|
@@ -141,7 +141,7 @@ exports[`handles data structure correctly 1`] = `
|
|
|
141
141
|
</div>
|
|
142
142
|
</div>
|
|
143
143
|
<div
|
|
144
|
-
className="CTACardstyle__CardWrapper-sc-si8xx1-5
|
|
144
|
+
className="CTACardstyle__CardWrapper-sc-si8xx1-5 hKmRek"
|
|
145
145
|
>
|
|
146
146
|
<a
|
|
147
147
|
className="CTACardstyle__CardLink-sc-si8xx1-4 eIrLL"
|
|
@@ -222,7 +222,7 @@ exports[`handles data structure correctly 1`] = `
|
|
|
222
222
|
</a>
|
|
223
223
|
</div>
|
|
224
224
|
<div
|
|
225
|
-
className="CTACardstyle__CardWrapper-sc-si8xx1-5
|
|
225
|
+
className="CTACardstyle__CardWrapper-sc-si8xx1-5 hKmRek"
|
|
226
226
|
>
|
|
227
227
|
<a
|
|
228
228
|
className="CTACardstyle__CardLink-sc-si8xx1-4 eIrLL"
|
|
@@ -580,7 +580,7 @@ exports[`renders carousel mode correctly 1`] = `
|
|
|
580
580
|
className="CTAMultiCardstyle__CardsContainer-sc-gsdqzv-3 fqrDP"
|
|
581
581
|
>
|
|
582
582
|
<div
|
|
583
|
-
className="CTACardstyle__CardWrapper-sc-si8xx1-5
|
|
583
|
+
className="CTACardstyle__CardWrapper-sc-si8xx1-5 hQbghF"
|
|
584
584
|
>
|
|
585
585
|
<a
|
|
586
586
|
className="CTACardstyle__CardLink-sc-si8xx1-4 eIrLL"
|
|
@@ -661,7 +661,7 @@ exports[`renders carousel mode correctly 1`] = `
|
|
|
661
661
|
</a>
|
|
662
662
|
</div>
|
|
663
663
|
<div
|
|
664
|
-
className="CTACardstyle__CardWrapper-sc-si8xx1-5
|
|
664
|
+
className="CTACardstyle__CardWrapper-sc-si8xx1-5 hQbghF"
|
|
665
665
|
>
|
|
666
666
|
<div
|
|
667
667
|
className="CTACardstyle__CardLink-sc-si8xx1-4 lbrljd"
|
|
@@ -707,7 +707,7 @@ exports[`renders carousel mode correctly 1`] = `
|
|
|
707
707
|
</div>
|
|
708
708
|
</div>
|
|
709
709
|
<div
|
|
710
|
-
className="CTACardstyle__CardWrapper-sc-si8xx1-5
|
|
710
|
+
className="CTACardstyle__CardWrapper-sc-si8xx1-5 hQbghF"
|
|
711
711
|
>
|
|
712
712
|
<a
|
|
713
713
|
className="CTACardstyle__CardLink-sc-si8xx1-4 eIrLL"
|
|
@@ -788,7 +788,7 @@ exports[`renders carousel mode correctly 1`] = `
|
|
|
788
788
|
</a>
|
|
789
789
|
</div>
|
|
790
790
|
<div
|
|
791
|
-
className="CTACardstyle__CardWrapper-sc-si8xx1-5
|
|
791
|
+
className="CTACardstyle__CardWrapper-sc-si8xx1-5 hQbghF"
|
|
792
792
|
>
|
|
793
793
|
<a
|
|
794
794
|
className="CTACardstyle__CardLink-sc-si8xx1-4 eIrLL"
|
|
@@ -863,7 +863,7 @@ exports[`renders correctly with data prop 1`] = `
|
|
|
863
863
|
className="CTAMultiCardstyle__CardsContainer-sc-gsdqzv-3 fqrDP"
|
|
864
864
|
>
|
|
865
865
|
<div
|
|
866
|
-
className="CTACardstyle__CardWrapper-sc-si8xx1-5
|
|
866
|
+
className="CTACardstyle__CardWrapper-sc-si8xx1-5 hQbghF"
|
|
867
867
|
>
|
|
868
868
|
<a
|
|
869
869
|
className="CTACardstyle__CardLink-sc-si8xx1-4 eIrLL"
|
|
@@ -944,7 +944,7 @@ exports[`renders correctly with data prop 1`] = `
|
|
|
944
944
|
</a>
|
|
945
945
|
</div>
|
|
946
946
|
<div
|
|
947
|
-
className="CTACardstyle__CardWrapper-sc-si8xx1-5
|
|
947
|
+
className="CTACardstyle__CardWrapper-sc-si8xx1-5 hQbghF"
|
|
948
948
|
>
|
|
949
949
|
<div
|
|
950
950
|
className="CTACardstyle__CardLink-sc-si8xx1-4 lbrljd"
|
|
@@ -990,7 +990,7 @@ exports[`renders correctly with data prop 1`] = `
|
|
|
990
990
|
</div>
|
|
991
991
|
</div>
|
|
992
992
|
<div
|
|
993
|
-
className="CTACardstyle__CardWrapper-sc-si8xx1-5
|
|
993
|
+
className="CTACardstyle__CardWrapper-sc-si8xx1-5 hQbghF"
|
|
994
994
|
>
|
|
995
995
|
<a
|
|
996
996
|
className="CTACardstyle__CardLink-sc-si8xx1-4 eIrLL"
|
|
@@ -1071,7 +1071,7 @@ exports[`renders correctly with data prop 1`] = `
|
|
|
1071
1071
|
</a>
|
|
1072
1072
|
</div>
|
|
1073
1073
|
<div
|
|
1074
|
-
className="CTACardstyle__CardWrapper-sc-si8xx1-5
|
|
1074
|
+
className="CTACardstyle__CardWrapper-sc-si8xx1-5 hQbghF"
|
|
1075
1075
|
>
|
|
1076
1076
|
<a
|
|
1077
1077
|
className="CTACardstyle__CardLink-sc-si8xx1-4 eIrLL"
|
|
@@ -175,7 +175,7 @@ const CardWrapper = exports.CardWrapper = _styledComponents.default.div.withConf
|
|
|
175
175
|
let {
|
|
176
176
|
columns
|
|
177
177
|
} = _ref28;
|
|
178
|
-
return columns === 3 ? (0, _styledComponents.css)(["flex:0 1 auto;width:clamp(
|
|
178
|
+
return columns === 3 ? (0, _styledComponents.css)(["flex:0 1 auto;width:clamp(261px,calc((100% - 4rem) / 3),363px);"]) : (0, _styledComponents.css)(["flex:0 1 auto;width:100%;"]);
|
|
179
179
|
});
|
|
180
180
|
});
|
|
181
181
|
const CopyAndLinkSection = exports.CopyAndLinkSection = _styledComponents.default.div.withConfig({
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
var _interopRequireWildcard = require("@babel/runtime/helpers/interopRequireWildcard").default;
|
|
4
|
+
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault").default;
|
|
5
|
+
Object.defineProperty(exports, "__esModule", {
|
|
6
|
+
value: true
|
|
7
|
+
});
|
|
8
|
+
exports.default = void 0;
|
|
9
|
+
var _throttle2 = _interopRequireDefault(require("lodash/throttle"));
|
|
10
|
+
var _floor2 = _interopRequireDefault(require("lodash/floor"));
|
|
11
|
+
var _orderBy2 = _interopRequireDefault(require("lodash/orderBy"));
|
|
12
|
+
var _react = _interopRequireWildcard(require("react"));
|
|
13
|
+
var _reactResponsive = require("react-responsive");
|
|
14
|
+
var _breakpoints = require("../../../theme/shared/breakpoints2026");
|
|
15
|
+
var _Button = _interopRequireDefault(require("../../Atoms/Button/Button"));
|
|
16
|
+
var _Lightbox = _interopRequireWildcard(require("./_Lightbox"));
|
|
17
|
+
var _DynamicGallery = require("./DynamicGallery.style");
|
|
18
|
+
var _DynamicGalleryColumn = _interopRequireDefault(require("./_DynamicGalleryColumn"));
|
|
19
|
+
var _types = require("./_types");
|
|
20
|
+
/**
|
|
21
|
+
* the Dynamic Gallery component displays a grid of images,
|
|
22
|
+
* by default using dynamic heights per image to create an more organic look
|
|
23
|
+
*/
|
|
24
|
+
const DynamicGallery = _ref => {
|
|
25
|
+
let {
|
|
26
|
+
pageBackgroundColour = 'transparent',
|
|
27
|
+
textColour = 'black',
|
|
28
|
+
gridWidth = 3,
|
|
29
|
+
maxWidth = '1500px',
|
|
30
|
+
loadingBehaviour = '25',
|
|
31
|
+
imageRatio = 'dynamic',
|
|
32
|
+
useLightbox = true,
|
|
33
|
+
nodes = [],
|
|
34
|
+
paddingTop = '0rem',
|
|
35
|
+
paddingBottom = '2rem'
|
|
36
|
+
} = _ref;
|
|
37
|
+
const hasNodes = (nodes === null || nodes === void 0 ? void 0 : nodes.length) > 0;
|
|
38
|
+
const containerRef = (0, _react.useRef)(null);
|
|
39
|
+
|
|
40
|
+
// handle loading behaviour;
|
|
41
|
+
// if we're in chunk mode, display images a chunk at a time
|
|
42
|
+
// (or the total number of images if less than the chunk size)
|
|
43
|
+
// or display all images at once
|
|
44
|
+
const isChunked = loadingBehaviour !== 'all';
|
|
45
|
+
const imageChunkSize = +loadingBehaviour;
|
|
46
|
+
const [imageCount, setImageCount] = (0, _react.useState)(isChunked ? Math.min(imageChunkSize, nodes.length) : nodes.length);
|
|
47
|
+
function handleLoadMore() {
|
|
48
|
+
setImageCount(imageCount + imageChunkSize);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// assign a manual tabbing order to gallery images based on their position in the DOM,
|
|
52
|
+
// starting from top-left and working downwards in a natural order
|
|
53
|
+
function updateTabOrder() {
|
|
54
|
+
if (!containerRef.current) return;
|
|
55
|
+
const galleryNodes = containerRef.current.querySelectorAll('.gallery-node');
|
|
56
|
+
const sortedNodes = (0, _orderBy2.default)(galleryNodes, node => {
|
|
57
|
+
const {
|
|
58
|
+
top,
|
|
59
|
+
left
|
|
60
|
+
} = node.getBoundingClientRect();
|
|
61
|
+
return (0, _floor2.default)(top, -2) + Math.floor(left) / 1000;
|
|
62
|
+
}, 'asc');
|
|
63
|
+
sortedNodes.forEach((galleryNode, index) => {
|
|
64
|
+
galleryNode.setAttribute('data-order', String(index));
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
// create a throttled version of the updateTabOrder function
|
|
68
|
+
const throttledUpdateTabOrder = (0, _react.useRef)((0, _throttle2.default)(updateTabOrder, 2000));
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* handle column counts;
|
|
72
|
+
* column count is based on a combination of the maxColumns prop and the window width
|
|
73
|
+
* - for small screens columns = 1
|
|
74
|
+
* - for medium screens columns = 2
|
|
75
|
+
* - for large and xl screens we use the maxColumns prop which defaults to 3
|
|
76
|
+
* .
|
|
77
|
+
* we need to use JS here rather than CSS because our columns are created dynamically;
|
|
78
|
+
* this is to allow us to assign nodes in the natural "horizontal" order rather than "vertically"
|
|
79
|
+
*/
|
|
80
|
+
const [columnCount, setColumnCount] = (0, _react.useState)(gridWidth);
|
|
81
|
+
const isSmall = (0, _reactResponsive.useMediaQuery)({
|
|
82
|
+
maxWidth: _breakpoints.breakpointValues2026.S
|
|
83
|
+
});
|
|
84
|
+
const isMedium = (0, _reactResponsive.useMediaQuery)({
|
|
85
|
+
maxWidth: _breakpoints.breakpointValues2026.M
|
|
86
|
+
});
|
|
87
|
+
(0, _react.useEffect)(() => {
|
|
88
|
+
let newColumnCount;
|
|
89
|
+
switch (true) {
|
|
90
|
+
case isSmall:
|
|
91
|
+
newColumnCount = 1;
|
|
92
|
+
break;
|
|
93
|
+
case isMedium:
|
|
94
|
+
newColumnCount = 2;
|
|
95
|
+
break;
|
|
96
|
+
default:
|
|
97
|
+
newColumnCount = gridWidth;
|
|
98
|
+
break;
|
|
99
|
+
}
|
|
100
|
+
setColumnCount(newColumnCount);
|
|
101
|
+
throttledUpdateTabOrder.current();
|
|
102
|
+
}, [isSmall, isMedium, gridWidth, setColumnCount]);
|
|
103
|
+
|
|
104
|
+
// handle selected gallery node
|
|
105
|
+
const [selectedNode, setSelectedNode] = (0, _react.useState)(null);
|
|
106
|
+
|
|
107
|
+
// handle next/previous node events from the lightbox
|
|
108
|
+
function handleNextNode(node) {
|
|
109
|
+
const nodeIndex = nodes.indexOf(node);
|
|
110
|
+
const nextNodeIndex = (nodeIndex + 1) % imageCount;
|
|
111
|
+
setSelectedNode(nodes[nextNodeIndex]);
|
|
112
|
+
}
|
|
113
|
+
function handlePreviousNode(node) {
|
|
114
|
+
const nodeIndex = nodes.indexOf(node);
|
|
115
|
+
const previousNodeIndex = (nodeIndex - 1 + imageCount) % imageCount;
|
|
116
|
+
setSelectedNode(nodes[previousNodeIndex]);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// handle keydown events,
|
|
120
|
+
// including image opening and tabbing
|
|
121
|
+
function handleKeyDown(event) {
|
|
122
|
+
switch (event.key) {
|
|
123
|
+
// if the lightbox is enabled, open the image in the lightbox when the user presses enter
|
|
124
|
+
case 'Enter':
|
|
125
|
+
{
|
|
126
|
+
if (useLightbox) {
|
|
127
|
+
event.preventDefault();
|
|
128
|
+
const nodeIndex = +event.target.dataset.nodeIndex;
|
|
129
|
+
if (Number.isNaN(nodeIndex)) return;
|
|
130
|
+
setSelectedNode(nodes[nodeIndex]);
|
|
131
|
+
}
|
|
132
|
+
break;
|
|
133
|
+
}
|
|
134
|
+
// handle tabbing between images;
|
|
135
|
+
// there doesn't seem to be a great way to handle this!
|
|
136
|
+
// it's tied into the way the grid is structured, and the way the nodes are rendered;
|
|
137
|
+
// ideal scenario would be a tabbable grid with nice ordering and no gaps,
|
|
138
|
+
// but this isn't currently possible without either getting a bit hacky with CSS or JS
|
|
139
|
+
// our options are:
|
|
140
|
+
// - use a standard CSS grid > ordered and tabble but gappy
|
|
141
|
+
// - use absolute positioning > no gaps but complex and weird DOM order (pinterest approach)
|
|
142
|
+
// - flex-column+order > no gaps but complex (https://mui.com/material-ui/react-masonry/)
|
|
143
|
+
// - columns + custom tabbing > what we're doing here
|
|
144
|
+
case 'Tab':
|
|
145
|
+
{
|
|
146
|
+
const nodeIndex = +event.target.dataset.order;
|
|
147
|
+
if (Number.isNaN(nodeIndex)) return;
|
|
148
|
+
const galleryContainer = event.target.closest('.gallery-container');
|
|
149
|
+
if (!galleryContainer) return;
|
|
150
|
+
let newNodeIndex;
|
|
151
|
+
if (event.shiftKey) {
|
|
152
|
+
// shift-tab: move to the previous image
|
|
153
|
+
newNodeIndex = nodeIndex - 1;
|
|
154
|
+
if (newNodeIndex < 0) return;
|
|
155
|
+
event.preventDefault();
|
|
156
|
+
galleryContainer.querySelector(`[data-order="${newNodeIndex}"]`).focus();
|
|
157
|
+
} else {
|
|
158
|
+
// tab: move to the next image
|
|
159
|
+
newNodeIndex = nodeIndex + 1;
|
|
160
|
+
if (newNodeIndex >= imageCount) {
|
|
161
|
+
// if we're on the last image, move to the focus trap
|
|
162
|
+
// before allowing the tab event to continue to the next natural element;
|
|
163
|
+
// this is a bit hacky but is needed for when the "last" image isn't in the last column;
|
|
164
|
+
// eg 10 images divided across 3 columns = [4, 3, 3]
|
|
165
|
+
// when this happens the browser tries to tab into the next column,
|
|
166
|
+
// rather than out of the grid and onwards
|
|
167
|
+
galleryContainer.querySelector('.gallery-focus-trap').focus();
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
event.preventDefault();
|
|
171
|
+
galleryContainer.querySelector(`[data-order="${newNodeIndex}"]`).focus();
|
|
172
|
+
}
|
|
173
|
+
break;
|
|
174
|
+
}
|
|
175
|
+
default:
|
|
176
|
+
break;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return /*#__PURE__*/_react.default.createElement(_DynamicGallery.Container, {
|
|
180
|
+
className: "gallery-container",
|
|
181
|
+
ref: containerRef,
|
|
182
|
+
maxWidth: maxWidth,
|
|
183
|
+
pageBackgroundColour: pageBackgroundColour,
|
|
184
|
+
textColour: textColour,
|
|
185
|
+
paddingTop: paddingTop,
|
|
186
|
+
paddingBottom: paddingBottom
|
|
187
|
+
}, /*#__PURE__*/_react.default.createElement(_Lightbox.LightboxContext.Provider, {
|
|
188
|
+
value: {
|
|
189
|
+
useLightbox,
|
|
190
|
+
selectedNode,
|
|
191
|
+
setSelectedNode,
|
|
192
|
+
nextNode: handleNextNode,
|
|
193
|
+
previousNode: handlePreviousNode
|
|
194
|
+
}
|
|
195
|
+
}, /*#__PURE__*/_react.default.createElement(_DynamicGallery.ImageGrid, {
|
|
196
|
+
className: "gallery-grid",
|
|
197
|
+
onKeyDown: event => handleKeyDown(event)
|
|
198
|
+
}, hasNodes && Array(columnCount).fill(null).map((column, columnIndex) => /*#__PURE__*/_react.default.createElement(_DynamicGalleryColumn.default
|
|
199
|
+
// disabling the lint rule here
|
|
200
|
+
// as we're chunking an array and have no unique keys
|
|
201
|
+
// eslint-disable-next-line react/no-array-index-key
|
|
202
|
+
, {
|
|
203
|
+
key: columnIndex,
|
|
204
|
+
columnIndex: columnIndex,
|
|
205
|
+
columnCount: columnCount,
|
|
206
|
+
nodes: nodes.slice(0, imageCount),
|
|
207
|
+
imageRatio: imageRatio,
|
|
208
|
+
updateTabOrder: throttledUpdateTabOrder.current
|
|
209
|
+
})), /*#__PURE__*/_react.default.createElement(_DynamicGallery.EmptyMessage, {
|
|
210
|
+
isEmpty: !hasNodes
|
|
211
|
+
}, "No images to display")), /*#__PURE__*/_react.default.createElement(_Lightbox.default, null), /*#__PURE__*/_react.default.createElement("div", {
|
|
212
|
+
className: "gallery-focus-trap",
|
|
213
|
+
tabIndex: 0
|
|
214
|
+
})), imageCount < nodes.length && /*#__PURE__*/_react.default.createElement(_Button.default, {
|
|
215
|
+
onClick: () => handleLoadMore()
|
|
216
|
+
}, "Load more"));
|
|
217
|
+
};
|
|
218
|
+
var _default = exports.default = DynamicGallery;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# Dynamic Gallery
|
|
2
|
+
|
|
3
|
+
### Empty gallery
|
|
4
|
+
|
|
5
|
+
```js
|
|
6
|
+
<DynamicGallery />
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
### Basic gallery
|
|
10
|
+
|
|
11
|
+
```js
|
|
12
|
+
const defaultData = require('../../../styleguide/data/data').defaultData;
|
|
13
|
+
import createMockGalleryNodes from './_utils';
|
|
14
|
+
<DynamicGallery nodes={createMockGalleryNodes(50)} />;
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
### Customised gallery with multiple options
|
|
18
|
+
```js
|
|
19
|
+
const defaultData = require('../../../styleguide/data/data').defaultData;
|
|
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} />;
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
### Gallery with max 5 columns
|
|
25
|
+
|
|
26
|
+
```js
|
|
27
|
+
const defaultData = require('../../../styleguide/data/data').defaultData;
|
|
28
|
+
import createMockGalleryNodes from './_utils';
|
|
29
|
+
<DynamicGallery gridWidth={5} nodes={createMockGalleryNodes(5)} />;
|
|
30
|
+
```
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
var _interopRequireWildcard = require("@babel/runtime/helpers/interopRequireWildcard").default;
|
|
4
|
+
Object.defineProperty(exports, "__esModule", {
|
|
5
|
+
value: true
|
|
6
|
+
});
|
|
7
|
+
exports.Title = exports.InteractiveGalleryNode = exports.ImageGrid = exports.ImageContainer = exports.GalleryNode = exports.EmptyMessage = exports.Details = exports.Container = exports.Column = exports.Caption = void 0;
|
|
8
|
+
var _styledComponents = _interopRequireWildcard(require("styled-components"));
|
|
9
|
+
const Container = exports.Container = _styledComponents.default.div.withConfig({
|
|
10
|
+
displayName: "DynamicGallerystyle__Container",
|
|
11
|
+
componentId: "sc-1kgt7yr-0"
|
|
12
|
+
})(["display:flex;flex-direction:column;align-items:center;gap:1rem;max-width:", ";background:", ";", " color:", ";"], _ref => {
|
|
13
|
+
let {
|
|
14
|
+
maxWidth
|
|
15
|
+
} = _ref;
|
|
16
|
+
return maxWidth;
|
|
17
|
+
}, _ref2 => {
|
|
18
|
+
let {
|
|
19
|
+
theme,
|
|
20
|
+
pageBackgroundColour
|
|
21
|
+
} = _ref2;
|
|
22
|
+
return theme.color(pageBackgroundColour);
|
|
23
|
+
}, _ref3 => {
|
|
24
|
+
let {
|
|
25
|
+
paddingTop,
|
|
26
|
+
paddingBottom
|
|
27
|
+
} = _ref3;
|
|
28
|
+
return (0, _styledComponents.css)(["padding:", " 2rem ", ";"], paddingTop, paddingBottom);
|
|
29
|
+
}, _ref4 => {
|
|
30
|
+
let {
|
|
31
|
+
theme,
|
|
32
|
+
textColour
|
|
33
|
+
} = _ref4;
|
|
34
|
+
return theme.color(textColour);
|
|
35
|
+
});
|
|
36
|
+
const ImageGrid = exports.ImageGrid = _styledComponents.default.div.withConfig({
|
|
37
|
+
displayName: "DynamicGallerystyle__ImageGrid",
|
|
38
|
+
componentId: "sc-1kgt7yr-1"
|
|
39
|
+
})(["display:flex;gap:1rem;width:100%;@media ", "{gap:2rem;}"], _ref5 => {
|
|
40
|
+
let {
|
|
41
|
+
theme
|
|
42
|
+
} = _ref5;
|
|
43
|
+
return theme.breakpoints2026('M');
|
|
44
|
+
});
|
|
45
|
+
const Column = exports.Column = _styledComponents.default.div.withConfig({
|
|
46
|
+
displayName: "DynamicGallerystyle__Column",
|
|
47
|
+
componentId: "sc-1kgt7yr-2"
|
|
48
|
+
})(["flex:1;display:flex;flex-direction:column;gap:1.1rem;@media ", "{gap:2rem;}"], _ref6 => {
|
|
49
|
+
let {
|
|
50
|
+
theme
|
|
51
|
+
} = _ref6;
|
|
52
|
+
return theme.breakpoints2026('M');
|
|
53
|
+
});
|
|
54
|
+
const EmptyMessage = exports.EmptyMessage = _styledComponents.default.div.withConfig({
|
|
55
|
+
displayName: "DynamicGallerystyle__EmptyMessage",
|
|
56
|
+
componentId: "sc-1kgt7yr-3"
|
|
57
|
+
})(["display:", ";"], _ref7 => {
|
|
58
|
+
let {
|
|
59
|
+
isEmpty
|
|
60
|
+
} = _ref7;
|
|
61
|
+
return isEmpty ? 'block' : 'none';
|
|
62
|
+
});
|
|
63
|
+
const GalleryNodeBase = (0, _styledComponents.css)(["display:flex;flex-direction:column;gap:0.8rem;padding:0;margin:0;background:none;border:none;text-align:left;"]);
|
|
64
|
+
const GalleryNode = exports.GalleryNode = _styledComponents.default.div.withConfig({
|
|
65
|
+
displayName: "DynamicGallerystyle__GalleryNode",
|
|
66
|
+
componentId: "sc-1kgt7yr-4"
|
|
67
|
+
})(["", ""], GalleryNodeBase);
|
|
68
|
+
const InteractiveGalleryNode = exports.InteractiveGalleryNode = _styledComponents.default.button.withConfig({
|
|
69
|
+
displayName: "DynamicGallerystyle__InteractiveGalleryNode",
|
|
70
|
+
componentId: "sc-1kgt7yr-5"
|
|
71
|
+
})(["", " cursor:pointer;color:inherit;& div:first-child{transition:all 0.1s ease-out;}&:focus-visible{outline:2px solid #000000;}& > div:first-child{&:hover{box-shadow:0px 3px 10px 0px rgba(0,0,0,0.4);}}"], GalleryNodeBase);
|
|
72
|
+
const ImageContainer = exports.ImageContainer = _styledComponents.default.div.withConfig({
|
|
73
|
+
displayName: "DynamicGallerystyle__ImageContainer",
|
|
74
|
+
componentId: "sc-1kgt7yr-6"
|
|
75
|
+
})(["display:flex;height:auto;width:100%;min-height:", ";max-height:", ";overflow:hidden;border-radius:1rem;background:rgba(0,0,0,0.05);box-shadow:0px 2px 8px 0px rgba(0,0,0,0.2);img{height:100%;opacity:0;transition:opacity 0.1s ease-out 0.3s;}"], _ref8 => {
|
|
76
|
+
let {
|
|
77
|
+
minHeight
|
|
78
|
+
} = _ref8;
|
|
79
|
+
return minHeight;
|
|
80
|
+
}, _ref9 => {
|
|
81
|
+
let {
|
|
82
|
+
maxHeight
|
|
83
|
+
} = _ref9;
|
|
84
|
+
return maxHeight;
|
|
85
|
+
});
|
|
86
|
+
const Details = exports.Details = _styledComponents.default.div.withConfig({
|
|
87
|
+
displayName: "DynamicGallerystyle__Details",
|
|
88
|
+
componentId: "sc-1kgt7yr-7"
|
|
89
|
+
})(["display:flex;flex-direction:column;gap:0.5rem;padding:0 1rem;"]);
|
|
90
|
+
const Title = exports.Title = _styledComponents.default.div.withConfig({
|
|
91
|
+
displayName: "DynamicGallerystyle__Title",
|
|
92
|
+
componentId: "sc-1kgt7yr-8"
|
|
93
|
+
})(["&:first-child{margin-bottom:0;}"]);
|
|
94
|
+
const Caption = exports.Caption = _styledComponents.default.div.withConfig({
|
|
95
|
+
displayName: "DynamicGallerystyle__Caption",
|
|
96
|
+
componentId: "sc-1kgt7yr-9"
|
|
97
|
+
})(["line-height:1;"]);
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault").default;
|
|
4
|
+
require("jest-styled-components");
|
|
5
|
+
var _react = _interopRequireDefault(require("react"));
|
|
6
|
+
var _shallowWithTheme = _interopRequireDefault(require("../../../../tests/hoc/shallowWithTheme"));
|
|
7
|
+
var _DynamicGallery = _interopRequireDefault(require("./DynamicGallery"));
|
|
8
|
+
it('renders an empty Dynamic Gallery with no options set', () => {
|
|
9
|
+
const galleryEl = (0, _shallowWithTheme.default)( /*#__PURE__*/_react.default.createElement(_DynamicGallery.default, null)).toJSON();
|
|
10
|
+
expect(galleryEl).toMatchSnapshot();
|
|
11
|
+
});
|
|
12
|
+
it('handle a Dynamic Gallery with mocked gallery nodes', () => {
|
|
13
|
+
const nodes = [{
|
|
14
|
+
image: 'image1.jpg',
|
|
15
|
+
title: 'Image 1',
|
|
16
|
+
caption: 'Caption 1',
|
|
17
|
+
body: 'Body 1'
|
|
18
|
+
}, {
|
|
19
|
+
image: 'image2.jpg',
|
|
20
|
+
title: 'Image 2',
|
|
21
|
+
caption: 'Caption 2',
|
|
22
|
+
body: 'Body 2'
|
|
23
|
+
}, {
|
|
24
|
+
image: 'image3.jpg',
|
|
25
|
+
title: 'Image 3',
|
|
26
|
+
caption: 'Caption 3',
|
|
27
|
+
body: 'Body 3'
|
|
28
|
+
}];
|
|
29
|
+
const galleryEl = (0, _shallowWithTheme.default)( /*#__PURE__*/_react.default.createElement(_DynamicGallery.default, {
|
|
30
|
+
nodes: nodes
|
|
31
|
+
})).toJSON();
|
|
32
|
+
expect(galleryEl).toMatchSnapshot();
|
|
33
|
+
});
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
var _interopRequireWildcard = require("@babel/runtime/helpers/interopRequireWildcard").default;
|
|
4
|
+
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault").default;
|
|
5
|
+
Object.defineProperty(exports, "__esModule", {
|
|
6
|
+
value: true
|
|
7
|
+
});
|
|
8
|
+
exports.default = DynamicGalleryColumn;
|
|
9
|
+
var _throttle2 = _interopRequireDefault(require("lodash/throttle"));
|
|
10
|
+
var _react = _interopRequireWildcard(require("react"));
|
|
11
|
+
var _Picture = _interopRequireDefault(require("../../Atoms/Picture/Picture"));
|
|
12
|
+
var _Lightbox = require("./_Lightbox");
|
|
13
|
+
var _DynamicGallery = require("./DynamicGallery.style");
|
|
14
|
+
var _types = require("./_types");
|
|
15
|
+
/**
|
|
16
|
+
* a separate component to handle columns of images;
|
|
17
|
+
* this component handles aspect ratio calculations to enfore a min/max ratio for its images
|
|
18
|
+
*/
|
|
19
|
+
function DynamicGalleryColumn(_ref) {
|
|
20
|
+
let {
|
|
21
|
+
updateTabOrder,
|
|
22
|
+
nodes,
|
|
23
|
+
imageRatio,
|
|
24
|
+
columnIndex,
|
|
25
|
+
columnCount
|
|
26
|
+
} = _ref;
|
|
27
|
+
const [minHeight, setMinHeight] = (0, _react.useState)();
|
|
28
|
+
const [maxHeight, setMaxHeight] = (0, _react.useState)();
|
|
29
|
+
const elRef = (0, _react.useRef)(null);
|
|
30
|
+
const updateMinMaxHeight = (0, _react.useCallback)(() => {
|
|
31
|
+
if (!elRef.current) return;
|
|
32
|
+
let minAspectRatio;
|
|
33
|
+
let maxAspectRatio;
|
|
34
|
+
|
|
35
|
+
// handle aspect ratio;
|
|
36
|
+
// for dynamic aspect ratio, we use a min/max ratio of 2.35:1 and 9:16
|
|
37
|
+
// but if a specific aspect ratio is provided, use that instead
|
|
38
|
+
switch (imageRatio) {
|
|
39
|
+
case '4:3':
|
|
40
|
+
minAspectRatio = 4 / 3;
|
|
41
|
+
maxAspectRatio = 4 / 3;
|
|
42
|
+
break;
|
|
43
|
+
default:
|
|
44
|
+
minAspectRatio = 2.35 / 1;
|
|
45
|
+
maxAspectRatio = 9 / 16;
|
|
46
|
+
break;
|
|
47
|
+
}
|
|
48
|
+
const columnWidth = elRef.current.clientWidth;
|
|
49
|
+
setMinHeight(columnWidth / minAspectRatio);
|
|
50
|
+
setMaxHeight(columnWidth / maxAspectRatio);
|
|
51
|
+
}, [imageRatio, setMinHeight, setMaxHeight]);
|
|
52
|
+
|
|
53
|
+
// call repeatedly on column resize
|
|
54
|
+
(0, _react.useEffect)(() => {
|
|
55
|
+
// when the column width changes, recalculate the min/max height for images
|
|
56
|
+
const handleResize = (0, _throttle2.default)(() => {
|
|
57
|
+
updateMinMaxHeight();
|
|
58
|
+
}, 500);
|
|
59
|
+
const resizeObserver = new ResizeObserver(handleResize);
|
|
60
|
+
resizeObserver.observe(elRef.current);
|
|
61
|
+
|
|
62
|
+
// call once on initial mount
|
|
63
|
+
updateMinMaxHeight();
|
|
64
|
+
return () => {
|
|
65
|
+
resizeObserver.disconnect();
|
|
66
|
+
};
|
|
67
|
+
}, [updateMinMaxHeight]);
|
|
68
|
+
const {
|
|
69
|
+
useLightbox,
|
|
70
|
+
setSelectedNode
|
|
71
|
+
} = (0, _react.useContext)(_Lightbox.LightboxContext);
|
|
72
|
+
|
|
73
|
+
// on click, open the image in the lightbox;
|
|
74
|
+
// conditionally enabled depending on the gallery settings
|
|
75
|
+
function handlePointerUp(node) {
|
|
76
|
+
setSelectedNode(node);
|
|
77
|
+
}
|
|
78
|
+
const NodeComponent = useLightbox ? _DynamicGallery.InteractiveGalleryNode : _DynamicGallery.GalleryNode;
|
|
79
|
+
return /*#__PURE__*/_react.default.createElement(_DynamicGallery.Column, {
|
|
80
|
+
ref: elRef,
|
|
81
|
+
className: "gallery-column"
|
|
82
|
+
}, nodes === null || nodes === void 0 ? void 0 : nodes.filter((_, nodeIndex) => nodeIndex % columnCount === columnIndex).map((node, nodeIndex) => /*#__PURE__*/_react.default.createElement(NodeComponent, {
|
|
83
|
+
key: String(nodeIndex) + node.title,
|
|
84
|
+
className: "gallery-node",
|
|
85
|
+
title: node.title,
|
|
86
|
+
"aria-label": node.title,
|
|
87
|
+
"data-node-index": nodeIndex,
|
|
88
|
+
onPointerUp: useLightbox ? () => handlePointerUp(node) : undefined,
|
|
89
|
+
tabIndex: 0
|
|
90
|
+
}, /*#__PURE__*/_react.default.createElement(_DynamicGallery.ImageContainer, {
|
|
91
|
+
className: "gallery-node-image"
|
|
92
|
+
// eslint-disable-next-line prefer-template
|
|
93
|
+
,
|
|
94
|
+
minHeight: String(minHeight) + 'px'
|
|
95
|
+
// eslint-disable-next-line prefer-template
|
|
96
|
+
,
|
|
97
|
+
maxHeight: String(maxHeight) + 'px'
|
|
98
|
+
}, /*#__PURE__*/_react.default.createElement(_Picture.default, {
|
|
99
|
+
image: node.image,
|
|
100
|
+
objectFit: "cover",
|
|
101
|
+
alt: node.title
|
|
102
|
+
// animate image in on load
|
|
103
|
+
,
|
|
104
|
+
onLoad: event => {
|
|
105
|
+
event.target.closest('.gallery-node-image').querySelector('img').style.setProperty('opacity', '1');
|
|
106
|
+
|
|
107
|
+
// update tab order once the image has loaded
|
|
108
|
+
updateTabOrder();
|
|
109
|
+
}
|
|
110
|
+
})), /*#__PURE__*/_react.default.createElement(_DynamicGallery.Details, null, /*#__PURE__*/_react.default.createElement(_DynamicGallery.Title, null, node.title), node.caption && /*#__PURE__*/_react.default.createElement(_DynamicGallery.Caption, null, node.caption)))));
|
|
111
|
+
}
|