@comicrelief/component-library 5.6.1 → 5.8.0

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.
@@ -0,0 +1,131 @@
1
+ name: Main
2
+
3
+ on: [push]
4
+
5
+ jobs:
6
+ lint:
7
+ name: Lint
8
+ runs-on: ubuntu-latest
9
+ steps:
10
+ - name: Checkout
11
+ uses: actions/checkout@v3
12
+
13
+ - name: Set up Node
14
+ uses: actions/setup-node@v3
15
+ with:
16
+ node-version: 14
17
+
18
+ - name: Restore packages cache
19
+ uses: actions/cache@v3
20
+ with:
21
+ path: '**/node_modules'
22
+ key: ${{ runner.os }}-modules-${{ hashFiles('**/yarn.lock') }}
23
+
24
+ - name: Install dependencies
25
+ run: yarn install --frozen-lockfile --ignore-scripts
26
+
27
+ - name: Run ESLint
28
+ run: yarn lint
29
+
30
+ unit-test:
31
+ name: Unit test
32
+ runs-on: ubuntu-latest
33
+ steps:
34
+ - name: Checkout
35
+ uses: actions/checkout@v3
36
+
37
+ - name: Set up Node
38
+ uses: actions/setup-node@v3
39
+ with:
40
+ node-version: 14
41
+
42
+ - name: Restore packages cache
43
+ uses: actions/cache@v3
44
+ with:
45
+ path: '**/node_modules'
46
+ key: ${{ runner.os }}-modules-${{ hashFiles('**/yarn.lock') }}
47
+
48
+ - name: Install dependencies
49
+ run: yarn install --frozen-lockfile --ignore-scripts
50
+
51
+ - name: Build
52
+ run: yarn build
53
+
54
+ - name: Run unit tests
55
+ run: yarn test --maxWorkers=2
56
+ env:
57
+ NODE_OPTIONS: --max_old_space_size=4096
58
+
59
+ cypress:
60
+ name: Cypress test
61
+ needs:
62
+ - lint
63
+ - unit-test
64
+ runs-on: ubuntu-latest
65
+ steps:
66
+ - name: Checkout
67
+ uses: actions/checkout@v3
68
+
69
+ - name: Set up Node
70
+ uses: actions/setup-node@v3
71
+ with:
72
+ node-version: 14
73
+
74
+ # see https://github.com/marketplace/actions/cypress-io
75
+ - name: Run Cypress tests
76
+ uses: cypress-io/github-action@v2
77
+ with:
78
+ start: yarn styleguide
79
+ command: yarn cy:run
80
+ wait-on: http://localhost:6060
81
+ env:
82
+ NODE_OPTIONS: --max_old_space_size=4096
83
+
84
+ # NOTE: screenshots will be generated only if the tests failed
85
+ # thus we store screenshots only on failures
86
+ - name: Upload screenshots
87
+ uses: actions/upload-artifact@v3
88
+ if: failure()
89
+ with:
90
+ name: cypress-screenshots
91
+ path: cypress/screenshots
92
+
93
+ # Test run video was always captured, so this action uses "always()" condition
94
+ - name: Upload videos
95
+ uses: actions/upload-artifact@v3
96
+ if: always()
97
+ with:
98
+ name: cypress-videos
99
+ path: cypress/videos
100
+
101
+ publish:
102
+ name: Publish package to npm
103
+ if: github.ref == 'refs/heads/master'
104
+ needs: cypress
105
+ runs-on: ubuntu-latest
106
+ steps:
107
+ - name: Checkout
108
+ uses: actions/checkout@v3
109
+
110
+ - name: Set up Node
111
+ uses: actions/setup-node@v3
112
+ with:
113
+ node-version: 14
114
+
115
+ - name: Restore packages cache
116
+ uses: actions/cache@v3
117
+ with:
118
+ path: '**/node_modules'
119
+ key: ${{ runner.os }}-modules-${{ hashFiles('**/yarn.lock') }}
120
+
121
+ - name: Install dependencies
122
+ run: yarn install --frozen-lockfile --ignore-scripts
123
+
124
+ - name: Build
125
+ run: yarn build
126
+
127
+ - name: Release
128
+ run: yarn semantic-release
129
+ env:
130
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
131
+ NPM_TOKEN: ${{ secrets.NPM_PUBLISHING_TOKEN }}
@@ -0,0 +1,17 @@
1
+ name: PR
2
+
3
+ on: [pull_request]
4
+
5
+ jobs:
6
+ # https://github.com/amannn/action-semantic-pull-request#example-config
7
+ check-semantic-pr:
8
+ name: Semantic pull request
9
+ runs-on: ubuntu-latest
10
+ steps:
11
+ # Please look up the latest version from
12
+ # https://github.com/amannn/action-semantic-pull-request/releases
13
+ - uses: amannn/action-semantic-pull-request@v4.4.0
14
+ env:
15
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
16
+ with:
17
+ validateSingleCommit: true
package/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  Comic Relief React Component Library
2
2
  --------------
