newspack-components 4.6.2 → 4.7.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 (148) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/colors/colors.module.scss +13 -7
  3. package/dist/cjs/autocomplete-tokenfield/style.scss +4 -2
  4. package/dist/cjs/breadcrumbs/format-count.js +33 -0
  5. package/dist/cjs/breadcrumbs/index.js +78 -20
  6. package/dist/cjs/breadcrumbs/index.test.js +110 -0
  7. package/dist/cjs/breadcrumbs/style.scss +6 -0
  8. package/dist/cjs/button/index.js +41 -17
  9. package/dist/cjs/button/index.test.js +273 -0
  10. package/dist/cjs/card/core-card.js +9 -4
  11. package/dist/cjs/card/core-card.test.js +29 -0
  12. package/dist/cjs/card/index.js +4 -2
  13. package/dist/cjs/card/style-core.scss +59 -0
  14. package/dist/cjs/card-feature/index.js +7 -1
  15. package/dist/cjs/card-feature/index.test.js +92 -0
  16. package/dist/cjs/color-picker/index.js +1 -1
  17. package/dist/cjs/confirm-dialog/index.js +26 -9
  18. package/dist/cjs/confirm-dialog/index.test.js +176 -0
  19. package/dist/cjs/dataviews/index.js +11 -2
  20. package/dist/cjs/divider/index.js +13 -0
  21. package/dist/cjs/drawer/action.js +74 -0
  22. package/dist/cjs/drawer/body.js +79 -0
  23. package/dist/cjs/drawer/close-icon.js +45 -0
  24. package/dist/cjs/drawer/content.js +72 -0
  25. package/dist/cjs/drawer/context.js +23 -0
  26. package/dist/cjs/drawer/context.test.js +43 -0
  27. package/dist/cjs/drawer/divider.js +24 -0
  28. package/dist/cjs/drawer/footer.js +26 -0
  29. package/dist/cjs/drawer/header.js +26 -0
  30. package/dist/cjs/drawer/index.js +42 -0
  31. package/dist/cjs/drawer/index.test.js +1287 -0
  32. package/dist/cjs/drawer/root.js +350 -0
  33. package/dist/cjs/drawer/slots.test.js +422 -0
  34. package/dist/cjs/drawer/style.scss +205 -0
  35. package/dist/cjs/drawer/title.js +54 -0
  36. package/dist/cjs/drawer/types.js +5 -0
  37. package/dist/cjs/drawer/use-exit-animation.js +59 -0
  38. package/dist/cjs/hooks/use-confirm-dialog.js +15 -3
  39. package/dist/cjs/hooks/use-unsaved-changes-dialog.js +19 -4
  40. package/dist/cjs/index.js +28 -1
  41. package/dist/cjs/page/index.js +1 -1
  42. package/dist/cjs/page-control/index.js +82 -0
  43. package/dist/cjs/plugin-settings/SettingsSection.js +7 -1
  44. package/dist/cjs/section-header/index.js +5 -2
  45. package/dist/cjs/section-header/style.scss +25 -12
  46. package/dist/cjs/table-card/index.js +67 -0
  47. package/dist/cjs/table-card/index.test.js +153 -0
  48. package/dist/cjs/with-wizard-screen/style.scss +8 -0
  49. package/dist/cjs/wizard/index.js +102 -38
  50. package/dist/esm/autocomplete-tokenfield/style.scss +4 -2
  51. package/dist/esm/breadcrumbs/format-count.js +27 -0
  52. package/dist/esm/breadcrumbs/index.js +81 -20
  53. package/dist/esm/breadcrumbs/index.test.js +110 -0
  54. package/dist/esm/breadcrumbs/style.scss +6 -0
  55. package/dist/esm/button/index.js +42 -18
  56. package/dist/esm/button/index.test.js +273 -0
  57. package/dist/esm/card/core-card.js +9 -4
  58. package/dist/esm/card/core-card.test.js +29 -0
  59. package/dist/esm/card/index.js +4 -2
  60. package/dist/esm/card/style-core.scss +59 -0
  61. package/dist/esm/card-feature/index.js +8 -2
  62. package/dist/esm/card-feature/index.test.js +96 -0
  63. package/dist/esm/color-picker/index.js +1 -1
  64. package/dist/esm/confirm-dialog/index.js +26 -9
  65. package/dist/esm/confirm-dialog/index.test.js +176 -0
  66. package/dist/esm/dataviews/index.js +11 -2
  67. package/dist/esm/divider/index.js +14 -0
  68. package/dist/esm/drawer/action.js +68 -0
  69. package/dist/esm/drawer/body.js +70 -0
  70. package/dist/esm/drawer/close-icon.js +37 -0
  71. package/dist/esm/drawer/content.js +63 -0
  72. package/dist/esm/drawer/context.js +17 -0
  73. package/dist/esm/drawer/context.test.js +40 -0
  74. package/dist/esm/drawer/divider.js +16 -0
  75. package/dist/esm/drawer/footer.js +18 -0
  76. package/dist/esm/drawer/header.js +18 -0
  77. package/dist/esm/drawer/index.js +35 -0
  78. package/dist/esm/drawer/index.test.js +1285 -0
  79. package/dist/esm/drawer/root.js +342 -0
  80. package/dist/esm/drawer/slots.test.js +422 -0
  81. package/dist/esm/drawer/style.scss +205 -0
  82. package/dist/esm/drawer/title.js +46 -0
  83. package/dist/esm/drawer/types.js +1 -0
  84. package/dist/esm/drawer/use-exit-animation.js +52 -0
  85. package/dist/esm/hooks/use-confirm-dialog.js +15 -3
  86. package/dist/esm/hooks/use-unsaved-changes-dialog.js +20 -5
  87. package/dist/esm/index.js +4 -0
  88. package/dist/esm/page/index.js +1 -1
  89. package/dist/esm/page-control/index.js +82 -0
  90. package/dist/esm/plugin-settings/SettingsSection.js +8 -2
  91. package/dist/esm/section-header/index.js +5 -2
  92. package/dist/esm/section-header/style.scss +25 -12
  93. package/dist/esm/table-card/index.js +60 -0
  94. package/dist/esm/table-card/index.test.js +150 -0
  95. package/dist/esm/with-wizard-screen/style.scss +8 -0
  96. package/dist/esm/wizard/index.js +104 -38
  97. package/package.json +2 -2
  98. package/src/autocomplete-tokenfield/style.scss +4 -2
  99. package/src/breadcrumbs/format-count.js +27 -0
  100. package/src/breadcrumbs/index.js +68 -8
  101. package/src/breadcrumbs/index.test.js +57 -0
  102. package/src/breadcrumbs/style.scss +6 -0
  103. package/src/button/index.test.js +143 -0
  104. package/src/button/index.tsx +32 -7
  105. package/src/card/core-card.js +15 -2
  106. package/src/card/core-card.test.js +19 -0
  107. package/src/card/index.js +3 -0
  108. package/src/card/style-core.scss +59 -0
  109. package/src/card-feature/README.md +4 -0
  110. package/src/card-feature/index.test.js +65 -0
  111. package/src/card-feature/index.tsx +13 -2
  112. package/src/color-picker/index.js +1 -1
  113. package/src/confirm-dialog/README.md +3 -2
  114. package/src/confirm-dialog/index.test.js +146 -0
  115. package/src/confirm-dialog/index.tsx +23 -9
  116. package/src/dataviews/index.tsx +11 -2
  117. package/src/divider/index.js +13 -0
  118. package/src/drawer/README.md +454 -0
  119. package/src/drawer/action.tsx +67 -0
  120. package/src/drawer/body.tsx +60 -0
  121. package/src/drawer/close-icon.tsx +37 -0
  122. package/src/drawer/content.tsx +54 -0
  123. package/src/drawer/context.test.js +31 -0
  124. package/src/drawer/context.ts +25 -0
  125. package/src/drawer/divider.tsx +13 -0
  126. package/src/drawer/footer.tsx +15 -0
  127. package/src/drawer/header.tsx +15 -0
  128. package/src/drawer/index.test.js +1012 -0
  129. package/src/drawer/index.tsx +33 -0
  130. package/src/drawer/root.tsx +328 -0
  131. package/src/drawer/slots.test.js +314 -0
  132. package/src/drawer/style.scss +205 -0
  133. package/src/drawer/title.tsx +40 -0
  134. package/src/drawer/types.ts +79 -0
  135. package/src/drawer/use-exit-animation.ts +46 -0
  136. package/src/hooks/use-confirm-dialog.tsx +14 -3
  137. package/src/hooks/use-unsaved-changes-dialog.tsx +17 -5
  138. package/src/index.js +4 -0
  139. package/src/page/index.js +1 -1
  140. package/src/page-control/index.js +63 -0
  141. package/src/plugin-settings/SettingsSection.js +8 -2
  142. package/src/section-header/index.js +5 -2
  143. package/src/section-header/style.scss +25 -12
  144. package/src/table-card/README.md +100 -0
  145. package/src/table-card/index.test.js +112 -0
  146. package/src/table-card/index.tsx +56 -0
  147. package/src/with-wizard-screen/style.scss +8 -0
  148. package/src/wizard/index.js +95 -34
