newspack-components 4.8.0 → 4.9.0-alpha.1

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 (103) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/DEVELOPMENT.md +18 -17
  3. package/dist/cjs/action-card/index.js +85 -18
  4. package/dist/cjs/action-card/index.test.js +192 -5
  5. package/dist/cjs/action-card/style.scss +3 -9
  6. package/dist/cjs/card-feature/index.js +28 -3
  7. package/dist/cjs/card-feature/index.test.js +100 -45
  8. package/dist/cjs/card-feature/style.scss +1 -5
  9. package/dist/cjs/debug-badge/index.js +44 -0
  10. package/dist/cjs/debug-badge/index.test.js +50 -0
  11. package/dist/cjs/debug-badge/style.scss +26 -0
  12. package/dist/cjs/empty-state/index.test.js +37 -24
  13. package/dist/cjs/empty-state/root.js +2 -1
  14. package/dist/cjs/footer/index.js +52 -16
  15. package/dist/cjs/footer/index.test.js +208 -0
  16. package/dist/cjs/footer/style.scss +17 -1
  17. package/dist/cjs/index.js +7 -0
  18. package/dist/cjs/integration-icon/index.js +4 -4
  19. package/dist/cjs/integration-icon/style.scss +18 -2
  20. package/dist/cjs/integration-icons/active-campaign.js +4 -7
  21. package/dist/cjs/integration-icons/beehiiv.js +19 -0
  22. package/dist/cjs/integration-icons/gravity-forms.js +19 -0
  23. package/dist/cjs/integration-icons/index.js +26 -6
  24. package/dist/cjs/integration-icons/salesforce.js +5 -3
  25. package/dist/cjs/notice/index.js +1 -4
  26. package/dist/cjs/notice/style.scss +0 -19
  27. package/dist/cjs/plugin-toggle/index.js +1 -1
  28. package/dist/cjs/with-wizard-screen/index.js +1 -3
  29. package/dist/cjs/with-wizard-screen/index.test.js +0 -3
  30. package/dist/cjs/wizard/breadcrumbs-select.js +17 -4
  31. package/dist/cjs/wizard/breadcrumbs-select.test.js +17 -0
  32. package/dist/cjs/wizard/index.js +50 -13
  33. package/dist/cjs/wizard/index.test.js +304 -5
  34. package/dist/cjs/wizard/store/index.js +2 -0
  35. package/dist/esm/action-card/index.js +86 -21
  36. package/dist/esm/action-card/index.test.js +192 -1
  37. package/dist/esm/action-card/style.scss +3 -9
  38. package/dist/esm/card-feature/index.js +29 -4
  39. package/dist/esm/card-feature/index.test.js +100 -45
  40. package/dist/esm/card-feature/style.scss +1 -5
  41. package/dist/esm/debug-badge/index.js +39 -0
  42. package/dist/esm/debug-badge/index.test.js +46 -0
  43. package/dist/esm/debug-badge/style.scss +26 -0
  44. package/dist/esm/empty-state/index.test.js +37 -24
  45. package/dist/esm/empty-state/root.js +2 -1
  46. package/dist/esm/footer/index.js +52 -17
  47. package/dist/esm/footer/index.test.js +210 -0
  48. package/dist/esm/footer/style.scss +17 -1
  49. package/dist/esm/index.js +1 -0
  50. package/dist/esm/integration-icon/index.js +5 -5
  51. package/dist/esm/integration-icon/style.scss +18 -2
  52. package/dist/esm/integration-icons/active-campaign.js +5 -8
  53. package/dist/esm/integration-icons/beehiiv.js +13 -0
  54. package/dist/esm/integration-icons/gravity-forms.js +13 -0
  55. package/dist/esm/integration-icons/index.js +15 -4
  56. package/dist/esm/integration-icons/salesforce.js +5 -3
  57. package/dist/esm/notice/index.js +2 -5
  58. package/dist/esm/notice/style.scss +0 -19
  59. package/dist/esm/plugin-toggle/index.js +1 -1
  60. package/dist/esm/with-wizard-screen/index.js +2 -4
  61. package/dist/esm/with-wizard-screen/index.test.js +0 -3
  62. package/dist/esm/wizard/breadcrumbs-select.js +16 -3
  63. package/dist/esm/wizard/breadcrumbs-select.test.js +18 -1
  64. package/dist/esm/wizard/index.js +53 -16
  65. package/dist/esm/wizard/index.test.js +304 -5
  66. package/dist/esm/wizard/store/index.js +2 -0
  67. package/package.json +1 -1
  68. package/src/action-card/action-card.d.ts +3 -2
  69. package/src/action-card/index.js +81 -8
  70. package/src/action-card/index.test.js +159 -0
  71. package/src/action-card/style.scss +3 -9
  72. package/src/card-feature/README.md +10 -11
  73. package/src/card-feature/index.test.js +45 -15
  74. package/src/card-feature/index.tsx +43 -24
  75. package/src/card-feature/style.scss +1 -5
  76. package/src/debug-badge/index.js +36 -0
  77. package/src/debug-badge/index.test.js +39 -0
  78. package/src/debug-badge/style.scss +26 -0
  79. package/src/empty-state/README.md +11 -10
  80. package/src/empty-state/index.test.js +10 -0
  81. package/src/empty-state/root.tsx +2 -1
  82. package/src/empty-state/types.ts +1 -1
  83. package/src/footer/index.js +47 -2
  84. package/src/footer/index.test.js +114 -0
  85. package/src/footer/style.scss +17 -1
  86. package/src/index.js +1 -0
  87. package/src/integration-icon/index.tsx +5 -5
  88. package/src/integration-icon/style.scss +18 -2
  89. package/src/integration-icons/active-campaign.js +1 -5
  90. package/src/integration-icons/beehiiv.js +10 -0
  91. package/src/integration-icons/gravity-forms.js +10 -0
  92. package/src/integration-icons/index.js +14 -3
  93. package/src/integration-icons/salesforce.js +3 -3
  94. package/src/notice/index.js +2 -17
  95. package/src/notice/style.scss +0 -19
  96. package/src/plugin-toggle/index.js +1 -1
  97. package/src/with-wizard-screen/index.js +2 -2
  98. package/src/with-wizard-screen/index.test.js +0 -2
  99. package/src/wizard/breadcrumbs-select.js +12 -4
  100. package/src/wizard/breadcrumbs-select.test.js +15 -1
  101. package/src/wizard/index.js +45 -14
  102. package/src/wizard/index.test.js +169 -3
  103. package/src/wizard/store/index.js +2 -0