3
3
 
4
- [![CircleCI](https://circleci.com/gh/comicrelief/component-library.svg?style=svg)](https://circleci.com/gh/comicrelief/component-library)
4
+ [![GitHub Actions](https://github.com/comicrelief/component-library/actions/workflows/main.yml/badge.svg)](https://github.com/comicrelief/component-library/actions)
5
5
  [![semantic-release](https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg)](https://github.com/semantic-release/semantic-release)
6
6
 
7
7
  React components to be shared across Comic Relief applications
@@ -27,24 +27,24 @@ import { HeroBanner } from '@comic-relief/component-library';
27
27
 
28
28
  ### Develop
29
29
 
30
- To install CR-CL locally run
30
+ To install CR-CL locally, run:
31
31
 
32
32
  ```
33
33
  $ yarn install
34
34
  ```
35
35
 
36
- To start
36
+ To start the dev build and server:
37
37
  ```
38
38
  $ yarn styleguide
39
39
  ```
40
40
 
41
- To test
41
+ To test:
42
42
  ```
43
43
  $ yarn test
44
44
  ```
45
- _Test will run through all jest tests and watch for any changes on snapshots._
45
+ _Test will run through all Jest tests and watch for any changes on snapshots._
46
46
 
47
- To update snapshots
47
+ To update snapshots with desired changes brought in through your work:
48
48
  ```
49
49
  $ yarn test -u
50
50
  ```
@@ -0,0 +1,91 @@
1
+ "use strict";
2
+
3
+ var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
4
+
5
+ Object.defineProperty(exports, "__esModule", {
6
+ value: true
7
+ });
8
+ exports.default = void 0;
9
+
10
+ var _slicedToArray2 = _interopRequireDefault(require("@babel/runtime/helpers/esm/slicedToArray"));
11
+
12
+ var _objectWithoutProperties2 = _interopRequireDefault(require("@babel/runtime/helpers/esm/objectWithoutProperties"));
13
+
14
+ var _react = _interopRequireWildcard(require("react"));
15
+
16
+ var _styledComponents = _interopRequireDefault(require("styled-components"));
17
+
18
+ var _ScaleLoader = _interopRequireDefault(require("react-spinners/ScaleLoader"));
19
+
20
+ var _spacing = _interopRequireDefault(require("../../../theme/shared/spacing"));
21
+
22
+ var _Button = _interopRequireDefault(require("../Button/Button"));
23
+
24
+ var _excluded = ["children", "loadingText", "loading", "disabled"];
25
+
26
+ 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
+
28
+ 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
+
30
+ var ButtonWithDisabledState = (0, _styledComponents.default)(_Button.default).withConfig({
31
+ displayName: "ButtonWithStates__ButtonWithDisabledState",
32
+ componentId: "sc-7gb81g-0"
33
+ })(["&:disabled{cursor:not-allowed;opacity:0.75;}"]);
34
+
35
+ var LoaderContainer = _styledComponents.default.div.withConfig({
36
+ displayName: "ButtonWithStates__LoaderContainer",
37
+ componentId: "sc-7gb81g-1"
38
+ })(["", ""], function (_ref) {
39
+ var withMargin = _ref.withMargin;
40
+ return withMargin ? "\n margin-top: ".concat((0, _spacing.default)('xsm'), ";\n margin-left: ").concat((0, _spacing.default)('md'), ";\n") : '';
41
+ }); // A button with loading and disabled states.
42
+
43
+
44
+ var ButtonWithStates = /*#__PURE__*/_react.default.forwardRef(function (_ref2, ref) {
45
+ var children = _ref2.children,
46
+ loadingText = _ref2.loadingText,
47
+ loading = _ref2.loading,
48
+ disabled = _ref2.disabled,
49
+ rest = (0, _objectWithoutProperties2.default)(_ref2, _excluded);
50
+
51
+ var _useState = (0, _react.useState)(null),
52
+ _useState2 = (0, _slicedToArray2.default)(_useState, 2),
53
+ loaderColour = _useState2[0],
54
+ setLoaderColour = _useState2[1]; // Can't see a simpler way to get the button's text colour, without reading the value
55
+ // via JavaScript.
56
+ // (e.g. the `theme.buttonColours` helper returns a CSS string split into an array -
57
+ // don't really want to be parsing that.)
58
+ // (And can't use inherit because ScaleLoader's color prop is actually setting its
59
+ // background color.)
60
+
61
+
62
+ var getLoaderColour = (0, _react.useCallback)(function (node) {
63
+ if (node && typeof window.getComputedStyle === 'function') {
64
+ var textColour = window.getComputedStyle(node).color;
65
+
66
+ if (textColour) {
67
+ setLoaderColour(textColour);
68
+ }
69
+ }
70
+ }, []);
71
+ return /*#__PURE__*/_react.default.createElement(ButtonWithDisabledState, Object.assign({
72
+ ref: ref,
73
+ disabled: disabled
74
+ }, rest), loading ? loadingText : children, /*#__PURE__*/_react.default.createElement(LoaderContainer, {
75
+ ref: getLoaderColour,
76
+ withMargin: loading
77
+ }, /*#__PURE__*/_react.default.createElement(_ScaleLoader.default, {
78
+ height: 16,
79
+ width: 2,
80
+ loading: loading,
81
+ color: loaderColour
82
+ })));
83
+ });
84
+
85
+ ButtonWithStates.defaultProps = {
86
+ loading: false,
87
+ disabled: false,
88
+ loadingText: 'Loading'
89
+ };
90
+ var _default = ButtonWithStates;
91
+ exports.default = _default;
@@ -0,0 +1,13 @@
1
+ ## Disabled and loading
2
+
3
+ ```js
4
+ import ButtonWithStates from './ButtonWithStates';
5
+
6
+ <ButtonWithStates
7
+ type="submit"
8
+ loading
9
+ disabled
10
+ >
11
+ Enter prize draw
12
+ </ButtonWithStates>
13
+ ```
@@ -7,9 +7,11 @@ Object.defineProperty(exports, "__esModule", {
7
7
  });
8
8
  exports.default = void 0;
9
9
 
10
+ var _slicedToArray2 = _interopRequireDefault(require("@babel/runtime/helpers/esm/slicedToArray"));
11
+
10
12
  var _objectWithoutProperties2 = _interopRequireDefault(require("@babel/runtime/helpers/esm/objectWithoutProperties"));
11
13
 
12
- var _react = _interopRequireDefault(require("react"));
14
+ var _react = _interopRequireWildcard(require("react"));
13
15
 
14
16
  var _Link = _interopRequireWildcard(require("./Link.style"));
15
17
 
@@ -37,13 +39,19 @@ var Link = function Link(_ref) {
37
39
  iconFirst = _ref.iconFirst,
38
40
  rest = (0, _objectWithoutProperties2.default)(_ref, _excluded);
39
41
 
42
+ var _useState = (0, _react.useState)(''),
43
+ _useState2 = (0, _slicedToArray2.default)(_useState, 2),
44
+ documentHost = _useState2[0],
45
+ setDocumentHost = _useState2[1];
40
46
  /**
41
47
  * If we haven't specifically set the target via props, check if
42
48
  * this is an internal link OR on our whitelist before making it a '_self' link
43
49
  */
50
+
51
+
44
52
  if (target === null) {
45
53
  // Use our helper function to determine the raw domains to compare
46
- var currentDomain = (0, _internalLinkHelper.getDomain)(document.location.host);
54
+ var currentDomain = (0, _internalLinkHelper.getDomain)(documentHost);
47
55
  var linkDomain = (0, _internalLinkHelper.getDomain)(href); // Additional check for applications that need more control
48
56
 
49
57
  var isWhiteListOverridden = rest.overrideWhiteList === true;
@@ -61,6 +69,9 @@ var Link = function Link(_ref) {
61
69
  }
62
70
 
63
71
  var hasIcon = icon !== null;
72
+ (0, _react.useEffect)(function () {
73
+ setDocumentHost(document.location.host);
74
+ }, []);
64
75
  return /*#__PURE__*/_react.default.createElement(_Link.default, Object.assign({}, rest, {
65
76
  color: color,
66
77
  href: href,
@@ -0,0 +1,201 @@
1
+ "use strict";
2
+
3
+ var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
4
+
5
+ Object.defineProperty(exports, "__esModule", {
6
+ value: true
7
+ });
8
+ exports.default = void 0;
9
+
10
+ var _regenerator = _interopRequireDefault(require("@babel/runtime/regenerator"));
11
+
12
+ var _asyncToGenerator2 = _interopRequireDefault(require("@babel/runtime/helpers/esm/asyncToGenerator"));
13
+
14
+ var _slicedToArray2 = _interopRequireDefault(require("@babel/runtime/helpers/esm/slicedToArray"));
15
+
16
+ var _objectWithoutProperties2 = _interopRequireDefault(require("@babel/runtime/helpers/esm/objectWithoutProperties"));
17
+
18
+ var _styledComponents = _interopRequireWildcard(require("styled-components"));
19
+
20
+ var _react = _interopRequireWildcard(require("react"));
21
+
22
+ var _TextInputWithDropdown = _interopRequireDefault(require("../../Atoms/TextInputWithDropdown/TextInputWithDropdown"));
23
+
24
+ var _spacing = _interopRequireDefault(require("../../../theme/shared/spacing"));
25
+
26
+ var _ButtonWithStates = _interopRequireDefault(require("../../Atoms/ButtonWithStates/ButtonWithStates"));
27
+
28
+ var _excluded = ["name", "label", "placeholder", "buttonText", "lookupHandler", "mapOptionToString", "onSelect", "noResultsMessage", "dropdownInstruction"];
29
+
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); }
31
+
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; }
33
+
34
+ var StyledButton = (0, _styledComponents.default)(_ButtonWithStates.default).withConfig({
35
+ displayName: "Lookup__StyledButton",
36
+ componentId: "sc-uu5bpv-0"
37
+ })(["", ""], function (_ref) {
38
+ var theme = _ref.theme;
39
+ return (0, _styledComponents.css)(["color:", ";border:2px solid ", ";background-color:", ";padding-left:", ";padding-right:", ";&:hover{color:", ";background-color:", ";}"], theme.color('grey_dark'), theme.color('grey_dark'), theme.color('white'), (0, _spacing.default)('lg'), (0, _spacing.default)('lg'), theme.color('grey_dark'), theme.color('white'));
40
+ });
41
+ var KEY_CODE_ENTER = 13;
42
+ /**
43
+ * A simple lookup component
44
+ *
45
+ * The `lookupHandler` should be an async function which is called when a lookup is triggered
46
+ * (either by hitting enter or clicking the button)
47
+ *
48
+ * It will receive the current search term and should:
49
+ * - take care of any validation on the search term
50
+ * - perform the actual lookup request
51
+ * - return an array of options (or an empty array if none were found)
52
+ * - only throw errors with user-friendly messages
53
+ *
54
+ * Any errors thrown will be caught and the message will be displayed to the user.
55
+ *
56
+ * The `onSelect` function will receive the chosen option.
57
+ *
58
+ * @param name
59
+ * @param label
60
+ * @param placeholder
61
+ * @param buttonText
62
+ * @param lookupHandler
63
+ * @param mapOptionToString
64
+ * @param onSelect
65
+ * @param noResultsMessage
66
+ * @param dropdownInstruction
67
+ * @param rest
68
+ * @returns {JSX.Element}
69
+ * @constructor
70
+ */
71
+
72
+ var Lookup = function Lookup(_ref2) {
73
+ var name = _ref2.name,
74
+ label = _ref2.label,
75
+ placeholder = _ref2.placeholder,
76
+ buttonText = _ref2.buttonText,
77
+ lookupHandler = _ref2.lookupHandler,
78
+ mapOptionToString = _ref2.mapOptionToString,
79
+ _onSelect = _ref2.onSelect,
80
+ noResultsMessage = _ref2.noResultsMessage,
81
+ dropdownInstruction = _ref2.dropdownInstruction,
82
+ rest = (0, _objectWithoutProperties2.default)(_ref2, _excluded);
83
+
84
+ var _useState = (0, _react.useState)(''),
85
+ _useState2 = (0, _slicedToArray2.default)(_useState, 2),
86
+ query = _useState2[0],
87
+ setQuery = _useState2[1];
88
+
89
+ var _useState3 = (0, _react.useState)(''),
90
+ _useState4 = (0, _slicedToArray2.default)(_useState3, 2),
91
+ errorMessage = _useState4[0],
92
+ setErrorMessage = _useState4[1];
93
+
94
+ var _useState5 = (0, _react.useState)([]),
95
+ _useState6 = (0, _slicedToArray2.default)(_useState5, 2),
96
+ options = _useState6[0],
97
+ setOptions = _useState6[1];
98
+
99
+ var _useState7 = (0, _react.useState)(false),
100
+ _useState8 = (0, _slicedToArray2.default)(_useState7, 2),
101
+ isSearching = _useState8[0],
102
+ setIsSearching = _useState8[1];
103
+
104
+ var handler = (0, _react.useCallback)( /*#__PURE__*/(0, _asyncToGenerator2.default)( /*#__PURE__*/_regenerator.default.mark(function _callee() {
105
+ var results;
106
+ return _regenerator.default.wrap(function _callee$(_context) {
107
+ while (1) {
108
+ switch (_context.prev = _context.next) {
109
+ case 0:
110
+ setErrorMessage('');
111
+ setIsSearching(true);
112
+ _context.prev = 2;
113
+ _context.next = 5;
114
+ return lookupHandler(query);
115
+
116
+ case 5:
117
+ results = _context.sent;
118
+ setOptions(results);
119
+
120
+ if (results.length === 0) {
121
+ setErrorMessage(noResultsMessage);
122
+ }
123
+
124
+ _context.next = 13;
125
+ break;
126
+
127
+ case 10:
128
+ _context.prev = 10;
129
+ _context.t0 = _context["catch"](2);
130
+ setErrorMessage(_context.t0.message);
131
+
132
+ case 13:
133
+ setIsSearching(false);
134
+
135
+ case 14:
136
+ case "end":
137
+ return _context.stop();
138
+ }
139
+ }
140
+ }, _callee, null, [[2, 10]]);
141
+ })), [query, setOptions, setErrorMessage, noResultsMessage, lookupHandler]);
142
+ return /*#__PURE__*/_react.default.createElement("div", rest, /*#__PURE__*/_react.default.createElement(_StyledTextInputWithDropdown, {
143
+ name: name,
144
+ id: name,
145
+ value: query,
146
+ options: options.map(mapOptionToString),
147
+ label: label,
148
+ placeholder: placeholder,
149
+ onChange: function onChange(e) {
150
+ setQuery(e.target.value);
151
+ setErrorMessage('');
152
+ setOptions([]);
153
+ },
154
+ onKeyPress: function onKeyPress(e) {
155
+ var keyCode = e.keyCode || e.which;
156
+
157
+ if (keyCode === KEY_CODE_ENTER) {
158
+ e.preventDefault();
159
+ return handler();
160
+ }
161
+
162
+ return null;
163
+ },
164
+ onSelect: function onSelect(text, index) {
165
+ var selection = options[index];
166
+
167
+ _onSelect(selection);
168
+
169
+ setQuery('');
170
+ setErrorMessage('');
171
+ setOptions([]);
172
+ },
173
+ errorMsg: errorMessage,
174
+ dropdownInstruction: dropdownInstruction,
175
+ $_css: (0, _spacing.default)('md')
176
+ }), /*#__PURE__*/_react.default.createElement(StyledButton, {
177
+ type: "button",
178
+ onClick: function onClick() {
179
+ return handler();
180
+ },
181
+ loading: isSearching,
182
+ disabled: isSearching,
183
+ loadingText: "Searching"
184
+ }, buttonText));
185
+ };
186
+
187
+ Lookup.defaultProps = {
188
+ noResultsMessage: 'Sorry, could not find any results for your search',
189
+ dropdownInstruction: ''
190
+ };
191
+ var _default = Lookup;
192
+ exports.default = _default;
193
+
194
+ var _StyledTextInputWithDropdown = (0, _styledComponents.default)(_TextInputWithDropdown.default).withConfig({
195
+ displayName: "Lookup___StyledTextInputWithDropdown",
196
+ componentId: "sc-uu5bpv-1"
197
+ })(function (p) {
198
+ return {
199
+ marginBottom: p.$_css
200
+ };
201
+ });
@@ -0,0 +1,112 @@
1
+ "use strict";
2
+
3
+ var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
4
+
5
+ Object.defineProperty(exports, "__esModule", {
6
+ value: true
7
+ });
8
+ exports.default = void 0;
9
+
10
+ var _objectWithoutProperties2 = _interopRequireDefault(require("@babel/runtime/helpers/esm/objectWithoutProperties"));
11
+
12
+ var _regenerator = _interopRequireDefault(require("@babel/runtime/regenerator"));
13
+
14
+ var _asyncToGenerator2 = _interopRequireDefault(require("@babel/runtime/helpers/esm/asyncToGenerator"));
15
+
16
+ var _react = _interopRequireDefault(require("react"));
17
+
18
+ var _axios = _interopRequireDefault(require("axios"));
19
+
20
+ var _Lookup = _interopRequireDefault(require("../Lookup/Lookup"));
21
+
22
+ var _excluded = ["onSelect"];
23
+
24
+ var schoolFetcher = /*#__PURE__*/function () {
25
+ var _ref = (0, _asyncToGenerator2.default)( /*#__PURE__*/_regenerator.default.mark(function _callee(query) {
26
+ var res;
27
+ return _regenerator.default.wrap(function _callee$(_context) {
28
+ while (1) {
29
+ switch (_context.prev = _context.next) {
30
+ case 0:
31
+ if (!(!query || !query.trim())) {
32
+ _context.next = 2;
33
+ break;
34
+ }
35
+
36
+ throw new Error('Please enter a search query');
37
+
38
+ case 2:
39
+ if (!(query.length < 2)) {
40
+ _context.next = 4;
41
+ break;
42
+ }
43
+
44
+ throw new Error('Please enter at least two characters');
45
+
46
+ case 4:
47
+ _context.prev = 4;
48
+ _context.next = 7;
49
+ return _axios.default.get('https://lookups.sls.comicrelief.com/schools/lookup', {
50
+ timeout: 10000,
51
+ params: {
52
+ query: query
53
+ }
54
+ });
55
+
56
+ case 7:
57
+ res = _context.sent;
58
+ return _context.abrupt("return", res.data.data.schools);
59
+
60
+ case 11:
61
+ _context.prev = 11;
62
+ _context.t0 = _context["catch"](4);
63
+ throw new Error('Sorry, something unexpected went wrong. Please try again or enter your school manually');
64
+
65
+ case 14:
66
+ case "end":
67
+ return _context.stop();
68
+ }
69
+ }
70
+ }, _callee, null, [[4, 11]]);
71
+ }));
72
+
73
+ return function schoolFetcher(_x) {
74
+ return _ref.apply(this, arguments);
75
+ };
76
+ }();
77
+
78
+ var schoolToString = function schoolToString(school) {
79
+ return "".concat(school.name, ", ").concat(school.post_code);
80
+ };
81
+ /**
82
+ * The component library's school lookup component uses a typeahead/search-as-you-type approach.
83
+ *
84
+ * Given the API we use is v flaky and can be slow, this isn't really ideal.
85
+ *
86
+ * This version just has a simple input + a button (or you can hit enter) to trigger the search.
87
+ *
88
+ * @param onSelect
89
+ * @param rest
90
+ * @returns {JSX.Element}
91
+ * @constructor
92
+ */
93
+
94
+
95
+ var SimpleSchoolLookup = function SimpleSchoolLookup(_ref2) {
96
+ var onSelect = _ref2.onSelect,
97
+ rest = (0, _objectWithoutProperties2.default)(_ref2, _excluded);
98
+ return /*#__PURE__*/_react.default.createElement(_Lookup.default, Object.assign({
99
+ name: "school_lookup",
100
+ label: "Enter the name or postcode of your organisation",
101
+ placeholder: "Enter name or postcode...",
102
+ buttonText: "Find school",
103
+ dropdownInstruction: "Please select an organisation from the list below",
104
+ noResultsMessage: "Sorry, could not find anything matching your search",
105
+ lookupHandler: schoolFetcher,
106
+ mapOptionToString: schoolToString,
107
+ onSelect: onSelect
108
+ }, rest));
109
+ };
110
+
111
+ var _default = SimpleSchoolLookup;
112
+ exports.default = _default;
@@ -0,0 +1,7 @@
1
+ ```js
2
+
3
+ import SimpleSchoolLookup from './SimpleSchoolLookup';
4
+
5
+ <SimpleSchoolLookup onSelect={data => console.log(data)}/>
6
+
7
+ ```
@@ -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;
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"));
@@ -13,6 +13,11 @@ var colors = {
13
13
  coral: '#F9686D',
14
14
  coral_dark: '#961D35',
15
15
  coral_light: '#FFCECE',
16
+ cwg22_blue: '#204794',
17
+ cwg22_orange: '#FB6326',
18
+ cwg22_purple: '#7B3ED3',
19
+ cwg22_red: '#E62731',
20
+ cwg22_yellow: '#F8E928',
16
21
  deep_violet_dark: '#2C0230',
17
22
  deep_violet: '#2C0230'
18
23
  /** Will be removed */
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.6.1",
4
+ "version": "5.8.0",
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;
@@ -0,0 +1,13 @@
1
+ ## Disabled and loading
2
+
3
+ ```js
4
+ import ButtonWithStates from './ButtonWithStates';
5
+
6
+ <ButtonWithStates
7
+ type="submit"
8
+ loading
9
+ disabled
10
+ >
11
+ Enter prize draw
12
+ </ButtonWithStates>
13
+ ```
@@ -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,13 +19,14 @@ 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(document.location.host);
29
+ const currentDomain = getDomain(documentHost);
29
30
  const linkDomain = getDomain(href);
30
31
 
31
32
  // Additional check for applications that need more control
@@ -45,6 +46,10 @@ const Link = ({
45
46
  }
46
47
  const hasIcon = icon !== null;
47
48
 
49
+ useEffect(() => {
50
+ setDocumentHost(document.location.host);
51
+ }, []);
52
+
48
53
  return (
49
54
  <StyledLink
50
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;
@@ -0,0 +1,7 @@
1
+ ```js
2
+
3
+ import SimpleSchoolLookup from './SimpleSchoolLookup';
4
+
5
+ <SimpleSchoolLookup onSelect={data => console.log(data)}/>
6
+
7
+ ```
@@ -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;
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';
@@ -7,6 +7,11 @@ const colors = {
7
7
  coral: '#F9686D',
8
8
  coral_dark: '#961D35',
9
9
  coral_light: '#FFCECE',
10
+ cwg22_blue: '#204794',
11
+ cwg22_orange: '#FB6326',
12
+ cwg22_purple: '#7B3ED3',
13
+ cwg22_red: '#E62731',
14
+ cwg22_yellow: '#F8E928',
10
15
  deep_violet_dark: '#2C0230',
11
16
  deep_violet: '#2C0230' /** Will be removed */,
12
17
  deep_violet_light: '#3e1c43',
@@ -1,54 +0,0 @@
1
- # Javascript Node CircleCI 2.0 configuration file
2
- #
3
- # Check https://circleci.com/docs/2.0/language-javascript/ for more details
4
- #
5
- version: 2
6
- jobs:
7
- build:
8
- docker:
9
-
10
- # See https://github.com/cypress-io/cypress-docker-images
11
- - image: cypress/browsers:node14.16.0-chrome89-ff77
12
- environment:
13
- TERM: xterm
14
- NODE_OPTIONS: --max_old_space_size=4096
15
-
16
- working_directory: ~/repo
17
-
18
- steps:
19
- - checkout
20
-
21
- # Download and cache dependencies
22
- - restore_cache:
23
- key: dependency-cache-{{ checksum "yarn.lock" }}
24
-
25
- - run: yarn install
26
-
27
- - save_cache:
28
- key: dependency-cache-{{ checksum "yarn.lock" }}
29
- paths:
30
- - node_modules
31
- - /root/.cache/Cypress
32
-
33
- # run tests!
34
- - run:
35
- name: Test Code Style
36
- command: yarn lint
37
-
38
- - run:
39
- name: Run unit tests
40
- command: yarn test --maxWorkers=2
41
-
42
- - run:
43
- name: Release to npm
44
- command: yarn semantic-release
45
-
46
- - run:
47
- name: Run feature tests
48
- command: yarn feature-test
49
-
50
- - store_artifacts:
51
- path: cypress/videos
52
-
53
- - store_artifacts:
54
- path: cypress/screenshots