@@ -4,7 +4,7 @@ var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefau
4
4
  Object.defineProperty(exports, "__esModule", {
5
5
  value: true
6
6
  });
7
- exports["default"] = void 0;
7
+ exports["default"] = exports.WizardBanner = void 0;
8
8
  var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty"));
9
9
  var _slicedToArray2 = _interopRequireDefault(require("@babel/runtime/helpers/slicedToArray"));
10
10
  var _classnames = _interopRequireDefault(require("classnames"));
@@ -26,11 +26,19 @@ function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t =
26
26
  * External dependencies.
27
27
  */ /**
28
28
  * WordPress dependencies.
29
- */ /**
29
+ */ // Notice is aliased: `Notice` below is Newspack's own, which this file also uses.
30
+ /**
30
31
  * Internal dependencies
31
32
  */
32
33
  (0, _store["default"])();
33
34
 
35
+ /**
36
+ * Renders a view's page-level banner outside the padded content column, so it sits
37
+ * flush beneath the header rather than indented within the section it describes.
38
+ */
39
+ var _createSlotFill = (0, _components.createSlotFill)('NewspackWizardBanner'),
40
+ WizardBannerSlot = _createSlotFill.Slot,
41
+ WizardBanner = exports.WizardBanner = _createSlotFill.Fill;
34
42
  /**
35
43
  * Icon registry for resolving icon name strings passed through the data store.
36
44
  * React elements from @wordpress/icons can't cross webpack entry point boundaries
@@ -53,6 +61,34 @@ var HashRouter = _router["default"].HashRouter,
53
61
  Switch = _router["default"].Switch,
54
62
  useLocation = _router["default"].useLocation;
55
63
 
64
+ /**
65
+ * Interpolate a translated message's named tags, falling back to plain text.
66
+ *
67
+ * The message is translated, so its tags are site-controlled: any .mo file under
68
+ * wp-content/languages/plugins, any GlotPress export, or any `gettext`-filter
69
+ * plugin can supply one. `createInterpolateElement` throws on an unbalanced
70
+ * closing tag whose name is in the conversion map - verified against core's own
71
+ * element.js, which is what runs here since the bundle externalizes wp-element.
72
+ * Nothing above this renders an error boundary, so an uncaught throw would blank
73
+ * the whole wizard, including the screen the inert-gating notice links to as the
74
+ * fix. Degraded copy is recoverable; a blank screen is not.
75
+ *
76
+ * The other malformed cases already degrade on their own and are left alone: an
77
+ * unclosed opener drops that tag and renders the rest as text, and a tag not in
78
+ * the map renders literally.
79
+ *
80
+ * @param {string} message Translated message carrying named tags.
81
+ * @param {Object} conversion Conversion map for createInterpolateElement.
82
+ * @return {JSX.Element|string} The interpolated message, or the message with its tags stripped.
83
+ */
84
+ var interpolateOrPlainText = function interpolateOrPlainText(message, conversion) {
85
+ try {
86
+ return (0, _element.createInterpolateElement)(message, conversion);
87
+ } catch (_unused) {
88
+ return message.replace(/<\/?[a-zA-Z][a-zA-Z0-9]*\s*\/?>/g, '');
89
+ }
90
+ };
91
+
56
92
  /**
57
93
  * Reset the header data when a new section is rendered.
58
94
  */
