newspack-components 4.5.0 → 4.6.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.
- package/CHANGELOG.md +40 -0
- package/DEVELOPMENT.md +1 -1
- package/dist/cjs/accordion/index.js +43 -26
- package/dist/cjs/accordion/index.test.js +80 -0
- package/dist/cjs/accordion/style.scss +25 -21
- package/dist/cjs/action-card/style.scss +0 -22
- package/dist/cjs/breadcrumbs/index.js +80 -0
- package/dist/cjs/breadcrumbs/index.test.js +109 -0
- package/dist/cjs/breadcrumbs/style.scss +41 -0
- package/dist/cjs/card/core-card.js +8 -4
- package/dist/cjs/card/index.js +1 -1
- package/dist/cjs/card/style-core.scss +14 -0
- package/dist/cjs/card/style.scss +0 -9
- package/dist/cjs/card-feature/index.js +25 -13
- package/dist/cjs/card-settings-group/index.js +3 -0
- package/dist/cjs/confirm-dialog/index.js +34 -8
- package/dist/cjs/confirm-dialog/index.test.js +173 -0
- package/dist/cjs/divider/style.scss +4 -2
- package/dist/cjs/grid/style.scss +5 -0
- package/dist/cjs/handoff/index.js +8 -5
- package/dist/cjs/index.js +34 -1
- package/dist/cjs/integration-icon/index.js +40 -0
- package/dist/cjs/integration-icon/style.scss +42 -0
- package/dist/cjs/integration-icons/index.js +21 -2
- package/dist/cjs/modal/style.scss +4 -0
- package/dist/cjs/page/index.js +121 -0
- package/dist/cjs/page/index.test.js +69 -0
- package/dist/cjs/page/style.scss +86 -0
- package/dist/cjs/route-match.js +59 -0
- package/dist/cjs/route-match.test.js +77 -0
- package/dist/cjs/section-header/index.js +8 -1
- package/dist/cjs/section-header/style.scss +11 -11
- package/dist/cjs/select-control/GroupedSelectControl.js +1 -1
- package/dist/cjs/select-control/style.scss +4 -0
- package/dist/cjs/style.scss +1 -4
- package/dist/cjs/tabbed-navigation/index.js +188 -40
- package/dist/cjs/tabbed-navigation/index.test.js +299 -0
- package/dist/cjs/tabbed-navigation/style.scss +77 -57
- package/dist/cjs/with-wizard/index.js +34 -45
- package/dist/cjs/with-wizard-screen/index.js +82 -41
- package/dist/cjs/with-wizard-screen/index.test.js +50 -0
- package/dist/cjs/with-wizard-screen/style.scss +15 -118
- package/dist/cjs/wizard/breadcrumbs-select.js +70 -0
- package/dist/cjs/wizard/breadcrumbs-select.test.js +200 -0
- package/dist/cjs/wizard/components/WizardError.js +8 -4
- package/dist/cjs/wizard/index.js +162 -141
- package/dist/esm/accordion/index.js +42 -26
- package/dist/esm/accordion/index.test.js +79 -0
- package/dist/esm/accordion/style.scss +25 -21
- package/dist/esm/action-card/style.scss +0 -22
- package/dist/esm/breadcrumbs/index.js +75 -0
- package/dist/esm/breadcrumbs/index.test.js +105 -0
- package/dist/esm/breadcrumbs/style.scss +41 -0
- package/dist/esm/card/core-card.js +8 -4
- package/dist/esm/card/index.js +1 -1
- package/dist/esm/card/style-core.scss +14 -0
- package/dist/esm/card/style.scss +0 -9
- package/dist/esm/card-feature/index.js +25 -13
- package/dist/esm/card-settings-group/index.js +3 -0
- package/dist/esm/confirm-dialog/index.js +34 -8
- package/dist/esm/confirm-dialog/index.test.js +173 -0
- package/dist/esm/divider/style.scss +4 -2
- package/dist/esm/grid/style.scss +5 -0
- package/dist/esm/handoff/index.js +7 -5
- package/dist/esm/index.js +5 -1
- package/dist/esm/integration-icon/index.js +34 -0
- package/dist/esm/integration-icon/style.scss +42 -0
- package/dist/esm/integration-icons/index.js +22 -1
- package/dist/esm/modal/style.scss +4 -0
- package/dist/esm/page/index.js +113 -0
- package/dist/esm/page/index.test.js +65 -0
- package/dist/esm/page/style.scss +86 -0
- package/dist/esm/route-match.js +51 -0
- package/dist/esm/route-match.test.js +74 -0
- package/dist/esm/section-header/index.js +8 -1
- package/dist/esm/section-header/style.scss +11 -11
- package/dist/esm/select-control/GroupedSelectControl.js +1 -1
- package/dist/esm/select-control/style.scss +4 -0
- package/dist/esm/style.scss +1 -4
- package/dist/esm/tabbed-navigation/index.js +192 -38
- package/dist/esm/tabbed-navigation/index.test.js +296 -0
- package/dist/esm/tabbed-navigation/style.scss +77 -57
- package/dist/esm/with-wizard/index.js +33 -45
- package/dist/esm/with-wizard-screen/index.js +78 -41
- package/dist/esm/with-wizard-screen/index.test.js +46 -0
- package/dist/esm/with-wizard-screen/style.scss +15 -118
- package/dist/esm/wizard/breadcrumbs-select.js +63 -0
- package/dist/esm/wizard/breadcrumbs-select.test.js +197 -0
- package/dist/esm/wizard/components/WizardError.js +7 -5
- package/dist/esm/wizard/index.js +165 -144
- package/package.json +10 -2
- package/src/accordion/index.js +27 -15
- package/src/accordion/index.test.js +62 -0
- package/src/accordion/style.scss +25 -21
- package/src/action-card/style.scss +0 -22
- package/src/breadcrumbs/index.js +63 -0
- package/src/breadcrumbs/index.test.js +59 -0
- package/src/breadcrumbs/style.scss +41 -0
- package/src/card/core-card.js +11 -6
- package/src/card/index.js +0 -1
- package/src/card/style-core.scss +14 -0
- package/src/card/style.scss +0 -9
- package/src/card-feature/README.md +3 -3
- package/src/card-feature/index.tsx +37 -21
- package/src/card-settings-group/index.tsx +4 -0
- package/src/card-sortable-list/index.tsx +1 -1
- package/src/confirm-dialog/index.test.js +142 -0
- package/src/confirm-dialog/index.tsx +34 -7
- package/src/divider/style.scss +4 -2
- package/src/grid/style.scss +5 -0
- package/src/handoff/index.js +4 -3
- package/src/index.js +5 -1
- package/src/integration-icon/index.tsx +35 -0
- package/src/integration-icon/style.scss +42 -0
- package/src/integration-icons/index.js +23 -0
- package/src/modal/style.scss +4 -0
- package/src/page/index.js +89 -0
- package/src/page/index.test.js +43 -0
- package/src/page/style.scss +86 -0
- package/src/route-match.js +47 -0
- package/src/route-match.test.js +50 -0
- package/src/section-header/index.js +7 -3
- package/src/section-header/style.scss +11 -11
- package/src/select-control/GroupedSelectControl.js +1 -1
- package/src/select-control/style.scss +4 -0
- package/src/style.scss +1 -4
- package/src/tabbed-navigation/index.js +181 -34
- package/src/tabbed-navigation/index.test.js +228 -0
- package/src/tabbed-navigation/style.scss +77 -57
- package/src/with-wizard/index.js +18 -39
- package/src/with-wizard-screen/index.js +70 -35
- package/src/with-wizard-screen/index.test.js +36 -0
- package/src/with-wizard-screen/style.scss +15 -118
- package/src/wizard/breadcrumbs-select.js +51 -0
- package/src/wizard/breadcrumbs-select.test.js +124 -0
- package/src/wizard/components/WizardError.js +4 -3
- package/src/wizard/index.js +160 -146
- package/tsconfig.json +7 -0
|
@@ -22,6 +22,8 @@ var CardSettingsGroup = function CardSettingsGroup(_ref) {
|
|
|
22
22
|
disabled = _ref$disabled === void 0 ? false : _ref$disabled,
|
|
23
23
|
_ref$icon = _ref.icon,
|
|
24
24
|
icon = _ref$icon === void 0 ? null : _ref$icon,
|
|
25
|
+
_ref$iconElement = _ref.iconElement,
|
|
26
|
+
iconElement = _ref$iconElement === void 0 ? null : _ref$iconElement,
|
|
25
27
|
headerAction = _ref.headerAction,
|
|
26
28
|
_ref$title = _ref.title,
|
|
27
29
|
title = _ref$title === void 0 ? '' : _ref$title,
|
|
@@ -50,6 +52,7 @@ var CardSettingsGroup = function CardSettingsGroup(_ref) {
|
|
|
50
52
|
onToggle: onEnable,
|
|
51
53
|
disabled: disabled,
|
|
52
54
|
icon: icon,
|
|
55
|
+
iconElement: iconElement,
|
|
53
56
|
iconBackgroundColor: true,
|
|
54
57
|
isActive: isActive,
|
|
55
58
|
title: title
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import _defineProperty from "@babel/runtime/helpers/defineProperty";
|
|
2
2
|
import _slicedToArray from "@babel/runtime/helpers/slicedToArray";
|
|
3
3
|
import _objectWithoutProperties from "@babel/runtime/helpers/objectWithoutProperties";
|
|
4
|
-
var _excluded = ["className", "size", "hideTitle", "isDestructive", "onConfirm", "onCancel", "when", "isOpen"];
|
|
4
|
+
var _excluded = ["className", "size", "hideTitle", "isDestructive", "onConfirm", "onCancel", "when", "isOpen", "children"];
|
|
5
5
|
function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
|
|
6
6
|
function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }
|
|
7
7
|
/**
|
|
@@ -52,6 +52,7 @@ function ConfirmDialog(_ref, ref) {
|
|
|
52
52
|
when = _ref$when === void 0 ? false : _ref$when,
|
|
53
53
|
_ref$isOpen = _ref.isOpen,
|
|
54
54
|
isOpen = _ref$isOpen === void 0 ? false : _ref$isOpen,
|
|
55
|
+
children = _ref.children,
|
|
55
56
|
otherProps = _objectWithoutProperties(_ref, _excluded);
|
|
56
57
|
var _useState = useState(isOpen),
|
|
57
58
|
_useState2 = _slicedToArray(_useState, 2),
|
|
@@ -59,6 +60,9 @@ function ConfirmDialog(_ref, ref) {
|
|
|
59
60
|
setShowDialog = _useState2[1];
|
|
60
61
|
var history = useHistory();
|
|
61
62
|
var pendingNavigation = useRef(null);
|
|
63
|
+
// While true, the blocker lets navigation through — used so the confirmed
|
|
64
|
+
// ("Discard changes") navigation isn't caught by the still-active blocker.
|
|
65
|
+
var bypassBlock = useRef(false);
|
|
62
66
|
var handleOnConfirm = useCallback(function () {
|
|
63
67
|
var _pendingNavigation$cu;
|
|
64
68
|
setShowDialog(false);
|
|
@@ -69,8 +73,16 @@ function ConfirmDialog(_ref, ref) {
|
|
|
69
73
|
var handleOnCancel = useCallback(function () {
|
|
70
74
|
setShowDialog(false);
|
|
71
75
|
pendingNavigation.current = null;
|
|
76
|
+
// A POP may have moved the URL to the target before it was blocked; put
|
|
77
|
+
// it back so the address bar matches the page the user chose to stay on.
|
|
78
|
+
bypassBlock.current = true;
|
|
79
|
+
try {
|
|
80
|
+
history.replace(history.location);
|
|
81
|
+
} finally {
|
|
82
|
+
bypassBlock.current = false;
|
|
83
|
+
}
|
|
72
84
|
onCancel();
|
|
73
|
-
}, [onCancel,
|
|
85
|
+
}, [onCancel, history]);
|
|
74
86
|
|
|
75
87
|
// Block navigation when there are unsaved changes.
|
|
76
88
|
useEffect(function () {
|
|
@@ -78,12 +90,25 @@ function ConfirmDialog(_ref, ref) {
|
|
|
78
90
|
return;
|
|
79
91
|
}
|
|
80
92
|
var unblock = history.block(function (location, action) {
|
|
93
|
+
// Let our own confirmed navigation through instead of re-blocking it.
|
|
94
|
+
if (bypassBlock.current) {
|
|
95
|
+
return undefined;
|
|
96
|
+
}
|
|
81
97
|
pendingNavigation.current = function () {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
98
|
+
bypassBlock.current = true;
|
|
99
|
+
try {
|
|
100
|
+
// A browser/anchor POP (e.g. a breadcrumb or back link) moves the URL
|
|
101
|
+
// to the target before navigation is blocked, and v5 leaves the hash
|
|
102
|
+
// there, so pushing the target would be a no-op. Re-sync to the
|
|
103
|
+
// still-current location first so the real navigation actually fires.
|
|
104
|
+
history.replace(history.location);
|
|
105
|
+
if (action === 'REPLACE') {
|
|
106
|
+
history.replace(location);
|
|
107
|
+
} else {
|
|
108
|
+
history.push(location);
|
|
109
|
+
}
|
|
110
|
+
} finally {
|
|
111
|
+
bypassBlock.current = false;
|
|
87
112
|
}
|
|
88
113
|
};
|
|
89
114
|
setShowDialog(true);
|
|
@@ -110,7 +135,8 @@ function ConfirmDialog(_ref, ref) {
|
|
|
110
135
|
ref: ref,
|
|
111
136
|
onConfirm: handleOnConfirm,
|
|
112
137
|
onCancel: handleOnCancel,
|
|
113
|
-
__experimentalHideHeader: false
|
|
138
|
+
__experimentalHideHeader: false,
|
|
139
|
+
children: children
|
|
114
140
|
}));
|
|
115
141
|
}
|
|
116
142
|
export default forwardRef(ConfirmDialog);
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import _defineProperty from "@babel/runtime/helpers/defineProperty";
|
|
2
|
+
function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
|
|
3
|
+
function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }
|
|
4
|
+
/**
|
|
5
|
+
* External dependencies.
|
|
6
|
+
*/
|
|
7
|
+
import { act, fireEvent, render, screen } from '@testing-library/react';
|
|
8
|
+
import { MemoryRouter, useHistory } from 'react-router-dom';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Internal dependencies.
|
|
12
|
+
*/
|
|
13
|
+
import ConfirmDialog from './';
|
|
14
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
15
|
+
var HistoryGrabber = function HistoryGrabber(_ref) {
|
|
16
|
+
var historyRef = _ref.historyRef;
|
|
17
|
+
historyRef.current = useHistory();
|
|
18
|
+
return null;
|
|
19
|
+
};
|
|
20
|
+
var renderWithHistory = function renderWithHistory() {
|
|
21
|
+
var routerProps = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
|
|
22
|
+
var historyRef = {
|
|
23
|
+
current: null
|
|
24
|
+
};
|
|
25
|
+
render(/*#__PURE__*/_jsxs(MemoryRouter, _objectSpread(_objectSpread({}, routerProps), {}, {
|
|
26
|
+
children: [/*#__PURE__*/_jsx(HistoryGrabber, {
|
|
27
|
+
historyRef: historyRef
|
|
28
|
+
}), /*#__PURE__*/_jsx(ConfirmDialog, {
|
|
29
|
+
when: true,
|
|
30
|
+
confirmButtonText: "Discard changes",
|
|
31
|
+
cancelButtonText: "Keep editing",
|
|
32
|
+
children: "Unsaved changes"
|
|
33
|
+
})]
|
|
34
|
+
})));
|
|
35
|
+
return historyRef.current;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
// The modal's X close button reuses the cancel label as its aria-label, so
|
|
39
|
+
// query by visible text to hit the actual footer buttons.
|
|
40
|
+
var confirmNavigation = function confirmNavigation() {
|
|
41
|
+
return fireEvent.click(screen.getByText('Discard changes', {
|
|
42
|
+
selector: 'button'
|
|
43
|
+
}));
|
|
44
|
+
};
|
|
45
|
+
var cancelNavigation = function cancelNavigation() {
|
|
46
|
+
return fireEvent.click(screen.getByText('Keep editing', {
|
|
47
|
+
selector: 'button'
|
|
48
|
+
}));
|
|
49
|
+
};
|
|
50
|
+
var dialog = function dialog() {
|
|
51
|
+
return screen.queryByText('Unsaved changes');
|
|
52
|
+
};
|
|
53
|
+
describe('ConfirmDialog navigation blocking', function () {
|
|
54
|
+
it('blocks navigation and stays on the current page on cancel', function () {
|
|
55
|
+
var history = renderWithHistory();
|
|
56
|
+
act(function () {
|
|
57
|
+
return history.push('/next');
|
|
58
|
+
});
|
|
59
|
+
expect(dialog()).toBeInTheDocument();
|
|
60
|
+
expect(history.location.pathname).toBe('/');
|
|
61
|
+
var replaceSpy = jest.spyOn(history, 'replace');
|
|
62
|
+
cancelNavigation();
|
|
63
|
+
expect(dialog()).not.toBeInTheDocument();
|
|
64
|
+
// The URL is re-synced to the page the user chose to stay on.
|
|
65
|
+
expect(replaceSpy).toHaveBeenCalledTimes(1);
|
|
66
|
+
expect(history.location.pathname).toBe('/');
|
|
67
|
+
});
|
|
68
|
+
it('replays a blocked push on confirm: re-sync replace first, then push, and re-arms the blocker', function () {
|
|
69
|
+
var history = renderWithHistory();
|
|
70
|
+
var replaceSpy = jest.spyOn(history, 'replace');
|
|
71
|
+
var pushSpy = jest.spyOn(history, 'push');
|
|
72
|
+
act(function () {
|
|
73
|
+
return history.push('/next');
|
|
74
|
+
});
|
|
75
|
+
expect(history.location.pathname).toBe('/');
|
|
76
|
+
confirmNavigation();
|
|
77
|
+
expect(history.location.pathname).toBe('/next');
|
|
78
|
+
|
|
79
|
+
// Replay order: re-sync replace fires before the target push.
|
|
80
|
+
var resyncOrder = replaceSpy.mock.invocationCallOrder[0];
|
|
81
|
+
var replayOrder = pushSpy.mock.invocationCallOrder[1];
|
|
82
|
+
expect(replaceSpy).toHaveBeenCalledTimes(1);
|
|
83
|
+
expect(pushSpy).toHaveBeenCalledTimes(2);
|
|
84
|
+
expect(resyncOrder).toBeLessThan(replayOrder);
|
|
85
|
+
|
|
86
|
+
// The bypass only lets the confirmed navigation through; the next one is blocked again.
|
|
87
|
+
act(function () {
|
|
88
|
+
return history.push('/another');
|
|
89
|
+
});
|
|
90
|
+
expect(history.location.pathname).toBe('/next');
|
|
91
|
+
expect(dialog()).toBeInTheDocument();
|
|
92
|
+
});
|
|
93
|
+
it('replays a blocked REPLACE with replace, not push', function () {
|
|
94
|
+
var history = renderWithHistory();
|
|
95
|
+
var pushSpy = jest.spyOn(history, 'push');
|
|
96
|
+
act(function () {
|
|
97
|
+
return history.replace('/replaced');
|
|
98
|
+
});
|
|
99
|
+
expect(dialog()).toBeInTheDocument();
|
|
100
|
+
expect(history.location.pathname).toBe('/');
|
|
101
|
+
confirmNavigation();
|
|
102
|
+
expect(history.location.pathname).toBe('/replaced');
|
|
103
|
+
expect(pushSpy).not.toHaveBeenCalled();
|
|
104
|
+
expect(history.length).toBe(1);
|
|
105
|
+
});
|
|
106
|
+
it('replays a blocked POP (back navigation) as a push after the re-sync', function () {
|
|
107
|
+
var history = renderWithHistory({
|
|
108
|
+
initialEntries: ['/start', '/current'],
|
|
109
|
+
initialIndex: 1
|
|
110
|
+
});
|
|
111
|
+
act(function () {
|
|
112
|
+
return history.goBack();
|
|
113
|
+
});
|
|
114
|
+
expect(dialog()).toBeInTheDocument();
|
|
115
|
+
expect(history.location.pathname).toBe('/current');
|
|
116
|
+
confirmNavigation();
|
|
117
|
+
expect(history.location.pathname).toBe('/start');
|
|
118
|
+
// Pushed rather than popped: the target becomes a new forward entry.
|
|
119
|
+
expect(history.length).toBe(3);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
// React re-reports errors thrown in event handlers as uncaught window
|
|
123
|
+
// errors; swallow them so the intentional throws don't fail the test.
|
|
124
|
+
var withNavigationError = function withNavigationError(fn) {
|
|
125
|
+
var consoleError = jest.spyOn(console, 'error').mockImplementation(function () {});
|
|
126
|
+
var onWindowError = function onWindowError(event) {
|
|
127
|
+
return event.preventDefault();
|
|
128
|
+
};
|
|
129
|
+
window.addEventListener('error', onWindowError);
|
|
130
|
+
try {
|
|
131
|
+
fn();
|
|
132
|
+
} finally {
|
|
133
|
+
window.removeEventListener('error', onWindowError);
|
|
134
|
+
consoleError.mockRestore();
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
it('keeps blocking after a confirmed navigation throws', function () {
|
|
138
|
+
var history = renderWithHistory();
|
|
139
|
+
act(function () {
|
|
140
|
+
return history.push('/next');
|
|
141
|
+
});
|
|
142
|
+
jest.spyOn(history, 'push').mockImplementationOnce(function () {
|
|
143
|
+
throw new Error('nav failed');
|
|
144
|
+
});
|
|
145
|
+
withNavigationError(function () {
|
|
146
|
+
return confirmNavigation();
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
// bypassBlock was reset by the finally, so navigation is still protected.
|
|
150
|
+
act(function () {
|
|
151
|
+
return history.push('/elsewhere');
|
|
152
|
+
});
|
|
153
|
+
expect(history.location.pathname).toBe('/');
|
|
154
|
+
expect(dialog()).toBeInTheDocument();
|
|
155
|
+
});
|
|
156
|
+
it('keeps blocking after a cancel URL re-sync throws', function () {
|
|
157
|
+
var history = renderWithHistory();
|
|
158
|
+
act(function () {
|
|
159
|
+
return history.push('/next');
|
|
160
|
+
});
|
|
161
|
+
jest.spyOn(history, 'replace').mockImplementationOnce(function () {
|
|
162
|
+
throw new Error('replace failed');
|
|
163
|
+
});
|
|
164
|
+
withNavigationError(function () {
|
|
165
|
+
return cancelNavigation();
|
|
166
|
+
});
|
|
167
|
+
act(function () {
|
|
168
|
+
return history.push('/elsewhere');
|
|
169
|
+
});
|
|
170
|
+
expect(history.location.pathname).toBe('/');
|
|
171
|
+
expect(dialog()).toBeInTheDocument();
|
|
172
|
+
});
|
|
173
|
+
});
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
// Variant
|
|
35
35
|
|
|
36
36
|
&--variant-default {
|
|
37
|
-
background-color: wp-colors.$gray-300;
|
|
37
|
+
background-color: var( --wpds-color-stroke-surface-neutral, #{wp-colors.$gray-300} );
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
&--variant-primary {
|
|
@@ -46,6 +46,8 @@
|
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
&--variant-tertiary {
|
|
49
|
-
|
|
49
|
+
// Fallback for consumers whose bundle doesn't carry the wpds token sheet
|
|
50
|
+
// (it ships with the Page header's stylesheet).
|
|
51
|
+
background-color: var( --wpds-color-stroke-surface-neutral-weak, #{wp-colors.$gray-100} );
|
|
50
52
|
}
|
|
51
53
|
}
|
package/dist/esm/grid/style.scss
CHANGED
|
@@ -19,12 +19,13 @@ function _isNativeReflectConstruct() { try { var t = !Boolean.prototype.valueOf.
|
|
|
19
19
|
*/
|
|
20
20
|
import apiFetch from '@wordpress/api-fetch';
|
|
21
21
|
import { Component, Fragment } from '@wordpress/element';
|
|
22
|
+
import { __experimentalHStack as HStack } from '@wordpress/components'; // eslint-disable-line @wordpress/no-unsafe-wp-apis
|
|
22
23
|
import { __ } from '@wordpress/i18n';
|
|
23
24
|
|
|
24
25
|
/**
|
|
25
26
|
* Internal dependencies.
|
|
26
27
|
*/
|
|
27
|
-
import { Button,
|
|
28
|
+
import { Button, Modal, Waiting } from '../';
|
|
28
29
|
|
|
29
30
|
/**
|
|
30
31
|
* External dependencies.
|
|
@@ -202,10 +203,11 @@ var Handoff = /*#__PURE__*/function (_Component) {
|
|
|
202
203
|
},
|
|
203
204
|
children: [/*#__PURE__*/_jsx("p", {
|
|
204
205
|
children: modalBody
|
|
205
|
-
}), /*#__PURE__*/_jsxs(
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
206
|
+
}), /*#__PURE__*/_jsxs(HStack, {
|
|
207
|
+
justify: "flex-end",
|
|
208
|
+
spacing: 4,
|
|
209
|
+
wrap: true,
|
|
210
|
+
className: "newspack-modal__footer",
|
|
209
211
|
children: [/*#__PURE__*/_jsx(Button, {
|
|
210
212
|
variant: "secondary",
|
|
211
213
|
onClick: function onClick() {
|
package/dist/esm/index.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
export { default as Accordion } from './accordion';
|
|
1
|
+
export { default as Accordion, AccordionPanel } from './accordion';
|
|
2
2
|
export { default as ActionCard } from './action-card';
|
|
3
3
|
export { default as AutocompleteTokenField } from './autocomplete-tokenfield';
|
|
4
4
|
export { default as AutocompleteWithSuggestions } from './autocomplete-with-suggestions';
|
|
5
5
|
export { default as AutocompleteWithLatestPosts } from './autocomplete-with-latest-posts';
|
|
6
6
|
export { default as Badge } from './badge';
|
|
7
|
+
export { default as Breadcrumbs } from './breadcrumbs';
|
|
7
8
|
export { default as Button } from './button';
|
|
8
9
|
export { default as BoxContrast } from './box-contrast';
|
|
9
10
|
export { default as Card } from './card';
|
|
@@ -11,6 +12,7 @@ export { default as CardFeature } from './card-feature';
|
|
|
11
12
|
export { default as CardForm } from './card-form';
|
|
12
13
|
export { default as CardSettingsGroup } from './card-settings-group';
|
|
13
14
|
export { default as CardSortableList } from './card-sortable-list';
|
|
15
|
+
export { default as IntegrationIcon } from './integration-icon';
|
|
14
16
|
export { default as CategoryAutocomplete } from './category-autocomplete';
|
|
15
17
|
export { default as ColorPicker } from './color-picker';
|
|
16
18
|
export { default as ConfirmDialog } from './confirm-dialog';
|
|
@@ -28,6 +30,7 @@ export { default as Grid } from './grid';
|
|
|
28
30
|
export { default as Modal } from './modal';
|
|
29
31
|
export { default as NewspackIcon } from './newspack-icon';
|
|
30
32
|
export { default as Notice } from './notice';
|
|
33
|
+
export { default as Page } from './page';
|
|
31
34
|
export { default as PluginInstaller } from './plugin-installer';
|
|
32
35
|
export { default as PluginSettings } from './plugin-settings';
|
|
33
36
|
export { default as PluginToggle } from './plugin-toggle';
|
|
@@ -53,5 +56,6 @@ export { default as useConfirmDialog } from './hooks/use-confirm-dialog';
|
|
|
53
56
|
export { default as useUnsavedChangesDialog } from './hooks/use-unsaved-changes-dialog';
|
|
54
57
|
import * as integrationIcons from './integration-icons';
|
|
55
58
|
export { integrationIcons };
|
|
59
|
+
export { espProviderOrder } from './integration-icons';
|
|
56
60
|
export { default as utils } from './utils';
|
|
57
61
|
import './style.scss';
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* External dependencies
|
|
3
|
+
*/
|
|
4
|
+
import classnames from 'classnames';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Internal dependencies
|
|
8
|
+
*/
|
|
9
|
+
import { espProviderIcons } from '../integration-icons';
|
|
10
|
+
import './style.scss';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* The single source for how an ESP provider is shown: brand mark plus badge
|
|
14
|
+
* background. A new provider needs an icon file, an `espProviderIcons` entry,
|
|
15
|
+
* and a background rule in this component's stylesheet.
|
|
16
|
+
*/
|
|
17
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
18
|
+
var IntegrationIcon = function IntegrationIcon(_ref) {
|
|
19
|
+
var provider = _ref.provider,
|
|
20
|
+
className = _ref.className;
|
|
21
|
+
var icons = espProviderIcons;
|
|
22
|
+
var node = Object.prototype.hasOwnProperty.call(espProviderIcons, provider) ? icons[provider] : null;
|
|
23
|
+
if (!node) {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
// Decorative: the provider is always named in adjacent visible text (the card
|
|
27
|
+
// title), so hide the mark from assistive tech to avoid duplicate announcements.
|
|
28
|
+
return /*#__PURE__*/_jsx("span", {
|
|
29
|
+
"aria-hidden": "true",
|
|
30
|
+
className: classnames('newspack-integration-icon', "newspack-integration-icon--".concat(provider.replace(/_/g, '-')), className),
|
|
31
|
+
children: node
|
|
32
|
+
});
|
|
33
|
+
};
|
|
34
|
+
export default IntegrationIcon;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
@use "~@wordpress/base-styles/colors" as wp-colors;
|
|
2
|
+
@use "~@wordpress/base-styles/variables" as wp-vars;
|
|
3
|
+
|
|
4
|
+
.newspack-integration-icon {
|
|
5
|
+
align-items: center;
|
|
6
|
+
border-radius: wp-vars.$radius-round;
|
|
7
|
+
display: flex;
|
|
8
|
+
flex: 0 0 wp-vars.$grid-unit-50;
|
|
9
|
+
height: wp-vars.$grid-unit-50;
|
|
10
|
+
justify-content: center;
|
|
11
|
+
width: wp-vars.$grid-unit-50;
|
|
12
|
+
|
|
13
|
+
svg {
|
|
14
|
+
display: block;
|
|
15
|
+
fill: currentcolor;
|
|
16
|
+
height: wp-vars.$grid-unit-30;
|
|
17
|
+
width: wp-vars.$grid-unit-30;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
&--active-campaign {
|
|
21
|
+
background: #004cff;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
&--mailchimp {
|
|
25
|
+
background: #ffe01b;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
&--constant-contact {
|
|
29
|
+
background: wp-colors.$gray-100;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
&--manual {
|
|
33
|
+
background: wp-colors.$gray-800;
|
|
34
|
+
color: wp-colors.$white;
|
|
35
|
+
|
|
36
|
+
// Set the fill on the path directly so the card's active/hover state
|
|
37
|
+
// (which recolours header svgs) can't override the white manual mark.
|
|
38
|
+
svg path {
|
|
39
|
+
fill: currentcolor;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -1,6 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WordPress dependencies
|
|
3
|
+
*/
|
|
4
|
+
import { atSymbol } from '@wordpress/icons';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Internal dependencies
|
|
8
|
+
*/
|
|
9
|
+
import activeCampaign from './active-campaign';
|
|
10
|
+
import constantContact from './constant-contact';
|
|
11
|
+
import mailchimp from './mailchimp';
|
|
1
12
|
export { default as activeCampaign } from './active-campaign';
|
|
2
13
|
export { default as constantContact } from './constant-contact';
|
|
3
14
|
export { default as fundraiseUp } from './fundraise-up';
|
|
4
15
|
export { default as mailchimp } from './mailchimp';
|
|
5
16
|
export { default as salesforce } from './salesforce';
|
|
6
|
-
export { default as wisepops } from './wisepops';
|
|
17
|
+
export { default as wisepops } from './wisepops';
|
|
18
|
+
|
|
19
|
+
// ESP provider brand marks keyed by the slug the backend reports
|
|
20
|
+
// (`Newspack_Newsletters::service_provider()`). Rendered via IntegrationIcon.
|
|
21
|
+
export var espProviderIcons = {
|
|
22
|
+
active_campaign: activeCampaign,
|
|
23
|
+
mailchimp: mailchimp,
|
|
24
|
+
constant_contact: constantContact,
|
|
25
|
+
manual: atSymbol
|
|
26
|
+
};
|
|
27
|
+
export var espProviderOrder = ['active_campaign', 'mailchimp', 'constant_contact', 'manual'];
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WordPress dependencies.
|
|
3
|
+
*/
|
|
4
|
+
import { NavigableRegion } from '@wordpress/admin-ui';
|
|
5
|
+
import { __experimentalHStack as HStack } from '@wordpress/components'; // eslint-disable-line @wordpress/no-unsafe-wp-apis
|
|
6
|
+
import { cloneElement, isValidElement } from '@wordpress/element';
|
|
7
|
+
import { Icon } from '@wordpress/icons';
|
|
8
|
+
import { Stack, Text } from '@wordpress/ui';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* External dependencies.
|
|
12
|
+
*/
|
|
13
|
+
import classnames from 'classnames';
|
|
14
|
+
import { newspack } from 'newspack-icons';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Internal dependencies.
|
|
18
|
+
*/
|
|
19
|
+
import Breadcrumbs from '../breadcrumbs';
|
|
20
|
+
import './style.scss';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Newspack page region: a sticky header block (breadcrumbs, actions, optional
|
|
24
|
+
* tabbed navigation) followed by the page content.
|
|
25
|
+
*
|
|
26
|
+
* This intentionally does not use `@wordpress/admin-ui`'s `Page`: that
|
|
27
|
+
* component has no slot for content rendered between the header and the body
|
|
28
|
+
* inside the sticky region, which is exactly where the tab bar must live. Until
|
|
29
|
+
* admin-ui grows such a slot, the header markup is assembled here from the same
|
|
30
|
+
* design-system primitives and tokens, so it can be swapped back later.
|
|
31
|
+
*
|
|
32
|
+
* @param {Object} props
|
|
33
|
+
* @param {Array} props.breadcrumbItems Trail items: `{ label, url? }`.
|
|
34
|
+
* @param {*} [props.badges]
|
|
35
|
+
* @param {*} [props.subTitle]
|
|
36
|
+
* @param {*} [props.actions]
|
|
37
|
+
* @param {*} [props.tabbedNavigation] A `TabbedNavigation` element; its bar renders inside
|
|
38
|
+
* the sticky header block and the page children render
|
|
39
|
+
* inside its active tab panel — or, when no visible tab
|
|
40
|
+
* owns the route, as a sibling of the panels.
|
|
41
|
+
* @param {string} [props.className]
|
|
42
|
+
* @param {*} props.children
|
|
43
|
+
* @return {JSX.Element} Page component.
|
|
44
|
+
*/
|
|
45
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
46
|
+
var Page = function Page(_ref) {
|
|
47
|
+
var _breadcrumbItems;
|
|
48
|
+
var _ref$breadcrumbItems = _ref.breadcrumbItems,
|
|
49
|
+
breadcrumbItems = _ref$breadcrumbItems === void 0 ? [] : _ref$breadcrumbItems,
|
|
50
|
+
badges = _ref.badges,
|
|
51
|
+
subTitle = _ref.subTitle,
|
|
52
|
+
actions = _ref.actions,
|
|
53
|
+
tabbedNavigation = _ref.tabbedNavigation,
|
|
54
|
+
className = _ref.className,
|
|
55
|
+
children = _ref.children;
|
|
56
|
+
var currentLabel = (_breadcrumbItems = breadcrumbItems[breadcrumbItems.length - 1]) === null || _breadcrumbItems === void 0 ? void 0 : _breadcrumbItems.label;
|
|
57
|
+
var renderShell = function renderShell(tabBar, content) {
|
|
58
|
+
return /*#__PURE__*/_jsxs(_Fragment, {
|
|
59
|
+
children: [/*#__PURE__*/_jsxs(Stack, {
|
|
60
|
+
direction: "column",
|
|
61
|
+
className: "newspack-page__header-region",
|
|
62
|
+
children: [/*#__PURE__*/_jsxs(Stack, {
|
|
63
|
+
direction: "column",
|
|
64
|
+
className: "newspack-page__header",
|
|
65
|
+
children: [/*#__PURE__*/_jsxs(Stack, {
|
|
66
|
+
direction: "row",
|
|
67
|
+
gap: "sm",
|
|
68
|
+
justify: "space-between",
|
|
69
|
+
children: [/*#__PURE__*/_jsxs(Stack, {
|
|
70
|
+
direction: "row",
|
|
71
|
+
gap: "sm",
|
|
72
|
+
align: "center",
|
|
73
|
+
justify: "start",
|
|
74
|
+
children: [/*#__PURE__*/_jsx("div", {
|
|
75
|
+
className: "newspack-page__header-visual",
|
|
76
|
+
"aria-hidden": "true",
|
|
77
|
+
children: /*#__PURE__*/_jsx(Icon, {
|
|
78
|
+
icon: newspack
|
|
79
|
+
})
|
|
80
|
+
}), /*#__PURE__*/_jsx(HStack, {
|
|
81
|
+
className: "newspack-page__breadcrumbs",
|
|
82
|
+
justify: "flex-start",
|
|
83
|
+
children: /*#__PURE__*/_jsx(Breadcrumbs, {
|
|
84
|
+
items: breadcrumbItems
|
|
85
|
+
})
|
|
86
|
+
}), badges]
|
|
87
|
+
}), actions && /*#__PURE__*/_jsx(Stack, {
|
|
88
|
+
direction: "row",
|
|
89
|
+
gap: "sm",
|
|
90
|
+
align: "center",
|
|
91
|
+
className: "newspack-page__header-actions",
|
|
92
|
+
children: actions
|
|
93
|
+
})]
|
|
94
|
+
}), subTitle && /*#__PURE__*/_jsx(Text, {
|
|
95
|
+
render: /*#__PURE__*/_jsx("p", {}),
|
|
96
|
+
variant: "body-md",
|
|
97
|
+
className: "newspack-page__header-subtitle",
|
|
98
|
+
children: subTitle
|
|
99
|
+
})]
|
|
100
|
+
}), tabBar]
|
|
101
|
+
}), content]
|
|
102
|
+
});
|
|
103
|
+
};
|
|
104
|
+
return /*#__PURE__*/_jsx(NavigableRegion, {
|
|
105
|
+
className: classnames('newspack-page', className),
|
|
106
|
+
ariaLabel: currentLabel,
|
|
107
|
+
children: isValidElement(tabbedNavigation) ? cloneElement(tabbedNavigation, {
|
|
108
|
+
renderShell: renderShell,
|
|
109
|
+
content: children
|
|
110
|
+
}) : renderShell(null, children)
|
|
111
|
+
});
|
|
112
|
+
};
|
|
113
|
+
export default Page;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* External dependencies.
|
|
3
|
+
*/
|
|
4
|
+
import { render, screen } from '@testing-library/react';
|
|
5
|
+
import { HashRouter } from 'react-router-dom';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Internal dependencies.
|
|
9
|
+
*/
|
|
10
|
+
import Page from './';
|
|
11
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
12
|
+
var renderInRouter = function renderInRouter(ui) {
|
|
13
|
+
return render(/*#__PURE__*/_jsx(HashRouter, {
|
|
14
|
+
children: ui
|
|
15
|
+
}));
|
|
16
|
+
};
|
|
17
|
+
describe('Page', function () {
|
|
18
|
+
it('renders the breadcrumb current item as h1 and the content', function () {
|
|
19
|
+
renderInRouter(/*#__PURE__*/_jsx(Page, {
|
|
20
|
+
breadcrumbItems: [{
|
|
21
|
+
label: 'Audience',
|
|
22
|
+
url: '/'
|
|
23
|
+
}, {
|
|
24
|
+
label: 'Access control'
|
|
25
|
+
}],
|
|
26
|
+
children: /*#__PURE__*/_jsx("div", {
|
|
27
|
+
children: "Body content"
|
|
28
|
+
})
|
|
29
|
+
}));
|
|
30
|
+
expect(screen.getByRole('heading', {
|
|
31
|
+
level: 1
|
|
32
|
+
})).toHaveTextContent('Access control');
|
|
33
|
+
expect(screen.getByText('Body content')).toBeInTheDocument();
|
|
34
|
+
});
|
|
35
|
+
it('renders actions and subtitle', function () {
|
|
36
|
+
renderInRouter(/*#__PURE__*/_jsx(Page, {
|
|
37
|
+
breadcrumbItems: [{
|
|
38
|
+
label: 'Audience'
|
|
39
|
+
}],
|
|
40
|
+
subTitle: "Manage readers",
|
|
41
|
+
actions: /*#__PURE__*/_jsx("button", {
|
|
42
|
+
children: "Add new"
|
|
43
|
+
}),
|
|
44
|
+
children: /*#__PURE__*/_jsx("div", {})
|
|
45
|
+
}));
|
|
46
|
+
expect(screen.getByRole('button', {
|
|
47
|
+
name: 'Add new'
|
|
48
|
+
})).toBeInTheDocument();
|
|
49
|
+
expect(screen.getByText('Manage readers')).toBeInTheDocument();
|
|
50
|
+
});
|
|
51
|
+
it('names the page region after the current breadcrumb item', function () {
|
|
52
|
+
renderInRouter(/*#__PURE__*/_jsx(Page, {
|
|
53
|
+
breadcrumbItems: [{
|
|
54
|
+
label: 'Audience',
|
|
55
|
+
url: '/'
|
|
56
|
+
}, {
|
|
57
|
+
label: 'Access control'
|
|
58
|
+
}],
|
|
59
|
+
children: /*#__PURE__*/_jsx("div", {})
|
|
60
|
+
}));
|
|
61
|
+
expect(screen.getByRole('region', {
|
|
62
|
+
name: 'Access control'
|
|
63
|
+
})).toBeInTheDocument();
|
|
64
|
+
});
|
|
65
|
+
});
|