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
@@ -2,17 +2,19 @@ import _defineProperty from "@babel/runtime/helpers/defineProperty";
2
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
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
4
  /**
5
- * WordPress dependencies.
5
+ * WordPress dependencies
6
6
  */
7
- import { __ } from '@wordpress/i18n';
8
- import { category } from '@wordpress/icons';
7
+ import { cloneElement } from '@wordpress/element';
9
8
 
10
9
  /**
11
10
  * Internal dependencies
12
11
  */
13
- import { Button, Handoff, NewspackIcon, Notice, HandoffMessage, TabbedNavigation } from '../';
12
+ import { Button, Handoff, Notice, HandoffMessage, TabbedNavigation, Page } from '../';
13
+ import { activeBreadcrumbs } from '../wizard/breadcrumbs-select';
14
14
  import { buttonProps } from '../button-props';
15
+ import Router from '../proxied-imports/router';
15
16
  import './style.scss';
17
+ var useLocation = Router.useLocation;
16
18
 
17
19
  /**
18
20
  * External dependencies
@@ -20,12 +22,31 @@ import './style.scss';
20
22
  import classnames from 'classnames';
21
23
 
22
24
  /**
23
- * Higher-Order Component to provide plugin management and error handling to Newspack Wizards.
25
+ * Derives the active-tab breadcrumb trail from the current route. Only rendered
26
+ * for tabbed wizards, which always mount inside a Router, so calling useLocation
27
+ * here keeps router-free consumers (e.g. standalone multibranded) from crashing.
28
+ *
29
+ * @param {Object} props Component props.
30
+ * @param {Array} props.sections Tabbed navigation sections.
31
+ * @param {Function} props.render Renders the page given the breadcrumb trail.
32
+ * @return {JSX.Element} The rendered page.
24
33
  */
25
34
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
35
+ var RouteBreadcrumbs = function RouteBreadcrumbs(_ref) {
36
+ var sections = _ref.sections,
37
+ render = _ref.render;
38
+ var _useLocation = useLocation(),
39
+ pathname = _useLocation.pathname;
40
+ return render(activeBreadcrumbs(sections, pathname));
41
+ };
42
+
43
+ /**
44
+ * Higher-Order Component to provide plugin management and error handling to Newspack Wizards.
45
+ */
26
46
  export default function withWizardScreen(WrappedComponent) {
27
- var _ref = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {},
28
- hidePrimaryButton = _ref.hidePrimaryButton;
47
+ var _ref2 = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {},
48
+ hidePrimaryButton = _ref2.hidePrimaryButton,
49
+ hideHeader = _ref2.hideHeader;
29
50
  var WrappedWithWizardScreen = function WrappedWithWizardScreen(props) {
30
51
  var className = props.className,
31
52
  buttonText = props.buttonText,
@@ -34,6 +55,8 @@ export default function withWizardScreen(WrappedComponent) {
34
55
  headerText = props.headerText,
35
56
  subHeaderText = props.subHeaderText,
36
57
  tabbedNavigation = props.tabbedNavigation,
58
+ breadcrumbItems = props.breadcrumbItems,
59
+ headerActions = props.headerActions,
37
60
  secondaryButtonText = props.secondaryButtonText,
38
61
  secondaryButtonAction = props.secondaryButtonAction,
39
62
  renderAboveContent = props.renderAboveContent,
@@ -59,40 +82,14 @@ export default function withWizardScreen(WrappedComponent) {
59
82
  children: buttonText
60
83
  }, retrievedButtonProps), overridingProps));
61
84
  };
