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
@@ -4,12 +4,12 @@
4
4
  import { Component, createRef, Fragment } from '@wordpress/element';
5
5
  import { __ } from '@wordpress/i18n';
6
6
  import apiFetch from '@wordpress/api-fetch';
7
- import { category } from '@wordpress/icons';
7
+ import { __experimentalHStack as HStack } from '@wordpress/components'; // eslint-disable-line @wordpress/no-unsafe-wp-apis
8
8
 
9
9
  /**
10
10
  * Internal dependencies.
11
11
  */
12
- import { Button, Card, Modal, NewspackIcon, Notice, PluginInstaller } from '../';
12
+ import { Button, Modal, Notice, Page, PluginInstaller } from '../';
13
13
  import Router from '../proxied-imports/router';
14
14
  import Footer from '../footer';
15
15
  import './style.scss';
@@ -99,11 +99,11 @@ export default function withWizard( WrappedComponent, requiredPlugins ) {
99
99
  return (
100
100
  <Modal title={ __( 'Unrecoverable error' ) } onRequestClose={ () => ( window.location = fallbackURL ) }>
101
101
  <Notice noticeText={ message } isError rawHTML />
102
- <Card buttonsCard noBorder className="justify-end">
102
+ <HStack justify="flex-end" spacing={ 4 } wrap className="newspack-modal__footer">
103
103
  <Button isPrimary href={ fallbackURL }>
104
104
  { __( 'Return to Dashboard', 'newspack-plugin' ) }
105
105
  </Button>
106
- </Card>
106
+ </HStack>
107
107
  </Modal>
108
108
  );
109
109
  };
@@ -203,41 +203,20 @@ export default function withWizard( WrappedComponent, requiredPlugins ) {
203
203
  if ( complete ) {
204
204
  return <Redirect from="/plugin-requirements" to="/" />;
205
205
  }
206
+ const headerText = requiredPlugins.length > 1 ? __( 'Required plugins', 'newspack-plugin' ) : __( 'Required plugin', 'newspack-plugin' );
207
+ const content = (
208
+ <div className="newspack-wizard newspack-wizard__content">
209
+ <PluginInstaller plugins={ requiredPlugins } onStatus={ status => this.pluginInstallationStatus( status ) } />
210
+ </div>
211
+ );
206
212
  return (
207
213
  <Route
208
214
  path="/"
209
- render={ () => (
210
- <Fragment>
211
- { complete !== null && (
212
- <div className="newspack-wizard__header">
213
- <div className="newspack-wizard__header__inner">
214
- <div className="newspack-wizard__title">
215
- <Button
216
- isLink
217
- href={ newspack_urls.dashboard }
218
- label={ __( 'Return to Dashboard', 'newspack-plugin' ) }
219
- showTooltip={ true }
220
- icon={ category }
221
- iconSize={ 36 }
222
- >
223
- <NewspackIcon size={ 36 } />
224
- </Button>
225
- <div>
226
- <h2>
227
- { requiredPlugins.length > 1
228
- ? __( 'Required plugins', 'newspack-plugin' )
229
- : __( 'Required plugin', 'newspack-plugin' ) }
230
- </h2>
231
- </div>
232
- </div>
233
- </div>
234
- </div>
235
- ) }
236
- <div className="newspack-wizard newspack-wizard__content">
237
- <PluginInstaller plugins={ requiredPlugins } onStatus={ status => this.pluginInstallationStatus( status ) } />
238
- </div>
239
- </Fragment>
240
- ) }
215
+ render={ () =>
216
+ // While `complete` is still null the requirements are being
217
+ // determined; render content without a header (matches prior behavior).
218
+ complete === null ? content : <Page breadcrumbItems={ [ { label: headerText } ] }>{ content }</Page>
219
+ }
241
220
  />
242
221
  );
243
222
  };
