@comicrelief/component-library 5.6.0 → 5.7.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/.github/workflows/main.yml +131 -0
- package/.github/workflows/pr-checks.yml +17 -0
- package/README.md +6 -6
- package/dist/components/Atoms/ButtonWithStates/ButtonWithStates.js +91 -0
- package/dist/components/Atoms/ButtonWithStates/ButtonWithStates.md +13 -0
- package/dist/components/Atoms/Link/Link.js +17 -4
- package/dist/components/Molecules/Lookup/Lookup.js +201 -0
- package/dist/components/Molecules/SimpleSchoolLookup/SimpleSchoolLookup.js +112 -0
- package/dist/components/Molecules/SimpleSchoolLookup/SimpleSchoolLookup.md +7 -0
- package/dist/components/Molecules/SingleMessage/__snapshots__/SingleMessage.test.js.snap +8 -0
- package/dist/components/Organisms/Footer/Footer.js +4 -3
- package/dist/components/Organisms/Footer/Footer.md +8 -1
- package/dist/components/Organisms/Footer/Nav/Nav.js +10 -5
- package/dist/index.js +24 -0
- package/package.json +2 -1
- package/src/components/Atoms/ButtonWithStates/ButtonWithStates.js +64 -0
- package/src/components/Atoms/ButtonWithStates/ButtonWithStates.md +13 -0
- package/src/components/Atoms/Link/Link.js +12 -4
- package/src/components/Molecules/Lookup/Lookup.js +148 -0
- package/src/components/Molecules/SimpleSchoolLookup/SimpleSchoolLookup.js +63 -0
- package/src/components/Molecules/SimpleSchoolLookup/SimpleSchoolLookup.md +7 -0
- package/src/components/Molecules/SingleMessage/__snapshots__/SingleMessage.test.js.snap +8 -0
- package/src/components/Organisms/Footer/Footer.js +5 -3
- package/src/components/Organisms/Footer/Footer.md +8 -1
- package/src/components/Organisms/Footer/Nav/Nav.js +3 -2
- package/src/index.js +3 -0
- package/.circleci/config.yml +0 -54
|
@@ -896,6 +896,10 @@ exports[`renders Single Message with full width image and no text correctly 1`]
|
|
|
896
896
|
z-index: 1;
|
|
897
897
|
}
|
|
898
898
|
|
|
899
|
+
@media (min-width:740px) {
|
|
900
|
+
|
|
901
|
+
}
|
|
902
|
+
|
|
899
903
|
@media (min-width:740px) {
|
|
900
904
|
.c0 {
|
|
901
905
|
-webkit-flex-direction: row;
|
|
@@ -1010,6 +1014,10 @@ exports[`renders Single Message with no Image correctly 1`] = `
|
|
|
1010
1014
|
padding: 1rem;
|
|
1011
1015
|
}
|
|
1012
1016
|
|
|
1017
|
+
@media (min-width:740px) {
|
|
1018
|
+
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1013
1021
|
@media (min-width:740px) {
|
|
1014
1022
|
.c0 {
|
|
1015
1023
|
-webkit-flex-direction: row;
|
|
@@ -42,9 +42,9 @@ var Footer = function Footer(_ref) {
|
|
|
42
42
|
sizeMd: "72px",
|
|
43
43
|
rotate: false,
|
|
44
44
|
campaign: campaign
|
|
45
|
-
}))), /*#__PURE__*/_react.default.createElement(_Nav.default, {
|
|
45
|
+
}))), /*#__PURE__*/_react.default.createElement(_Nav.default, Object.assign({
|
|
46
46
|
navItems: navItems
|
|
47
|
-
}), /*#__PURE__*/_react.default.createElement(_Footer.FooterCopyright, null, /*#__PURE__*/_react.default.createElement(_Text.default, {
|
|
47
|
+
}, rest)), /*#__PURE__*/_react.default.createElement(_Footer.FooterCopyright, null, /*#__PURE__*/_react.default.createElement(_Text.default, {
|
|
48
48
|
tag: "p",
|
|
49
49
|
color: "grey"
|
|
50
50
|
}, footerCopy)))));
|
|
@@ -53,7 +53,8 @@ var Footer = function Footer(_ref) {
|
|
|
53
53
|
Footer.defaultProps = {
|
|
54
54
|
navItems: {},
|
|
55
55
|
footerCopy: '',
|
|
56
|
-
campaign: 'Comic Relief'
|
|
56
|
+
campaign: 'Comic Relief',
|
|
57
|
+
overrideWhiteList: false
|
|
57
58
|
};
|
|
58
59
|
var _default = Footer;
|
|
59
60
|
exports.default = _default;
|
|
@@ -4,5 +4,12 @@
|
|
|
4
4
|
import data from './data/data';
|
|
5
5
|
import footerCopy from './data/footerCopy';
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
<>
|
|
8
|
+
<p>Standard footer</p>
|
|
9
|
+
<Footer navItems={data} footerCopy={footerCopy.copy} campaign="Comic Relief" />
|
|
10
|
+
|
|
11
|
+
<p>Overrides whitelist functionality for external usage</p>
|
|
12
|
+
<Footer navItems={data} footerCopy={footerCopy.copy} campaign="Comic Relief" overrideWhiteList />
|
|
13
|
+
</>
|
|
14
|
+
|
|
8
15
|
```
|
|
@@ -11,6 +11,8 @@ var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/es
|
|
|
11
11
|
|
|
12
12
|
var _slicedToArray2 = _interopRequireDefault(require("@babel/runtime/helpers/esm/slicedToArray"));
|
|
13
13
|
|
|
14
|
+
var _objectWithoutProperties2 = _interopRequireDefault(require("@babel/runtime/helpers/esm/objectWithoutProperties"));
|
|
15
|
+
|
|
14
16
|
var _react = _interopRequireWildcard(require("react"));
|
|
15
17
|
|
|
16
18
|
var _Text = _interopRequireDefault(require("../../../Atoms/Text/Text"));
|
|
@@ -23,12 +25,15 @@ var _internalLinkHelper = require("../../../../utils/internalLinkHelper");
|
|
|
23
25
|
|
|
24
26
|
var _Nav = require("./Nav.style");
|
|
25
27
|
|
|
28
|
+
var _excluded = ["navItems"];
|
|
29
|
+
|
|
26
30
|
function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function _getRequireWildcardCache(nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); }
|
|
27
31
|
|
|
28
32
|
function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; }
|
|
29
33
|
|
|
30
34
|
var FooterNav = function FooterNav(_ref) {
|
|
31
|
-
var navItems = _ref.navItems
|
|
35
|
+
var navItems = _ref.navItems,
|
|
36
|
+
rest = (0, _objectWithoutProperties2.default)(_ref, _excluded);
|
|
32
37
|
var menuGroups = navItems.menuGroups;
|
|
33
38
|
|
|
34
39
|
var _useState = (0, _react.useState)(false),
|
|
@@ -96,14 +101,14 @@ var FooterNav = function FooterNav(_ref) {
|
|
|
96
101
|
}, !isSmallBreakpoint ? /*#__PURE__*/_react.default.createElement(_Text.default, {
|
|
97
102
|
color: "white",
|
|
98
103
|
weight: "bold"
|
|
99
|
-
}, group.title) : /*#__PURE__*/_react.default.createElement(_Nav.NavLink, {
|
|
104
|
+
}, group.title) : /*#__PURE__*/_react.default.createElement(_Nav.NavLink, Object.assign({
|
|
100
105
|
href: "#",
|
|
101
106
|
inline: true,
|
|
102
107
|
"aria-expanded": !!isSubMenuOpen[group.id],
|
|
103
108
|
"aria-haspopup": "true",
|
|
104
109
|
role: "button",
|
|
105
110
|
onClick: toggleSubMenu(group.id)
|
|
106
|
-
}, /*#__PURE__*/_react.default.createElement(_Text.default, {
|
|
111
|
+
}, rest), /*#__PURE__*/_react.default.createElement(_Text.default, {
|
|
107
112
|
color: "white"
|
|
108
113
|
}, group.title)), group.links && group.links.length > 0 && /*#__PURE__*/_react.default.createElement(_Nav.SubNavMenu, {
|
|
109
114
|
role: "list",
|
|
@@ -117,11 +122,11 @@ var FooterNav = function FooterNav(_ref) {
|
|
|
117
122
|
return /*#__PURE__*/_react.default.createElement(_Nav.SubNavItem, {
|
|
118
123
|
key: thisUrl,
|
|
119
124
|
column: group.links.length % 2 === 0 && group.links.length > 2
|
|
120
|
-
}, /*#__PURE__*/_react.default.createElement(_Nav.SubNavLink, {
|
|
125
|
+
}, /*#__PURE__*/_react.default.createElement(_Nav.SubNavLink, Object.assign({
|
|
121
126
|
href: thisUrl,
|
|
122
127
|
inline: true,
|
|
123
128
|
role: "menuitem"
|
|
124
|
-
}, /*#__PURE__*/_react.default.createElement(_Text.default, {
|
|
129
|
+
}, rest), /*#__PURE__*/_react.default.createElement(_Text.default, {
|
|
125
130
|
color: "white"
|
|
126
131
|
}, child.title)));
|
|
127
132
|
})));
|
package/dist/index.js
CHANGED
|
@@ -167,6 +167,12 @@ Object.defineProperty(exports, "Label", {
|
|
|
167
167
|
return _Label.default;
|
|
168
168
|
}
|
|
169
169
|
});
|
|
170
|
+
Object.defineProperty(exports, "ButtonWithStates", {
|
|
171
|
+
enumerable: true,
|
|
172
|
+
get: function get() {
|
|
173
|
+
return _ButtonWithStates.default;
|
|
174
|
+
}
|
|
175
|
+
});
|
|
170
176
|
Object.defineProperty(exports, "HeroBanner", {
|
|
171
177
|
enumerable: true,
|
|
172
178
|
get: function get() {
|
|
@@ -329,6 +335,18 @@ Object.defineProperty(exports, "Descriptor", {
|
|
|
329
335
|
return _Descriptor.default;
|
|
330
336
|
}
|
|
331
337
|
});
|
|
338
|
+
Object.defineProperty(exports, "Lookup", {
|
|
339
|
+
enumerable: true,
|
|
340
|
+
get: function get() {
|
|
341
|
+
return _Lookup.default;
|
|
342
|
+
}
|
|
343
|
+
});
|
|
344
|
+
Object.defineProperty(exports, "SimpleSchoolLookup", {
|
|
345
|
+
enumerable: true,
|
|
346
|
+
get: function get() {
|
|
347
|
+
return _SimpleSchoolLookup.default;
|
|
348
|
+
}
|
|
349
|
+
});
|
|
332
350
|
Object.defineProperty(exports, "EmailSignUp", {
|
|
333
351
|
enumerable: true,
|
|
334
352
|
get: function get() {
|
|
@@ -416,6 +434,8 @@ var _ErrorText = _interopRequireDefault(require("./components/Atoms/ErrorText/Er
|
|
|
416
434
|
|
|
417
435
|
var _Label = _interopRequireDefault(require("./components/Atoms/Label/Label"));
|
|
418
436
|
|
|
437
|
+
var _ButtonWithStates = _interopRequireDefault(require("./components/Atoms/ButtonWithStates/ButtonWithStates"));
|
|
438
|
+
|
|
419
439
|
var _HeroBanner = _interopRequireDefault(require("./components/Molecules/HeroBanner/HeroBanner"));
|
|
420
440
|
|
|
421
441
|
var _InfoBanner = _interopRequireDefault(require("./components/Molecules/InfoBanner/InfoBanner"));
|
|
@@ -470,6 +490,10 @@ var _Chip = _interopRequireDefault(require("./components/Molecules/Chip/Chip"));
|
|
|
470
490
|
|
|
471
491
|
var _Descriptor = _interopRequireDefault(require("./components/Molecules/Descriptor/Descriptor"));
|
|
472
492
|
|
|
493
|
+
var _Lookup = _interopRequireDefault(require("./components/Molecules/Lookup/Lookup"));
|
|
494
|
+
|
|
495
|
+
var _SimpleSchoolLookup = _interopRequireDefault(require("./components/Molecules/SimpleSchoolLookup/SimpleSchoolLookup"));
|
|
496
|
+
|
|
473
497
|
var _EmailSignUp = _interopRequireDefault(require("./components/Organisms/EmailSignUp/EmailSignUp"));
|
|
474
498
|
|
|
475
499
|
var _CookieBanner = _interopRequireDefault(require("./components/Organisms/CookieBanner/CookieBanner"));
|
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": "5.
|
|
4
|
+
"version": "5.7.1",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"license": "ISC",
|
|
7
7
|
"jest": {
|
|
@@ -35,6 +35,7 @@
|
|
|
35
35
|
"react-hook-form": "^6.3.0",
|
|
36
36
|
"react-modal": "^3.14.3",
|
|
37
37
|
"react-scripts": "4.0.3",
|
|
38
|
+
"react-spinners": "^0.11.0",
|
|
38
39
|
"react-styleguidist": "^11.1.7",
|
|
39
40
|
"react-test-renderer": "^17.0.2",
|
|
40
41
|
"react-uid": "^2.2.0",
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import React, { useCallback, useState } from 'react';
|
|
2
|
+
import PropTypes from 'prop-types';
|
|
3
|
+
import styled from 'styled-components';
|
|
4
|
+
import ScaleLoader from 'react-spinners/ScaleLoader';
|
|
5
|
+
import spacing from '../../../theme/shared/spacing';
|
|
6
|
+
import Button from '../Button/Button';
|
|
7
|
+
|
|
8
|
+
const ButtonWithDisabledState = styled(Button)`
|
|
9
|
+
&:disabled {
|
|
10
|
+
cursor: not-allowed;
|
|
11
|
+
opacity: 0.75;
|
|
12
|
+
}
|
|
13
|
+
`;
|
|
14
|
+
|
|
15
|
+
const LoaderContainer = styled.div`${({ withMargin }) => (withMargin ? `
|
|
16
|
+
margin-top: ${spacing('xsm')};
|
|
17
|
+
margin-left: ${spacing('md')};
|
|
18
|
+
` : '')}`;
|
|
19
|
+
|
|
20
|
+
// A button with loading and disabled states.
|
|
21
|
+
const ButtonWithStates = React.forwardRef(({
|
|
22
|
+
children, loadingText, loading, disabled, ...rest
|
|
23
|
+
}, ref) => {
|
|
24
|
+
const [loaderColour, setLoaderColour] = useState(null);
|
|
25
|
+
|
|
26
|
+
// Can't see a simpler way to get the button's text colour, without reading the value
|
|
27
|
+
// via JavaScript.
|
|
28
|
+
// (e.g. the `theme.buttonColours` helper returns a CSS string split into an array -
|
|
29
|
+
// don't really want to be parsing that.)
|
|
30
|
+
// (And can't use inherit because ScaleLoader's color prop is actually setting its
|
|
31
|
+
// background color.)
|
|
32
|
+
const getLoaderColour = useCallback(node => {
|
|
33
|
+
if (node && typeof window.getComputedStyle === 'function') {
|
|
34
|
+
const textColour = window.getComputedStyle(node).color;
|
|
35
|
+
if (textColour) {
|
|
36
|
+
setLoaderColour(textColour);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}, []);
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<ButtonWithDisabledState ref={ref} disabled={disabled} {...rest}>
|
|
43
|
+
{loading ? loadingText : children}
|
|
44
|
+
<LoaderContainer ref={getLoaderColour} withMargin={loading}>
|
|
45
|
+
<ScaleLoader height={16} width={2} loading={loading} color={loaderColour} />
|
|
46
|
+
</LoaderContainer>
|
|
47
|
+
</ButtonWithDisabledState>
|
|
48
|
+
);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
ButtonWithStates.propTypes = {
|
|
52
|
+
children: PropTypes.node.isRequired,
|
|
53
|
+
loadingText: PropTypes.string,
|
|
54
|
+
loading: PropTypes.bool,
|
|
55
|
+
disabled: PropTypes.bool
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
ButtonWithStates.defaultProps = {
|
|
59
|
+
loading: false,
|
|
60
|
+
disabled: false,
|
|
61
|
+
loadingText: 'Loading'
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
export default ButtonWithStates;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React from 'react';
|
|
1
|
+
import React, { useEffect, useState } from 'react';
|
|
2
2
|
import PropTypes from 'prop-types';
|
|
3
3
|
|
|
4
4
|
import StyledLink, { HelperText, IconWrapper } from './Link.style';
|
|
@@ -19,29 +19,37 @@ const Link = ({
|
|
|
19
19
|
iconFirst,
|
|
20
20
|
...rest
|
|
21
21
|
}) => {
|
|
22
|
+
const [documentHost, setDocumentHost] = useState('');
|
|
22
23
|
/**
|
|
23
24
|
* If we haven't specifically set the target via props, check if
|
|
24
25
|
* this is an internal link OR on our whitelist before making it a '_self' link
|
|
25
26
|
*/
|
|
26
27
|
if (target === null) {
|
|
27
28
|
// Use our helper function to determine the raw domains to compare
|
|
28
|
-
const currentDomain = getDomain(
|
|
29
|
+
const currentDomain = getDomain(documentHost);
|
|
29
30
|
const linkDomain = getDomain(href);
|
|
30
31
|
|
|
32
|
+
// Additional check for applications that need more control
|
|
33
|
+
const isWhiteListOverridden = rest.overrideWhiteList === true;
|
|
34
|
+
|
|
31
35
|
/**
|
|
32
36
|
* If the link has no domain supplied (likely '/' or '#')
|
|
33
37
|
* OR has the same domain as the current page, don't open
|
|
34
38
|
* in a new tab
|
|
35
39
|
*/
|
|
36
40
|
const isExternalLink = linkDomain !== '' && (currentDomain !== linkDomain);
|
|
37
|
-
|
|
38
41
|
const isWhiteListed = whiteListed(href);
|
|
39
|
-
|
|
42
|
+
|
|
43
|
+
window = isExternalLink && (isWhiteListOverridden || !isWhiteListed) ? '_blank' : '_self';
|
|
40
44
|
} else {
|
|
41
45
|
window = target === 'blank' ? '_blank' : '_self';
|
|
42
46
|
}
|
|
43
47
|
const hasIcon = icon !== null;
|
|
44
48
|
|
|
49
|
+
useEffect(() => {
|
|
50
|
+
setDocumentHost(document.location.host);
|
|
51
|
+
}, []);
|
|
52
|
+
|
|
45
53
|
return (
|
|
46
54
|
<StyledLink
|
|
47
55
|
{...rest}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import React, { useCallback, useState } from 'react';
|
|
2
|
+
import PropTypes from 'prop-types';
|
|
3
|
+
import styled, { css } from 'styled-components';
|
|
4
|
+
import TextInputWithDropdown from '../../Atoms/TextInputWithDropdown/TextInputWithDropdown';
|
|
5
|
+
import spacing from '../../../theme/shared/spacing';
|
|
6
|
+
import ButtonWithStates from '../../Atoms/ButtonWithStates/ButtonWithStates';
|
|
7
|
+
|
|
8
|
+
const StyledButton = styled(ButtonWithStates)`${({ theme }) => css`
|
|
9
|
+
color: ${theme.color('grey_dark')};
|
|
10
|
+
border: 2px solid ${theme.color('grey_dark')};
|
|
11
|
+
background-color: ${theme.color('white')};
|
|
12
|
+
padding-left: ${spacing('lg')};
|
|
13
|
+
padding-right: ${spacing('lg')};
|
|
14
|
+
|
|
15
|
+
&:hover {
|
|
16
|
+
color: ${theme.color('grey_dark')};
|
|
17
|
+
background-color: ${theme.color('white')};
|
|
18
|
+
}
|
|
19
|
+
`}`;
|
|
20
|
+
|
|
21
|
+
const KEY_CODE_ENTER = 13;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* A simple lookup component
|
|
25
|
+
*
|
|
26
|
+
* The `lookupHandler` should be an async function which is called when a lookup is triggered
|
|
27
|
+
* (either by hitting enter or clicking the button)
|
|
28
|
+
*
|
|
29
|
+
* It will receive the current search term and should:
|
|
30
|
+
* - take care of any validation on the search term
|
|
31
|
+
* - perform the actual lookup request
|
|
32
|
+
* - return an array of options (or an empty array if none were found)
|
|
33
|
+
* - only throw errors with user-friendly messages
|
|
34
|
+
*
|
|
35
|
+
* Any errors thrown will be caught and the message will be displayed to the user.
|
|
36
|
+
*
|
|
37
|
+
* The `onSelect` function will receive the chosen option.
|
|
38
|
+
*
|
|
39
|
+
* @param name
|
|
40
|
+
* @param label
|
|
41
|
+
* @param placeholder
|
|
42
|
+
* @param buttonText
|
|
43
|
+
* @param lookupHandler
|
|
44
|
+
* @param mapOptionToString
|
|
45
|
+
* @param onSelect
|
|
46
|
+
* @param noResultsMessage
|
|
47
|
+
* @param dropdownInstruction
|
|
48
|
+
* @param rest
|
|
49
|
+
* @returns {JSX.Element}
|
|
50
|
+
* @constructor
|
|
51
|
+
*/
|
|
52
|
+
const Lookup = ({
|
|
53
|
+
name,
|
|
54
|
+
label,
|
|
55
|
+
placeholder,
|
|
56
|
+
buttonText,
|
|
57
|
+
lookupHandler,
|
|
58
|
+
mapOptionToString,
|
|
59
|
+
onSelect,
|
|
60
|
+
noResultsMessage,
|
|
61
|
+
dropdownInstruction,
|
|
62
|
+
...rest
|
|
63
|
+
}) => {
|
|
64
|
+
const [query, setQuery] = useState('');
|
|
65
|
+
const [errorMessage, setErrorMessage] = useState('');
|
|
66
|
+
const [options, setOptions] = useState([]);
|
|
67
|
+
const [isSearching, setIsSearching] = useState(false);
|
|
68
|
+
|
|
69
|
+
const handler = useCallback(async () => {
|
|
70
|
+
setErrorMessage('');
|
|
71
|
+
setIsSearching(true);
|
|
72
|
+
try {
|
|
73
|
+
// If lookupHandler throws an error, the message will be displayed to the user
|
|
74
|
+
const results = await lookupHandler(query);
|
|
75
|
+
setOptions(results);
|
|
76
|
+
if (results.length === 0) {
|
|
77
|
+
setErrorMessage(noResultsMessage);
|
|
78
|
+
}
|
|
79
|
+
} catch (error) {
|
|
80
|
+
setErrorMessage(error.message);
|
|
81
|
+
}
|
|
82
|
+
setIsSearching(false);
|
|
83
|
+
}, [query, setOptions, setErrorMessage, noResultsMessage, lookupHandler]);
|
|
84
|
+
|
|
85
|
+
return (
|
|
86
|
+
<div {...rest}>
|
|
87
|
+
<TextInputWithDropdown
|
|
88
|
+
css={{ marginBottom: spacing('md') }}
|
|
89
|
+
name={name}
|
|
90
|
+
id={name}
|
|
91
|
+
value={query}
|
|
92
|
+
options={options.map(mapOptionToString)}
|
|
93
|
+
label={label}
|
|
94
|
+
placeholder={placeholder}
|
|
95
|
+
onChange={e => {
|
|
96
|
+
setQuery(e.target.value);
|
|
97
|
+
setErrorMessage('');
|
|
98
|
+
setOptions([]);
|
|
99
|
+
}}
|
|
100
|
+
onKeyPress={e => {
|
|
101
|
+
const keyCode = e.keyCode || e.which;
|
|
102
|
+
if (keyCode === KEY_CODE_ENTER) {
|
|
103
|
+
e.preventDefault();
|
|
104
|
+
return handler();
|
|
105
|
+
}
|
|
106
|
+
return null;
|
|
107
|
+
}}
|
|
108
|
+
onSelect={(text, index) => {
|
|
109
|
+
const selection = options[index];
|
|
110
|
+
onSelect(selection);
|
|
111
|
+
setQuery('');
|
|
112
|
+
setErrorMessage('');
|
|
113
|
+
setOptions([]);
|
|
114
|
+
}}
|
|
115
|
+
errorMsg={errorMessage}
|
|
116
|
+
dropdownInstruction={dropdownInstruction}
|
|
117
|
+
/>
|
|
118
|
+
<StyledButton
|
|
119
|
+
type="button"
|
|
120
|
+
onClick={() => handler()}
|
|
121
|
+
loading={isSearching}
|
|
122
|
+
disabled={isSearching}
|
|
123
|
+
loadingText="Searching"
|
|
124
|
+
>
|
|
125
|
+
{buttonText}
|
|
126
|
+
</StyledButton>
|
|
127
|
+
</div>
|
|
128
|
+
);
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
Lookup.propTypes = {
|
|
132
|
+
name: PropTypes.string.isRequired,
|
|
133
|
+
label: PropTypes.string.isRequired,
|
|
134
|
+
placeholder: PropTypes.string.isRequired,
|
|
135
|
+
buttonText: PropTypes.string.isRequired,
|
|
136
|
+
lookupHandler: PropTypes.func.isRequired,
|
|
137
|
+
mapOptionToString: PropTypes.func.isRequired,
|
|
138
|
+
onSelect: PropTypes.func.isRequired,
|
|
139
|
+
noResultsMessage: PropTypes.string,
|
|
140
|
+
dropdownInstruction: PropTypes.string
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
Lookup.defaultProps = {
|
|
144
|
+
noResultsMessage: 'Sorry, could not find any results for your search',
|
|
145
|
+
dropdownInstruction: ''
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
export default Lookup;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import PropTypes from 'prop-types';
|
|
3
|
+
import axios from 'axios';
|
|
4
|
+
|
|
5
|
+
import Lookup from '../Lookup/Lookup';
|
|
6
|
+
|
|
7
|
+
const schoolFetcher = async query => {
|
|
8
|
+
if (!query || !query.trim()) {
|
|
9
|
+
throw new Error('Please enter a search query');
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
if (query.length < 2) {
|
|
13
|
+
throw new Error('Please enter at least two characters');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
try {
|
|
17
|
+
const res = await axios.get(
|
|
18
|
+
'https://lookups.sls.comicrelief.com/schools/lookup',
|
|
19
|
+
{ timeout: 10000, params: { query } }
|
|
20
|
+
);
|
|
21
|
+
return res.data.data.schools;
|
|
22
|
+
} catch (error) {
|
|
23
|
+
// if (typeof Sentry !== 'undefined') {
|
|
24
|
+
// Sentry.captureException(error);
|
|
25
|
+
// }
|
|
26
|
+
throw new Error('Sorry, something unexpected went wrong. Please try again or enter your school manually');
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const schoolToString = school => `${school.name}, ${school.post_code}`;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* The component library's school lookup component uses a typeahead/search-as-you-type approach.
|
|
34
|
+
*
|
|
35
|
+
* Given the API we use is v flaky and can be slow, this isn't really ideal.
|
|
36
|
+
*
|
|
37
|
+
* This version just has a simple input + a button (or you can hit enter) to trigger the search.
|
|
38
|
+
*
|
|
39
|
+
* @param onSelect
|
|
40
|
+
* @param rest
|
|
41
|
+
* @returns {JSX.Element}
|
|
42
|
+
* @constructor
|
|
43
|
+
*/
|
|
44
|
+
const SimpleSchoolLookup = ({ onSelect, ...rest }) => (
|
|
45
|
+
<Lookup
|
|
46
|
+
name="school_lookup"
|
|
47
|
+
label="Enter the name or postcode of your organisation"
|
|
48
|
+
placeholder="Enter name or postcode..."
|
|
49
|
+
buttonText="Find school"
|
|
50
|
+
dropdownInstruction="Please select an organisation from the list below"
|
|
51
|
+
noResultsMessage="Sorry, could not find anything matching your search"
|
|
52
|
+
lookupHandler={schoolFetcher}
|
|
53
|
+
mapOptionToString={schoolToString}
|
|
54
|
+
onSelect={onSelect}
|
|
55
|
+
{...rest}
|
|
56
|
+
/>
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
SimpleSchoolLookup.propTypes = {
|
|
60
|
+
onSelect: PropTypes.func.isRequired
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
export default SimpleSchoolLookup;
|
|
@@ -896,6 +896,10 @@ exports[`renders Single Message with full width image and no text correctly 1`]
|
|
|
896
896
|
z-index: 1;
|
|
897
897
|
}
|
|
898
898
|
|
|
899
|
+
@media (min-width:740px) {
|
|
900
|
+
|
|
901
|
+
}
|
|
902
|
+
|
|
899
903
|
@media (min-width:740px) {
|
|
900
904
|
.c0 {
|
|
901
905
|
-webkit-flex-direction: row;
|
|
@@ -1010,6 +1014,10 @@ exports[`renders Single Message with no Image correctly 1`] = `
|
|
|
1010
1014
|
padding: 1rem;
|
|
1011
1015
|
}
|
|
1012
1016
|
|
|
1017
|
+
@media (min-width:740px) {
|
|
1018
|
+
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1013
1021
|
@media (min-width:740px) {
|
|
1014
1022
|
.c0 {
|
|
1015
1023
|
-webkit-flex-direction: row;
|
|
@@ -31,7 +31,7 @@ const Footer = ({
|
|
|
31
31
|
<Logo sizeSm="48px" sizeMd="72px" rotate={false} campaign={campaign} />
|
|
32
32
|
</Brand>
|
|
33
33
|
</FooterBranding>
|
|
34
|
-
<FooterNav navItems={navItems} />
|
|
34
|
+
<FooterNav navItems={navItems} {...rest} />
|
|
35
35
|
<FooterCopyright>
|
|
36
36
|
<Text tag="p" color="grey">
|
|
37
37
|
{footerCopy}
|
|
@@ -46,13 +46,15 @@ const Footer = ({
|
|
|
46
46
|
Footer.propTypes = {
|
|
47
47
|
navItems: PropTypes.objectOf(PropTypes.shape),
|
|
48
48
|
footerCopy: PropTypes.string,
|
|
49
|
-
campaign: PropTypes.string
|
|
49
|
+
campaign: PropTypes.string,
|
|
50
|
+
overrideWhiteList: PropTypes.bool
|
|
50
51
|
};
|
|
51
52
|
|
|
52
53
|
Footer.defaultProps = {
|
|
53
54
|
navItems: {},
|
|
54
55
|
footerCopy: '',
|
|
55
|
-
campaign: 'Comic Relief'
|
|
56
|
+
campaign: 'Comic Relief',
|
|
57
|
+
overrideWhiteList: false
|
|
56
58
|
};
|
|
57
59
|
|
|
58
60
|
export default Footer;
|
|
@@ -4,5 +4,12 @@
|
|
|
4
4
|
import data from './data/data';
|
|
5
5
|
import footerCopy from './data/footerCopy';
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
<>
|
|
8
|
+
<p>Standard footer</p>
|
|
9
|
+
<Footer navItems={data} footerCopy={footerCopy.copy} campaign="Comic Relief" />
|
|
10
|
+
|
|
11
|
+
<p>Overrides whitelist functionality for external usage</p>
|
|
12
|
+
<Footer navItems={data} footerCopy={footerCopy.copy} campaign="Comic Relief" overrideWhiteList />
|
|
13
|
+
</>
|
|
14
|
+
|
|
8
15
|
```
|
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
SubNavLink
|
|
17
17
|
} from './Nav.style';
|
|
18
18
|
|
|
19
|
-
const FooterNav = ({ navItems }) => {
|
|
19
|
+
const FooterNav = ({ navItems, ...rest }) => {
|
|
20
20
|
const { menuGroups } = navItems;
|
|
21
21
|
const [isExpandable] = useState(false);
|
|
22
22
|
const [isSubMenuOpen, setIsSubMenuOpen] = useState({});
|
|
@@ -78,6 +78,7 @@ const FooterNav = ({ navItems }) => {
|
|
|
78
78
|
aria-haspopup="true"
|
|
79
79
|
role="button"
|
|
80
80
|
onClick={toggleSubMenu(group.id)}
|
|
81
|
+
{...rest}
|
|
81
82
|
>
|
|
82
83
|
<Text color="white">{group.title}</Text>
|
|
83
84
|
</NavLink>
|
|
@@ -103,7 +104,7 @@ const FooterNav = ({ navItems }) => {
|
|
|
103
104
|
group.links.length % 2 === 0 && group.links.length > 2
|
|
104
105
|
}
|
|
105
106
|
>
|
|
106
|
-
<SubNavLink href={thisUrl} inline role="menuitem">
|
|
107
|
+
<SubNavLink href={thisUrl} inline role="menuitem" {...rest}>
|
|
107
108
|
<Text color="white">{child.title}</Text>
|
|
108
109
|
</SubNavLink>
|
|
109
110
|
</SubNavItem>
|
package/src/index.js
CHANGED
|
@@ -29,6 +29,7 @@ export { default as SocialIcons } from './components/Atoms/SocialIcons/SocialIco
|
|
|
29
29
|
export { default as TextInputWithDropdown } from './components/Atoms/TextInputWithDropdown/TextInputWithDropdown';
|
|
30
30
|
export { default as ErrorText } from './components/Atoms/ErrorText/ErrorText';
|
|
31
31
|
export { default as Label } from './components/Atoms/Label/Label';
|
|
32
|
+
export { default as ButtonWithStates } from './components/Atoms/ButtonWithStates/ButtonWithStates';
|
|
32
33
|
|
|
33
34
|
/* Molecules */
|
|
34
35
|
export { default as HeroBanner } from './components/Molecules/HeroBanner/HeroBanner';
|
|
@@ -58,6 +59,8 @@ export { default as Countdown } from './components/Molecules/Countdown/Countdown
|
|
|
58
59
|
export { default as Banner } from './components/Molecules/Banner/Banner';
|
|
59
60
|
export { default as Chip } from './components/Molecules/Chip/Chip';
|
|
60
61
|
export { default as Descriptor } from './components/Molecules/Descriptor/Descriptor';
|
|
62
|
+
export { default as Lookup } from './components/Molecules/Lookup/Lookup';
|
|
63
|
+
export { default as SimpleSchoolLookup } from './components/Molecules/SimpleSchoolLookup/SimpleSchoolLookup';
|
|
61
64
|
|
|
62
65
|
/* Organisms */
|
|
63
66
|
export { default as EmailSignUp } from './components/Organisms/EmailSignUp/EmailSignUp';
|