@@ -112,7 +148,7 @@ var WizardHeaderRegion = function WizardHeaderRegion(_ref) {
112
148
 
113
149
  /**
114
150
  * @typedef {Object} WizardProps
115
- * @property {string} headerText The header text.
151
+ * @property {string} [headerText] Fallback heading, used only when no section declares breadcrumbs.
116
152
  * @property {string} [subHeaderText] The sub-header text, optional.
117
153
  * @property {string} [apiSlug] The API slug, optional.
118
154
  * @property {string} [className] CSS classes, optional.
@@ -132,6 +168,7 @@ var WizardHeaderRegion = function WizardHeaderRegion(_ref) {
132
168
  * @return {JSX.Element} Wizard component
133
169
  */
134
170
  var Wizard = function Wizard(_ref2, ref) {
171
+ var _window$newspack_aux_;
135
172
  var _ref2$sections = _ref2.sections,
136
173
  sections = _ref2$sections === void 0 ? [] : _ref2$sections,
137
174
  headerText = _ref2.headerText,
@@ -211,10 +248,35 @@ var Wizard = function Wizard(_ref2, ref) {
211
248
  items: displayedSections,
212
249
  children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_WizardError["default"], {})
213
250
  });
251
+
252
+ // Rendered here rather than as a core admin notice, which wizards strip at
253
+ // priority -9999. Sits as the first child of .newspack-wizard__main so it lands
254
+ // flush beneath the header region and spans the full width in every view: this
255
+ // describes the state of the whole site, not of the section below it, so it reads
256
+ // as page chrome rather than as content.
257
+ var inertGating = (_window$newspack_aux_ = window.newspack_aux_data) === null || _window$newspack_aux_ === void 0 ? void 0 : _window$newspack_aux_.inert_gating;
258
+ var inertGatingNotice = (inertGating === null || inertGating === void 0 ? void 0 : inertGating.show) && /*#__PURE__*/(0, _jsxRuntime.jsx)(_components.Notice, {
259
+ status: "warning",
260
+ isDismissible: false,
261
+ className: "newspack-wizard__inert-gating-notice",
262
+ children: interpolateOrPlainText(inertGating.message, {
263
+ /* eslint-disable jsx-a11y/anchor-has-content */
264
+ accessControl: /*#__PURE__*/(0, _jsxRuntime.jsx)("a", {
265
+ href: inertGating.urls.accessControl
266
+ }),
267
+ audience: /*#__PURE__*/(0, _jsxRuntime.jsx)("a", {
268
+ href: inertGating.urls.audience
269
+ }),
270
+ /* eslint-enable jsx-a11y/anchor-has-content */
271
+ strong: /*#__PURE__*/(0, _jsxRuntime.jsx)("strong", {})
272
+ })
273
+ });
214
274
  var content = /*#__PURE__*/(0, _jsxRuntime.jsxs)(_jsxRuntime.Fragment, {
215
- children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_.HandoffMessage, {}), sections.length > 1 && /*#__PURE__*/(0, _jsxRuntime.jsx)(ResetHeaderData, {}), /*#__PURE__*/(0, _jsxRuntime.jsx)("div", {
275
+ children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_.HandoffMessage, {}), sections.length > 1 && /*#__PURE__*/(0, _jsxRuntime.jsx)(ResetHeaderData, {}), /*#__PURE__*/(0, _jsxRuntime.jsxs)("div", {
216
276
  className: "newspack-wizard__main",
217
- children: /*#__PURE__*/(0, _jsxRuntime.jsxs)(Switch, {
277
+ children: [inertGatingNotice, /*#__PURE__*/(0, _jsxRuntime.jsx)(WizardBannerSlot, {
278
+ bubblesVirtually: true
279
+ }), /*#__PURE__*/(0, _jsxRuntime.jsxs)(Switch, {
218
280
  children: [routedSections.map(function (section, index) {
219
281
  var _section$exact;
220
282
  var SectionComponent = section.render;
@@ -245,7 +307,7 @@ var Wizard = function Wizard(_ref2, ref) {
245
307
  }), /*#__PURE__*/(0, _jsxRuntime.jsx)(Redirect, {
246
308
  to: displayedSections[0].path
247
309
  })]
248
- })
310
+ })]
249
311
  })]
250
312
  });