@@ -256,7 +235,7 @@ export default function withWizard( WrappedComponent, requiredPlugins ) {
256
235
  confirmAction = options => {
257
236
  const modalOptions = {
258
237
  title: null,
259
- message: __( 'Are you sure?', 'newpack-plugin' ),
238
+ message: __( 'Are you sure?', 'newspack-plugin' ),
260
239
  confirmText: __( 'OK', 'newspack-plugin' ),
261
240
  cancelText: __( 'Cancel', 'newspack-plugin' ),
262
241
  callback: null,
@@ -281,7 +260,7 @@ export default function withWizard( WrappedComponent, requiredPlugins ) {
281
260
  callback && (
282
261
  <Modal size="small" hideTitle={ ! title } title={ title } onRequestClose={ () => this.setState( { confirmation: null } ) }>
283
262
  <p>{ message }</p>
284
- <Card buttonsCard noBorder className="justify-end">
263
+ <HStack justify="flex-end" spacing={ 4 } wrap className="newspack-modal__footer">
285
264
  <Button variant="secondary" onClick={ () => this.setState( { confirmation: null } ) }>
286
265
  { cancelText }
287
266
  </Button>
@@ -294,7 +273,7 @@ export default function withWizard( WrappedComponent, requiredPlugins ) {
294
273
  >
295
274
  { confirmText }
296
275
  </Button>
297
- </Card>
276
+ </HStack>
298
277
  </Modal>
299
278
  )
300
279
  );
@@ -1,25 +1,43 @@
1
1
  /**
2
- * WordPress dependencies.
2
+ * WordPress dependencies
3
3
  */
4
- import { __ } from '@wordpress/i18n';
5
- import { category } from '@wordpress/icons';
4
+ import { cloneElement } from '@wordpress/element';
6
5
 
7
6
  /**
8
7
  * Internal dependencies
9
8
  */
10
- import { Button, Handoff, NewspackIcon, Notice, HandoffMessage, TabbedNavigation } from '../';
9
+ import { Button, Handoff, Notice, HandoffMessage, TabbedNavigation, Page } from '../';
10
+ import { activeBreadcrumbs } from '../wizard/breadcrumbs-select';
11
11
  import { buttonProps } from '../button-props';
12
+ import Router from '../proxied-imports/router';
12
13
  import './style.scss';
13
14
 
15
+ const { useLocation } = Router;
16
+
14
17
  /**
15
18
  * External dependencies
16
19
  */
17
20
  import classnames from 'classnames';
18
21
 
22
+ /**
23
+ * Derives the active-tab breadcrumb trail from the current route. Only rendered
24
+ * for tabbed wizards, which always mount inside a Router, so calling useLocation
25
+ * here keeps router-free consumers (e.g. standalone multibranded) from crashing.
26
+ *
27
+ * @param {Object} props Component props.
28
+ * @param {Array} props.sections Tabbed navigation sections.
29
+ * @param {Function} props.render Renders the page given the breadcrumb trail.
30
+ * @return {JSX.Element} The rendered page.
31
+ */
32
+ const RouteBreadcrumbs = ( { sections, render } ) => {
33
+ const { pathname } = useLocation();
34
+ return render( activeBreadcrumbs( sections, pathname ) );
35
+ };
36
+
19
37
  /**
20
38
  * Higher-Order Component to provide plugin management and error handling to Newspack Wizards.
21
39
  */
22
- export default function withWizardScreen( WrappedComponent, { hidePrimaryButton } = {} ) {
40
+ export default function withWizardScreen( WrappedComponent, { hidePrimaryButton, hideHeader } = {} ) {
23
41
  const WrappedWithWizardScreen = props => {
24
42
  const {
25
43
  className,
@@ -29,6 +47,8 @@ export default function withWizardScreen( WrappedComponent, { hidePrimaryButton
29
47
  headerText,
30
48
  subHeaderText,
31
49
  tabbedNavigation,
50
+ breadcrumbItems,
51
+ headerActions,
32
52
  secondaryButtonText,
33
53
  secondaryButtonAction,
34
54
  renderAboveContent,
@@ -57,37 +77,15 @@ export default function withWizardScreen( WrappedComponent, { hidePrimaryButton
57
77
  { ...overridingProps }
58
78
  />
59
79
  );
60
- return (
61
- <>
62
- { newspack_aux_data.is_debug_mode && <Notice debugMode /> }
63
- <div className="newspack-wizard__header">
64
- <div className="newspack-wizard__header__inner">
65
- <div className="newspack-wizard__title">
66
- <Button
67
- isLink
68
- href={ newspack_urls.dashboard }
69
- label={ __( 'Return to Dashboard', 'newspack-plugin' ) }
70
- showTooltip={ true }
71
- icon={ category }
72
- iconSize={ 36 }
73
- >
74
- <NewspackIcon size={ 36 } />
75
- </Button>
76
- <div>
77
- { headerText && <h2>{ headerText }</h2> }
78
- { subHeaderText && <span>{ subHeaderText }</span> }
79
- </div>
80
- </div>
81
- </div>
82
- </div>
83
-
84
- { tabbedNavigation && (
85
- <TabbedNavigation
86
- disableUpcoming={ disableUpcomingInTabbedNavigation }
87
- items={ tabbedNavigation.filter( item => ! item.isHiddenInNav ) }
88
- />
89
- ) }
80
+ const tabbedNavigationRegion = tabbedNavigation && (
81
+ <TabbedNavigation
82
+ disableUpcoming={ disableUpcomingInTabbedNavigation }
83
+ items={ tabbedNavigation.filter( item => ! item.isHiddenInNav ) }
84
+ />
85
+ );
90
86
 
87
+ const content = (
88
+ <>
91
89
  <HandoffMessage />
92
90
 
93
91
  <div className={ classnames( 'newspack-wizard newspack-wizard__content', className ) }>
@@ -106,6 +104,43 @@ export default function withWizardScreen( WrappedComponent, { hidePrimaryButton
106
104
  </div>
107
105
  </>
108
106
  );
107
+
108
+ const renderPage = crumbs => {
109
+ let pageBreadcrumbs = crumbs ?? [];
110
+ if ( ! pageBreadcrumbs.length && headerText ) {
111
+ pageBreadcrumbs = [ { label: headerText } ];
112
+ }
113
+ return (
114
+ <>
115
+ { newspack_aux_data.is_debug_mode && <Notice debugMode /> }
116
+ { hideHeader ? (
117
+ // Without the Page shell the tabs still own the content: it
118
+ // renders inside the active tab's panel.
119
+ <>{ tabbedNavigationRegion ? cloneElement( tabbedNavigationRegion, { content } ) : content }</>
120
+ ) : (
121
+ <Page
122
+ breadcrumbItems={ pageBreadcrumbs }
123
+ subTitle={ subHeaderText }
124
+ actions={ headerActions }
125
+ tabbedNavigation={ tabbedNavigationRegion }
126
+ >
127
+ { content }
128
+ </Page>
129
+ ) }
130
+ </>
131
+ );
132
+ };
133
+
134
+ if ( hideHeader ) {
135
+ return renderPage();
136
+ }
137
+ if ( breadcrumbItems ) {
138
+ return renderPage( breadcrumbItems );
139
+ }
140
+ if ( tabbedNavigation ) {
141
+ return <RouteBreadcrumbs sections={ tabbedNavigation } render={ renderPage } />;
142
+ }
143
+ return renderPage();
109
144
  };
110
145
  return WrappedWithWizardScreen;
111
146
  }
@@ -0,0 +1,36 @@
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
+
12
+ global.newspack_aux_data = { is_debug_mode: false };
13
+
14
+ describe( 'withWizardScreen', () => {
15
+ it( 'falls back to a single headerText breadcrumb when no breadcrumbItems are provided', () => {
16
+ const WrappedComponent = () => <div>Body content</div>;
17
+ const ScreenWithHeader = withWizardScreen( WrappedComponent );
18
+
19
+ render(
20
+ <HashRouter>
21
+ <ScreenWithHeader headerText="Brands" />
22
+ </HashRouter>
23
+ );
24
+
25
+ expect( screen.getByRole( 'heading', { level: 1 } ) ).toHaveTextContent( 'Brands' );
26
+ } );
27
+
28
+ it( 'renders without a Router when no tabbed navigation is provided', () => {
29
+ const WrappedComponent = () => <div>Body content</div>;
30
+ const ScreenWithHeader = withWizardScreen( WrappedComponent );
31
+
32
+ render( <ScreenWithHeader headerText="Brands" /> );
33
+
34
+ expect( screen.getByRole( 'heading', { level: 1 } ) ).toHaveTextContent( 'Brands' );
35
+ } );
36
+ } );
@@ -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,51 @@
1
+ /**
2
+ * Internal dependencies.
3
+ */
4
+ import { matchesRoute } from '../route-match';
5
+
6
+ /**
7
+ * Select the active section's explicit breadcrumb trail by current route. Falls
8
+ * back to the first section, then to an empty trail.
9
+ *
10
+ * @param {Array} sections Wizard sections (`{ path, breadcrumbs, exact?, activeTabPaths? }`).
11
+ * @param {string} pathname Current router pathname.
12
+ * @return {Array} Breadcrumb items `{ label, url? }`.
13
+ */
14
+ export const activeBreadcrumbs = ( sections = [], pathname ) => {
15
+ if ( ! sections?.length ) {
16
+ return [];
17
+ }
18
+ const match = sections.find( section => matchesRoute( section, pathname ) ) || sections[ 0 ];
19
+ return match.breadcrumbs || [];
20
+ };
21
+
22
+ /**
23
+ * Append a section's render-time current-page breadcrumb(s) to a trail.
24
+ *
25
+ * A section can supply a leaf via `headerData.sectionName` — either a single
26
+ * label (e.g. an integration name, or Add/Edit) or an ordered array of
27
+ * `{ label, url? }` crumbs when the leaf needs its own linked ancestors. Each
28
+ * crumb is appended in turn.
29
+ *
30
+ * Convention: a route authors its current-page leaf in exactly ONE place —
31
+ * the static `breadcrumbs` array for a constant label, or `sectionName` for a
32
+ * render-time label (e.g. Add/Edit) whose static trail then holds ancestors
33
+ * only. Never both. The trailing-label skip below is a defensive guard against
34
+ * a route accidentally doing both; it is not a supported pattern to rely on.
35
+ *
36
+ * @param {Array} breadcrumbItems Base trail `{ label, url? }[]`.
37
+ * @param {string|Array|undefined} sectionName A label, an array of crumbs, or falsy.
38
+ * @return {Array} A new trail with the section crumb(s) appended (deduped).
39
+ */
40
+ export const appendSectionName = ( breadcrumbItems = [], sectionName ) => {
41
+ if ( ! sectionName ) {
42
+ return breadcrumbItems;
43
+ }
44
+ const extraCrumbs = ( Array.isArray( sectionName ) ? sectionName : [ { label: sectionName } ] ).filter( crumb => crumb?.label );
45
+ return extraCrumbs.reduce( ( trail, crumb ) => {
46
+ if ( trail[ trail.length - 1 ]?.label === crumb.label ) {
47
+ return trail;
48
+ }
49
+ return [ ...trail, crumb ];
50
+ }, breadcrumbItems );
51
+ };
@@ -0,0 +1,124 @@
1
+ /**
2
+ * Internal dependencies.
3
+ */
4
+ import { activeBreadcrumbs, appendSectionName } from './breadcrumbs-select';
5
+
6
+ const SECTIONS = [
7
+ { path: '/', breadcrumbs: [ { label: 'Audience Management' }, { label: 'Configuration' } ] },
8
+ { path: '/donations', breadcrumbs: [ { label: 'Audience Management' }, { label: 'Donations' } ] },
9
+ ];
10
+
11
+ describe( 'activeBreadcrumbs', () => {
12
+ it( 'returns the matching section breadcrumbs by exact path', () => {
13
+ expect( activeBreadcrumbs( SECTIONS, '/donations' ) ).toEqual( [ { label: 'Audience Management' }, { label: 'Donations' } ] );
14
+ } );
15
+
16
+ it( 'falls back to the first section when no path matches', () => {
17
+ expect( activeBreadcrumbs( SECTIONS, '/nope' ) ).toEqual( SECTIONS[ 0 ].breadcrumbs );
18
+ } );
19
+
20
+ it( 'returns [] when sections is empty', () => {
21
+ expect( activeBreadcrumbs( [], '/x' ) ).toEqual( [] );
22
+ } );
23
+
24
+ it( 'returns [] when sections is null or undefined', () => {
25
+ expect( activeBreadcrumbs( null, '/x' ) ).toEqual( [] );
26
+ expect( activeBreadcrumbs( undefined, '/x' ) ).toEqual( [] );
27
+ } );
28
+
29
+ it( 'matches a non-exact section by path prefix', () => {
30
+ const sections = [
31
+ { path: '/', breadcrumbs: [ { label: 'Root' } ] },
32
+ { path: '/access', exact: false, breadcrumbs: [ { label: 'Root' }, { label: 'Access control' } ] },
33
+ ];
34
+ expect( activeBreadcrumbs( sections, '/access/institutions' ) ).toEqual( [ { label: 'Root' }, { label: 'Access control' } ] );
35
+ } );
36
+
37
+ it( 'matches a parameterized route to its own breadcrumbs', () => {
38
+ const sections = [
39
+ { path: '/content-gates', breadcrumbs: [ { label: 'Access control' } ] },
40
+ {
41
+ path: '/institutions/:id',
42
+ exact: true,
43
+ breadcrumbs: [ { label: 'Access control' }, { label: 'Edit institution' } ],
44
+ },
45
+ ];
46
+ expect( activeBreadcrumbs( sections, '/institutions/123' ) ).toEqual( [ { label: 'Access control' }, { label: 'Edit institution' } ] );
47
+ } );
48
+
49
+ it( 'matches via activeTabPaths wildcard', () => {
50
+ const sections = [
51
+ { path: '/', breadcrumbs: [ { label: 'Root' } ] },
52
+ {
53
+ path: '/segments',
54
+ activeTabPaths: [ '/segments/*' ],
55
+ breadcrumbs: [ { label: 'Root' }, { label: 'Segments' } ],
56
+ },
57
+ ];
58
+ expect( activeBreadcrumbs( sections, '/segments/123' ) ).toEqual( [ { label: 'Root' }, { label: 'Segments' } ] );
59
+ } );
60
+
61
+ it( 'prefers an exact path match over the first section', () => {
62
+ expect( activeBreadcrumbs( SECTIONS, '/' ) ).toEqual( SECTIONS[ 0 ].breadcrumbs );
63
+ } );
64
+
65
+ it( 'returns [] when the matched section has no breadcrumbs', () => {
66
+ expect( activeBreadcrumbs( [ { path: '/' } ], '/' ) ).toEqual( [] );
67
+ } );
68
+ } );
69
+
70
+ describe( 'appendSectionName', () => {
71
+ const base = [ { label: 'Access Control' } ];
72
+
73
+ it( 'returns the base trail unchanged when sectionName is falsy', () => {
74
+ expect( appendSectionName( base, undefined ) ).toBe( base );
75
+ expect( appendSectionName( base, '' ) ).toBe( base );
76
+ expect( appendSectionName( base, null ) ).toBe( base );
77
+ } );
78
+
79
+ it( 'appends a string sectionName as a leaf crumb', () => {
80
+ expect( appendSectionName( base, 'Content Gifting' ) ).toEqual( [ { label: 'Access Control' }, { label: 'Content Gifting' } ] );
81
+ } );
82
+
83
+ it( 'appends an array of crumbs in order', () => {
84
+ const sectionName = [ { label: 'Mailchimp', url: '/mailchimp' }, { label: 'Logs' } ];
85
+ expect( appendSectionName( base, sectionName ) ).toEqual( [
86
+ { label: 'Access Control' },
87
+ { label: 'Mailchimp', url: '/mailchimp' },
88
+ { label: 'Logs' },
89
+ ] );
90
+ } );
91
+
92
+ it( 'dedupes a string leaf that repeats the current trailing label', () => {
93
+ const trail = [ { label: 'Access Control' }, { label: 'Metered Countdown' } ];
94
+ expect( appendSectionName( trail, 'Metered Countdown' ) ).toEqual( trail );
95
+ } );
96
+
97
+ it( 'dedupes an array leaf that repeats the current trailing label', () => {
98
+ const trail = [ { label: 'Access Control' }, { label: 'Metered Countdown' } ];
99
+ expect( appendSectionName( trail, [ { label: 'Metered Countdown' } ] ) ).toEqual( trail );
100
+ } );
101
+
102
+ it( 'skips crumbs without a label', () => {
103
+ expect( appendSectionName( base, [ { url: '/no-label' }, { label: 'Edit' } ] ) ).toEqual( [
104
+ { label: 'Access Control' },
105
+ { label: 'Edit' },
106
+ ] );
107
+ } );
108
+
109
+ it( 'only dedupes against the immediate predecessor, not earlier crumbs', () => {
110
+ const trail = [ { label: 'Access Control' }, { label: 'Institutions' } ];
111
+ // 'Access Control' repeats an earlier (non-adjacent) crumb, so it is kept.
112
+ expect( appendSectionName( trail, [ { label: 'Access Control' } ] ) ).toEqual( [
113
+ { label: 'Access Control' },
114
+ { label: 'Institutions' },
115
+ { label: 'Access Control' },
116
+ ] );
117
+ } );
118
+
119
+ it( 'does not mutate the input trail', () => {
120
+ const trail = [ { label: 'Access Control' } ];
121
+ appendSectionName( trail, 'Content Gifting' );
122
+ expect( trail ).toEqual( [ { label: 'Access Control' } ] );
123
+ } );
124
+ } );
@@ -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
 
13
14
  const parseError = ( { data, message, code } ) => {
@@ -36,11 +37,11 @@ const WizardError = () => {
36
37
  <Modal title={ __( 'Unrecoverable error' ) } onRequestClose={ fallbackURL ? () => ( window.location = fallbackURL ) : undefined }>
37
38
  <Notice noticeText={ message } isError rawHTML />
38
39
  { fallbackURL && (
39
- <Card buttonsCard noBorder className="justify-end">
40
+ <HStack justify="flex-end" spacing={ 4 } wrap className="newspack-modal__footer">
40
41
  <Button isPrimary href={ fallbackURL }>
41
42
  { __( 'Return to Dashboard', 'newspack-plugin' ) }
42
43
  </Button>
43
- </Card>
44
+ </HStack>
44
45
  ) }
45
46
  </Modal>
46
47
  );