newspack-components 4.6.0 → 4.7.0-hotfix-content-gate-force-public-fallback.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 (91) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/dist/cjs/button/index.js +41 -17
  3. package/dist/cjs/button/index.test.js +273 -0
  4. package/dist/cjs/confirm-dialog/index.js +26 -9
  5. package/dist/cjs/confirm-dialog/index.test.js +176 -0
  6. package/dist/cjs/drawer/action.js +74 -0
  7. package/dist/cjs/drawer/body.js +79 -0
  8. package/dist/cjs/drawer/close-icon.js +45 -0
  9. package/dist/cjs/drawer/content.js +72 -0
  10. package/dist/cjs/drawer/context.js +23 -0
  11. package/dist/cjs/drawer/context.test.js +43 -0
  12. package/dist/cjs/drawer/divider.js +24 -0
  13. package/dist/cjs/drawer/footer.js +26 -0
  14. package/dist/cjs/drawer/header.js +26 -0
  15. package/dist/cjs/drawer/index.js +42 -0
  16. package/dist/cjs/drawer/index.test.js +1287 -0
  17. package/dist/cjs/drawer/root.js +350 -0
  18. package/dist/cjs/drawer/slots.test.js +422 -0
  19. package/dist/cjs/drawer/style.scss +205 -0
  20. package/dist/cjs/drawer/title.js +54 -0
  21. package/dist/cjs/drawer/types.js +5 -0
  22. package/dist/cjs/drawer/use-exit-animation.js +59 -0
  23. package/dist/cjs/hooks/use-confirm-dialog.js +15 -3
  24. package/dist/cjs/hooks/use-unsaved-changes-dialog.js +19 -4
  25. package/dist/cjs/index.js +14 -0
  26. package/dist/cjs/page-control/index.js +82 -0
  27. package/dist/cjs/plugin-settings/SettingsSection.js +7 -1
  28. package/dist/cjs/section-header/index.js +8 -8
  29. package/dist/cjs/with-wizard-screen/style.scss +8 -0
  30. package/dist/cjs/wizard/index.js +57 -4
  31. package/dist/esm/button/index.js +42 -18
  32. package/dist/esm/button/index.test.js +273 -0
  33. package/dist/esm/confirm-dialog/index.js +26 -9
  34. package/dist/esm/confirm-dialog/index.test.js +176 -0
  35. package/dist/esm/drawer/action.js +68 -0
  36. package/dist/esm/drawer/body.js +70 -0
  37. package/dist/esm/drawer/close-icon.js +37 -0
  38. package/dist/esm/drawer/content.js +63 -0
  39. package/dist/esm/drawer/context.js +17 -0
  40. package/dist/esm/drawer/context.test.js +40 -0
  41. package/dist/esm/drawer/divider.js +16 -0
  42. package/dist/esm/drawer/footer.js +18 -0
  43. package/dist/esm/drawer/header.js +18 -0
  44. package/dist/esm/drawer/index.js +35 -0
  45. package/dist/esm/drawer/index.test.js +1285 -0
  46. package/dist/esm/drawer/root.js +342 -0
  47. package/dist/esm/drawer/slots.test.js +422 -0
  48. package/dist/esm/drawer/style.scss +205 -0
  49. package/dist/esm/drawer/title.js +46 -0
  50. package/dist/esm/drawer/types.js +1 -0
  51. package/dist/esm/drawer/use-exit-animation.js +52 -0
  52. package/dist/esm/hooks/use-confirm-dialog.js +15 -3
  53. package/dist/esm/hooks/use-unsaved-changes-dialog.js +20 -5
  54. package/dist/esm/index.js +2 -0
  55. package/dist/esm/page-control/index.js +82 -0
  56. package/dist/esm/plugin-settings/SettingsSection.js +8 -2
  57. package/dist/esm/section-header/index.js +8 -8
  58. package/dist/esm/with-wizard-screen/style.scss +8 -0
  59. package/dist/esm/wizard/index.js +58 -5
  60. package/package.json +3 -3
  61. package/src/button/index.test.js +143 -0
  62. package/src/button/index.tsx +32 -7
  63. package/src/confirm-dialog/README.md +3 -2
  64. package/src/confirm-dialog/index.test.js +146 -0
  65. package/src/confirm-dialog/index.tsx +23 -9
  66. package/src/drawer/README.md +454 -0
  67. package/src/drawer/action.tsx +67 -0
  68. package/src/drawer/body.tsx +60 -0
  69. package/src/drawer/close-icon.tsx +37 -0
  70. package/src/drawer/content.tsx +54 -0
  71. package/src/drawer/context.test.js +31 -0
  72. package/src/drawer/context.ts +25 -0
  73. package/src/drawer/divider.tsx +13 -0
  74. package/src/drawer/footer.tsx +15 -0
  75. package/src/drawer/header.tsx +15 -0
  76. package/src/drawer/index.test.js +1012 -0
  77. package/src/drawer/index.tsx +33 -0
  78. package/src/drawer/root.tsx +328 -0
  79. package/src/drawer/slots.test.js +314 -0
  80. package/src/drawer/style.scss +205 -0
  81. package/src/drawer/title.tsx +40 -0
  82. package/src/drawer/types.ts +79 -0
  83. package/src/drawer/use-exit-animation.ts +46 -0
  84. package/src/hooks/use-confirm-dialog.tsx +14 -3
  85. package/src/hooks/use-unsaved-changes-dialog.tsx +17 -5
  86. package/src/index.js +2 -0
  87. package/src/page-control/index.js +63 -0
  88. package/src/plugin-settings/SettingsSection.js +8 -2
  89. package/src/section-header/index.js +8 -7
  90. package/src/with-wizard-screen/style.scss +8 -0
  91. package/src/wizard/index.js +52 -2