package/CHANGELOG.md CHANGED
@@ -1,3 +1,13 @@
1
+ # newspack-components [4.9.0-alpha.1](https://github.com/Automattic/newspack-workspace/compare/newspack-components@4.8.1-alpha.1...newspack-components@4.9.0-alpha.1) (2026-09-28)
2
+
3
+
4
+ ### Features
5
+
6
+ * **audience:** refresh the Integrations brand marks ([#1155](https://github.com/Automattic/newspack-workspace/issues/1155)) ([af2729c](https://github.com/Automattic/newspack-workspace/commit/af2729cc6a5dd69cebe18456762eb21c55ae16a3))
7
+ * **audience:** refresh the Integrations screen icons, order and copy ([#1151](https://github.com/Automattic/newspack-workspace/issues/1151)) ([1d3f5a6](https://github.com/Automattic/newspack-workspace/commit/1d3f5a6c074f18491b3047c0e679e6c4f7b1f3cd))
8
+ * **reader-activation:** split Gravity Forms into its own integration ([#1170](https://github.com/Automattic/newspack-workspace/issues/1170)) ([7bd756b](https://github.com/Automattic/newspack-workspace/commit/7bd756bc30b12856091c126c6d054d45e6b78244))
9
+ * **sync:** read the push log in a sync activity tab ([#1114](https://github.com/Automattic/newspack-workspace/issues/1114)) ([c1a2d2d](https://github.com/Automattic/newspack-workspace/commit/c1a2d2d299b8a0150ce1c4de37e14edf437176b3))
10
+
1
11
  # newspack-components [4.8.0](https://github.com/Automattic/newspack-workspace/compare/newspack-components@4.7.0...newspack-components@4.8.0) (2026-09-14)
2
12
 
3
13
 
package/DEVELOPMENT.md CHANGED
@@ -100,7 +100,7 @@ When building a screen, use the **spacing scale** (8px unit: 16, 24, 32, 48, 64)
100
100
 
101
101
  - **`ActionCard`** – Use when one concept (e.g. a feature or setting) can be toggled on/off and may have extra content below. Internal padding (24px default; 16px/8px for isMedium/isSmall) and 24px between regions keep hierarchy clear; expandable content uses 24px top padding and 24px between siblings.
102
102
  - **`CollapsibleGroup`** / **`CollapsibleGroup.Item`** – A stack of independently collapsible items, separated by dividers and flush with the surrounding column. Not a W3C accordion: the items do not coordinate.
103
- - **`Notice`** – Use for outcome feedback (success/error/warning) or short contextual messages. Vertical margin is 32px so notices don’t collide with cards; keep one primary message per area when possible.
103
+ - **`Notice`** – Being retired in favour of core’s `Notice` from `@wordpress/components`, which announces itself to assistive technology. Reach for core’s in new work; ours stays for the screens still on it. Where ours is used, vertical margin is 32px so notices don’t collide with cards, and one primary message per area reads best.
104
104
  - **`ProgressBar`** – Progress indicator.
105
105
  - **`StepsList`** / **`StepsListItem`** – Step-by-step list components.
106
106
  - **`StyleCard`** – Style preview card.
@@ -131,6 +131,7 @@ When building a screen, use the **spacing scale** (8px unit: 16, 24, 32, 48, 64)
131
131
 
132
132
  ### Utility Components
133
133
 
134
+ - **`DebugBadge`** - Floating indicator that Newspack debug mode is on. Takes no props and renders nothing unless the site defines `WP_NEWSPACK_DEBUG`
134
135
  - **`GlobalNotices`** - Global notice system component
135
136
  - **`InfoButton`** - Reveals supplementary context from a `description` prop. Anything a reader needs in order to use a control belongs in visible help text instead
136
137
  - **`Modal`** - Modal dialog component
@@ -176,7 +177,7 @@ import {
176
177
  ActionCard,
177
178
  Button,
178
179
  Card,
179
- Notice,
180
+ SectionHeader,
180
181
  TextControl,
181
182
  } from '../../../../../packages/components/src';
182
183
  ```
@@ -187,21 +188,21 @@ import {
187
188
 
188
189
  ```jsx
189
190
  // ✅ CORRECT - Within newspack-plugin monorepo
190
- import { Button, Card, Notice } from '../../../../../packages/components/src';
191
+ import { Button, Card, SectionHeader } from '../../../../../packages/components/src';
191
192
  ```
192
193
 
193
194
  **As an npm package:** If `newspack-components` is installed as a dependency in another plugin, import from the package name:
194
195
 
195
196
  ```jsx
196
197
  // ✅ CORRECT - When installed as npm package
197
- import { Button, Card, Notice } from 'newspack-components';
198
+ import { Button, Card, SectionHeader } from 'newspack-components';
198
199
  ```
199
200
 
200
201
  **Import individual components** – Import only what you need; do not import the whole namespace. List named imports **alphabetically** (e.g. from `@wordpress/components` or `newspack-components`):
201
202
 
202
203
  ```jsx
203
204
  // ✅ CORRECT – alphabetical
204
- import { Button, Card, Notice } from '../../../../../packages/components/src';
205
+ import { Button, Card, SectionHeader } from '../../../../../packages/components/src';
205
206
 
206
207
  // ❌ WRONG – Don't import all
207
208
  import * as Components from '../../../../../packages/components/src';
@@ -253,7 +254,7 @@ This section shows how to use components **by context** (backend, blocks, fronte
253
254
  /**
254
255
  * WordPress dependencies
255
256
  */
256
- import { CheckboxControl, ExternalLink, RangeControl } from '@wordpress/components';
257
+ import { CheckboxControl, ExternalLink, Notice, RangeControl } from '@wordpress/components';
257
258
 
258
259
  /**
259
260
  * Internal dependencies
@@ -262,9 +263,8 @@ import {
262
263
  ActionCard,
263
264
  Button,
264
265
  Card,
265
- Grid,
266
- Notice,
267
266
  Divider,
267
+ Grid,
268
268
  SectionHeader,
269
269
  TextControl,
270
270
  Waiting,
@@ -273,14 +273,13 @@ import {
273
273
 
274
274
  **Example – Audience Setup Wizard (real reference):**
275
275
  ```jsx
276
- import { CheckboxControl, ExternalLink, RangeControl } from '@wordpress/components';
276
+ import { CheckboxControl, ExternalLink, Notice, RangeControl } from '@wordpress/components';
277
277
  import {
278
278
  ActionCard,
279
279
  Button,
280
280
  Card,
281
- Grid,
282
- Notice,
283
281
  Divider,
282
+ Grid,
284
283
  PluginInstaller,
285
284
  SectionHeader,
286
285
  TextControl,
@@ -291,7 +290,9 @@ import {
291
290
  export default withWizardScreen( ( { config, updateConfig } ) => {
292
291
  return (
293
292
  <>
294
- <Notice noticeText={ __( 'Audience Management is enabled.', 'newspack-plugin' ) } isSuccess />
293
+ <Notice status="success" isDismissible={ false }>
294
+ { __( 'Audience Management is enabled.', 'newspack-plugin' ) }
295
+ </Notice>
295
296
  <Card noBorder>
296
297
  <ActionCard
297
298
  title={ __( 'Present newsletter signup after checkout', 'newspack-plugin' ) }
@@ -374,7 +375,7 @@ import { Fragment } from '@wordpress/element';
374
375
  /**
375
376
  * Internal dependencies
376
377
  */
377
- import { GlobalNotices, Notice, Wizard } from '../../../../../packages/components/src';
378
+ import { GlobalNotices, Wizard } from '../../../../../packages/components/src';
378
379
  import sections from './sections';
379
380
 
380
381
  function Dashboard() {
@@ -541,7 +542,7 @@ When Newspack components don't provide what you need, use these WordPress compon
541
542
  ### Feedback and overlays
542
543
 
543
544
  - **`Spinner`** – Loading spinner
544
- - **`Notice`** – Inline notice (success/error/warning); prefer Newspack `Notice` in wizards when it fits
545
+ - **`Notice`** – Inline notice (success/error/warning); prefer this over the Newspack `Notice`, which is being retired
545
546
  - **`Placeholder`** – Empty state in blocks
546
547
  - **`Modal`** – Modal dialog
547
548
  - **`Popover`** – Popover (e.g. webhooks endpoint actions, corrections modal)
@@ -622,7 +623,7 @@ Spacing is based on an **8px unit**. Use these values so new styles match existi
622
623
  | **8px** | Tight gaps, badge padding, small padding (e.g. ActionCard is-small) | Inline or dense UI |
623
624
  | **16px** | Gaps between related controls, buttons card gap, margins inside ActionCard region-children for Card/Grid/TextControl | Related items, form rows |
624
625
  | **24px** | Default ActionCard region padding, toggle/region gaps, expandable content padding and sibling spacing | Default internal padding and gaps within a card |
625
- | **32px** | Card vertical margin, Grid default gap and margin, SectionHeader first-child top, Notice margin, Divider margin | Section rhythm, between blocks |
626
+ | **32px** | Card vertical margin, Grid default gap and margin, SectionHeader first-child top, Newspack Notice margin, Divider margin | Section rhythm, between blocks |
626
627
  | **48px** | SectionHeader container margin-top, Card horizontal padding (small screens) | Section separation |
627
628
  | **64px** | SectionHeader top margin, Divider margins (large breakpoint), buttons card margin, Card horizontal padding (large screens) | Major section separation |
628
629
 
@@ -693,7 +694,7 @@ Components rely on WordPress design system states where applicable; a few Newspa
693
694
  - **ActionCard (clickable)** – Hover: `box-shadow: 0 4px 8px rgba(black, 0.08)`; transition 125ms ease-in-out. Use for cards that navigate or open.
694
695
  - **Button** – Primary, secondary, disabled, and link variants follow `@wordpress/components` Button; focus and hover come from WordPress base styles.
695
696
  - **Toggle (inside ActionCard)** – Checked/unchecked and focus states from WordPress ToggleControl; label is visually hidden but available for accessibility.
696
- - **Notice** – Success (green), error (red), warning (yellow), info (gray) variants; use for feedback only and one primary message per area when possible.
697
+ - **Notice** – Core's `Notice` carries the status in a tinted, bordered box (info, success, warning, error) and announces itself. The Newspack one, still in place on the screens that have not moved, tints the background and adds an icon. Either way, use notices for feedback only, and one primary message per area when possible.
697
698
 
698
699
  When adding new interactive components, preserve focus visibility and use the same state patterns (hover shadow, transition) so the UI feels consistent.
699
700
 
@@ -724,7 +725,7 @@ When adding new interactive components, preserve focus visibility and use the sa
724
725
  - **[@wordpress/components](https://www.npmjs.com/package/@wordpress/components)** – WordPress component library (fallback when Newspack components don’t exist)
725
726
  - **[@wordpress/base-styles](https://www.npmjs.com/package/@wordpress/base-styles)** – WordPress Design System styles and colors
726
727
 
727
- **Design resources:** For layout, components, and spacing, refer to the [WordPress Figma Library](https://www.figma.com/community/file/1149596986784498103/wordpress-design-library) and the [Block Editor Handbook](https://developer.wordpress.org/block-editor/reference-guides/components/). The **Components Demo** (`/wp-admin/admin.php?page=newspack-components-demo`) is the live reference for how Newspack components look and behave; use it to confirm spacing and hierarchy when building new screens.
728
+ **Design resources:** For layout, components, and spacing, refer to the [WordPress Figma Library](https://www.figma.com/community/file/1149596986784498103/wordpress-design-library) and the [Block Editor Handbook](https://developer.wordpress.org/block-editor/reference-guides/components/). The **Components Demo** (`/wp-admin/admin.php?page=newspack-components-demo`) is the live reference for how Newspack components look and behave; use it to confirm spacing and hierarchy when building new screens. It no longer catalogues `Notice`; the Action Card examples are the place to see one rendered.
728
729
 
729
730
  ## Component Patterns
730
731
 
@@ -9,6 +9,7 @@ var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/de
9
9
  var _slicedToArray2 = _interopRequireDefault(require("@babel/runtime/helpers/slicedToArray"));
10
10
  var _components = require("@wordpress/components");
11
11
  var _element = require("@wordpress/element");
12
+ var _htmlEntities = require("@wordpress/html-entities");
12
13
  var _i18n = require("@wordpress/i18n");
13
14
  var _icons = require("@wordpress/icons");
14
15
  var _ui = require("@wordpress/ui");
@@ -26,8 +27,80 @@ function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t =
26
27
  * Internal dependencies
27
28
  */ /**
28
29
  * External dependencies
29
- */ /**
30
+ */
31
+ var NOTIFICATION_LEVELS = ['error', 'info', 'success', 'warning'];
32
+
33
+ /**
34
+ * Collect a notification's text, including text nested inside React elements.
35
+ *
36
+ * Segments join with a space so that adjacent elements do not weld together: by the
37
+ * time speak() sees this string the markup that separated them is already gone.
38
+ *
39
+ * @param {*} value Notification content.
40
+ * @return {string} The collected text.
41
+ */
42
+ var _getNotificationText = function getNotificationText(value) {
43
+ var _value$props;
44
+ if (typeof value === 'string') {
45
+ return value;
46
+ }
47
+ if (typeof value === 'number') {
48
+ return String(value);
49
+ }
50
+ if (value instanceof Error) {
51
+ return value.message;
52
+ }
53
+ if (Array.isArray(value)) {
54
+ return value.map(_getNotificationText).join(' ');
55
+ }
56
+ return (value === null || value === void 0 || (_value$props = value.props) === null || _value$props === void 0 ? void 0 : _value$props.children) === undefined ? '' : _getNotificationText(value.props.children);
57
+ };
58
+
59
+ /**
60
+ * Derive a plain-text announcement from a notification.
61
+ *
62
+ * Notice defaults spokenMessage to its children and runs renderToString over them
63
+ * during render, which corrupts the hook dispatcher when those children are
64
+ * components. A notification can be any element, so the announcement is always
65
+ * built from its text instead of letting that default apply. Tags in an HTML
66
+ * notification stay in, since speak() strips them itself. Entities are decoded so
67
+ * the announcement matches the text on screen, which getNotificationContent decodes
68
+ * for the same reason.
69
+ *
70
+ * @param {*} notification Notification content.
71
+ * @return {string} Message to announce.
72
+ */
73
+ var getSpokenNotification = function getSpokenNotification(notification) {
74
+ return (0, _htmlEntities.decodeEntities)(_getNotificationText(notification)).replace(/\s+/g, ' ').trim();
75
+ };
76
+
77
+ /**
78
+ * Decode a notification's own text for display.
79
+ *
80
+ * React does not decode entities in text children, and notification text is often
81
+ * server-sourced, so a message carrying `&#8217;` would otherwise show the entity
82
+ * and announce the character. The HTML branch is left alone: there the browser's
83
+ * parser decodes as it renders.
84
+ *
85
+ * @param {*} value Notification content.
86
+ * @return {*} The content, with its own strings decoded.
87
+ */
88
+ var _getNotificationContent = function getNotificationContent(value) {
89
+ if (value instanceof Error) {
90
+ return (0, _htmlEntities.decodeEntities)(value.message);
91
+ }
92
+ if (typeof value === 'string') {
93
+ return (0, _htmlEntities.decodeEntities)(value);
94
+ }
95
+ if (Array.isArray(value)) {
96
+ return value.map(_getNotificationContent);
97
+ }
98
+ return value;
99
+ };
100
+
101
+ /**
30
102
  * ActionCard component
103
+ *
31
104
  * @param {ActionCardProps} props Component props.
32
105
  * @return {JSX.Element} ActionCard component.
33
106
  */
@@ -124,6 +197,7 @@ var ActionCard = function ActionCard(_ref) {
124
197
  }
125
198
  }, [collapse]);
126
199
  var hasChildren = notification || children;
200
+ var notificationContent = _getNotificationContent(notification);
127
201
  var classes = (0, _classnames["default"])('newspack-action-card', simple && 'newspack-card--is-clickable', hasGreyHeader && 'newspack-card--has-grey-header', hasWhiteHeader && 'newspack-card--has-white-header', hasChildren && 'newspack-card--has-children', indent && 'newspack-card--indent', isSmall && 'is-small', isMedium && 'is-medium', checkbox && 'has-checkbox', expandable && 'is-expandable', draggable && 'is-draggable', actionContent && 'has-action-content', className);
128
202
  var backgroundImageStyles = function backgroundImageStyles(url) {
129
203
  return url ? {
@@ -247,24 +321,17 @@ var ActionCard = function ActionCard(_ref) {
247
321
  width: 24
248
322
  })
249
323
  })]
250
- }), notification && /*#__PURE__*/(0, _jsxRuntime.jsxs)("div", {
324
+ }), notification && NOTIFICATION_LEVELS.includes(notificationLevel) && /*#__PURE__*/(0, _jsxRuntime.jsx)("div", {
251
325
  className: "newspack-action-card__notification newspack-action-card__region-children",
252
- children: ['error' === notificationLevel && /*#__PURE__*/(0, _jsxRuntime.jsx)(_.Notice, {
253
- noticeText: notification,
254
- isError: true,
255
- rawHTML: notificationHTML
256
- }), 'info' === notificationLevel && /*#__PURE__*/(0, _jsxRuntime.jsx)(_.Notice, {
257
- noticeText: notification,
258
- rawHTML: notificationHTML
259
- }), 'success' === notificationLevel && /*#__PURE__*/(0, _jsxRuntime.jsx)(_.Notice, {
260
- noticeText: notification,
261
- isSuccess: true,
262
- rawHTML: notificationHTML
263
- }), 'warning' === notificationLevel && /*#__PURE__*/(0, _jsxRuntime.jsx)(_.Notice, {
264
- noticeText: notification,
265
- isWarning: true,
266
- rawHTML: notificationHTML
267
- })]
326
+ children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_components.Notice, {
327
+ status: notificationLevel,
328
+ isDismissible: false,
329
+ spokenMessage: getSpokenNotification(notification),
330
+ children: notificationHTML && typeof notification === 'string' ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_element.RawHTML, {
331
+ className: "newspack-action-card__notification-html",
332
+ children: notification
333
+ }) : notificationContent
334
+ })
268
335
  }), children && (expandable && expanded || !expandable) ? /*#__PURE__*/(0, _jsxRuntime.jsx)("div", {
269
336
  className: "newspack-action-card__region-children",
270
337
  children: children
@@ -1,16 +1,26 @@
1
1
  "use strict";
2
2
 
3
3
  var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
4
+ var _slicedToArray2 = _interopRequireDefault(require("@babel/runtime/helpers/slicedToArray"));
5
+ var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty"));
4
6
  var _react = require("@testing-library/react");
7
+ var _a11y = require("@wordpress/a11y");
8
+ var _element = require("@wordpress/element");
5
9
  var _index = _interopRequireDefault(require("./index"));
6
10
  var _jsxRuntime = require("react/jsx-runtime");
7
- /**
11
+ 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; }
12
+ 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) { (0, _defineProperty2["default"])(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; } /**
8
13
  * External dependencies.
9
- */
10
-
11
- /**
14
+ */ /**
15
+ * WordPress dependencies.
16
+ */ /**
12
17
  * Internal dependencies.
13
18
  */
19
+ jest.mock('@wordpress/a11y', function () {
20
+ return _objectSpread(_objectSpread({}, jest.requireActual('@wordpress/a11y')), {}, {
21
+ speak: jest.fn()
22
+ });
23
+ });
14
24
 
15
25
  /**
16
26
  * The library Badge styles its wrapper rather than its text, so a badge with no label
@@ -18,7 +28,8 @@ var _jsxRuntime = require("react/jsx-runtime");
18
28
  * absent — a payment gateway reports no connection status until it is enabled, a prompt
19
29
  * can sit on a placement with no entry in the placement map — and an array literal is
20
30
  * always non-empty, so the component has to drop those itself.
21
- */describe('ActionCard badges', function () {
31
+ */
32
+ describe('ActionCard badges', function () {
22
33
  var badgeText = function badgeText() {
23
34
  return _react.screen.queryAllByText(function (_, node) {
24
35
  return /__badge/.test((node === null || node === void 0 ? void 0 : node.className) || '');
@@ -88,4 +99,180 @@ var _jsxRuntime = require("react/jsx-runtime");
88
99
  // An empty array used to fall through as a literal `0` in the heading.
89
100
  expect(withEmptyArray.textContent).not.toContain('0');
90
101
  });
102
+ });
103
+
104
+ /**
105
+ * ActionCard derives the notice's spoken message rather than letting core default it to
106
+ * the children. Core runs `renderToString` over that default during render, which corrupts
107
+ * the hook dispatcher when the children are components — so the derivation is what keeps a
108
+ * notification carrying a `<Button>` from crashing the card on a later re-render.
109
+ */
110
+ describe('ActionCard notifications', function () {
111
+ beforeEach(function () {
112
+ _a11y.speak.mockClear();
113
+ });
114
+ var spoken = function spoken() {
115
+ return _a11y.speak.mock.calls.map(function (call) {
116
+ return call[0];
117
+ });
118
+ };
119
+ it('renders a notice for a recognised level', function () {
120
+ var _render5 = (0, _react.render)(/*#__PURE__*/(0, _jsxRuntime.jsx)(_index["default"], {
121
+ title: "Ad Manager",
122
+ notification: "Plugin cannot be installed",
123
+ notificationLevel: "error"
124
+ })),
125
+ container = _render5.container;
126
+ expect(container.querySelector('.components-notice.is-error')).toBeInTheDocument();
127
+ });
128
+ it('renders no notice for an unrecognised or absent level', function () {
129
+ var _render6 = (0, _react.render)(/*#__PURE__*/(0, _jsxRuntime.jsx)(_index["default"], {
130
+ title: "Ad Manager",
131
+ notification: "Something happened",
132
+ notificationLevel: "critical"
133
+ })),
134
+ unknown = _render6.container;
135
+ expect(unknown.querySelector('.components-notice')).not.toBeInTheDocument();
136
+ var _render7 = (0, _react.render)(/*#__PURE__*/(0, _jsxRuntime.jsx)(_index["default"], {
137
+ title: "Ad Manager",
138
+ notification: "Something happened"
139
+ })),
140
+ absent = _render7.container;
141
+ expect(absent.querySelector('.components-notice')).not.toBeInTheDocument();
142
+ });
143
+ it('announces the text inside an element-only notification', function () {
144
+ (0, _react.render)(/*#__PURE__*/(0, _jsxRuntime.jsx)(_index["default"], {
145
+ title: "Ad Manager",
146
+ notificationLevel: "error",
147
+ notification: [/*#__PURE__*/(0, _jsxRuntime.jsx)("button", {
148
+ children: "Click here to connect your account."
149
+ }, "connect")]
150
+ }));
151
+ expect(spoken()).toContain('Click here to connect your account.');
152
+ });
153
+ it('announces strings and element text together', function () {
154
+ (0, _react.render)(/*#__PURE__*/(0, _jsxRuntime.jsx)(_index["default"], {
155
+ title: "Ad Manager",
156
+ notificationLevel: "success",
157
+ notification: ['Created custom targeting keys: ',
158
+ /*#__PURE__*/
159
+ // eslint-disable-next-line react/jsx-indent
160
+ (0, _jsxRuntime.jsx)("a", {
161
+ href: "https://example.org",
162
+ children: "Visit your GAM dashboard"
163
+ }, "dashboard")]
164
+ }));
165
+ expect(spoken()).toContain('Created custom targeting keys: Visit your GAM dashboard');
166
+ });
167
+ it('announces an Error and renders its message rather than throwing', function () {
168
+ (0, _react.render)(/*#__PURE__*/(0, _jsxRuntime.jsx)(_index["default"], {
169
+ title: "Ad Manager",
170
+ notificationLevel: "error",
171
+ notification: new Error('Network unreachable')
172
+ }));
173
+ expect(_react.screen.getByText('Network unreachable')).toBeInTheDocument();
174
+ expect(spoken()).toContain('Network unreachable');
175
+ });
176
+ it('renders HTML notifications in their own wrapper and hands speak() the tags to strip', function () {
177
+ var _render8 = (0, _react.render)(/*#__PURE__*/(0, _jsxRuntime.jsx)(_index["default"], {
178
+ title: "Ad Manager",
179
+ notificationLevel: "error",
180
+ notification: '<p>Install failed.</p><p>Try again.</p>',
181
+ notificationHTML: true
182
+ })),
183
+ container = _render8.container;
184
+ expect(container.querySelectorAll('.newspack-action-card__notification-html > p')).toHaveLength(2);
185
+ expect(spoken()[0]).toContain('<p>Install failed.</p>');
186
+ });
187
+ it('decodes entities in both the announcement and the visible text', function () {
188
+ (0, _react.render)(/*#__PURE__*/(0, _jsxRuntime.jsx)(_index["default"], {
189
+ title: "Ad Manager",
190
+ notificationLevel: "error",
191
+ notification: 'Plugin &#8220;Foo&#8221; could not be activated'
192
+ }));
193
+ expect(_react.screen.getByText('Plugin “Foo” could not be activated')).toBeInTheDocument();
194
+ expect(spoken()[0]).toContain('“Foo”');
195
+ });
196
+ it('decodes entities inside an array of notification parts', function () {
197
+ (0, _react.render)(/*#__PURE__*/(0, _jsxRuntime.jsx)(_index["default"], {
198
+ title: "Ad Manager",
199
+ notificationLevel: "error",
200
+ notification: ['Could not reach the publisher&#8217;s network.', ' ']
201
+ }));
202
+ expect(_react.screen.getByText(/Could not reach the publisher’s network\./)).toBeInTheDocument();
203
+ expect(spoken()[0]).toContain('publisher’s');
204
+ });
205
+ it('leaves entity decoding to the browser on the HTML branch', function () {
206
+ var _render9 = (0, _react.render)(/*#__PURE__*/(0, _jsxRuntime.jsx)(_index["default"], {
207
+ title: "Ad Manager",
208
+ notificationLevel: "error",
209
+ notification: 'Get it from <a href="https://example.org">the plugin&#8217;s site</a>',
210
+ notificationHTML: true
211
+ })),
212
+ container = _render9.container;
213
+ expect(container.querySelector('.newspack-action-card__notification-html a').textContent).toBe('the plugin’s site');
214
+ expect(spoken()[0]).toContain('plugin’s site');
215
+ });
216
+ it('renders a non-string notification as children even when notificationHTML is set', function () {
217
+ var _render0 = (0, _react.render)(/*#__PURE__*/(0, _jsxRuntime.jsx)(_index["default"], {
218
+ title: "Ad Manager",
219
+ notificationLevel: "error",
220
+ notification: /*#__PURE__*/(0, _jsxRuntime.jsx)("span", {
221
+ children: "Network unreachable"
222
+ }),
223
+ notificationHTML: true
224
+ })),
225
+ container = _render0.container;
226
+ expect(_react.screen.getByText('Network unreachable')).toBeInTheDocument();
227
+ expect(container.querySelector('.newspack-action-card__notification-html')).not.toBeInTheDocument();
228
+ });
229
+ it('keeps adjacent element texts apart in the announcement', function () {
230
+ (0, _react.render)(/*#__PURE__*/(0, _jsxRuntime.jsx)(_index["default"], {
231
+ title: "Ad Manager",
232
+ notificationLevel: "error",
233
+ notification: [
234
+ /*#__PURE__*/
235
+ // eslint-disable-next-line react/jsx-indent
236
+ (0, _jsxRuntime.jsx)("a", {
237
+ href: "https://example.org",
238
+ children: "Visit your GAM dashboard"
239
+ }, "dashboard"), /*#__PURE__*/(0, _jsxRuntime.jsx)("button", {
240
+ type: "button",
241
+ children: "Click here to connect your account."
242
+ }, "connect")]
243
+ }));
244
+ expect(spoken()[0]).toBe('Visit your GAM dashboard Click here to connect your account.');
245
+ });
246
+ it('survives a re-render that changes how many hooked components the notification holds', function () {
247
+ var HookedAction = function HookedAction(_ref) {
248
+ var children = _ref.children;
249
+ var _useState = (0, _element.useState)(children),
250
+ _useState2 = (0, _slicedToArray2["default"])(_useState, 1),
251
+ label = _useState2[0];
252
+ return /*#__PURE__*/(0, _jsxRuntime.jsx)("button", {
253
+ type: "button",
254
+ children: label
255
+ });
256
+ };
257
+ var card = function card(notification) {
258
+ return /*#__PURE__*/(0, _jsxRuntime.jsx)(_index["default"], {
259
+ title: "Ad Manager",
260
+ notificationLevel: "error",
261
+ notification: notification
262
+ });
263
+ };
264
+ var _render1 = (0, _react.render)(card([/*#__PURE__*/(0, _jsxRuntime.jsx)(HookedAction, {
265
+ children: "Retry"
266
+ }, "retry"), /*#__PURE__*/(0, _jsxRuntime.jsx)(HookedAction, {
267
+ children: "Documentation"
268
+ }, "docs")])),
269
+ rerender = _render1.rerender,
270
+ container = _render1.container;
271
+ expect(function () {
272
+ return rerender(card([/*#__PURE__*/(0, _jsxRuntime.jsx)(HookedAction, {
273
+ children: "Retry"
274
+ }, "retry")]));
275
+ }).not.toThrow();
276
+ expect(container.querySelector('.components-notice.is-error')).toBeInTheDocument();
277
+ });
91
278
  });
@@ -339,18 +339,12 @@
339
339
 
340
340
  /* Notifications */
341
341
  .newspack-action-card__notification {
342
- .newspack-notice {
342
+ .components-notice {
343
343
  margin: 0;
344
-
345
- &__heading {
346
- margin-top: 0;
347
- }
348
344
  }
349
345
 
350
- &.newspack-action-card__region-children {
351
- .newspack-notice {
352
- margin-top: 32px;
353
- }
346
+ .newspack-action-card__notification-html > * {
347
+ margin: 0;
354
348
  }
355
349
  }
356
350
 
@@ -80,6 +80,30 @@ var _CardFeature = function CardFeature(_ref) {
80
80
  // translators: %1$s: the button's visible action label, e.g. "Enable". %2$s: the feature's name. The visible label must stay first (WCAG 2.5.3).
81
81
  (0, _i18n._x)('%1$s %2$s', 'accessible button name: visible action label, then feature name', 'newspack-plugin'), buttonLabel, title);
82
82
  var showMoreControls = enabled && !!(moreControls !== null && moreControls !== void 0 && moreControls.length) && (!requirements || requirementsActionable);
83
+ // A Configure button with nothing behind it would click to nowhere.
84
+ var showPrimaryButton = !isConfigureState || !!onConfigure;
85
+
86
+ // Enabling a feature with nothing to configure removes the button that was
87
+ // just used, so its focus goes to the More menu instead of the page. React
88
+ // detaches a ref before removing the node, which is the last point the
89
+ // button can still be read as focused.
90
+ var actionsRef = (0, _element.useRef)(null);
91
+ var primaryButtonNode = (0, _element.useRef)(null);
92
+ var primaryButtonLeftWithFocus = (0, _element.useRef)(false);
93
+ var primaryButtonRef = (0, _element.useCallback)(function (node) {
94
+ if (!node && primaryButtonNode.current) {
95
+ primaryButtonLeftWithFocus.current = primaryButtonNode.current === primaryButtonNode.current.ownerDocument.activeElement;
96
+ }
97
+ primaryButtonNode.current = node;
98
+ }, []);
99
+ (0, _element.useEffect)(function () {
100
+ var _actionsRef$current;
101
+ if (showPrimaryButton || !primaryButtonLeftWithFocus.current) {
102
+ return;
103
+ }
104
+ primaryButtonLeftWithFocus.current = false;
105
+ (_actionsRef$current = actionsRef.current) === null || _actionsRef$current === void 0 || (_actionsRef$current = _actionsRef$current.querySelector('.components-dropdown-menu__toggle')) === null || _actionsRef$current === void 0 || _actionsRef$current.focus();
106
+ }, [showPrimaryButton]);
83
107
  var handleButtonClick = function handleButtonClick() {
84
108
  if (isConfigureState) {
85
109
  onConfigure === null || onConfigure === void 0 || onConfigure();
@@ -89,8 +113,7 @@ var _CardFeature = function CardFeature(_ref) {
89
113
  };
90
114
  var iconDescriptor = icon && !(0, _element.isValidElement)(icon) ? icon : null;
91
115
  var iconClasses = iconDescriptor ? (0, _classnames["default"])('newspack-card-feature__icon', {
92
- 'newspack-card-feature__icon--radius-small': !!iconDescriptor.backgroundColor && iconDescriptor.radius !== 'full',
93
- 'newspack-card-feature__icon--radius-full': iconDescriptor.radius === 'full'
116
+ 'newspack-card-feature__icon--has-background': !!iconDescriptor.backgroundColor
94
117
  }) : undefined;
95
118
  var renderedIcon = null;
96
119
  if ((0, _element.isValidElement)(icon)) {
@@ -137,10 +160,12 @@ var _CardFeature = function CardFeature(_ref) {
137
160
  gap: "sm",
138
161
  wrap: "wrap",
139
162
  children: [/*#__PURE__*/(0, _jsxRuntime.jsxs)(_ui.Stack, {
163
+ ref: actionsRef,
140
164
  direction: "row",
141
165
  align: "center",
142
166
  gap: "sm",
143
- children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_button["default"], {
167
+ children: [showPrimaryButton && /*#__PURE__*/(0, _jsxRuntime.jsx)(_button["default"], {
168
+ ref: primaryButtonRef,
144
169
  variant: isConfigureState ? 'tertiary' : 'secondary',
145
170
  accessibleWhenDisabled: true,
146
171
  "aria-describedby": describedById,