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.
Files changed (138) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/DEVELOPMENT.md +1 -1
  3. package/dist/cjs/accordion/index.js +43 -26
  4. package/dist/cjs/accordion/index.test.js +80 -0
  5. package/dist/cjs/accordion/style.scss +25 -21
  6. package/dist/cjs/action-card/style.scss +0 -22
  7. package/dist/cjs/breadcrumbs/index.js +80 -0
  8. package/dist/cjs/breadcrumbs/index.test.js +109 -0
  9. package/dist/cjs/breadcrumbs/style.scss +41 -0
  10. package/dist/cjs/card/core-card.js +8 -4
  11. package/dist/cjs/card/index.js +1 -1
  12. package/dist/cjs/card/style-core.scss +14 -0
  13. package/dist/cjs/card/style.scss +0 -9
  14. package/dist/cjs/card-feature/index.js +25 -13
  15. package/dist/cjs/card-settings-group/index.js +3 -0
  16. package/dist/cjs/confirm-dialog/index.js +34 -8
  17. package/dist/cjs/confirm-dialog/index.test.js +173 -0
  18. package/dist/cjs/divider/style.scss +4 -2
  19. package/dist/cjs/grid/style.scss +5 -0
  20. package/dist/cjs/handoff/index.js +8 -5
  21. package/dist/cjs/index.js +34 -1
  22. package/dist/cjs/integration-icon/index.js +40 -0
  23. package/dist/cjs/integration-icon/style.scss +42 -0
  24. package/dist/cjs/integration-icons/index.js +21 -2
  25. package/dist/cjs/modal/style.scss +4 -0
  26. package/dist/cjs/page/index.js +121 -0
  27. package/dist/cjs/page/index.test.js +69 -0
  28. package/dist/cjs/page/style.scss +86 -0
  29. package/dist/cjs/route-match.js +59 -0
  30. package/dist/cjs/route-match.test.js +77 -0
  31. package/dist/cjs/section-header/index.js +8 -1
  32. package/dist/cjs/section-header/style.scss +11 -11
  33. package/dist/cjs/select-control/GroupedSelectControl.js +1 -1
  34. package/dist/cjs/select-control/style.scss +4 -0
  35. package/dist/cjs/style.scss +1 -4
  36. package/dist/cjs/tabbed-navigation/index.js +188 -40
  37. package/dist/cjs/tabbed-navigation/index.test.js +299 -0
  38. package/dist/cjs/tabbed-navigation/style.scss +77 -57
  39. package/dist/cjs/with-wizard/index.js +34 -45
  40. package/dist/cjs/with-wizard-screen/index.js +82 -41
  41. package/dist/cjs/with-wizard-screen/index.test.js +50 -0
  42. package/dist/cjs/with-wizard-screen/style.scss +15 -118
  43. package/dist/cjs/wizard/breadcrumbs-select.js +70 -0
  44. package/dist/cjs/wizard/breadcrumbs-select.test.js +200 -0
  45. package/dist/cjs/wizard/components/WizardError.js +8 -4
  46. package/dist/cjs/wizard/index.js +162 -141
  47. package/dist/esm/accordion/index.js +42 -26
  48. package/dist/esm/accordion/index.test.js +79 -0
  49. package/dist/esm/accordion/style.scss +25 -21
  50. package/dist/esm/action-card/style.scss +0 -22
  51. package/dist/esm/breadcrumbs/index.js +75 -0
  52. package/dist/esm/breadcrumbs/index.test.js +105 -0
  53. package/dist/esm/breadcrumbs/style.scss +41 -0
  54. package/dist/esm/card/core-card.js +8 -4
  55. package/dist/esm/card/index.js +1 -1
  56. package/dist/esm/card/style-core.scss +14 -0
  57. package/dist/esm/card/style.scss +0 -9
  58. package/dist/esm/card-feature/index.js +25 -13
  59. package/dist/esm/card-settings-group/index.js +3 -0
  60. package/dist/esm/confirm-dialog/index.js +34 -8
  61. package/dist/esm/confirm-dialog/index.test.js +173 -0
  62. package/dist/esm/divider/style.scss +4 -2
  63. package/dist/esm/grid/style.scss +5 -0
  64. package/dist/esm/handoff/index.js +7 -5
  65. package/dist/esm/index.js +5 -1
  66. package/dist/esm/integration-icon/index.js +34 -0
  67. package/dist/esm/integration-icon/style.scss +42 -0
  68. package/dist/esm/integration-icons/index.js +22 -1
  69. package/dist/esm/modal/style.scss +4 -0
  70. package/dist/esm/page/index.js +113 -0
  71. package/dist/esm/page/index.test.js +65 -0
  72. package/dist/esm/page/style.scss +86 -0
  73. package/dist/esm/route-match.js +51 -0
  74. package/dist/esm/route-match.test.js +74 -0
  75. package/dist/esm/section-header/index.js +8 -1
  76. package/dist/esm/section-header/style.scss +11 -11
  77. package/dist/esm/select-control/GroupedSelectControl.js +1 -1
  78. package/dist/esm/select-control/style.scss +4 -0
  79. package/dist/esm/style.scss +1 -4
  80. package/dist/esm/tabbed-navigation/index.js +192 -38
  81. package/dist/esm/tabbed-navigation/index.test.js +296 -0
  82. package/dist/esm/tabbed-navigation/style.scss +77 -57
  83. package/dist/esm/with-wizard/index.js +33 -45
  84. package/dist/esm/with-wizard-screen/index.js +78 -41
  85. package/dist/esm/with-wizard-screen/index.test.js +46 -0
  86. package/dist/esm/with-wizard-screen/style.scss +15 -118
  87. package/dist/esm/wizard/breadcrumbs-select.js +63 -0
  88. package/dist/esm/wizard/breadcrumbs-select.test.js +197 -0
  89. package/dist/esm/wizard/components/WizardError.js +7 -5
  90. package/dist/esm/wizard/index.js +165 -144
  91. package/package.json +10 -2
  92. package/src/accordion/index.js +27 -15
  93. package/src/accordion/index.test.js +62 -0
  94. package/src/accordion/style.scss +25 -21
  95. package/src/action-card/style.scss +0 -22
  96. package/src/breadcrumbs/index.js +63 -0
  97. package/src/breadcrumbs/index.test.js +59 -0
  98. package/src/breadcrumbs/style.scss +41 -0
  99. package/src/card/core-card.js +11 -6
  100. package/src/card/index.js +0 -1
  101. package/src/card/style-core.scss +14 -0
  102. package/src/card/style.scss +0 -9
  103. package/src/card-feature/README.md +3 -3
  104. package/src/card-feature/index.tsx +37 -21
  105. package/src/card-settings-group/index.tsx +4 -0
  106. package/src/card-sortable-list/index.tsx +1 -1
  107. package/src/confirm-dialog/index.test.js +142 -0
  108. package/src/confirm-dialog/index.tsx +34 -7
  109. package/src/divider/style.scss +4 -2
  110. package/src/grid/style.scss +5 -0
  111. package/src/handoff/index.js +4 -3
  112. package/src/index.js +5 -1
  113. package/src/integration-icon/index.tsx +35 -0
  114. package/src/integration-icon/style.scss +42 -0
  115. package/src/integration-icons/index.js +23 -0
  116. package/src/modal/style.scss +4 -0
  117. package/src/page/index.js +89 -0
  118. package/src/page/index.test.js +43 -0
  119. package/src/page/style.scss +86 -0
  120. package/src/route-match.js +47 -0
  121. package/src/route-match.test.js +50 -0
  122. package/src/section-header/index.js +7 -3
  123. package/src/section-header/style.scss +11 -11
  124. package/src/select-control/GroupedSelectControl.js +1 -1
  125. package/src/select-control/style.scss +4 -0
  126. package/src/style.scss +1 -4
  127. package/src/tabbed-navigation/index.js +181 -34
  128. package/src/tabbed-navigation/index.test.js +228 -0
  129. package/src/tabbed-navigation/style.scss +77 -57
  130. package/src/with-wizard/index.js +18 -39
  131. package/src/with-wizard-screen/index.js +70 -35
  132. package/src/with-wizard-screen/index.test.js +36 -0
  133. package/src/with-wizard-screen/style.scss +15 -118
  134. package/src/wizard/breadcrumbs-select.js +51 -0
  135. package/src/wizard/breadcrumbs-select.test.js +124 -0
  136. package/src/wizard/components/WizardError.js +4 -3
  137. package/src/wizard/index.js +160 -146
  138. 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, pendingNavigation]);
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
- unblock();
83
- if (action === 'REPLACE') {
84
- history.replace(location);
85
- } else {
86
- history.push(location);
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
- background-color: wp-colors.$gray-100;
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
  }
@@ -61,6 +61,11 @@
61
61
  grid-row-gap: 16px;
62
62
  }
63
63
 
64
+ // Row Gap 24
65
+ &__row-gap-24 {
66
+ grid-row-gap: 24px;
67
+ }
68
+
64
69
  // Row Gap 32
65
70
  &__row-gap-32 {
66
71
  grid-row-gap: 32px;
@@ -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, Card, Modal, Waiting } from '../';
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(Card, {
206
- buttonsCard: true,
207
- noBorder: true,
208
- className: "justify-end",
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'];
@@ -99,3 +99,7 @@
99
99
  .modal-open .components-popover__content {
100
100
  opacity: 0;
101
101
  }
102
+
103
+ .newspack-modal__footer {
104
+ margin-top: 32px;
105
+ }
@@ -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
+ });