package/CHANGELOG.md CHANGED
@@ -1,3 +1,24 @@
1
+ # newspack-components [4.7.0-hotfix-content-gate-force-public-fallback.1](https://github.com/Automattic/newspack-workspace/compare/newspack-components@4.6.0...newspack-components@4.7.0-hotfix-content-gate-force-public-fallback.1) (2026-08-12)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * **content-gate:** surface the Institutions entry point when institutions exist (NPPD-1492, [#708](https://github.com/Automattic/newspack-workspace/issues/708)) ([6ebb8c4](https://github.com/Automattic/newspack-workspace/commit/6ebb8c40f43b3f6d890900b683514eb9e6d65f2f))
7
+
8
+
9
+ ### Features
10
+
11
+ * **access-control:** make Audience Management an explicit prerequisite (NPPD-1846) ([#765](https://github.com/Automattic/newspack-workspace/issues/765)) ([b5efde2](https://github.com/Automattic/newspack-workspace/commit/b5efde2aa3d050bda9fc629f1ed91e4864a91d33))
12
+ * **campaigns:** Contextual Prompts, AI-assisted story donation asks ([#686](https://github.com/Automattic/newspack-workspace/issues/686)) ([f896387](https://github.com/Automattic/newspack-workspace/commit/f896387b5c3c096b6ed0cacbf2fe89b3e7f6b703)), closes [#719](https://github.com/Automattic/newspack-workspace/issues/719)
13
+ * **campaigns:** pick donor landing page via autocomplete search ([#761](https://github.com/Automattic/newspack-workspace/issues/761)) ([01fa615](https://github.com/Automattic/newspack-workspace/commit/01fa61514954742094dff2fd0e5a4ff3bb4c04b6))
14
+ * **components:** add a drawer component with a compound slot api ([#810](https://github.com/Automattic/newspack-workspace/issues/810)) ([5134ae9](https://github.com/Automattic/newspack-workspace/commit/5134ae9cab353bc8e10e239f7d54f8e3abb24fae))
15
+
16
+
17
+ ### Dependencies
18
+
19
+ * **newspack-icons:** upgraded to 1.1.1-hotfix-content-gate-force-public-fallback.1
20
+ * **newspack-scripts:** upgraded to 5.11.0-hotfix-content-gate-force-public-fallback.1
21
+
1
22
  # newspack-components [4.6.0](https://github.com/Automattic/newspack-workspace/compare/newspack-components@4.5.0...newspack-components@4.6.0) (2026-08-03)
2
23
 
3
24
 
@@ -29,6 +29,11 @@ function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t =
29
29
  function _regenerator() { /*! regenerator-runtime -- Copyright (c) 2014-present, Facebook, Inc. -- license (MIT): https://github.com/babel/babel/blob/main/packages/babel-helpers/LICENSE */ var e, t, r = "function" == typeof Symbol ? Symbol : {}, n = r.iterator || "@@iterator", o = r.toStringTag || "@@toStringTag"; function i(r, n, o, i) { var c = n && n.prototype instanceof Generator ? n : Generator, u = Object.create(c.prototype); return _regeneratorDefine2(u, "_invoke", function (r, n, o) { var i, c, u, f = 0, p = o || [], y = !1, G = { p: 0, n: 0, v: e, a: d, f: d.bind(e, 4), d: function d(t, r) { return i = t, c = 0, u = e, G.n = r, a; } }; function d(r, n) { for (c = r, u = n, t = 0; !y && f && !o && t < p.length; t++) { var o, i = p[t], d = G.p, l = i[2]; r > 3 ? (o = l === n) && (u = i[(c = i[4]) ? 5 : (c = 3, 3)], i[4] = i[5] = e) : i[0] <= d && ((o = r < 2 && d < i[1]) ? (c = 0, G.v = n, G.n = i[1]) : d < l && (o = r < 3 || i[0] > n || n > l) && (i[4] = r, i[5] = n, G.n = l, c = 0)); } if (o || r > 1) return a; throw y = !0, n; } return function (o, p, l) { if (f > 1) throw TypeError("Generator is already running"); for (y && 1 === p && d(p, l), c = p, u = l; (t = c < 2 ? e : u) || !y;) { i || (c ? c < 3 ? (c > 1 && (G.n = -1), d(c, u)) : G.n = u : G.v = u); try { if (f = 2, i) { if (c || (o = "next"), t = i[o]) { if (!(t = t.call(i, u))) throw TypeError("iterator result is not an object"); if (!t.done) return t; u = t.value, c < 2 && (c = 0); } else 1 === c && (t = i["return"]) && t.call(i), c < 2 && (u = TypeError("The iterator does not provide a '" + o + "' method"), c = 1); i = e; } else if ((t = (y = G.n < 0) ? u : r.call(n, G)) !== a) break; } catch (t) { i = e, c = 1, u = t; } finally { f = 1; } } return { value: t, done: y }; }; }(r, o, i), !0), u; } var a = {}; function Generator() {} function GeneratorFunction() {} function GeneratorFunctionPrototype() {} t = Object.getPrototypeOf; var c = [][n] ? t(t([][n]())) : (_regeneratorDefine2(t = {}, n, function () { return this; }), t), u = GeneratorFunctionPrototype.prototype = Generator.prototype = Object.create(c); function f(e) { return Object.setPrototypeOf ? Object.setPrototypeOf(e, GeneratorFunctionPrototype) : (e.__proto__ = GeneratorFunctionPrototype, _regeneratorDefine2(e, o, "GeneratorFunction")), e.prototype = Object.create(u), e; } return GeneratorFunction.prototype = GeneratorFunctionPrototype, _regeneratorDefine2(u, "constructor", GeneratorFunctionPrototype), _regeneratorDefine2(GeneratorFunctionPrototype, "constructor", GeneratorFunction), GeneratorFunction.displayName = "GeneratorFunction", _regeneratorDefine2(GeneratorFunctionPrototype, o, "GeneratorFunction"), _regeneratorDefine2(u), _regeneratorDefine2(u, o, "Generator"), _regeneratorDefine2(u, n, function () { return this; }), _regeneratorDefine2(u, "toString", function () { return "[object Generator]"; }), (_regenerator = function _regenerator() { return { w: i, m: f }; })(); }
30
30
  function _regeneratorDefine2(e, r, n, t) { var i = Object.defineProperty; try { i({}, "", {}); } catch (e) { i = 0; } _regeneratorDefine2 = function _regeneratorDefine(e, r, n, t) { function o(r, n) { _regeneratorDefine2(e, r, function (e) { return this._invoke(r, n, e); }); } r ? i ? i(e, r, { value: n, enumerable: !t, configurable: !t, writable: !t }) : e[r] = n : (o("next", 0), o("throw", 1), o("return", 2)); }, _regeneratorDefine2(e, r, n, t); }
31
31
  var useHistory = _router["default"].useHistory;
32
+ // Control characters are stripped first, as browsers do, so `java\nscript:`
33
+ // cannot slip past.
34
+ var isJavascriptHref = function isJavascriptHref(href) {
35
+ return 'string' === typeof href && href.replace(/[\u0000-\u0020]/g, '').toLowerCase().startsWith('javascript:');
36
+ };
32
37
  var Button = function Button(_ref) {
33
38
  var href = _ref.href,
34
39
  _ref$loading = _ref.loading,
@@ -40,26 +45,45 @@ var Button = function Button(_ref) {
40
45
  _useState2 = (0, _slicedToArray2["default"])(_useState, 2),
41
46
  isAwaitingOnClick = _useState2[0],
42
47
  setIsAwaitingOnClick = _useState2[1];
48
+ var isUnsafeHref = isJavascriptHref(href);
49
+ var safeHref = isUnsafeHref ? undefined : href;
50
+ (0, _element.useEffect)(function () {
51
+ if ('production' === process.env.NODE_ENV || !isUnsafeHref) {
52
+ return;
53
+ }
54
+ // eslint-disable-next-line no-console
55
+ console.warn('Button: a `javascript:` href is not a link and has been dropped.');
56
+ }, [isUnsafeHref]);
43
57
 
44
58
  // If both onClick and href are present, await the onClick action an then redirect.
45
- if (href && onClick) {
46
- otherProps.onClick = /*#__PURE__*/(0, _asyncToGenerator2["default"])(/*#__PURE__*/_regenerator().m(function _callee() {
47
- return _regenerator().w(function (_context) {
48
- while (1) switch (_context.n) {
49
- case 0:
50
- setIsAwaitingOnClick(true);
51
- _context.n = 1;
52
- return onClick();
53
- case 1:
54
- setIsAwaitingOnClick(false);
55
- history.push((href || '').replace('#', ''));
56
- case 2:
57
- return _context.a(2);
58
- }
59
- }, _callee);
60
- }));
59
+ if (safeHref && onClick) {
60
+ otherProps.onClick = /*#__PURE__*/function () {
61
+ var _ref2 = (0, _asyncToGenerator2["default"])(/*#__PURE__*/_regenerator().m(function _callee(event) {
62
+ return _regenerator().w(function (_context) {
63
+ while (1) switch (_context.n) {
64
+ case 0:
65
+ setIsAwaitingOnClick(true);
66
+ _context.n = 1;
67
+ return onClick(event);
68
+ case 1:
69
+ setIsAwaitingOnClick(false);
70
+ // Outside a Router the href can only mean a real URL.
71
+ if (history) {
72
+ history.push(safeHref.replace('#', ''));
73
+ } else {
74
+ window.location.href = safeHref;
75
+ }
76
+ case 2:
77
+ return _context.a(2);
78
+ }
79
+ }, _callee);
80
+ }));
81
+ return function (_x) {
82
+ return _ref2.apply(this, arguments);
83
+ };
84
+ }();
61
85
  } else {
62
- otherProps.href = href;
86
+ otherProps.href = safeHref;
63
87
  otherProps.onClick = onClick;
64
88
  }
65
89
  if (isAwaitingOnClick) {
@@ -0,0 +1,273 @@
1
+ "use strict";
2
+
3
+ var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
4
+ var _asyncToGenerator2 = _interopRequireDefault(require("@babel/runtime/helpers/asyncToGenerator"));
5
+ var _react = require("@testing-library/react");
6
+ var _reactRouterDom = require("react-router-dom");
7
+ var _ = _interopRequireDefault(require("./"));
8
+ var _jsxRuntime = require("react/jsx-runtime");
9
+ function _regenerator() { /*! regenerator-runtime -- Copyright (c) 2014-present, Facebook, Inc. -- license (MIT): https://github.com/babel/babel/blob/main/packages/babel-helpers/LICENSE */ var e, t, r = "function" == typeof Symbol ? Symbol : {}, n = r.iterator || "@@iterator", o = r.toStringTag || "@@toStringTag"; function i(r, n, o, i) { var c = n && n.prototype instanceof Generator ? n : Generator, u = Object.create(c.prototype); return _regeneratorDefine2(u, "_invoke", function (r, n, o) { var i, c, u, f = 0, p = o || [], y = !1, G = { p: 0, n: 0, v: e, a: d, f: d.bind(e, 4), d: function d(t, r) { return i = t, c = 0, u = e, G.n = r, a; } }; function d(r, n) { for (c = r, u = n, t = 0; !y && f && !o && t < p.length; t++) { var o, i = p[t], d = G.p, l = i[2]; r > 3 ? (o = l === n) && (u = i[(c = i[4]) ? 5 : (c = 3, 3)], i[4] = i[5] = e) : i[0] <= d && ((o = r < 2 && d < i[1]) ? (c = 0, G.v = n, G.n = i[1]) : d < l && (o = r < 3 || i[0] > n || n > l) && (i[4] = r, i[5] = n, G.n = l, c = 0)); } if (o || r > 1) return a; throw y = !0, n; } return function (o, p, l) { if (f > 1) throw TypeError("Generator is already running"); for (y && 1 === p && d(p, l), c = p, u = l; (t = c < 2 ? e : u) || !y;) { i || (c ? c < 3 ? (c > 1 && (G.n = -1), d(c, u)) : G.n = u : G.v = u); try { if (f = 2, i) { if (c || (o = "next"), t = i[o]) { if (!(t = t.call(i, u))) throw TypeError("iterator result is not an object"); if (!t.done) return t; u = t.value, c < 2 && (c = 0); } else 1 === c && (t = i["return"]) && t.call(i), c < 2 && (u = TypeError("The iterator does not provide a '" + o + "' method"), c = 1); i = e; } else if ((t = (y = G.n < 0) ? u : r.call(n, G)) !== a) break; } catch (t) { i = e, c = 1, u = t; } finally { f = 1; } } return { value: t, done: y }; }; }(r, o, i), !0), u; } var a = {}; function Generator() {} function GeneratorFunction() {} function GeneratorFunctionPrototype() {} t = Object.getPrototypeOf; var c = [][n] ? t(t([][n]())) : (_regeneratorDefine2(t = {}, n, function () { return this; }), t), u = GeneratorFunctionPrototype.prototype = Generator.prototype = Object.create(c); function f(e) { return Object.setPrototypeOf ? Object.setPrototypeOf(e, GeneratorFunctionPrototype) : (e.__proto__ = GeneratorFunctionPrototype, _regeneratorDefine2(e, o, "GeneratorFunction")), e.prototype = Object.create(u), e; } return GeneratorFunction.prototype = GeneratorFunctionPrototype, _regeneratorDefine2(u, "constructor", GeneratorFunctionPrototype), _regeneratorDefine2(GeneratorFunctionPrototype, "constructor", GeneratorFunction), GeneratorFunction.displayName = "GeneratorFunction", _regeneratorDefine2(GeneratorFunctionPrototype, o, "GeneratorFunction"), _regeneratorDefine2(u), _regeneratorDefine2(u, o, "Generator"), _regeneratorDefine2(u, n, function () { return this; }), _regeneratorDefine2(u, "toString", function () { return "[object Generator]"; }), (_regenerator = function _regenerator() { return { w: i, m: f }; })(); }
10
+ function _regeneratorDefine2(e, r, n, t) { var i = Object.defineProperty; try { i({}, "", {}); } catch (e) { i = 0; } _regeneratorDefine2 = function _regeneratorDefine(e, r, n, t) { function o(r, n) { _regeneratorDefine2(e, r, function (e) { return this._invoke(r, n, e); }); } r ? i ? i(e, r, { value: n, enumerable: !t, configurable: !t, writable: !t }) : e[r] = n : (o("next", 0), o("throw", 1), o("return", 2)); }, _regeneratorDefine2(e, r, n, t); } /**
11
+ * External dependencies.
12
+ */ /**
13
+ * Internal dependencies.
14
+ */
15
+ var HistoryGrabber = function HistoryGrabber(_ref) {
16
+ var historyRef = _ref.historyRef;
17
+ historyRef.current = (0, _reactRouterDom.useHistory)();
18
+ return null;
19
+ };
20
+ var clickButton = /*#__PURE__*/function () {
21
+ var _ref2 = (0, _asyncToGenerator2["default"])(/*#__PURE__*/_regenerator().m(function _callee2() {
22
+ return _regenerator().w(function (_context2) {
23
+ while (1) switch (_context2.n) {
24
+ case 0:
25
+ _context2.n = 1;
26
+ return (0, _react.act)(/*#__PURE__*/(0, _asyncToGenerator2["default"])(/*#__PURE__*/_regenerator().m(function _callee() {
27
+ return _regenerator().w(function (_context) {
28
+ while (1) switch (_context.n) {
29
+ case 0:
30
+ _react.screen.getByRole('button', {
31
+ name: 'Save'
32
+ }).click();
33
+ case 1:
34
+ return _context.a(2);
35
+ }
36
+ }, _callee);
37
+ })));
38
+ case 1:
39
+ return _context2.a(2);
40
+ }
41
+ }, _callee2);
42
+ }));
43
+ return function clickButton() {
44
+ return _ref2.apply(this, arguments);
45
+ };
46
+ }();
47
+ describe('Button href with onClick', function () {
48
+ it('awaits the onClick, then pushes the route', /*#__PURE__*/(0, _asyncToGenerator2["default"])(/*#__PURE__*/_regenerator().m(function _callee3() {
49
+ var historyRef, onClick;
50
+ return _regenerator().w(function (_context3) {
51
+ while (1) switch (_context3.n) {
52
+ case 0:
53
+ historyRef = {
54
+ current: null
55
+ };
56
+ onClick = jest.fn().mockResolvedValue(undefined);
57
+ (0, _react.render)(/*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactRouterDom.MemoryRouter, {
58
+ children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(HistoryGrabber, {
59
+ historyRef: historyRef
60
+ }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_["default"], {
61
+ href: "#/next",
62
+ onClick: onClick,
63
+ children: "Save"
64
+ })]
65
+ }));
66
+ _context3.n = 1;
67
+ return clickButton();
68
+ case 1:
69
+ expect(onClick).toHaveBeenCalled();
70
+ // The leading '#' is stripped so the router sees a route, not a fragment.
71
+ expect(historyRef.current.location.pathname).toBe('/next');
72
+ case 2:
73
+ return _context3.a(2);
74
+ }
75
+ }, _callee3);
76
+ })));
77
+ });
78
+
79
+ // `useHistory` returns undefined outside a wizard, leaving no route table for the
80
+ // href to name.
81
+ describe('Button href with onClick outside a router', function () {
82
+ // jsdom's real location logs "Not implemented: navigation" rather than
83
+ // navigating, so swap in a plain object to assert against and put the
84
+ // original back afterwards.
85
+ var withStubbedLocation = /*#__PURE__*/function () {
86
+ var _ref5 = (0, _asyncToGenerator2["default"])(/*#__PURE__*/_regenerator().m(function _callee4(fn) {
87
+ var original;
88
+ return _regenerator().w(function (_context4) {
89
+ while (1) switch (_context4.p = _context4.n) {
90
+ case 0:
91
+ original = window.location;
92
+ delete window.location;
93
+ window.location = {
94
+ href: ''
95
+ };
96
+ _context4.p = 1;
97
+ _context4.n = 2;
98
+ return fn();
99
+ case 2:
100
+ return _context4.a(2, _context4.v);
101
+ case 3:
102
+ _context4.p = 3;
103
+ window.location = original;
104
+ return _context4.f(3);
105
+ case 4:
106
+ return _context4.a(2);
107
+ }
108
+ }, _callee4, null, [[1,, 3, 4]]);
109
+ }));
110
+ return function withStubbedLocation(_x) {
111
+ return _ref5.apply(this, arguments);
112
+ };
113
+ }();
114
+ it('awaits the onClick, then navigates to the href', /*#__PURE__*/(0, _asyncToGenerator2["default"])(/*#__PURE__*/_regenerator().m(function _callee6() {
115
+ var onClick;
116
+ return _regenerator().w(function (_context6) {
117
+ while (1) switch (_context6.n) {
118
+ case 0:
119
+ onClick = jest.fn().mockResolvedValue(undefined);
120
+ _context6.n = 1;
121
+ return withStubbedLocation(/*#__PURE__*/(0, _asyncToGenerator2["default"])(/*#__PURE__*/_regenerator().m(function _callee5() {
122
+ return _regenerator().w(function (_context5) {
123
+ while (1) switch (_context5.n) {
124
+ case 0:
125
+ (0, _react.render)(/*#__PURE__*/(0, _jsxRuntime.jsx)(_["default"], {
126
+ href: "/wp-admin/admin.php?page=next",
127
+ onClick: onClick,
128
+ children: "Save"
129
+ }));
130
+ _context5.n = 1;
131
+ return clickButton();
132
+ case 1:
133
+ expect(onClick).toHaveBeenCalled();
134
+ expect(window.location.href).toBe('/wp-admin/admin.php?page=next');
135
+ case 2:
136
+ return _context5.a(2);
137
+ }
138
+ }, _callee5);
139
+ })));
140
+ case 1:
141
+ return _context6.a(2);
142
+ }
143
+ }, _callee6);
144
+ })));
145
+ it('hands the click event to a combined href and onClick handler', /*#__PURE__*/(0, _asyncToGenerator2["default"])(/*#__PURE__*/_regenerator().m(function _callee8() {
146
+ var onClick;
147
+ return _regenerator().w(function (_context8) {
148
+ while (1) switch (_context8.n) {
149
+ case 0:
150
+ onClick = jest.fn();
151
+ _context8.n = 1;
152
+ return withStubbedLocation(/*#__PURE__*/(0, _asyncToGenerator2["default"])(/*#__PURE__*/_regenerator().m(function _callee7() {
153
+ return _regenerator().w(function (_context7) {
154
+ while (1) switch (_context7.n) {
155
+ case 0:
156
+ (0, _react.render)(/*#__PURE__*/(0, _jsxRuntime.jsx)(_["default"], {
157
+ href: "/wp-admin/admin.php?page=next",
158
+ onClick: onClick,
159
+ children: "Save"
160
+ }));
161
+ _context7.n = 1;
162
+ return clickButton();
163
+ case 1:
164
+ expect(onClick).toHaveBeenCalledWith(expect.objectContaining({
165
+ type: 'click'
166
+ }));
167
+ case 2:
168
+ return _context7.a(2);
169
+ }
170
+ }, _callee7);
171
+ })));
172
+ case 1:
173
+ return _context8.a(2);
174
+ }
175
+ }, _callee8);
176
+ })));
177
+ it('runs an onClick with no href without touching navigation', /*#__PURE__*/(0, _asyncToGenerator2["default"])(/*#__PURE__*/_regenerator().m(function _callee0() {
178
+ var onClick;
179
+ return _regenerator().w(function (_context0) {
180
+ while (1) switch (_context0.n) {
181
+ case 0:
182
+ onClick = jest.fn();
183
+ _context0.n = 1;
184
+ return withStubbedLocation(/*#__PURE__*/(0, _asyncToGenerator2["default"])(/*#__PURE__*/_regenerator().m(function _callee9() {
185
+ return _regenerator().w(function (_context9) {
186
+ while (1) switch (_context9.n) {
187
+ case 0:
188
+ (0, _react.render)(/*#__PURE__*/(0, _jsxRuntime.jsx)(_["default"], {
189
+ onClick: onClick,
190
+ children: "Save"
191
+ }));
192
+ _context9.n = 1;
193
+ return clickButton();
194
+ case 1:
195
+ expect(onClick).toHaveBeenCalled();
196
+ expect(window.location.href).toBe('');
197
+ case 2:
198
+ return _context9.a(2);
199
+ }
200
+ }, _callee9);
201
+ })));
202
+ case 1:
203
+ return _context0.a(2);
204
+ }
205
+ }, _callee0);
206
+ })));
207
+ it('drops a javascript: href and still runs the onClick', /*#__PURE__*/(0, _asyncToGenerator2["default"])(/*#__PURE__*/_regenerator().m(function _callee10() {
208
+ var warn, onClick;
209
+ return _regenerator().w(function (_context10) {
210
+ while (1) switch (_context10.n) {
211
+ case 0:
212
+ warn = jest.spyOn(console, 'warn').mockImplementation(function () {});
213
+ onClick = jest.fn().mockResolvedValue(undefined);
214
+ _context10.n = 1;
215
+ return withStubbedLocation(/*#__PURE__*/(0, _asyncToGenerator2["default"])(/*#__PURE__*/_regenerator().m(function _callee1() {
216
+ return _regenerator().w(function (_context1) {
217
+ while (1) switch (_context1.n) {
218
+ case 0:
219
+ // eslint-disable-next-line no-script-url
220
+ (0, _react.render)(/*#__PURE__*/(0, _jsxRuntime.jsx)(_["default"], {
221
+ href: "javascript:alert(1)",
222
+ onClick: onClick,
223
+ children: "Save"
224
+ }));
225
+ _context1.n = 1;
226
+ return clickButton();
227
+ case 1:
228
+ expect(onClick).toHaveBeenCalled();
229
+ expect(window.location.href).toBe('');
230
+ case 2:
231
+ return _context1.a(2);
232
+ }
233
+ }, _callee1);
234
+ })));
235
+ case 1:
236
+ expect(warn).toHaveBeenCalledWith(expect.stringContaining('javascript:'));
237
+ warn.mockRestore();
238
+ case 2:
239
+ return _context10.a(2);
240
+ }
241
+ }, _callee10);
242
+ })));
243
+ });
244
+ describe('Button with a javascript: href alone', function () {
245
+ var link = function link() {
246
+ return _react.screen.getByText('Save').closest('a');
247
+ };
248
+ it('renders no link', function () {
249
+ var warn = jest.spyOn(console, 'warn').mockImplementation(function () {});
250
+ (0, _react.render)(/*#__PURE__*/(0, _jsxRuntime.jsx)(_["default"], {
251
+ href: "JavaScript:alert(1)",
252
+ children: "Save"
253
+ }));
254
+ expect(link()).toBeNull();
255
+ warn.mockRestore();
256
+ });
257
+ it('sees through interleaved control characters', function () {
258
+ var warn = jest.spyOn(console, 'warn').mockImplementation(function () {});
259
+ (0, _react.render)(/*#__PURE__*/(0, _jsxRuntime.jsx)(_["default"], {
260
+ href: 'java\nscript:alert(1)',
261
+ children: "Save"
262
+ }));
263
+ expect(link()).toBeNull();
264
+ warn.mockRestore();
265
+ });
266
+ it('leaves an ordinary href alone', function () {
267
+ (0, _react.render)(/*#__PURE__*/(0, _jsxRuntime.jsx)(_["default"], {
268
+ href: "/wp-admin/admin.php?page=next",
269
+ children: "Save"
270
+ }));
271
+ expect(link()).toHaveAttribute('href', '/wp-admin/admin.php?page=next');
272
+ });
273
+ });
@@ -79,21 +79,26 @@ function ConfirmDialog(_ref, ref) {
79
79
  }, [onConfirm, pendingNavigation]);
80
80
  var handleOnCancel = (0, _element.useCallback)(function () {
81
81
  setShowDialog(false);
82
+ var hadPendingNavigation = !!pendingNavigation.current;
82
83
  pendingNavigation.current = null;
83
84
  // A POP may have moved the URL to the target before it was blocked; put
84
85
  // it back so the address bar matches the page the user chose to stay on.
85
- bypassBlock.current = true;
86
- try {
87
- history.replace(history.location);
88
- } finally {
89
- bypassBlock.current = false;
86
+ // Only when this instance blocked it: v5 keeps one prompt slot, so an
87
+ // unprompted replace is caught by whichever dialog did install a blocker.
88
+ if (hadPendingNavigation && history) {
89
+ bypassBlock.current = true;
90
+ try {
91
+ history.replace(history.location);
92
+ } finally {
93
+ bypassBlock.current = false;
94
+ }
90
95
  }
91
96
  onCancel();
92
97
  }, [onCancel, history]);
93
98
 
94
99
  // Block navigation when there are unsaved changes.
95
100
  (0, _element.useEffect)(function () {
96
- if (!when) {
101
+ if (!when || !history) {
97
102
  return;
98
103
  }
99
104
  var unblock = history.block(function (location, action) {
@@ -124,10 +129,22 @@ function ConfirmDialog(_ref, ref) {
124
129
  return unblock;
125
130
  }, [when, history]);
126
131
 
127
- // Show the dialog imperatively without blocking navigation.
132
+ // During render: a commit later the dialog would still hold focus while the
133
+ // caller unmounted. The blocker above raises it without touching `isOpen`.
134
+ var _useState3 = (0, _element.useState)(isOpen),
135
+ _useState4 = (0, _slicedToArray2["default"])(_useState3, 2),
136
+ wasOpen = _useState4[0],
137
+ setWasOpen = _useState4[1];
138
+ if (wasOpen !== isOpen) {
139
+ setWasOpen(isOpen);
140
+ setShowDialog(isOpen);
141
+ }
142
+
143
+ // A withdrawn prompt releases what the blocker held, as confirm and cancel do.
144
+ // After the commit, so a discarded render cannot drop a live one.
128
145
  (0, _element.useEffect)(function () {
129
- if (isOpen) {
130
- setShowDialog(true);
146
+ if (!isOpen) {
147
+ pendingNavigation.current = null;
131
148
  }
132
149
  }, [isOpen]);
133
150
  if (!showDialog) {
@@ -65,6 +65,34 @@ describe('ConfirmDialog navigation blocking', function () {
65
65
  expect(replaceSpy).toHaveBeenCalledTimes(1);
66
66
  expect(history.location.pathname).toBe('/');
67
67
  });
68
+
69
+ // v5 keeps a single prompt slot, so an unprompted re-sync would be caught by
70
+ // whichever dialog on the screen did install a blocker.
71
+ it('cancels an unprompted dialog without tripping another blocker', function () {
72
+ var historyRef = {
73
+ current: null
74
+ };
75
+ (0, _react.render)(/*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactRouterDom.MemoryRouter, {
76
+ children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(HistoryGrabber, {
77
+ historyRef: historyRef
78
+ }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_["default"], {
79
+ when: true,
80
+ confirmButtonText: "Leave",
81
+ cancelButtonText: "Stay",
82
+ children: "Wizard guard"
83
+ }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_["default"], {
84
+ isOpen: true,
85
+ confirmButtonText: "Discard changes",
86
+ cancelButtonText: "Keep editing",
87
+ children: "Unsaved changes"
88
+ })]
89
+ }));
90
+ var replaceSpy = jest.spyOn(historyRef.current, 'replace');
91
+ cancelNavigation();
92
+ expect(dialog()).not.toBeInTheDocument();
93
+ expect(_react.screen.queryByText('Wizard guard')).not.toBeInTheDocument();
94
+ expect(replaceSpy).not.toHaveBeenCalled();
95
+ });
68
96
  it('replays a blocked push on confirm: re-sync replace first, then push, and re-arms the blocker', function () {
69
97
  var history = renderWithHistory();
70
98
  var replaceSpy = jest.spyOn(history, 'replace');
@@ -170,4 +198,152 @@ describe('ConfirmDialog navigation blocking', function () {
170
198
  expect(history.location.pathname).toBe('/');
171
199
  expect(dialog()).toBeInTheDocument();
172
200
  });
201
+ });
202
+
203
+ // `useHistory` returns undefined outside a wizard, where `newspack-multibranded-site`
204
+ // runs react-router v6 against this package's v5.
205
+ describe('ConfirmDialog without a router', function () {
206
+ var renderUnrouted = function renderUnrouted() {
207
+ var props = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
208
+ return (0, _react.render)(/*#__PURE__*/(0, _jsxRuntime.jsx)(_["default"], _objectSpread(_objectSpread({
209
+ isOpen: true,
210
+ confirmButtonText: "Discard changes",
211
+ cancelButtonText: "Keep editing"
212
+ }, props), {}, {
213
+ children: "Unsaved changes"
214
+ })));
215
+ };
216
+ it('cancels with no history to re-sync', function () {
217
+ var onCancel = jest.fn();
218
+ renderUnrouted({
219
+ onCancel: onCancel
220
+ });
221
+ expect(dialog()).toBeInTheDocument();
222
+ cancelNavigation();
223
+ expect(onCancel).toHaveBeenCalled();
224
+ expect(dialog()).not.toBeInTheDocument();
225
+ });
226
+ it('confirms with no history to re-sync', function () {
227
+ var onConfirm = jest.fn();
228
+ renderUnrouted({
229
+ onConfirm: onConfirm
230
+ });
231
+ confirmNavigation();
232
+ expect(onConfirm).toHaveBeenCalled();
233
+ expect(dialog()).not.toBeInTheDocument();
234
+ });
235
+ it('installs no blocker when asked to block without a router', function () {
236
+ expect(function () {
237
+ return renderUnrouted({
238
+ when: true
239
+ });
240
+ }).not.toThrow();
241
+ expect(dialog()).toBeInTheDocument();
242
+ });
243
+ });
244
+ describe('ConfirmDialog controlled by isOpen', function () {
245
+ var tree = function tree(isOpen) {
246
+ return /*#__PURE__*/(0, _jsxRuntime.jsx)(_["default"], {
247
+ isOpen: isOpen,
248
+ confirmButtonText: "Discard changes",
249
+ cancelButtonText: "Keep editing",
250
+ children: "Unsaved changes"
251
+ });
252
+ };
253
+ it('closes when isOpen goes false', function () {
254
+ var _render = (0, _react.render)(tree(true)),
255
+ rerender = _render.rerender;
256
+ expect(dialog()).toBeInTheDocument();
257
+ rerender(tree(false));
258
+ expect(dialog()).not.toBeInTheDocument();
259
+ });
260
+ it('opens again afterwards', function () {
261
+ var _render2 = (0, _react.render)(tree(true)),
262
+ rerender = _render2.rerender;
263
+ rerender(tree(false));
264
+ rerender(tree(true));
265
+ expect(dialog()).toBeInTheDocument();
266
+ });
267
+ it('leaves a blocked navigation prompt up', function () {
268
+ var historyRef = {
269
+ current: null
270
+ };
271
+ (0, _react.render)(/*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactRouterDom.MemoryRouter, {
272
+ children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(HistoryGrabber, {
273
+ historyRef: historyRef
274
+ }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_["default"], {
275
+ when: true,
276
+ isOpen: false,
277
+ confirmButtonText: "Discard changes",
278
+ cancelButtonText: "Keep editing",
279
+ children: "Unsaved changes"
280
+ })]
281
+ }));
282
+ (0, _react.act)(function () {
283
+ return historyRef.current.push('/next');
284
+ });
285
+ expect(dialog()).toBeInTheDocument();
286
+ });
287
+ it('withdraws a blocked navigation prompt when isOpen goes false', function () {
288
+ var historyRef = {
289
+ current: null
290
+ };
291
+ var withOpen = function withOpen(isOpen) {
292
+ return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactRouterDom.MemoryRouter, {
293
+ children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(HistoryGrabber, {
294
+ historyRef: historyRef
295
+ }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_["default"], {
296
+ when: true,
297
+ isOpen: isOpen,
298
+ confirmButtonText: "Discard changes",
299
+ cancelButtonText: "Keep editing",
300
+ children: "Unsaved changes"
301
+ })]
302
+ });
303
+ };
304
+ var _render3 = (0, _react.render)(withOpen(true)),
305
+ rerender = _render3.rerender;
306
+ (0, _react.act)(function () {
307
+ return historyRef.current.push('/next');
308
+ });
309
+ expect(dialog()).toBeInTheDocument();
310
+ rerender(withOpen(false));
311
+ expect(dialog()).not.toBeInTheDocument();
312
+ expect(historyRef.current.location.pathname).toBe('/');
313
+
314
+ // Confirming the next prompt must not replay the abandoned destination.
315
+ rerender(withOpen(true));
316
+ confirmNavigation();
317
+ expect(historyRef.current.location.pathname).toBe('/');
318
+ });
319
+ });
320
+ describe('ConfirmDialog named by a hidden title', function () {
321
+ it('takes its accessible name from the title while hiding the header', function () {
322
+ (0, _react.render)(/*#__PURE__*/(0, _jsxRuntime.jsx)(_["default"], {
323
+ isOpen: true,
324
+ hideTitle: true,
325
+ title: "Unsaved changes",
326
+ confirmButtonText: "Discard changes",
327
+ children: "You have unsaved changes that will be lost. Discard changes?"
328
+ }));
329
+ var frame = _react.screen.getByRole('dialog', {
330
+ name: 'Unsaved changes'
331
+ });
332
+ expect(frame).toHaveClass('newspack-modal--hide-title');
333
+ expect(frame.querySelector('.components-modal__header')).toContainElement(_react.screen.getByRole('heading', {
334
+ name: 'Unsaved changes'
335
+ }));
336
+ });
337
+ it('has no accessible name without a title', function () {
338
+ (0, _react.render)(/*#__PURE__*/(0, _jsxRuntime.jsx)(_["default"], {
339
+ isOpen: true,
340
+ hideTitle: true,
341
+ confirmButtonText: "Discard changes",
342
+ children: "You have unsaved changes that will be lost. Discard changes?"
343
+ }));
344
+ expect(_react.screen.queryByRole('dialog', {
345
+ name: 'Unsaved changes'
346
+ })).not.toBeInTheDocument();
347
+ expect(_react.screen.getByRole('dialog')).not.toHaveAttribute('aria-labelledby');
348
+ });
173
349
  });