newspack-components 4.8.1-alpha.1 → 4.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +9 -1
- package/dist/cjs/card-feature/index.js +28 -3
- package/dist/cjs/card-feature/index.test.js +100 -45
- package/dist/cjs/card-feature/style.scss +1 -5
- package/dist/cjs/integration-icon/index.js +4 -4
- package/dist/cjs/integration-icon/style.scss +18 -2
- package/dist/cjs/integration-icons/active-campaign.js +4 -7
- package/dist/cjs/integration-icons/beehiiv.js +19 -0
- package/dist/cjs/integration-icons/gravity-forms.js +19 -0
- package/dist/cjs/integration-icons/index.js +26 -6
- package/dist/cjs/integration-icons/salesforce.js +5 -3
- package/dist/cjs/wizard/breadcrumbs-select.js +17 -4
- package/dist/cjs/wizard/breadcrumbs-select.test.js +17 -0
- package/dist/cjs/wizard/index.js +42 -6
- package/dist/cjs/wizard/index.test.js +146 -0
- package/dist/esm/card-feature/index.js +29 -4
- package/dist/esm/card-feature/index.test.js +100 -45
- package/dist/esm/card-feature/style.scss +1 -5
- package/dist/esm/integration-icon/index.js +5 -5
- package/dist/esm/integration-icon/style.scss +18 -2
- package/dist/esm/integration-icons/active-campaign.js +5 -8
- package/dist/esm/integration-icons/beehiiv.js +13 -0
- package/dist/esm/integration-icons/gravity-forms.js +13 -0
- package/dist/esm/integration-icons/index.js +15 -4
- package/dist/esm/integration-icons/salesforce.js +5 -3
- package/dist/esm/wizard/breadcrumbs-select.js +16 -3
- package/dist/esm/wizard/breadcrumbs-select.test.js +18 -1
- package/dist/esm/wizard/index.js +42 -6
- package/dist/esm/wizard/index.test.js +146 -0
- package/package.json +1 -1
- package/src/card-feature/README.md +10 -11
- package/src/card-feature/index.test.js +45 -15
- package/src/card-feature/index.tsx +43 -24
- package/src/card-feature/style.scss +1 -5
- package/src/integration-icon/index.tsx +5 -5
- package/src/integration-icon/style.scss +18 -2
- package/src/integration-icons/active-campaign.js +1 -5
- package/src/integration-icons/beehiiv.js +10 -0
- package/src/integration-icons/gravity-forms.js +10 -0
- package/src/integration-icons/index.js +14 -3
- package/src/integration-icons/salesforce.js +3 -3
- package/src/wizard/breadcrumbs-select.js +12 -4
- package/src/wizard/breadcrumbs-select.test.js +15 -1
- package/src/wizard/index.js +34 -5
- package/src/wizard/index.test.js +73 -0
|
@@ -61,7 +61,7 @@ describe( 'CardFeature', () => {
|
|
|
61
61
|
} );
|
|
62
62
|
|
|
63
63
|
it( 'names the feature in the configure state too', () => {
|
|
64
|
-
render( <CardFeature title="Content Gifting" enabled /> );
|
|
64
|
+
render( <CardFeature title="Content Gifting" enabled onConfigure={ () => {} } /> );
|
|
65
65
|
expect( screen.getByRole( 'button', { name: 'Configure Content Gifting' } ) ).toBeInTheDocument();
|
|
66
66
|
} );
|
|
67
67
|
|
|
@@ -108,6 +108,43 @@ describe( 'CardFeature', () => {
|
|
|
108
108
|
expect( onEnable ).not.toHaveBeenCalled();
|
|
109
109
|
} );
|
|
110
110
|
|
|
111
|
+
// A button reading Configure with nothing behind it would click to nowhere.
|
|
112
|
+
it( 'drops the primary button once enabled when there is nothing to configure', () => {
|
|
113
|
+
render(
|
|
114
|
+
<CardFeature title="Content gifting" enabled onEnable={ jest.fn() } moreControls={ [ { title: 'Disable', onClick: jest.fn() } ] } />
|
|
115
|
+
);
|
|
116
|
+
expect( screen.queryByRole( 'button', { name: /Configure/ } ) ).toBeNull();
|
|
117
|
+
expect( moreMenu() ).toBeInTheDocument();
|
|
118
|
+
} );
|
|
119
|
+
|
|
120
|
+
// Enabling removes the button that was just used; without a hand-off,
|
|
121
|
+
// keyboard focus falls to the page.
|
|
122
|
+
it( 'hands focus to the More menu when enabling removes the focused button', () => {
|
|
123
|
+
const props = { title: 'Content gifting', onEnable: jest.fn(), moreControls: [ { title: 'Disable', onClick: jest.fn() } ] };
|
|
124
|
+
const { rerender } = render( <CardFeature { ...props } /> );
|
|
125
|
+
primaryButton().focus();
|
|
126
|
+
rerender( <CardFeature { ...props } enabled /> );
|
|
127
|
+
expect( moreMenu() ).toHaveFocus();
|
|
128
|
+
} );
|
|
129
|
+
|
|
130
|
+
it( 'leaves focus alone when the removed button did not have it', () => {
|
|
131
|
+
const props = { title: 'Content gifting', onEnable: jest.fn(), moreControls: [ { title: 'Disable', onClick: jest.fn() } ] };
|
|
132
|
+
const { rerender } = render(
|
|
133
|
+
<>
|
|
134
|
+
<CardFeature { ...props } />
|
|
135
|
+
<button>Elsewhere</button>
|
|
136
|
+
</>
|
|
137
|
+
);
|
|
138
|
+
screen.getByRole( 'button', { name: 'Elsewhere' } ).focus();
|
|
139
|
+
rerender(
|
|
140
|
+
<>
|
|
141
|
+
<CardFeature { ...props } enabled />
|
|
142
|
+
<button>Elsewhere</button>
|
|
143
|
+
</>
|
|
144
|
+
);
|
|
145
|
+
expect( screen.getByRole( 'button', { name: 'Elsewhere' } ) ).toHaveFocus();
|
|
146
|
+
} );
|
|
147
|
+
|
|
111
148
|
it( 'still routes to onEnable when enabled with an unmet requirement, since the button reads Enable', () => {
|
|
112
149
|
const onEnable = jest.fn();
|
|
113
150
|
const onConfigure = jest.fn();
|
|
@@ -162,7 +199,7 @@ describe( 'CardFeature', () => {
|
|
|
162
199
|
} );
|
|
163
200
|
|
|
164
201
|
it( 'leaves the enabled badge unlinked, since it explains nothing about the button', () => {
|
|
165
|
-
render( <CardFeature title="Content gifting" enabled /> );
|
|
202
|
+
render( <CardFeature title="Content gifting" enabled onConfigure={ () => {} } /> );
|
|
166
203
|
expect( primaryButton() ).not.toHaveAttribute( 'aria-describedby' );
|
|
167
204
|
} );
|
|
168
205
|
|
|
@@ -180,7 +217,7 @@ describe( 'CardFeature', () => {
|
|
|
180
217
|
it( 'accepts custom labels for both states', () => {
|
|
181
218
|
const { rerender } = render( <CardFeature title="Apple News" enableLabel="Connect" configureLabel="Manage connection" /> );
|
|
182
219
|
expect( screen.getByRole( 'button', { name: 'Connect Apple News' } ) ).toBeInTheDocument();
|
|
183
|
-
rerender( <CardFeature title="Apple News" enabled enableLabel="Connect" configureLabel="Manage connection" /> );
|
|
220
|
+
rerender( <CardFeature title="Apple News" enabled enableLabel="Connect" configureLabel="Manage connection" onConfigure={ () => {} } /> );
|
|
184
221
|
expect( screen.getByRole( 'button', { name: 'Manage connection Apple News' } ) ).toBeInTheDocument();
|
|
185
222
|
} );
|
|
186
223
|
} );
|
|
@@ -234,16 +271,16 @@ describe( 'CardFeature', () => {
|
|
|
234
271
|
expect( container.querySelector( '.newspack-card-feature__icon' ) ).toBeNull();
|
|
235
272
|
} );
|
|
236
273
|
|
|
237
|
-
it( 'applies the descriptor colours inline and rounds
|
|
274
|
+
it( 'applies the descriptor colours inline and rounds a backed icon', () => {
|
|
238
275
|
const { container } = render(
|
|
239
276
|
<CardFeature
|
|
240
277
|
title="Content gifting"
|
|
241
|
-
icon={ { node: <span data-testid="descriptor-icon" />, fill: '#003da5', backgroundColor: '#dfe7f4'
|
|
278
|
+
icon={ { node: <span data-testid="descriptor-icon" />, fill: '#003da5', backgroundColor: '#dfe7f4' } }
|
|
242
279
|
/>
|
|
243
280
|
);
|
|
244
281
|
const iconContainer = container.querySelector( '.newspack-card-feature__icon' );
|
|
245
282
|
expect( screen.getByTestId( 'descriptor-icon' ) ).toBeInTheDocument();
|
|
246
|
-
expect( iconContainer ).toHaveClass( 'newspack-card-feature__icon--
|
|
283
|
+
expect( iconContainer ).toHaveClass( 'newspack-card-feature__icon--has-background' );
|
|
247
284
|
expect( iconContainer ).toHaveStyle( { backgroundColor: '#dfe7f4', color: '#003da5' } );
|
|
248
285
|
} );
|
|
249
286
|
|
|
@@ -252,17 +289,10 @@ describe( 'CardFeature', () => {
|
|
|
252
289
|
expect( container.querySelector( '.newspack-card-feature__icon' ) ).toHaveAttribute( 'aria-hidden', 'true' );
|
|
253
290
|
} );
|
|
254
291
|
|
|
255
|
-
it( '
|
|
256
|
-
const { container } = render( <CardFeature title="Content gifting" icon={ { node: <span />, backgroundColor: '#dfe7f4' } } /> );
|
|
257
|
-
const iconContainer = container.querySelector( '.newspack-card-feature__icon' );
|
|
258
|
-
expect( iconContainer ).toHaveClass( 'newspack-card-feature__icon--radius-small' );
|
|
259
|
-
expect( iconContainer ).not.toHaveClass( 'newspack-card-feature__icon--radius-full' );
|
|
260
|
-
} );
|
|
261
|
-
|
|
262
|
-
it( 'leaves an unbacked descriptor icon without a radius class', () => {
|
|
292
|
+
it( 'leaves an unbacked descriptor icon without the background class', () => {
|
|
263
293
|
const { container } = render( <CardFeature title="Content gifting" icon={ { node: <span />, fill: '#003da5' } } /> );
|
|
264
294
|
const iconContainer = container.querySelector( '.newspack-card-feature__icon' );
|
|
265
|
-
expect( iconContainer ).not.toHaveClass( 'newspack-card-feature__icon--
|
|
295
|
+
expect( iconContainer ).not.toHaveClass( 'newspack-card-feature__icon--has-background' );
|
|
266
296
|
} );
|
|
267
297
|
} );
|
|
268
298
|
|
|
@@ -7,7 +7,7 @@ import classnames from 'classnames';
|
|
|
7
7
|
* WordPress dependencies
|
|
8
8
|
*/
|
|
9
9
|
import { __, _x, sprintf } from '@wordpress/i18n';
|
|
10
|
-
import { createElement, isValidElement } from '@wordpress/element';
|
|
10
|
+
import { createElement, isValidElement, useCallback, useEffect, useRef } from '@wordpress/element';
|
|
11
11
|
import { useInstanceId } from '@wordpress/compose';
|
|
12
12
|
import { DropdownMenu } from '@wordpress/components';
|
|
13
13
|
import { moreVertical } from '@wordpress/icons';
|
|
@@ -25,14 +25,8 @@ type CardFeatureIcon = {
|
|
|
25
25
|
node: React.ReactNode;
|
|
26
26
|
/** SVG fill colour, applied via currentColor. */
|
|
27
27
|
fill?: string;
|
|
28
|
-
/** Background colour for the icon container. */
|
|
28
|
+
/** Background colour for the icon container, which is always a circle. */
|
|
29
29
|
backgroundColor?: string;
|
|
30
|
-
/**
|
|
31
|
-
* Border-radius of the icon container.
|
|
32
|
-
* 'small' uses $radius-small (2px), 'full' uses $radius-round (50%).
|
|
33
|
-
* Only relevant when backgroundColor is set, where it defaults to 'small'.
|
|
34
|
-
*/
|
|
35
|
-
radius?: 'small' | 'full';
|
|
36
30
|
};
|
|
37
31
|
|
|
38
32
|
type MoreControl = {
|
|
@@ -77,7 +71,7 @@ type CardFeatureProps = {
|
|
|
77
71
|
* where the requirement rather than the feature is what the button acts on.
|
|
78
72
|
*/
|
|
79
73
|
onEnable?: () => void;
|
|
80
|
-
/** Called when the primary button is clicked while it reads "Configure": enabled, with no unmet requirements. */
|
|
74
|
+
/** Called when the primary button is clicked while it reads "Configure": enabled, with no unmet requirements. Omit it for a feature with nothing to configure, and that state shows no primary button. */
|
|
81
75
|
onConfigure?: () => void;
|
|
82
76
|
/** Controls rendered inside the "More" dropdown, shown when enabled — including the unmet-requirements state when `requirementsActionable`. */
|
|
83
77
|
moreControls?: MoreControl[];
|
|
@@ -134,6 +128,29 @@ const CardFeature = ( {
|
|
|
134
128
|
title
|
|
135
129
|
);
|
|
136
130
|
const showMoreControls = enabled && !! moreControls?.length && ( ! requirements || requirementsActionable );
|
|
131
|
+
// A Configure button with nothing behind it would click to nowhere.
|
|
132
|
+
const showPrimaryButton = ! isConfigureState || !! onConfigure;
|
|
133
|
+
|
|
134
|
+
// Enabling a feature with nothing to configure removes the button that was
|
|
135
|
+
// just used, so its focus goes to the More menu instead of the page. React
|
|
136
|
+
// detaches a ref before removing the node, which is the last point the
|
|
137
|
+
// button can still be read as focused.
|
|
138
|
+
const actionsRef = useRef< HTMLDivElement >( null );
|
|
139
|
+
const primaryButtonNode = useRef< HTMLElement | null >( null );
|
|
140
|
+
const primaryButtonLeftWithFocus = useRef( false );
|
|
141
|
+
const primaryButtonRef = useCallback( ( node: HTMLElement | null ) => {
|
|
142
|
+
if ( ! node && primaryButtonNode.current ) {
|
|
143
|
+
primaryButtonLeftWithFocus.current = primaryButtonNode.current === primaryButtonNode.current.ownerDocument.activeElement;
|
|
144
|
+
}
|
|
145
|
+
primaryButtonNode.current = node;
|
|
146
|
+
}, [] );
|
|
147
|
+
useEffect( () => {
|
|
148
|
+
if ( showPrimaryButton || ! primaryButtonLeftWithFocus.current ) {
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
primaryButtonLeftWithFocus.current = false;
|
|
152
|
+
actionsRef.current?.querySelector< HTMLElement >( '.components-dropdown-menu__toggle' )?.focus();
|
|
153
|
+
}, [ showPrimaryButton ] );
|
|
137
154
|
|
|
138
155
|
const handleButtonClick = () => {
|
|
139
156
|
if ( isConfigureState ) {
|
|
@@ -146,8 +163,7 @@ const CardFeature = ( {
|
|
|
146
163
|
const iconDescriptor = icon && ! isValidElement( icon ) ? ( icon as CardFeatureIcon ) : null;
|
|
147
164
|
const iconClasses = iconDescriptor
|
|
148
165
|
? classnames( 'newspack-card-feature__icon', {
|
|
149
|
-
'newspack-card-feature__icon--
|
|
150
|
-
'newspack-card-feature__icon--radius-full': iconDescriptor.radius === 'full',
|
|
166
|
+
'newspack-card-feature__icon--has-background': !! iconDescriptor.backgroundColor,
|
|
151
167
|
} )
|
|
152
168
|
: undefined;
|
|
153
169
|
|
|
@@ -183,19 +199,22 @@ const CardFeature = ( {
|
|
|
183
199
|
</Card.Header>
|
|
184
200
|
<Card.Content className="newspack-card-feature__actions">
|
|
185
201
|
<Stack direction="row" align="center" justify="space-between" gap="sm" wrap="wrap">
|
|
186
|
-
<Stack direction="row" align="center" gap="sm">
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
202
|
+
<Stack ref={ actionsRef } direction="row" align="center" gap="sm">
|
|
203
|
+
{ showPrimaryButton && (
|
|
204
|
+
<Button
|
|
205
|
+
ref={ primaryButtonRef }
|
|
206
|
+
variant={ isConfigureState ? 'tertiary' : 'secondary' }
|
|
207
|
+
accessibleWhenDisabled
|
|
208
|
+
aria-describedby={ describedById }
|
|
209
|
+
aria-label={ buttonAccessibleLabel }
|
|
210
|
+
disabled={ ( isMuted && ! requirementsActionable ) || busy }
|
|
211
|
+
isBusy={ busy }
|
|
212
|
+
onClick={ handleButtonClick }
|
|
213
|
+
size="compact"
|
|
214
|
+
>
|
|
215
|
+
{ buttonLabel }
|
|
216
|
+
</Button>
|
|
217
|
+
) }
|
|
199
218
|
{ showMoreControls && (
|
|
200
219
|
<DropdownMenu
|
|
201
220
|
icon={ moreVertical }
|
|
@@ -6,17 +6,17 @@ import classnames from 'classnames';
|
|
|
6
6
|
/**
|
|
7
7
|
* Internal dependencies
|
|
8
8
|
*/
|
|
9
|
-
import {
|
|
9
|
+
import { providerIcons } from '../integration-icons';
|
|
10
10
|
import './style.scss';
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
|
-
* The single source for how
|
|
14
|
-
* background. A new provider needs an icon file,
|
|
13
|
+
* The single source for how a provider is shown: brand mark plus badge
|
|
14
|
+
* background. A new provider needs an icon file, a `providerIcons` entry,
|
|
15
15
|
* and a background rule in this component's stylesheet.
|
|
16
16
|
*/
|
|
17
17
|
const IntegrationIcon = ( { provider, className }: { provider: string; className?: string } ) => {
|
|
18
|
-
const icons: Record< string, React.ReactNode > =
|
|
19
|
-
const node = Object.prototype.hasOwnProperty.call(
|
|
18
|
+
const icons: Record< string, React.ReactNode > = providerIcons;
|
|
19
|
+
const node = Object.prototype.hasOwnProperty.call( providerIcons, provider ) ? icons[ provider ] : null;
|
|
20
20
|
if ( ! node ) {
|
|
21
21
|
return null;
|
|
22
22
|
}
|
|
@@ -21,14 +21,26 @@
|
|
|
21
21
|
background: #004cff;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
&--
|
|
25
|
-
background: #
|
|
24
|
+
&--beehiiv {
|
|
25
|
+
background: #060419;
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
&--constant-contact {
|
|
29
29
|
background: wp-colors.$gray-100;
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
&--fundraiseup {
|
|
33
|
+
background: #2bb77b;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
&--gravity-forms {
|
|
37
|
+
background: #ff4f00;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
&--mailchimp {
|
|
41
|
+
background: #ffe01b;
|
|
42
|
+
}
|
|
43
|
+
|
|
32
44
|
&--manual {
|
|
33
45
|
background: wp-colors.$gray-800;
|
|
34
46
|
color: wp-colors.$white;
|
|
@@ -39,4 +51,8 @@
|
|
|
39
51
|
fill: currentcolor;
|
|
40
52
|
}
|
|
41
53
|
}
|
|
54
|
+
|
|
55
|
+
&--salesforce {
|
|
56
|
+
background: #0d9dda;
|
|
57
|
+
}
|
|
42
58
|
}
|
|
@@ -1,11 +1,7 @@
|
|
|
1
1
|
const activeCampaign = (
|
|
2
2
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none">
|
|
3
3
|
<path
|
|
4
|
-
d="
|
|
5
|
-
fill="white"
|
|
6
|
-
/>
|
|
7
|
-
<path
|
|
8
|
-
d="M6.56445 8.77441C6.56445 8.35519 7.04853 8.12873 7.37109 8.35449L12.6611 11.9355L11.9512 12.4189C11.4996 12.7091 10.9193 12.7092 10.4678 12.4189L9.30664 11.6777L6.56445 9.83887V8.77441Z"
|
|
4
|
+
d="M17.28 10.4037C17.8244 10.7666 18.115 11.3468 18.115 11.9637C18.115 12.5805 17.788 13.1609 17.28 13.5238L5.88501 21V19.4399C5.88501 18.9681 6.10324 18.4965 6.5387 18.2062L15.9375 11.9637L6.5387 5.68506C6.13946 5.43097 5.88501 4.95947 5.88501 4.45129V3L17.28 10.4037ZM5.88501 8.37122C5.88501 7.89959 6.4296 7.64482 6.79248 7.8988L12.7438 11.9275L11.9451 12.4713C11.4371 12.7977 10.7842 12.7978 10.2762 12.4713L8.96997 11.6375L5.88501 9.56873V8.37122Z"
|
|
9
5
|
fill="white"
|
|
10
6
|
/>
|
|
11
7
|
</svg>
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
const beehiiv = (
|
|
2
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none">
|
|
3
|
+
<path
|
|
4
|
+
d="M18.6602 11.4894H5.35668C5.19293 11.4883 5.03627 11.4225 4.92088 11.3063C4.80549 11.1901 4.74073 11.033 4.74073 10.8692V9.92487C4.7398 9.67122 4.78886 9.41988 4.88509 9.1852C4.98132 8.95052 5.12284 8.73709 5.30157 8.55712C5.4803 8.37714 5.69274 8.23414 5.92675 8.13629C6.16076 8.03843 6.41176 7.98764 6.6654 7.9868H17.3353C17.5889 7.98754 17.8398 8.03822 18.0738 8.13595C18.3078 8.23368 18.5203 8.37654 18.6991 8.55637C18.8779 8.7362 19.0195 8.94949 19.1158 9.18406C19.2122 9.41862 19.2614 9.66987 19.2607 9.92346V10.8685C19.2638 10.9491 19.2506 11.0296 19.2218 11.1049C19.193 11.1803 19.1492 11.249 19.0931 11.307C19.0369 11.365 18.9697 11.411 18.8953 11.4422C18.8209 11.4734 18.7409 11.4899 18.6602 11.4894ZM15.8574 6.96492H8.15947C7.99191 6.96343 7.8317 6.89593 7.71361 6.77704C7.59552 6.65816 7.52908 6.4975 7.52872 6.32994C7.52872 4.74919 8.80643 3.46302 10.3773 3.46302H13.641C15.2119 3.46302 16.4896 4.74919 16.4896 6.32994C16.4907 6.41334 16.4752 6.49613 16.4439 6.57345C16.4126 6.65077 16.3662 6.72106 16.3074 6.7802C16.2486 6.83934 16.1785 6.88615 16.1014 6.91787C16.0243 6.94959 15.9416 6.96558 15.8582 6.96492H15.8574ZM19.2755 12.5113H4.74073C3.7858 12.5113 3 13.3013 3 14.2619C3 15.2225 3.7858 16.0132 4.74073 16.0132H9.89879C10.0834 15.0216 10.9453 14.2619 11.9919 14.2619C12.4923 14.2631 12.9767 14.4386 13.3618 14.7582C13.7468 15.0778 14.0085 15.5216 14.1019 16.0132H19.2607C20.2149 16.0132 21 15.2225 21 14.2619C21 13.3013 20.2304 12.5113 19.2755 12.5113ZM18.6454 17.0358H14.1336V20.537H17.5214C18.4756 20.537 19.2607 19.7477 19.2607 18.7864V17.6708C19.2636 17.5882 19.2499 17.506 19.2202 17.4289C19.1906 17.3518 19.1457 17.2816 19.0883 17.2223C19.0308 17.163 18.962 17.1159 18.8859 17.0838C18.8098 17.0518 18.728 17.0354 18.6454 17.0358ZM5.35668 17.0358C5.189 17.0371 5.02861 17.1045 4.91037 17.2234C4.79213 17.3423 4.72559 17.5031 4.72523 17.6708V18.7864C4.72523 19.747 5.51102 20.537 6.46596 20.537H9.85298V17.0358H5.35668Z"
|
|
5
|
+
fill="white"
|
|
6
|
+
/>
|
|
7
|
+
</svg>
|
|
8
|
+
);
|
|
9
|
+
|
|
10
|
+
export default beehiiv;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
const gravityForms = (
|
|
2
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none">
|
|
3
|
+
<path
|
|
4
|
+
d="M8.74072 8.98989H24V4.73383H8.77618C6.75022 4.67092 4.79489 5.46964 3.41 6.92583C0.0886559 10.4377 0 19.2696 0 19.2696H23.8818V11.2749H19.5912V15.0019H4.58018C4.67474 13.6123 5.31889 11.1296 6.53633 9.84462C7.10785 9.25496 7.91399 8.9424 8.74072 8.98989Z"
|
|
5
|
+
fill="white"
|
|
6
|
+
/>
|
|
7
|
+
</svg>
|
|
8
|
+
);
|
|
9
|
+
|
|
10
|
+
export default gravityForms;
|
|
@@ -7,23 +7,34 @@ import { atSymbol } from '@wordpress/icons';
|
|
|
7
7
|
* Internal dependencies
|
|
8
8
|
*/
|
|
9
9
|
import activeCampaign from './active-campaign';
|
|
10
|
+
import beehiiv from './beehiiv';
|
|
10
11
|
import constantContact from './constant-contact';
|
|
12
|
+
import fundraiseUp from './fundraise-up';
|
|
13
|
+
import gravityForms from './gravity-forms';
|
|
11
14
|
import mailchimp from './mailchimp';
|
|
15
|
+
import salesforce from './salesforce';
|
|
12
16
|
|
|
13
17
|
export { default as activeCampaign } from './active-campaign';
|
|
18
|
+
export { default as beehiiv } from './beehiiv';
|
|
14
19
|
export { default as constantContact } from './constant-contact';
|
|
15
20
|
export { default as fundraiseUp } from './fundraise-up';
|
|
21
|
+
export { default as gravityForms } from './gravity-forms';
|
|
16
22
|
export { default as mailchimp } from './mailchimp';
|
|
17
23
|
export { default as salesforce } from './salesforce';
|
|
18
24
|
export { default as wisepops } from './wisepops';
|
|
19
25
|
|
|
20
|
-
//
|
|
21
|
-
// (`Newspack_Newsletters::service_provider()`)
|
|
22
|
-
|
|
26
|
+
// Brand marks keyed by the ESP slug the backend reports
|
|
27
|
+
// (`Newspack_Newsletters::service_provider()`), by integration ID, or by the
|
|
28
|
+
// brand slug an integration maps to. Rendered via IntegrationIcon.
|
|
29
|
+
export const providerIcons = {
|
|
23
30
|
active_campaign: activeCampaign,
|
|
24
31
|
mailchimp,
|
|
25
32
|
constant_contact: constantContact,
|
|
26
33
|
manual: atSymbol,
|
|
34
|
+
salesforce,
|
|
35
|
+
beehiiv,
|
|
36
|
+
fundraiseup: fundraiseUp,
|
|
37
|
+
gravity_forms: gravityForms,
|
|
27
38
|
};
|
|
28
39
|
|
|
29
40
|
export const espProviderOrder = [ 'active_campaign', 'mailchimp', 'constant_contact', 'manual' ];
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
const salesforce = (
|
|
2
|
-
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0
|
|
2
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none">
|
|
3
3
|
<path
|
|
4
|
-
d="
|
|
5
|
-
fill="
|
|
4
|
+
d="M10.0838 5.745C10.8213 4.9775 11.8463 4.5 12.9813 4.5C14.4913 4.5 15.8063 5.3425 16.5088 6.59C17.1188 6.3175 17.7938 6.165 18.5013 6.165C21.2238 6.165 23.4288 8.39 23.4288 11.1375C23.4288 13.885 21.2213 16.11 18.5013 16.11C18.1688 16.11 17.8438 16.0775 17.5313 16.0125C16.9138 17.1125 15.7363 17.8575 14.3863 17.8575C13.8213 17.8575 13.2863 17.7275 12.8113 17.495C12.1863 18.9675 10.7263 20 9.02876 20C7.33126 20 5.75126 18.88 5.17126 17.31C4.91876 17.3625 4.65626 17.3925 4.38626 17.3925C2.27876 17.3925 0.571259 15.6675 0.571259 13.5375C0.571259 12.11 1.33876 10.865 2.47876 10.1975C2.24376 9.6575 2.11376 9.06 2.11376 8.435C2.11376 5.985 4.10126 4 6.55376 4C7.99376 4 9.27376 4.685 10.0838 5.745Z"
|
|
5
|
+
fill="white"
|
|
6
6
|
/>
|
|
7
7
|
</svg>
|
|
8
8
|
);
|
|
@@ -11,12 +11,20 @@ import { matchesRoute } from '../route-match';
|
|
|
11
11
|
* @param {string} pathname Current router pathname.
|
|
12
12
|
* @return {Array} Breadcrumb items `{ label, url? }`.
|
|
13
13
|
*/
|
|
14
|
-
export const activeBreadcrumbs = ( sections = [], pathname ) =>
|
|
14
|
+
export const activeBreadcrumbs = ( sections = [], pathname ) => activeSection( sections, pathname )?.breadcrumbs || [];
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Select the section matching the current route, falling back to the first.
|
|
18
|
+
*
|
|
19
|
+
* @param {Array} sections Wizard sections.
|
|
20
|
+
* @param {string} pathname Current router pathname.
|
|
21
|
+
* @return {Object|undefined} The active section, if any.
|
|
22
|
+
*/
|
|
23
|
+
export const activeSection = ( sections = [], pathname ) => {
|
|
15
24
|
if ( ! sections?.length ) {
|
|
16
|
-
return
|
|
25
|
+
return undefined;
|
|
17
26
|
}
|
|
18
|
-
|
|
19
|
-
return match.breadcrumbs || [];
|
|
27
|
+
return sections.find( section => matchesRoute( section, pathname ) ) || sections[ 0 ];
|
|
20
28
|
};
|
|
21
29
|
|
|
22
30
|
/**
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Internal dependencies.
|
|
3
3
|
*/
|
|
4
|
-
import { activeBreadcrumbs, appendSectionName } from './breadcrumbs-select';
|
|
4
|
+
import { activeBreadcrumbs, activeSection, appendSectionName } from './breadcrumbs-select';
|
|
5
5
|
|
|
6
6
|
const SECTIONS = [
|
|
7
7
|
{ path: '/', breadcrumbs: [ { label: 'Audience Management' }, { label: 'Configuration' } ] },
|
|
@@ -122,3 +122,17 @@ describe( 'appendSectionName', () => {
|
|
|
122
122
|
expect( trail ).toEqual( [ { label: 'Access Control' } ] );
|
|
123
123
|
} );
|
|
124
124
|
} );
|
|
125
|
+
|
|
126
|
+
describe( 'activeSection', () => {
|
|
127
|
+
const sections = [ { path: '/settings', exact: true, subHeaderText: 'Intro' }, { path: '/settings/:integrationId' } ];
|
|
128
|
+
|
|
129
|
+
it( 'returns the section matching the route', () => {
|
|
130
|
+
expect( activeSection( sections, '/settings/esp' ) ).toBe( sections[ 1 ] );
|
|
131
|
+
expect( activeSection( sections, '/settings' ).subHeaderText ).toBe( 'Intro' );
|
|
132
|
+
} );
|
|
133
|
+
|
|
134
|
+
it( 'falls back to the first section, or nothing without sections', () => {
|
|
135
|
+
expect( activeSection( sections, '/unknown' ) ).toBe( sections[ 0 ] );
|
|
136
|
+
expect( activeSection( [], '/settings' ) ).toBeUndefined();
|
|
137
|
+
} );
|
|
138
|
+
} );
|
package/src/wizard/index.js
CHANGED
|
@@ -25,7 +25,7 @@ import { category, chevronLeft, moreVertical } from '@wordpress/icons';
|
|
|
25
25
|
* Internal dependencies
|
|
26
26
|
*/
|
|
27
27
|
import { Footer, DebugBadge, Button, TabbedNavigation, PluginInstaller, SectionHeader, HandoffMessage, Page, Waiting } from '../';
|
|
28
|
-
import { activeBreadcrumbs, appendSectionName } from './breadcrumbs-select';
|
|
28
|
+
import { activeBreadcrumbs, activeSection, appendSectionName } from './breadcrumbs-select';
|
|
29
29
|
import Router from '../proxied-imports/router';
|
|
30
30
|
import registerStore, { WIZARD_STORE_NAMESPACE } from './store';
|
|
31
31
|
import WizardSnackbar from './components/WizardSnackbar';
|
|
@@ -54,7 +54,7 @@ const resolveIcon = icon => {
|
|
|
54
54
|
return icon;
|
|
55
55
|
};
|
|
56
56
|
|
|
57
|
-
const { HashRouter, Redirect, Route, Switch, useLocation } = Router;
|
|
57
|
+
const { HashRouter, Redirect, Route, Switch, matchPath, useLocation } = Router;
|
|
58
58
|
|
|
59
59
|
/**
|
|
60
60
|
* Interpolate a translated message's named tags, falling back to plain text.
|
|
@@ -105,9 +105,35 @@ const ResetHeaderData = () => {
|
|
|
105
105
|
* Wizard header + content region. Rendered inside the wizard's HashRouter so it
|
|
106
106
|
* can read the current route and derive the active-tab breadcrumb.
|
|
107
107
|
*/
|
|
108
|
-
const WizardHeaderRegion = ( {
|
|
108
|
+
const WizardHeaderRegion = ( {
|
|
109
|
+
hideHeader,
|
|
110
|
+
headerText,
|
|
111
|
+
sections,
|
|
112
|
+
sectionName,
|
|
113
|
+
subTitle,
|
|
114
|
+
actions,
|
|
115
|
+
tabbedNavigation: wizardTabbedNavigation,
|
|
116
|
+
children,
|
|
117
|
+
} ) => {
|
|
109
118
|
const { pathname } = useLocation();
|
|
110
119
|
|
|
120
|
+
// A section can carry its own tabs, built from its route params, in place of
|
|
121
|
+
// the wizard's. The first match wins, as it does in the wizard's `<Switch>`.
|
|
122
|
+
let tabbedNavigation = wizardTabbedNavigation;
|
|
123
|
+
for ( const section of sections ) {
|
|
124
|
+
const match = matchPath( pathname, { path: section.path, exact: section.exact ?? false } );
|
|
125
|
+
if ( match ) {
|
|
126
|
+
if ( typeof section.tabbedNavigation === 'function' ) {
|
|
127
|
+
tabbedNavigation = (
|
|
128
|
+
<TabbedNavigation items={ section.tabbedNavigation( match.params ) }>
|
|
129
|
+
<WizardError />
|
|
130
|
+
</TabbedNavigation>
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
break;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
111
137
|
if ( hideHeader ) {
|
|
112
138
|
// Without the Page shell the tabs still own the content: it renders
|
|
113
139
|
// inside the active tab's panel.
|
|
@@ -125,8 +151,10 @@ const WizardHeaderRegion = ( { hideHeader, headerText, sections, sectionName, su
|
|
|
125
151
|
// headerData.sectionName (deduped against the current trailing label).
|
|
126
152
|
breadcrumbItems = appendSectionName( breadcrumbItems, sectionName );
|
|
127
153
|
|
|
154
|
+
const sectionSubTitle = activeSection( sections, pathname )?.subHeaderText;
|
|
155
|
+
|
|
128
156
|
return (
|
|
129
|
-
<Page breadcrumbItems={ breadcrumbItems } subTitle={ subTitle } actions={ actions } tabbedNavigation={ tabbedNavigation }>
|
|
157
|
+
<Page breadcrumbItems={ breadcrumbItems } subTitle={ sectionSubTitle ?? subTitle } actions={ actions } tabbedNavigation={ tabbedNavigation }>
|
|
130
158
|
{ children }
|
|
131
159
|
</Page>
|
|
132
160
|
);
|
|
@@ -138,7 +166,8 @@ const WizardHeaderRegion = ( { hideHeader, headerText, sections, sectionName, su
|
|
|
138
166
|
* @property {string} [subHeaderText] The sub-header text, optional.
|
|
139
167
|
* @property {string} [apiSlug] The API slug, optional.
|
|
140
168
|
* @property {string} [className] CSS classes, optional.
|
|
141
|
-
* @property {any[]} sections Array of sections.
|
|
169
|
+
* @property {any[]} sections Array of sections. A section's own `subHeaderText` replaces the wizard's while it is active.
|
|
170
|
+
* Its optional `tabbedNavigation( params )` returns the tab items shown while its route matches.
|
|
142
171
|
* @property {boolean} [hasSimpleFooter] Indicates if a simple footer is used, optional.
|
|
143
172
|
* @property {() => void} [renderAboveSections] Function to render content above sections, optional.
|
|
144
173
|
* @property {string[]} [requiredPlugins] Array of required plugin strings, optional.
|
package/src/wizard/index.test.js
CHANGED
|
@@ -247,3 +247,76 @@ describe( 'Wizard content width', () => {
|
|
|
247
247
|
expect( select( WIZARD_STORE_NAMESPACE ).getHeaderData().fullWidth ).toBe( false );
|
|
248
248
|
} );
|
|
249
249
|
} );
|
|
250
|
+
|
|
251
|
+
describe( 'Wizard section tabs', () => {
|
|
252
|
+
const sections = [
|
|
253
|
+
{ label: 'List', path: '/', exact: true, render: () => <div>List content</div> },
|
|
254
|
+
{
|
|
255
|
+
path: '/items/:itemId/logs/:tab?',
|
|
256
|
+
isHidden: true,
|
|
257
|
+
render: () => <div>Logs content</div>,
|
|
258
|
+
tabbedNavigation: ( { itemId } ) => [
|
|
259
|
+
{ label: 'First Tab', path: `/items/${ itemId }/logs`, exact: true },
|
|
260
|
+
{ label: 'Second Tab', path: `/items/${ itemId }/logs/second`, exact: true },
|
|
261
|
+
],
|
|
262
|
+
},
|
|
263
|
+
];
|
|
264
|
+
|
|
265
|
+
beforeEach( () => {
|
|
266
|
+
apiFetch.mockReset();
|
|
267
|
+
} );
|
|
268
|
+
|
|
269
|
+
afterEach( () => {
|
|
270
|
+
window.location.hash = '';
|
|
271
|
+
} );
|
|
272
|
+
|
|
273
|
+
it( 'shows the tabs a section builds from its route params while it matches', async () => {
|
|
274
|
+
window.location.hash = '#/items/42/logs/second';
|
|
275
|
+
render( <Wizard headerText="Test wizard" sections={ sections } /> );
|
|
276
|
+
|
|
277
|
+
expect( await screen.findByText( 'Logs content' ) ).toBeInTheDocument();
|
|
278
|
+
expect( screen.getByRole( 'tab', { name: 'First Tab' } ) ).toHaveAttribute( 'href', '#/items/42/logs' );
|
|
279
|
+
expect( screen.getByRole( 'tab', { name: 'Second Tab' } ) ).toHaveAttribute( 'aria-selected', 'true' );
|
|
280
|
+
} );
|
|
281
|
+
|
|
282
|
+
it( 'leaves the tabs out on a route the section does not match', async () => {
|
|
283
|
+
window.location.hash = '#/';
|
|
284
|
+
render( <Wizard headerText="Test wizard" sections={ sections } /> );
|
|
285
|
+
|
|
286
|
+
expect( await screen.findByText( 'List content' ) ).toBeInTheDocument();
|
|
287
|
+
expect( screen.queryByRole( 'tab' ) ).toBeNull();
|
|
288
|
+
} );
|
|
289
|
+
} );
|
|
290
|
+
|
|
291
|
+
describe( 'Wizard section subtitle', () => {
|
|
292
|
+
beforeEach( () => {
|
|
293
|
+
apiFetch.mockReset();
|
|
294
|
+
apiFetch.mockResolvedValue( {} );
|
|
295
|
+
} );
|
|
296
|
+
|
|
297
|
+
it( "shows the active section's subHeaderText in the header, falling back to the wizard's", async () => {
|
|
298
|
+
window.location.hash = '#/list';
|
|
299
|
+
const { container } = render(
|
|
300
|
+
<Wizard
|
|
301
|
+
headerText="Test wizard"
|
|
302
|
+
subHeaderText="Wizard intro"
|
|
303
|
+
sections={ [
|
|
304
|
+
{ label: 'List', path: '/list', exact: true, subHeaderText: 'List intro', render: () => <div>List view</div> },
|
|
305
|
+
{ label: 'Item', path: '/list/:id', isHidden: true, render: () => <div>Item view</div> },
|
|
306
|
+
] }
|
|
307
|
+
/>
|
|
308
|
+
);
|
|
309
|
+
const subtitle = () => container.querySelector( '.newspack-page__header-subtitle' )?.textContent;
|
|
310
|
+
|
|
311
|
+
expect( await screen.findByText( 'List view' ) ).toBeInTheDocument();
|
|
312
|
+
expect( subtitle() ).toBe( 'List intro' );
|
|
313
|
+
|
|
314
|
+
act( () => {
|
|
315
|
+
window.location.hash = '#/list/esp';
|
|
316
|
+
window.dispatchEvent( new HashChangeEvent( 'hashchange' ) );
|
|
317
|
+
} );
|
|
318
|
+
|
|
319
|
+
expect( await screen.findByText( 'Item view' ) ).toBeInTheDocument();
|
|
320
|
+
expect( subtitle() ).toBe( 'Wizard intro' );
|
|
321
|
+
} );
|
|
322
|
+
} );
|