@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.
Files changed (38) hide show
  1. package/dist/components/Atoms/Icons/Cross.js +40 -0
  2. package/dist/components/Atoms/Picture/Picture.js +3 -1
  3. package/dist/components/Molecules/CTA/CTAMultiCard/__snapshots__/CTAMultiCard.test.js.snap +12 -12
  4. package/dist/components/Molecules/CTA/shared/CTACard.style.js +1 -1
  5. package/dist/components/Organisms/DynamicGallery/DynamicGallery.js +218 -0
  6. package/dist/components/Organisms/DynamicGallery/DynamicGallery.md +30 -0
  7. package/dist/components/Organisms/DynamicGallery/DynamicGallery.style.js +97 -0
  8. package/dist/components/Organisms/DynamicGallery/DynamicGallery.test.js +33 -0
  9. package/dist/components/Organisms/DynamicGallery/_DynamicGalleryColumn.js +111 -0
  10. package/dist/components/Organisms/DynamicGallery/_Lightbox.js +218 -0
  11. package/dist/components/Organisms/DynamicGallery/_Lightbox.style.js +86 -0
  12. package/dist/components/Organisms/DynamicGallery/_ScrollFix.js +57 -0
  13. package/dist/components/Organisms/DynamicGallery/__snapshots__/DynamicGallery.test.js.snap +1113 -0
  14. package/dist/components/Organisms/DynamicGallery/_types.js +18 -0
  15. package/dist/components/Organisms/DynamicGallery/_utils.js +24 -0
  16. package/dist/index.js +8 -1
  17. package/dist/styleguide/assets/tall.jpg +0 -0
  18. package/dist/styleguide/assets/wide.jpg +0 -0
  19. package/package.json +1 -1
  20. package/playwright/components/organisms/dynamicGallery.spec.js +9 -0
  21. package/src/components/Atoms/Icons/Cross.js +37 -0
  22. package/src/components/Atoms/Picture/Picture.js +4 -1
  23. package/src/components/Molecules/CTA/CTAMultiCard/__snapshots__/CTAMultiCard.test.js.snap +12 -12
  24. package/src/components/Molecules/CTA/shared/CTACard.style.js +1 -1
  25. package/src/components/Organisms/DynamicGallery/DynamicGallery.js +243 -0
  26. package/src/components/Organisms/DynamicGallery/DynamicGallery.md +30 -0
  27. package/src/components/Organisms/DynamicGallery/DynamicGallery.style.js +107 -0
  28. package/src/components/Organisms/DynamicGallery/DynamicGallery.test.js +34 -0
  29. package/src/components/Organisms/DynamicGallery/_DynamicGalleryColumn.js +144 -0
  30. package/src/components/Organisms/DynamicGallery/_Lightbox.js +242 -0
  31. package/src/components/Organisms/DynamicGallery/_Lightbox.style.js +159 -0
  32. package/src/components/Organisms/DynamicGallery/_ScrollFix.js +60 -0
  33. package/src/components/Organisms/DynamicGallery/__snapshots__/DynamicGallery.test.js.snap +1113 -0
  34. package/src/components/Organisms/DynamicGallery/_types.js +12 -0
  35. package/src/components/Organisms/DynamicGallery/_utils.js +28 -0
  36. package/src/index.js +1 -0
  37. package/src/styleguide/assets/tall.jpg +0 -0
  38. package/src/styleguide/assets/wide.jpg +0 -0