251
313
  var headerActions = (actions === null || actions === void 0 ? void 0 : actions.length) > 0 ? /*#__PURE__*/(0, _jsxRuntime.jsxs)(_jsxRuntime.Fragment, {
@@ -296,40 +358,42 @@ var Wizard = function Wizard(_ref2, ref) {
296
358
  }
297
359
  })]
298
360
  }) : undefined;
299
- return /*#__PURE__*/(0, _jsxRuntime.jsxs)("div", {
300
- ref: ref,
301
- children: [/*#__PURE__*/(0, _jsxRuntime.jsxs)("div", {
302
- className: (0, _classnames["default"])(isLoading ? 'newspack-wizard__is-loading' : 'newspack-wizard__is-loaded', {
303
- 'newspack-wizard__is-loading-quiet': isQuietLoading
304
- }),
305
- children: [/*#__PURE__*/(0, _jsxRuntime.jsxs)(HashRouter, {
306
- hashType: "slash",
307
- children: [newspack_aux_data.is_debug_mode && /*#__PURE__*/(0, _jsxRuntime.jsx)(_.Notice, {
308
- debugMode: true
309
- }), /*#__PURE__*/(0, _jsxRuntime.jsx)(WizardHeaderRegion, {
310
- hideHeader: hideHeader,
311
- headerText: headerText,
312
- sections: routedSections,
313
- sectionName: sectionName,
314
- subTitle: subHeaderText,
315
- actions: headerActions,
316
- tabbedNavigation: tabbedNavigation,
317
- children: content
361
+ return /*#__PURE__*/(0, _jsxRuntime.jsx)(_components.SlotFillProvider, {
362
+ children: /*#__PURE__*/(0, _jsxRuntime.jsxs)("div", {
363
+ ref: ref,
364
+ children: [/*#__PURE__*/(0, _jsxRuntime.jsxs)("div", {
365
+ className: (0, _classnames["default"])(isLoading ? 'newspack-wizard__is-loading' : 'newspack-wizard__is-loaded', {
366
+ 'newspack-wizard__is-loading-quiet': isQuietLoading
367
+ }),
368
+ children: [/*#__PURE__*/(0, _jsxRuntime.jsxs)(HashRouter, {
369
+ hashType: "slash",
370
+ children: [newspack_aux_data.is_debug_mode && /*#__PURE__*/(0, _jsxRuntime.jsx)(_.Notice, {
371
+ debugMode: true
372
+ }), /*#__PURE__*/(0, _jsxRuntime.jsx)(WizardHeaderRegion, {
373
+ hideHeader: hideHeader,
374
+ headerText: headerText,
375
+ sections: routedSections,
376
+ sectionName: sectionName,
377
+ subTitle: subHeaderText,
378
+ actions: headerActions,
379
+ tabbedNavigation: tabbedNavigation,
380
+ children: content
381
+ })]
382
+ }), (notices === null || notices === void 0 ? void 0 : notices.length) > 0 && /*#__PURE__*/(0, _jsxRuntime.jsx)("div", {
383
+ className: "newspack-wizard__snackbar-list",
384
+ children: notices.map(function (notice, index) {
385
+ return /*#__PURE__*/(0, _jsxRuntime.jsx)(_WizardSnackbar["default"], {
386
+ id: notice.id,
387
+ type: notice.type,
388
+ actions: notice.actions,
389
+ children: notice.message
390
+ }, notice.id || index);
391
+ })
318
392
  })]
319
- }), (notices === null || notices === void 0 ? void 0 : notices.length) > 0 && /*#__PURE__*/(0, _jsxRuntime.jsx)("div", {
320
- className: "newspack-wizard__snackbar-list",
321
- children: notices.map(function (notice, index) {
322
- return /*#__PURE__*/(0, _jsxRuntime.jsx)(_WizardSnackbar["default"], {
323
- id: notice.id,
324
- type: notice.type,
325
- actions: notice.actions,
326
- children: notice.message
327
- }, notice.id || index);
328
- })
393
+ }), !isLoading && /*#__PURE__*/(0, _jsxRuntime.jsx)(_.Footer, {
394
+ simple: hasSimpleFooter
329
395
  })]
330
- }), !isLoading && /*#__PURE__*/(0, _jsxRuntime.jsx)(_.Footer, {
331
- simple: hasSimpleFooter
332
- })]
396
+ })
333
397
  });
334
398
  };
335
399
  var _default = exports["default"] = (0, _element.forwardRef)(Wizard);
@@ -3,6 +3,7 @@
3
3
  */
4
4
 
5
5
  @use "~@wordpress/base-styles/colors" as wp-colors;
6
+ @use "~@wordpress/base-styles/variables" as wp-vars;
6
7
 
7
8
  .newspack-autocomplete-tokenfield {
8
9
  .components-spinner {
@@ -34,8 +35,9 @@
34
35
 
35
36
  .newspack-autocomplete-tokenfield__help {
36
37
  color: wp-colors.$gray-700;
37
- font-size: inherit;
38
- margin-top: 0;
38
+ font-size: wp-vars.$helptext-font-size;
39
+ line-height: wp-vars.$default-line-height;
40
+ margin: wp-vars.$grid-unit-10 0 0;
39
41
  }
40
42
 
41
43
  .components-form-token-field__suggestions-list {
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Group a number for the site's locale rather than the browser's, so a crumb's count
3
+ * matches both the translated phrasing beside it and every server-rendered figure on
4
+ * the page. Callers that build their own `countLabel` share this so the visible count
5
+ * and the spoken one are grouped the same way.
6
+ *
7
+ * WordPress ships locales `Intl` rejects outright — `pt_PT_ao90` yields the BCP-47
8
+ * tag `pt-PT-ao90`, whose four-character variant subtag must start with a digit — so
9
+ * the tag is shortened a subtag at a time until one is accepted. Falling straight
10
+ * through to the browser's locale would group `1,234` beside a server-rendered
11
+ * `1.234`, which is the divergence this helper exists to prevent.
12
+ *
13
+ * @param {number} count Number to format.
14
+ * @return {string} The grouped number.
15
+ */
16
+ export var formatCount = function formatCount(count) {
17
+ var lang = typeof document !== 'undefined' && document.documentElement.lang || '';
18
+ var subtags = lang.replace(/_/g, '-').split('-').filter(Boolean);
19
+ while (subtags.length) {
20
+ try {
21
+ return Number(count).toLocaleString(subtags.join('-'));
22
+ } catch (_unused) {
23
+ subtags.pop();
24
+ }
25
+ }
26
+ return Number(count).toLocaleString();
27
+ };
@@ -1,14 +1,71 @@
1
+ import _defineProperty from "@babel/runtime/helpers/defineProperty";
2
+ function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
3
+ function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }
1
4
  /**
2
5
  * WordPress dependencies.
3
6
  */
4
- import { __ } from '@wordpress/i18n';
7
+ import { __, _n, sprintf } from '@wordpress/i18n';
5
8
  import { Link, Stack, Text } from '@wordpress/ui';
6
9
 
7
10
  /**
8
11
  * Internal dependencies.
9
12
  */
13
+ import { formatCount } from './format-count';
10
14
  import './style.scss';
11
15
 
16
+ /**
17
+ * A crumb's count, shown in parentheses and spoken as a phrase.
18
+ *
19
+ * @param {Object} props
20
+ * @param {number} [props.count] Item count.
21
+ * @param {string} [props.countLabel] Translated accessible phrasing for the count.
22
+ * @return {JSX.Element|null} The count, or null when the crumb carries none.
23
+ */
24
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
25
+ var CrumbCount = function CrumbCount(_ref) {
26
+ var count = _ref.count,
27
+ countLabel = _ref.countLabel;
28
+ if (!Number.isFinite(count)) {
29
+ return null;
30
+ }
31
+ var formatted = formatCount(count);
32
+ return /*#__PURE__*/_jsxs("span", {
33
+ className: "newspack-breadcrumbs__count",
34
+ children: [/*#__PURE__*/_jsx("span", {
35
+ "aria-hidden": "true",
36
+ children: "(".concat(formatted, ")")
37
+ }), /*#__PURE__*/_jsx("span", {
38
+ className: "screen-reader-text",
39
+ children: countLabel || sprintf(/* translators: %s: number of items in the current view. */
40
+ _n('%s item', '%s items', count, 'newspack-plugin'), formatted)
41
+ })]
42
+ });
43
+ };
44
+
45
+ /**
46
+ * A crumb's label, with its count appended when the item carries one.
47
+ *
48
+ * @param {Object} props
49
+ * @param {string} props.label Crumb label.
50
+ * @param {number} [props.count] Item count to annotate the label with.
51
+ * @param {string} [props.countLabel] Translated accessible phrasing for the count.
52
+ * @return {JSX.Element|string} The label, annotated when a count is present.
53
+ */
54
+ var CrumbLabel = function CrumbLabel(_ref2) {
55
+ var label = _ref2.label,
56
+ count = _ref2.count,
57
+ countLabel = _ref2.countLabel;
58
+ if (!Number.isFinite(count)) {
59
+ return label;
60
+ }
61
+ return /*#__PURE__*/_jsxs(_Fragment, {
62
+ children: [label, " ", /*#__PURE__*/_jsx(CrumbCount, {
63
+ count: count,
64
+ countLabel: countLabel
65
+ })]
66
+ });
67
+ };
68
+
12
69
  /**
13
70
  * Newspack breadcrumb trail.
14
71
  *
@@ -18,13 +75,12 @@ import './style.scss';
18
75
  * component needs no router context.
19
76
  *
20
77
  * @param {Object} props
21
- * @param {Array} props.items Trail items: `{ label, url? }`.
78
+ * @param {Array} props.items Trail items: `{ label, url?, count?, countLabel? }`.
22
79
  * @return {JSX.Element|null} Breadcrumbs component.
23
80
  */
24
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
25
- var Breadcrumbs = function Breadcrumbs(_ref) {
26
- var _ref$items = _ref.items,
27
- items = _ref$items === void 0 ? [] : _ref$items;
81
+ var Breadcrumbs = function Breadcrumbs(_ref3) {
82
+ var _ref3$items = _ref3.items,
83
+ items = _ref3$items === void 0 ? [] : _ref3$items;
28
84
  if (!items.length) {
29
85
  return null;
30
86
  }
@@ -39,21 +95,26 @@ var Breadcrumbs = function Breadcrumbs(_ref) {
39
95
  className: "newspack-breadcrumbs__list",
40
96
  children: [preceding.map(function (item, index) {
41
97
  return /*#__PURE__*/_jsxs("li", {
42
- children: [item.url ?
43
- /*#__PURE__*/
44
- // eslint-disable-next-line jsx-a11y/anchor-has-content -- content is supplied via the Text children through @wordpress/ui's render prop.
45
- _jsx(Text, {
46
- variant: "body-lg",
47
- render: /*#__PURE__*/_jsx(Link, {
48
- tone: "neutral",
49
- render: /*#__PURE__*/_jsx("a", {
50
- href: item.url
51
- })
52
- }),
53
- children: item.label
98
+ children: [item.url ? /*#__PURE__*/_jsxs(_Fragment, {
99
+ children: [/*#__PURE__*/_jsx(Text, {
100
+ variant: "body-lg",
101
+ render: /*#__PURE__*/_jsx(Link, {
102
+ tone: "neutral",
103
+ render: /*#__PURE__*/_jsx("a", {
104
+ href: item.url
105
+ })
106
+ }),
107
+ children: item.label
108
+ }), Number.isFinite(item.count) && /*#__PURE__*/_jsxs(Text, {
109
+ variant: "body-lg",
110
+ children: [' ', /*#__PURE__*/_jsx(CrumbCount, {
111
+ count: item.count,
112
+ countLabel: item.countLabel
113
+ })]
114
+ })]
54
115
  }) : /*#__PURE__*/_jsx(Text, {
55
116
  variant: "body-lg",
56
- children: item.label
117
+ children: /*#__PURE__*/_jsx(CrumbLabel, _objectSpread({}, item))
57
118
  }), /*#__PURE__*/_jsx(Text, {
58
119
  variant: "body-lg",
59
120
  "aria-hidden": "true",
@@ -66,7 +127,7 @@ var Breadcrumbs = function Breadcrumbs(_ref) {
66
127
  variant: "heading-lg",
67
128
  render: /*#__PURE__*/_jsx("h1", {}),
68
129
  className: "newspack-breadcrumbs__current",
69
- children: last.label
130
+ children: /*#__PURE__*/_jsx(CrumbLabel, _objectSpread({}, last))
70
131
  })
71
132
  })]
72
133
  })
@@ -9,6 +9,10 @@ import { render, screen } from '@testing-library/react';
9
9
  import Breadcrumbs from './';
10
10
  import { jsx as _jsx } from "react/jsx-runtime";
11
11
  describe('Breadcrumbs', function () {
12
+ // Unconditional, so a failing assertion cannot leak a language into later tests.
13
+ afterEach(function () {
14
+ document.documentElement.lang = '';
15
+ });
12
16
  it('renders the last item as the only h1 and never a link', function () {
13
17
  render(/*#__PURE__*/_jsx(Breadcrumbs, {
14
18
  items: [{
@@ -102,4 +106,110 @@ describe('Breadcrumbs', function () {
102
106
  container = _render3.container;
103
107
  expect(container.querySelector('nav')).not.toBeInTheDocument();
104
108
  });
109
+ it('annotates a crumb with its count, hiding the parens from assistive tech', function () {
110
+ var _render4 = render(/*#__PURE__*/_jsx(Breadcrumbs, {
111
+ items: [{
112
+ label: 'Plans'
113
+ }, {
114
+ label: 'Subscriptions',
115
+ count: 33
116
+ }]
117
+ })),
118
+ container = _render4.container;
119
+ var count = container.querySelector('.newspack-breadcrumbs__count');
120
+ expect(count).not.toBeNull();
121
+ expect(count.querySelector('[aria-hidden="true"]')).toHaveTextContent('(33)');
122
+ expect(count.querySelector('.screen-reader-text')).toHaveTextContent('33 items');
123
+ expect(screen.getByRole('heading', {
124
+ level: 1
125
+ })).toContainElement(count);
126
+ });
127
+ it('announces a single item in the singular', function () {
128
+ var _render5 = render(/*#__PURE__*/_jsx(Breadcrumbs, {
129
+ items: [{
130
+ label: 'Donations',
131
+ count: 1
132
+ }]
133
+ })),
134
+ container = _render5.container;
135
+ expect(container.querySelector('.newspack-breadcrumbs__count .screen-reader-text').textContent).toBe('1 item');
136
+ });
137
+ it('renders a zero count rather than dropping the annotation', function () {
138
+ var _render6 = render(/*#__PURE__*/_jsx(Breadcrumbs, {
139
+ items: [{
140
+ label: 'Donations',
141
+ count: 0
142
+ }]
143
+ })),
144
+ container = _render6.container;
145
+ expect(container.querySelector('.newspack-breadcrumbs__count [aria-hidden="true"]')).toHaveTextContent('(0)');
146
+ });
147
+ it('omits the annotation when a crumb carries no usable count', function () {
148
+ var _render7 = render(/*#__PURE__*/_jsx(Breadcrumbs, {
149
+ items: [{
150
+ label: 'Plans'
151
+ }, {
152
+ label: 'Bundles',
153
+ count: undefined
154
+ }]
155
+ })),
156
+ container = _render7.container;
157
+ expect(container.querySelector('.newspack-breadcrumbs__count')).toBeNull();
158
+ expect(screen.getByRole('heading', {
159
+ level: 1
160
+ })).toHaveTextContent('Bundles');
161
+ });
162
+ it('prefers a supplied countLabel for the accessible phrasing', function () {
163
+ var _render8 = render(/*#__PURE__*/_jsx(Breadcrumbs, {
164
+ items: [{
165
+ label: 'Subscribers',
166
+ count: 85,
167
+ countLabel: '85 subscribers total'
168
+ }]
169
+ })),
170
+ container = _render8.container;
171
+ expect(container.querySelector('.newspack-breadcrumbs__count .screen-reader-text')).toHaveTextContent('85 subscribers total');
172
+ });
173
+ it('annotates a linked crumb outside the link, so the link is named for its destination', function () {
174
+ var _render9 = render(/*#__PURE__*/_jsx(Breadcrumbs, {
175
+ items: [{
176
+ label: 'Plans',
177
+ url: '#/',
178
+ count: 12
179
+ }, {
180
+ label: 'Add plan'
181
+ }]
182
+ })),
183
+ container = _render9.container;
184
+ var link = screen.getByRole('link', {
185
+ name: 'Plans'
186
+ });
187
+ expect(link.getAttribute('href')).toBe('#/');
188
+ expect(link).not.toHaveTextContent('(12)');
189
+ var count = container.querySelector('.newspack-breadcrumbs__count');
190
+ expect(count.querySelector('[aria-hidden="true"]')).toHaveTextContent('(12)');
191
+ expect(link).not.toContainElement(count);
192
+ });
193
+ it('groups the count for the document language', function () {
194
+ document.documentElement.lang = 'de-DE';
195
+ var _render0 = render(/*#__PURE__*/_jsx(Breadcrumbs, {
196
+ items: [{
197
+ label: 'Abonnenten',
198
+ count: 1234
199
+ }]
200
+ })),
201
+ container = _render0.container;
202
+ expect(container.querySelector('.newspack-breadcrumbs__count [aria-hidden="true"]')).toHaveTextContent('(1.234)');
203
+ });
204
+ it('falls back to the default grouping when the document language is not a locale Intl accepts', function () {
205
+ document.documentElement.lang = 'pt-PT-ao90';
206
+ var _render1 = render(/*#__PURE__*/_jsx(Breadcrumbs, {
207
+ items: [{
208
+ label: 'Planos',
209
+ count: 1234
210
+ }]
211
+ })),
212
+ container = _render1.container;
213
+ expect(container.querySelector('.newspack-breadcrumbs__count [aria-hidden="true"]').textContent).toMatch(/^\(1\D?234\)$/);
214
+ });
105
215
  });
@@ -1,4 +1,5 @@
1
1
  @use "~@wordpress/base-styles/colors" as wp-colors;
2
+ @use "~@wordpress/base-styles/variables" as wp-vars;
2
3
 
3
4
  .newspack-breadcrumbs__list {
4
5
  list-style: none;
@@ -26,6 +27,11 @@
26
27
  }
27
28
  }
28
29
 
30
+ .newspack-breadcrumbs__count {
31
+ color: var(--wpds-color-foreground-content-neutral-weak, #{wp-colors.$gray-700});
32
+ font-weight: wp-vars.$font-weight-regular;
33
+ }
34
+
29
35
  .newspack-breadcrumbs__separator {
30
36
  color: var(--wpds-color-stroke-surface-neutral, #dbdbdb);
31
37
  margin: 0 var(--wpds-dimension-gap-sm, 8px);
@@ -15,7 +15,7 @@ function _regeneratorDefine2(e, r, n, t) { var i = Object.defineProperty; try {
15
15
  * WordPress dependencies.
16
16
  */
17
17
  import { Button as BaseComponent } from '@wordpress/components';
18
- import { useState } from '@wordpress/element';
18
+ import { useEffect, useState } from '@wordpress/element';
19
19
 
20
20
  /**
21
21
  * Internal dependencies
@@ -24,6 +24,11 @@ import Router from '../proxied-imports/router';
24
24
  import './style.scss';
25
25
  import { jsx as _jsx } from "react/jsx-runtime";
26
26
  var useHistory = Router.useHistory;
27
+ // Control characters are stripped first, as browsers do, so `java\nscript:`
28
+ // cannot slip past.
29
+ var isJavascriptHref = function isJavascriptHref(href) {
30
+ return 'string' === typeof href && href.replace(/[\u0000-\u0020]/g, '').toLowerCase().startsWith('javascript:');
31
+ };
27
32
  var Button = function Button(_ref) {
28
33
  var href = _ref.href,
29
34
  _ref$loading = _ref.loading,
@@ -35,26 +40,45 @@ var Button = function Button(_ref) {
35
40
  _useState2 = _slicedToArray(_useState, 2),
36
41
  isAwaitingOnClick = _useState2[0],
37
42
  setIsAwaitingOnClick = _useState2[1];
43
+ var isUnsafeHref = isJavascriptHref(href);
44
+ var safeHref = isUnsafeHref ? undefined : href;
45
+ useEffect(function () {
46
+ if ('production' === process.env.NODE_ENV || !isUnsafeHref) {
47
+ return;
48
+ }
49
+ // eslint-disable-next-line no-console
50
+ console.warn('Button: a `javascript:` href is not a link and has been dropped.');
51
+ }, [isUnsafeHref]);
38
52
 
39
53
  // If both onClick and href are present, await the onClick action an then redirect.
40
- if (href && onClick) {
41
- otherProps.onClick = /*#__PURE__*/_asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee() {
42
- return _regenerator().w(function (_context) {
43
- while (1) switch (_context.n) {
44
- case 0:
45
- setIsAwaitingOnClick(true);
46
- _context.n = 1;
47
- return onClick();
48
- case 1:
49
- setIsAwaitingOnClick(false);
50
- history.push((href || '').replace('#', ''));
51
- case 2:
52
- return _context.a(2);
53
- }
54
- }, _callee);
55
- }));
54
+ if (safeHref && onClick) {
55
+ otherProps.onClick = /*#__PURE__*/function () {
56
+ var _ref2 = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee(event) {
57
+ return _regenerator().w(function (_context) {
58
+ while (1) switch (_context.n) {
59
+ case 0:
60
+ setIsAwaitingOnClick(true);
61
+ _context.n = 1;
62
+ return onClick(event);
63
+ case 1:
64
+ setIsAwaitingOnClick(false);
65
+ // Outside a Router the href can only mean a real URL.
66
+ if (history) {
67
+ history.push(safeHref.replace('#', ''));
68
+ } else {
69
+ window.location.href = safeHref;
70
+ }
71
+ case 2:
72
+ return _context.a(2);
73
+ }
74
+ }, _callee);
75
+ }));
76
+ return function (_x) {
77
+ return _ref2.apply(this, arguments);
78
+ };
79
+ }();
56
80
  } else {
57
- otherProps.href = href;
81
+ otherProps.href = safeHref;
58
82
  otherProps.onClick = onClick;
59
83
  }
60
84
  if (isAwaitingOnClick) {