62
- return /*#__PURE__*/_jsxs(_Fragment, {
63
- children: [newspack_aux_data.is_debug_mode && /*#__PURE__*/_jsx(Notice, {
64
- debugMode: true
65
- }), /*#__PURE__*/_jsx("div", {
66
- className: "newspack-wizard__header",
67
- children: /*#__PURE__*/_jsx("div", {
68
- className: "newspack-wizard__header__inner",
69
- children: /*#__PURE__*/_jsxs("div", {
70
- className: "newspack-wizard__title",
71
- children: [/*#__PURE__*/_jsx(Button, {
72
- isLink: true,
73
- href: newspack_urls.dashboard,
74
- label: __('Return to Dashboard', 'newspack-plugin'),
75
- showTooltip: true,
76
- icon: category,
77
- iconSize: 36,
78
- children: /*#__PURE__*/_jsx(NewspackIcon, {
79
- size: 36
80
- })
81
- }), /*#__PURE__*/_jsxs("div", {
82
- children: [headerText && /*#__PURE__*/_jsx("h2", {
83
- children: headerText
84
- }), subHeaderText && /*#__PURE__*/_jsx("span", {
85
- children: subHeaderText
86
- })]
87
- })]
88
- })
89
- })
90
- }), tabbedNavigation && /*#__PURE__*/_jsx(TabbedNavigation, {
91
- disableUpcoming: disableUpcomingInTabbedNavigation,
92
- items: tabbedNavigation.filter(function (item) {
93
- return !item.isHiddenInNav;
94
- })
95
- }), /*#__PURE__*/_jsx(HandoffMessage, {}), /*#__PURE__*/_jsxs("div", {
85
+ var tabbedNavigationRegion = tabbedNavigation && /*#__PURE__*/_jsx(TabbedNavigation, {
86
+ disableUpcoming: disableUpcomingInTabbedNavigation,
87
+ items: tabbedNavigation.filter(function (item) {
88
+ return !item.isHiddenInNav;
89
+ })
90
+ });
91
+ var content = /*#__PURE__*/_jsxs(_Fragment, {
92
+ children: [/*#__PURE__*/_jsx(HandoffMessage, {}), /*#__PURE__*/_jsxs("div", {
96
93
  className: classnames('newspack-wizard newspack-wizard__content', className),
97
94
  children: [typeof renderAboveContent === 'function' ? renderAboveContent() : null, /*#__PURE__*/_jsx(WrappedComponent, _objectSpread(_objectSpread({}, props), {}, {
98
95
  renderPrimaryButton: renderPrimaryButton
@@ -106,6 +103,46 @@ export default function withWizardScreen(WrappedComponent) {
106
103
  })]
107
104
  })]
108
105
  });
106
+ var renderPage = function renderPage(crumbs) {
107
+ var pageBreadcrumbs = crumbs !== null && crumbs !== void 0 ? crumbs : [];
108
+ if (!pageBreadcrumbs.length && headerText) {
109
+ pageBreadcrumbs = [{
110
+ label: headerText
111
+ }];
112
+ }
113
+ return /*#__PURE__*/_jsxs(_Fragment, {
114
+ children: [newspack_aux_data.is_debug_mode && /*#__PURE__*/_jsx(Notice, {
115
+ debugMode: true
116
+ }), hideHeader ?
117
+ /*#__PURE__*/
118
+ // Without the Page shell the tabs still own the content: it
119
+ // renders inside the active tab's panel.
120
+ _jsx(_Fragment, {
121
+ children: tabbedNavigationRegion ? cloneElement(tabbedNavigationRegion, {
122
+ content: content
123
+ }) : content
124
+ }) : /*#__PURE__*/_jsx(Page, {
125
+ breadcrumbItems: pageBreadcrumbs,
126
+ subTitle: subHeaderText,
127
+ actions: headerActions,
128
+ tabbedNavigation: tabbedNavigationRegion,
129
+ children: content
130
+ })]
131
+ });
132
+ };
133
+ if (hideHeader) {
134
+ return renderPage();
135
+ }
136
+ if (breadcrumbItems) {
137
+ return renderPage(breadcrumbItems);
138
+ }
139
+ if (tabbedNavigation) {
140
+ return /*#__PURE__*/_jsx(RouteBreadcrumbs, {
141
+ sections: tabbedNavigation,
142
+ render: renderPage
143
+ });
144
+ }
145
+ return renderPage();
109
146
  };
110
147
  return WrappedWithWizardScreen;
111
148
  }
@@ -0,0 +1,46 @@
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 withWizardScreen from './';
11
+ import { jsx as _jsx } from "react/jsx-runtime";
12
+ global.newspack_aux_data = {
13
+ is_debug_mode: false
14
+ };
15
+ describe('withWizardScreen', function () {
16
+ it('falls back to a single headerText breadcrumb when no breadcrumbItems are provided', function () {
17
+ var WrappedComponent = function WrappedComponent() {
18
+ return /*#__PURE__*/_jsx("div", {
19
+ children: "Body content"
20
+ });
21
+ };
22
+ var ScreenWithHeader = withWizardScreen(WrappedComponent);
23
+ render(/*#__PURE__*/_jsx(HashRouter, {
24
+ children: /*#__PURE__*/_jsx(ScreenWithHeader, {
25
+ headerText: "Brands"
26
+ })
27
+ }));
28
+ expect(screen.getByRole('heading', {
29
+ level: 1
30
+ })).toHaveTextContent('Brands');
31
+ });
32
+ it('renders without a Router when no tabbed navigation is provided', function () {
33
+ var WrappedComponent = function WrappedComponent() {
34
+ return /*#__PURE__*/_jsx("div", {
35
+ children: "Body content"
36
+ });
37
+ };
38
+ var ScreenWithHeader = withWizardScreen(WrappedComponent);
39
+ render(/*#__PURE__*/_jsx(ScreenWithHeader, {
40
+ headerText: "Brands"
41
+ }));
42
+ expect(screen.getByRole('heading', {
43
+ level: 1
44
+ })).toHaveTextContent('Brands');
45
+ });
46
+ });
@@ -45,129 +45,12 @@
45
45
  }
46
46
  }
47
47
 
48
- &__header {
49
- align-items: center;
50
- background: white;
51
- border-bottom: 1px solid wp-colors.$gray-300;
52
- display: flex;
53
- .newspack-wizard__fixed-header & {
54
- position: sticky;
55
- top: 0;
56
- width: 100%;
57
- z-index: 1000;
58
- .admin-bar & {
59
- @media only screen and ( min-width: 601px ) {
60
- top: 46px;
61
- }
62
- @media only screen and ( min-width: 782px ) {
63
- top: 32px;
64
- }
65
- }
66
- }
67
- &__inner {
68
- flex: 1 1 100%;
69
- }
70
- &__actions {
71
- align-items: center;
72
- display: flex;
73
- gap: 8px;
74
- justify-content: flex-end;
75
- padding: 0 24px 0 0;
76
- > .newspack-wizard__header__actions__main {
77
- display: none;
78
- }
79
- @media only screen and ( min-width: 782px ) {
80
- flex: 1 1 auto;
81
- > .newspack-wizard__header__actions__main {
82
- display: flex;
83
- }
84
- &__more {
85
- &--primary-only,
86
- .newspack-wizard__header__actions__more__main {
87
- display: none;
88
- }
89
- }
90
- }
91
- }
92
- }
93
-
94
48
  &__main {
95
- overflow-x: hidden;
49
+ overflow: hidden;
96
50
  padding-bottom: 1px; /* Prevents borders from being cut off */
97
51
  }
98
52
 
99
53
 
100
- &__title {
101
- align-items: center;
102
- display: flex;
103
-
104
- h2 {
105
- margin: 0;
106
- padding: 0;
107
- }
108
-
109
- > div {
110
- align-items: baseline;
111
- display: flex;
112
- padding: 0 24px;
113
-
114
- > span {
115
- color: wp-colors.$gray-700;
116
- display: none;
117
- margin-left: 8px;
118
-
119
- @media screen and ( min-width: 744px ) {
120
- display: block;
121
- }
122
- }
123
- }
124
-
125
- svg {
126
- fill: white;
127
- margin: 0 !important;
128
-
129
- &.newspack-icon {
130
- flex: 0 0 36px;
131
- margin-bottom: -1px !important;
132
- }
133
-
134
- &:not(.newspack-icon) {
135
- background: var(--wp-admin-theme-color);
136
- left: 50%;
137
- margin: -28px 0 0 -28px !important;
138
- opacity: 0;
139
- padding: 10px;
140
- position: absolute;
141
- top: 50%;
142
- transition: opacity 125ms ease-in-out;
143
- }
144
- }
145
-
146
- .components-button.has-icon {
147
- padding: 0;
148
- position: relative;
149
-
150
- &:hover {
151
- svg {
152
- opacity: 1;
153
- }
154
- }
155
-
156
- &:focus-visible {
157
- outline: none !important;
158
-
159
- &::before {
160
- border: 2px solid white;
161
- border-radius: 2px;
162
- content: "";
163
- display: block;
164
- inset: 6px 6px 5px;
165
- position: absolute;
166
- }
167
- }
168
- }
169
- }
170
-
171
54
  &__section-header {
172
55
  margin: 0 auto 32px;
173
56
  max-width: calc(calc(var(--newspack-wizard-section-space) * 2) + var(--newspack-wizard-section-width));
@@ -182,6 +65,7 @@
182
65
 
183
66
  &__content {
184
67
  margin: 0 auto;
68
+ width: 100%;
185
69
  max-width: calc(calc(var(--newspack-wizard-section-space) * 2) + var(--newspack-wizard-section-width));
186
70
  padding: calc(var(--newspack-wizard-section-space) * 2) var(--newspack-wizard-section-space) 0;
187
71
 
@@ -225,3 +109,16 @@
225
109
  margin: 0;
226
110
  }
227
111
  }
112
+
113
+ /**
114
+ * Wizard header overflow ("More") menu, rendered by the Wizard component into
115
+ * admin-ui's Page actions slot. Hide the toggle when there are no overflow
116
+ * actions, and hide the duplicated primary items inside the menu on wide
117
+ * viewports.
118
+ */
119
+ @media only screen and ( min-width: 782px ) {
120
+ .newspack-wizard__actions__more--primary-only,
121
+ .newspack-wizard__actions__more .newspack-wizard__actions__more__main {
122
+ display: none;
123
+ }
124
+ }
@@ -0,0 +1,63 @@
1
+ import _toConsumableArray from "@babel/runtime/helpers/toConsumableArray";
2
+ /**
3
+ * Internal dependencies.
4
+ */
5
+ import { matchesRoute } from '../route-match';
6
+
7
+ /**
8
+ * Select the active section's explicit breadcrumb trail by current route. Falls
9
+ * back to the first section, then to an empty trail.
10
+ *
11
+ * @param {Array} sections Wizard sections (`{ path, breadcrumbs, exact?, activeTabPaths? }`).
12
+ * @param {string} pathname Current router pathname.
13
+ * @return {Array} Breadcrumb items `{ label, url? }`.
14
+ */
15
+ export var activeBreadcrumbs = function activeBreadcrumbs() {
16
+ var sections = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : [];
17
+ var pathname = arguments.length > 1 ? arguments[1] : undefined;
18
+ if (!(sections !== null && sections !== void 0 && sections.length)) {
19
+ return [];
20
+ }
21
+ var match = sections.find(function (section) {
22
+ return matchesRoute(section, pathname);
23
+ }) || sections[0];
24
+ return match.breadcrumbs || [];
25
+ };
26
+
27
+ /**
28
+ * Append a section's render-time current-page breadcrumb(s) to a trail.
29
+ *
30
+ * A section can supply a leaf via `headerData.sectionName` — either a single
31
+ * label (e.g. an integration name, or Add/Edit) or an ordered array of
32
+ * `{ label, url? }` crumbs when the leaf needs its own linked ancestors. Each
33
+ * crumb is appended in turn.
34
+ *
35
+ * Convention: a route authors its current-page leaf in exactly ONE place —
36
+ * the static `breadcrumbs` array for a constant label, or `sectionName` for a
37
+ * render-time label (e.g. Add/Edit) whose static trail then holds ancestors
38
+ * only. Never both. The trailing-label skip below is a defensive guard against
39
+ * a route accidentally doing both; it is not a supported pattern to rely on.
40
+ *
41
+ * @param {Array} breadcrumbItems Base trail `{ label, url? }[]`.
42
+ * @param {string|Array|undefined} sectionName A label, an array of crumbs, or falsy.
43
+ * @return {Array} A new trail with the section crumb(s) appended (deduped).
44
+ */
45
+ export var appendSectionName = function appendSectionName() {
46
+ var breadcrumbItems = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : [];
47
+ var sectionName = arguments.length > 1 ? arguments[1] : undefined;
48
+ if (!sectionName) {
49
+ return breadcrumbItems;
50
+ }
51
+ var extraCrumbs = (Array.isArray(sectionName) ? sectionName : [{
52
+ label: sectionName
53
+ }]).filter(function (crumb) {
54
+ return crumb === null || crumb === void 0 ? void 0 : crumb.label;
55
+ });
56
+ return extraCrumbs.reduce(function (trail, crumb) {
57
+ var _trail;
58
+ if (((_trail = trail[trail.length - 1]) === null || _trail === void 0 ? void 0 : _trail.label) === crumb.label) {
59
+ return trail;
60
+ }
61
+ return [].concat(_toConsumableArray(trail), [crumb]);
62
+ }, breadcrumbItems);
63
+ };
@@ -0,0 +1,197 @@
1
+ /**
2
+ * Internal dependencies.
3
+ */
4
+ import { activeBreadcrumbs, appendSectionName } from './breadcrumbs-select';
5
+ var SECTIONS = [{
6
+ path: '/',
7
+ breadcrumbs: [{
8
+ label: 'Audience Management'
9
+ }, {
10
+ label: 'Configuration'
11
+ }]
12
+ }, {
13
+ path: '/donations',
14
+ breadcrumbs: [{
15
+ label: 'Audience Management'
16
+ }, {
17
+ label: 'Donations'
18
+ }]
19
+ }];
20
+ describe('activeBreadcrumbs', function () {
21
+ it('returns the matching section breadcrumbs by exact path', function () {
22
+ expect(activeBreadcrumbs(SECTIONS, '/donations')).toEqual([{
23
+ label: 'Audience Management'
24
+ }, {
25
+ label: 'Donations'
26
+ }]);
27
+ });
28
+ it('falls back to the first section when no path matches', function () {
29
+ expect(activeBreadcrumbs(SECTIONS, '/nope')).toEqual(SECTIONS[0].breadcrumbs);
30
+ });
31
+ it('returns [] when sections is empty', function () {
32
+ expect(activeBreadcrumbs([], '/x')).toEqual([]);
33
+ });
34
+ it('returns [] when sections is null or undefined', function () {
35
+ expect(activeBreadcrumbs(null, '/x')).toEqual([]);
36
+ expect(activeBreadcrumbs(undefined, '/x')).toEqual([]);
37
+ });
38
+ it('matches a non-exact section by path prefix', function () {
39
+ var sections = [{
40
+ path: '/',
41
+ breadcrumbs: [{
42
+ label: 'Root'
43
+ }]
44
+ }, {
45
+ path: '/access',
46
+ exact: false,
47
+ breadcrumbs: [{
48
+ label: 'Root'
49
+ }, {
50
+ label: 'Access control'
51
+ }]
52
+ }];
53
+ expect(activeBreadcrumbs(sections, '/access/institutions')).toEqual([{
54
+ label: 'Root'
55
+ }, {
56
+ label: 'Access control'
57
+ }]);
58
+ });
59
+ it('matches a parameterized route to its own breadcrumbs', function () {
60
+ var sections = [{
61
+ path: '/content-gates',
62
+ breadcrumbs: [{
63
+ label: 'Access control'
64
+ }]
65
+ }, {
66
+ path: '/institutions/:id',
67
+ exact: true,
68
+ breadcrumbs: [{
69
+ label: 'Access control'
70
+ }, {
71
+ label: 'Edit institution'
72
+ }]
73
+ }];
74
+ expect(activeBreadcrumbs(sections, '/institutions/123')).toEqual([{
75
+ label: 'Access control'
76
+ }, {
77
+ label: 'Edit institution'
78
+ }]);
79
+ });
80
+ it('matches via activeTabPaths wildcard', function () {
81
+ var sections = [{
82
+ path: '/',
83
+ breadcrumbs: [{
84
+ label: 'Root'
85
+ }]
86
+ }, {
87
+ path: '/segments',
88
+ activeTabPaths: ['/segments/*'],
89
+ breadcrumbs: [{
90
+ label: 'Root'
91
+ }, {
92
+ label: 'Segments'
93
+ }]
94
+ }];
95
+ expect(activeBreadcrumbs(sections, '/segments/123')).toEqual([{
96
+ label: 'Root'
97
+ }, {
98
+ label: 'Segments'
99
+ }]);
100
+ });
101
+ it('prefers an exact path match over the first section', function () {
102
+ expect(activeBreadcrumbs(SECTIONS, '/')).toEqual(SECTIONS[0].breadcrumbs);
103
+ });
104
+ it('returns [] when the matched section has no breadcrumbs', function () {
105
+ expect(activeBreadcrumbs([{
106
+ path: '/'
107
+ }], '/')).toEqual([]);
108
+ });
109
+ });
110
+ describe('appendSectionName', function () {
111
+ var base = [{
112
+ label: 'Access Control'
113
+ }];
114
+ it('returns the base trail unchanged when sectionName is falsy', function () {
115
+ expect(appendSectionName(base, undefined)).toBe(base);
116
+ expect(appendSectionName(base, '')).toBe(base);
117
+ expect(appendSectionName(base, null)).toBe(base);
118
+ });
119
+ it('appends a string sectionName as a leaf crumb', function () {
120
+ expect(appendSectionName(base, 'Content Gifting')).toEqual([{
121
+ label: 'Access Control'
122
+ }, {
123
+ label: 'Content Gifting'
124
+ }]);
125
+ });
126
+ it('appends an array of crumbs in order', function () {
127
+ var sectionName = [{
128
+ label: 'Mailchimp',
129
+ url: '/mailchimp'
130
+ }, {
131
+ label: 'Logs'
132
+ }];
133
+ expect(appendSectionName(base, sectionName)).toEqual([{
134
+ label: 'Access Control'
135
+ }, {
136
+ label: 'Mailchimp',
137
+ url: '/mailchimp'
138
+ }, {
139
+ label: 'Logs'
140
+ }]);
141
+ });
142
+ it('dedupes a string leaf that repeats the current trailing label', function () {
143
+ var trail = [{
144
+ label: 'Access Control'
145
+ }, {
146
+ label: 'Metered Countdown'
147
+ }];
148
+ expect(appendSectionName(trail, 'Metered Countdown')).toEqual(trail);
149
+ });
150
+ it('dedupes an array leaf that repeats the current trailing label', function () {
151
+ var trail = [{
152
+ label: 'Access Control'
153
+ }, {
154
+ label: 'Metered Countdown'
155
+ }];
156
+ expect(appendSectionName(trail, [{
157
+ label: 'Metered Countdown'
158
+ }])).toEqual(trail);
159
+ });
160
+ it('skips crumbs without a label', function () {
161
+ expect(appendSectionName(base, [{
162
+ url: '/no-label'
163
+ }, {
164
+ label: 'Edit'
165
+ }])).toEqual([{
166
+ label: 'Access Control'
167
+ }, {
168
+ label: 'Edit'
169
+ }]);
170
+ });
171
+ it('only dedupes against the immediate predecessor, not earlier crumbs', function () {
172
+ var trail = [{
173
+ label: 'Access Control'
174
+ }, {
175
+ label: 'Institutions'
176
+ }];
177
+ // 'Access Control' repeats an earlier (non-adjacent) crumb, so it is kept.
178
+ expect(appendSectionName(trail, [{
179
+ label: 'Access Control'
180
+ }])).toEqual([{
181
+ label: 'Access Control'
182
+ }, {
183
+ label: 'Institutions'
184
+ }, {
185
+ label: 'Access Control'
186
+ }]);
187
+ });
188
+ it('does not mutate the input trail', function () {
189
+ var trail = [{
190
+ label: 'Access Control'
191
+ }];
192
+ appendSectionName(trail, 'Content Gifting');
193
+ expect(trail).toEqual([{
194
+ label: 'Access Control'
195
+ }]);
196
+ });
197
+ });
@@ -3,11 +3,12 @@
3
3
  */
4
4
  import { __ } from '@wordpress/i18n';
5
5
  import { useSelect } from '@wordpress/data';
6
+ import { __experimentalHStack as HStack } from '@wordpress/components'; // eslint-disable-line @wordpress/no-unsafe-wp-apis
6
7
 
7
8
  /**
8
9
  * Internal dependencies.
9
10
  */
10
- import { Button, Card, Modal, Notice } from '../..';
11
+ import { Button, Modal, Notice } from '../..';
11
12
  import { WIZARD_STORE_NAMESPACE } from '../store';
12
13
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
13
14
  var parseError = function parseError(_ref) {
@@ -46,10 +47,11 @@ var WizardError = function WizardError() {
46
47
  noticeText: message,
47
48
  isError: true,
48
49
  rawHTML: true
49
- }), fallbackURL && /*#__PURE__*/_jsx(Card, {
50
- buttonsCard: true,
51
- noBorder: true,
52
- className: "justify-end",
50
+ }), fallbackURL && /*#__PURE__*/_jsx(HStack, {
51
+ justify: "flex-end",
52
+ spacing: 4,
53
+ wrap: true,
54
+ className: "newspack-modal__footer",
53
55
  children: /*#__PURE__*/_jsx(Button, {
54
56
  isPrimary: true,
55
57
  href: fallbackURL,