@@ -0,0 +1,218 @@
1
+ "use strict";
2
+
3
+ var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault").default;
4
+ var _interopRequireWildcard = require("@babel/runtime/helpers/interopRequireWildcard").default;
5
+ Object.defineProperty(exports, "__esModule", {
6
+ value: true
7
+ });
8
+ exports.default = exports.LightboxContext = void 0;
9
+ var _react = _interopRequireWildcard(require("react"));
10
+ var _PulseLoader = _interopRequireDefault(require("react-spinners/PulseLoader"));
11
+ var _Arrow = _interopRequireDefault(require("../../Atoms/Icons/Arrow"));
12
+ var _Cross = _interopRequireDefault(require("../../Atoms/Icons/Cross"));
13
+ var _Picture = _interopRequireDefault(require("../../Atoms/Picture/Picture"));
14
+ var _Lightbox = require("./_Lightbox.style");
15
+ var _ScrollFix = _interopRequireDefault(require("./_ScrollFix"));
16
+ /**
17
+ * lightbox context:
18
+ * - selectedNode: the node that is currently selected
19
+ * - setSelectedNode: set the selected node
20
+ * - nextNode/previousNode: navigate to the next/previous node
21
+ */
22
+ const LightboxContext = exports.LightboxContext = /*#__PURE__*/_react.default.createContext(null);
23
+
24
+ // get all focusable elements within the dialog
25
+ function getFocusableElements(element) {
26
+ if (!(element instanceof Element)) return [];
27
+ const focusableSelectors = ['button:not([disabled])', '[href]', 'input:not([disabled])', 'select:not([disabled])', 'textarea:not([disabled])', '[tabindex]:not([tabindex="-1"])'].join(', ');
28
+ return Array.from(element.querySelectorAll(focusableSelectors));
29
+ }
30
+
31
+ /**
32
+ * the Lightbox component is a modal that displays a single image,
33
+ * along with UI to navigate through the gallery
34
+ * .
35
+ * accessibility features like tabbing and focus management are currently implemented here;
36
+ * a better long-term approach would be to use an established modal/dialog library,
37
+ * but to avoid friction with the build process we've gone custom for now
38
+ * .
39
+ * TODO: hide the main window scroll bar in a nicer way, see:
40
+ * https://www.npmjs.com/package/react-remove-scroll
41
+ */
42
+ const Lightbox = () => {
43
+ const {
44
+ selectedNode,
45
+ setSelectedNode,
46
+ nextNode,
47
+ previousNode
48
+ } = (0, _react.useContext)(LightboxContext);
49
+ const hasNode = Boolean(selectedNode);
50
+ const dialogRef = (0, _react.useRef)(null);
51
+ const previousFocusRef = (0, _react.useRef)(null);
52
+
53
+ /**
54
+ * handle keyboard events within the lightbox;
55
+ * - trapped focus between UI elements
56
+ * - navigation between images
57
+ * - closing the lightbox
58
+ */
59
+ (0, _react.useEffect)(() => {
60
+ // trap focus within the dialog
61
+ function handleTabKey(event) {
62
+ if (!hasNode) return;
63
+ const focusableElements = getFocusableElements(dialogRef.current);
64
+ if (focusableElements.length === 0) return;
65
+ const firstElement = focusableElements[0];
66
+ const lastElement = focusableElements[focusableElements.length - 1];
67
+ const currentElement = document.activeElement;
68
+
69
+ // if shift+tab is pressed and we're on the first element, move to the last
70
+ if (event.shiftKey && currentElement === firstElement) {
71
+ event.preventDefault();
72
+ lastElement.focus();
73
+ } else if (!event.shiftKey && currentElement === lastElement) {
74
+ // if tab is pressed and we're on the last element, move to the first
75
+ event.preventDefault();
76
+ firstElement.focus();
77
+ }
78
+ }
79
+ function handleKeyDown(event) {
80
+ switch (event.key) {
81
+ case 'Escape':
82
+ setSelectedNode(null);
83
+ break;
84
+ case 'Tab':
85
+ handleTabKey(event);
86
+ break;
87
+ case 'ArrowLeft':
88
+ previousNode(selectedNode);
89
+ break;
90
+ case 'ArrowRight':
91
+ nextNode(selectedNode);
92
+ break;
93
+ default:
94
+ break;
95
+ }
96
+ }
97
+ if (hasNode) {
98
+ window.addEventListener('keydown', handleKeyDown);
99
+ }
100
+ return () => {
101
+ window.removeEventListener('keydown', handleKeyDown);
102
+ };
103
+ }, [hasNode, selectedNode, setSelectedNode, previousNode, nextNode]);
104
+
105
+ // handle focus management when dialog opens/closes
106
+ (0, _react.useEffect)(() => {
107
+ // when the lightbox is opened, store the previously focused element
108
+ // and move focus to the first focusable element in the dialog
109
+ if (hasNode) {
110
+ // store the previously focused element
111
+ previousFocusRef.current = document.activeElement;
112
+ // move focus to the first focusable element in the dialog
113
+ setTimeout(() => {
114
+ const focusableElements = getFocusableElements(dialogRef.current);
115
+ if (focusableElements.length > 0) {
116
+ focusableElements[0].focus();
117
+ }
118
+ }, 0);
119
+ return;
120
+ }
121
+
122
+ // when the lightbox is closed, restore focus to the previously focused element
123
+ if (previousFocusRef.current && typeof previousFocusRef.current.focus === 'function') {
124
+ previousFocusRef.current.focus();
125
+ previousFocusRef.current = null;
126
+ }
127
+ }, [hasNode]);
128
+
129
+ /**
130
+ * close the lightbox when the backdrop is clicked
131
+ */
132
+ function handleBackdropClick() {
133
+ setSelectedNode(null);
134
+ }
135
+
136
+ // handle transitions between images nicely;
137
+ const [imageDimensions, setImageDimensions] = (0, _react.useState)({
138
+ width: '0px',
139
+ height: '0px'
140
+ });
141
+
142
+ /**
143
+ * when the image loads, check to see how best we can fit it on screen,
144
+ * then set width and height on the element;
145
+ * this lets us transition nicely to the new size
146
+ */
147
+ function onLoad(event) {
148
+ const {
149
+ target
150
+ } = event;
151
+ const imageWidth = target.naturalWidth;
152
+ const imageHeight = target.naturalHeight;
153
+ const maxWidth = Math.min.apply(null, [imageWidth, 1024, window.innerWidth * 0.85]);
154
+ const maxHeight = Math.min.apply(null, [imageHeight, 1024, window.innerHeight * 0.5]);
155
+ const scaleX = maxWidth / imageWidth;
156
+ const scaleY = maxHeight / imageHeight;
157
+ const scale = Math.min(scaleX, scaleY);
158
+ const width = imageWidth * scale;
159
+ const height = imageHeight * scale;
160
+
161
+ // set the width and height on the image element, and make it visible
162
+ setImageDimensions({
163
+ width: `${width}px`,
164
+ height: `${height}px`
165
+ });
166
+ target.style.opacity = '1';
167
+ }
168
+ return /*#__PURE__*/_react.default.createElement(_Lightbox.Container, {
169
+ isOpen: hasNode
170
+ }, /*#__PURE__*/_react.default.createElement(_Lightbox.Backdrop, {
171
+ onPointerUp: () => handleBackdropClick()
172
+ }), /*#__PURE__*/_react.default.createElement(_Lightbox.Dialog, {
173
+ ref: dialogRef,
174
+ "aria-labelledby": "lightboxTitle",
175
+ "aria-describedby": "lightboxDescription"
176
+ }, hasNode && /*#__PURE__*/_react.default.createElement(_ScrollFix.default, null), /*#__PURE__*/_react.default.createElement(_Lightbox.LightboxContent, null, /*#__PURE__*/_react.default.createElement(_Lightbox.LightboxImage, {
177
+ className: "lightbox-image"
178
+ }, /*#__PURE__*/_react.default.createElement(_Lightbox.LightboxSpinner, null, /*#__PURE__*/_react.default.createElement(_PulseLoader.default, {
179
+ height: 16,
180
+ width: 2,
181
+ color: "#E1E2E3"
182
+ })), hasNode && /*#__PURE__*/_react.default.createElement(_Picture.default, {
183
+ key: selectedNode === null || selectedNode === void 0 ? void 0 : selectedNode.image,
184
+ alt: selectedNode === null || selectedNode === void 0 ? void 0 : selectedNode.title,
185
+ image: selectedNode === null || selectedNode === void 0 ? void 0 : selectedNode.image,
186
+ width: imageDimensions.width,
187
+ height: imageDimensions.height,
188
+ objectFit: "contain",
189
+ onLoad: event => onLoad(event)
190
+ })), /*#__PURE__*/_react.default.createElement(_Lightbox.LightboxDetails, {
191
+ id: "lightboxDescription",
192
+ "aria-live": "polite",
193
+ "aria-atomic": "true"
194
+ }, /*#__PURE__*/_react.default.createElement("div", {
195
+ id: "lightboxTitle"
196
+ }, selectedNode === null || selectedNode === void 0 ? void 0 : selectedNode.title), (selectedNode === null || selectedNode === void 0 ? void 0 : selectedNode.caption) && /*#__PURE__*/_react.default.createElement("div", null, selectedNode === null || selectedNode === void 0 ? void 0 : selectedNode.caption), (selectedNode === null || selectedNode === void 0 ? void 0 : selectedNode.body) && /*#__PURE__*/_react.default.createElement("div", null, selectedNode.body)), /*#__PURE__*/_react.default.createElement(_Lightbox.CloseButton, {
197
+ type: "button",
198
+ onClick: () => setSelectedNode(null)
199
+ }, /*#__PURE__*/_react.default.createElement(_Lightbox.ScreenReaderOnly, null, "Close"), /*#__PURE__*/_react.default.createElement(_Cross.default, {
200
+ colour: "black",
201
+ size: 16
202
+ })), /*#__PURE__*/_react.default.createElement(_Lightbox.PreviousButton, {
203
+ type: "button",
204
+ onClick: () => previousNode(selectedNode)
205
+ }, /*#__PURE__*/_react.default.createElement(_Lightbox.ScreenReaderOnly, null, "Previous"), /*#__PURE__*/_react.default.createElement(_Arrow.default, {
206
+ direction: "left",
207
+ colour: "black",
208
+ size: 16
209
+ })), /*#__PURE__*/_react.default.createElement(_Lightbox.NextButton, {
210
+ type: "button",
211
+ onClick: () => nextNode(selectedNode)
212
+ }, /*#__PURE__*/_react.default.createElement(_Lightbox.ScreenReaderOnly, null, "Next"), /*#__PURE__*/_react.default.createElement(_Arrow.default, {
213
+ direction: "right",
214
+ colour: "black",
215
+ size: 16
216
+ })))));
217
+ };
218
+ var _default = exports.default = Lightbox;
@@ -0,0 +1,86 @@
1
+ "use strict";
2
+
3
+ var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault").default;
4
+ Object.defineProperty(exports, "__esModule", {
5
+ value: true
6
+ });
7
+ exports.ScreenReaderOnly = exports.PreviousButton = exports.NextButton = exports.NavButton = exports.LightboxSpinner = exports.LightboxImage = exports.LightboxDetails = exports.LightboxContent = exports.Dialog = exports.Container = exports.CloseButton = exports.Backdrop = void 0;
8
+ var _styledComponents = _interopRequireDefault(require("styled-components"));
9
+ const Container = exports.Container = _styledComponents.default.div.withConfig({
10
+ displayName: "_Lightboxstyle__Container",
11
+ componentId: "sc-twdy7x-0"
12
+ })(["position:fixed;top:0;left:0;width:100%;height:100%;display:flex;justify-content:center;align-items:center;z-index:2000;visibility:", ";"], _ref => {
13
+ let {
14
+ isOpen
15
+ } = _ref;
16
+ return isOpen ? 'visible' : 'hidden';
17
+ });
18
+ const Backdrop = exports.Backdrop = _styledComponents.default.div.withConfig({
19
+ displayName: "_Lightboxstyle__Backdrop",
20
+ componentId: "sc-twdy7x-1"
21
+ })(["position:absolute;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.8);z-index:0;"]);
22
+ const Dialog = exports.Dialog = _styledComponents.default.dialog.withConfig({
23
+ displayName: "_Lightboxstyle__Dialog",
24
+ componentId: "sc-twdy7x-2"
25
+ })(["display:block;padding:0.5rem;background:transparent;border:none;z-index:1;margin-top:72px;@media ", "{margin-top:84px;}"], _ref2 => {
26
+ let {
27
+ theme
28
+ } = _ref2;
29
+ return theme.breakpoints2026('L');
30
+ });
31
+ const LightboxContent = exports.LightboxContent = _styledComponents.default.div.withConfig({
32
+ displayName: "_Lightboxstyle__LightboxContent",
33
+ componentId: "sc-twdy7x-3"
34
+ })(["display:flex;flex-direction:column;align-items:center;gap:1rem;position:relative;padding:1rem;background:#ffffff;border-radius:1rem;"]);
35
+ const LightboxImage = exports.LightboxImage = _styledComponents.default.div.withConfig({
36
+ displayName: "_Lightboxstyle__LightboxImage",
37
+ componentId: "sc-twdy7x-4"
38
+ })(["position:relative;display:flex;align-items:center;justify-content:center;min-width:128px;min-height:32px;border-radius:0.6rem;overflow:hidden;& > div{display:flex;align-items:center;justify-content:center;transition:width 0.3s ease-in-out,height 0.3s ease-in-out;}& img{opacity:0;transition:opacity 0.1s ease-out 0.3s;}"]);
39
+ const LightboxSpinner = exports.LightboxSpinner = _styledComponents.default.div.withConfig({
40
+ displayName: "_Lightboxstyle__LightboxSpinner",
41
+ componentId: "sc-twdy7x-5"
42
+ })(["position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);"]);
43
+ const LightboxDetails = exports.LightboxDetails = _styledComponents.default.div.withConfig({
44
+ displayName: "_Lightboxstyle__LightboxDetails",
45
+ componentId: "sc-twdy7x-6"
46
+ })(["display:flex;flex-direction:column;align-items:stretch;gap:0.5rem;width:100%;padding:0 1rem;"]);
47
+ const NavButton = exports.NavButton = _styledComponents.default.button.withConfig({
48
+ displayName: "_Lightboxstyle__NavButton",
49
+ componentId: "sc-twdy7x-7"
50
+ })(["position:absolute;display:flex;align-items:center;justify-content:center;width:2.5rem;height:2.5rem;border-radius:0.5rem;border:none;background-color:white;cursor:pointer;z-index:10;svg{transition:all 0.1s ease-out;}&:hover{svg{fill:", ";}}&:focus-visible{outline:2px solid ", ";}"], _ref3 => {
51
+ let {
52
+ theme
53
+ } = _ref3;
54
+ return theme.color('red');
55
+ }, _ref4 => {
56
+ let {
57
+ theme
58
+ } = _ref4;
59
+ return theme.color('red');
60
+ });
61
+ const CloseButton = exports.CloseButton = (0, _styledComponents.default)(NavButton).withConfig({
62
+ displayName: "_Lightboxstyle__CloseButton",
63
+ componentId: "sc-twdy7x-8"
64
+ })(["top:0;right:0;"]);
65
+ const PreviousButton = exports.PreviousButton = (0, _styledComponents.default)(NavButton).withConfig({
66
+ displayName: "_Lightboxstyle__PreviousButton",
67
+ componentId: "sc-twdy7x-9"
68
+ })(["top:30%;left:0;transform:translate(0,-50%);border-top-left-radius:0;border-bottom-left-radius:0;@media ", "{position:fixed;top:50%;}"], _ref5 => {
69
+ let {
70
+ theme
71
+ } = _ref5;
72
+ return theme.breakpoints2026('L');
73
+ });
74
+ const NextButton = exports.NextButton = (0, _styledComponents.default)(NavButton).withConfig({
75
+ displayName: "_Lightboxstyle__NextButton",
76
+ componentId: "sc-twdy7x-10"
77
+ })(["top:30%;right:0;transform:translate(0,-50%);border-top-right-radius:0;border-bottom-right-radius:0;@media ", "{position:fixed;top:50%;}"], _ref6 => {
78
+ let {
79
+ theme
80
+ } = _ref6;
81
+ return theme.breakpoints2026('L');
82
+ });
83
+ const ScreenReaderOnly = exports.ScreenReaderOnly = _styledComponents.default.span.withConfig({
84
+ displayName: "_Lightboxstyle__ScreenReaderOnly",
85
+ componentId: "sc-twdy7x-11"
86
+ })(["position:absolute;width:1px;height:1px;margin:-1px;border:0;padding:0;white-space:nowrap;clip-path:inset(100%);clip:rect(0 0 0 0);overflow:hidden;"]);
@@ -0,0 +1,57 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.default = ScrollFix;
7
+ var _react = require("react");
8
+ let scrollPadding;
9
+
10
+ /**
11
+ * small utility component that allows us to lock the main window scrolling,
12
+ * while also avoiding flicker/judder as the scrollbar is added and removed from the DOM.
13
+ * used in the Lightbox component to ensure the scrollbar is hidden when the lightbox is open.
14
+ */
15
+ function ScrollFix() {
16
+ // create a DOM element with a known width and a scrollbar,
17
+ // then measure an inner element to find the missing width taken up by the scrollbar
18
+ if (scrollPadding === undefined) {
19
+ const container = document.createElement('div');
20
+ container.className = 'scroll-fix-container';
21
+ container.style.setProperty('position', 'absolute');
22
+ container.style.setProperty('top', '0');
23
+ container.style.setProperty('left', '-9999px');
24
+ container.style.setProperty('width', '100px');
25
+ container.style.setProperty('height', '100px');
26
+ container.style.setProperty('overflow-y', 'scroll');
27
+ const inner = document.createElement('div');
28
+ inner.style.setProperty('width', '100%');
29
+ container.appendChild(inner);
30
+ // edge needs the element to be in the DOM to measure the scrollbar width
31
+ document.body.appendChild(container);
32
+ scrollPadding = 100 - inner.getBoundingClientRect().width;
33
+ // remove the element from the DOM
34
+ document.body.removeChild(container);
35
+ }
36
+
37
+ // when the component mounts, add a padding to the document to compensate for the scrollbar;
38
+ // this is then removed when the component unmounts
39
+ (0, _react.useLayoutEffect)(() => {
40
+ function resetScrollbar() {
41
+ document.documentElement.style.setProperty('overflow', 'auto');
42
+ document.documentElement.style.setProperty('padding-right', '0px');
43
+ }
44
+
45
+ // check that the page content is longer than the viewport
46
+ if (document.documentElement.scrollHeight <= window.innerHeight) {
47
+ resetScrollbar();
48
+ } else {
49
+ document.documentElement.style.setProperty('overflow', 'hidden');
50
+ document.documentElement.style.setProperty('padding-right', `${scrollPadding}px`);
51
+ }
52
+ return () => {
53
+ resetScrollbar();
54
+ };
55
+ }, []);
56
+ return null;
57
+ }