newspack-components 4.5.0 → 4.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (138) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/DEVELOPMENT.md +1 -1
  3. package/dist/cjs/accordion/index.js +43 -26
  4. package/dist/cjs/accordion/index.test.js +80 -0
  5. package/dist/cjs/accordion/style.scss +25 -21
  6. package/dist/cjs/action-card/style.scss +0 -22
  7. package/dist/cjs/breadcrumbs/index.js +80 -0
  8. package/dist/cjs/breadcrumbs/index.test.js +109 -0
  9. package/dist/cjs/breadcrumbs/style.scss +41 -0
  10. package/dist/cjs/card/core-card.js +8 -4
  11. package/dist/cjs/card/index.js +1 -1
  12. package/dist/cjs/card/style-core.scss +14 -0
  13. package/dist/cjs/card/style.scss +0 -9
  14. package/dist/cjs/card-feature/index.js +25 -13
  15. package/dist/cjs/card-settings-group/index.js +3 -0
  16. package/dist/cjs/confirm-dialog/index.js +34 -8
  17. package/dist/cjs/confirm-dialog/index.test.js +173 -0
  18. package/dist/cjs/divider/style.scss +4 -2
  19. package/dist/cjs/grid/style.scss +5 -0
  20. package/dist/cjs/handoff/index.js +8 -5
  21. package/dist/cjs/index.js +34 -1
  22. package/dist/cjs/integration-icon/index.js +40 -0
  23. package/dist/cjs/integration-icon/style.scss +42 -0
  24. package/dist/cjs/integration-icons/index.js +21 -2
  25. package/dist/cjs/modal/style.scss +4 -0
  26. package/dist/cjs/page/index.js +121 -0
  27. package/dist/cjs/page/index.test.js +69 -0
  28. package/dist/cjs/page/style.scss +86 -0
  29. package/dist/cjs/route-match.js +59 -0
  30. package/dist/cjs/route-match.test.js +77 -0
  31. package/dist/cjs/section-header/index.js +8 -1
  32. package/dist/cjs/section-header/style.scss +11 -11
  33. package/dist/cjs/select-control/GroupedSelectControl.js +1 -1
  34. package/dist/cjs/select-control/style.scss +4 -0
  35. package/dist/cjs/style.scss +1 -4
  36. package/dist/cjs/tabbed-navigation/index.js +188 -40
  37. package/dist/cjs/tabbed-navigation/index.test.js +299 -0
  38. package/dist/cjs/tabbed-navigation/style.scss +77 -57
  39. package/dist/cjs/with-wizard/index.js +34 -45
  40. package/dist/cjs/with-wizard-screen/index.js +82 -41
  41. package/dist/cjs/with-wizard-screen/index.test.js +50 -0
  42. package/dist/cjs/with-wizard-screen/style.scss +15 -118
  43. package/dist/cjs/wizard/breadcrumbs-select.js +70 -0
  44. package/dist/cjs/wizard/breadcrumbs-select.test.js +200 -0
  45. package/dist/cjs/wizard/components/WizardError.js +8 -4
  46. package/dist/cjs/wizard/index.js +162 -141
  47. package/dist/esm/accordion/index.js +42 -26
  48. package/dist/esm/accordion/index.test.js +79 -0
  49. package/dist/esm/accordion/style.scss +25 -21
  50. package/dist/esm/action-card/style.scss +0 -22
  51. package/dist/esm/breadcrumbs/index.js +75 -0
  52. package/dist/esm/breadcrumbs/index.test.js +105 -0
  53. package/dist/esm/breadcrumbs/style.scss +41 -0
  54. package/dist/esm/card/core-card.js +8 -4
  55. package/dist/esm/card/index.js +1 -1
  56. package/dist/esm/card/style-core.scss +14 -0
  57. package/dist/esm/card/style.scss +0 -9
  58. package/dist/esm/card-feature/index.js +25 -13
  59. package/dist/esm/card-settings-group/index.js +3 -0
  60. package/dist/esm/confirm-dialog/index.js +34 -8
  61. package/dist/esm/confirm-dialog/index.test.js +173 -0
  62. package/dist/esm/divider/style.scss +4 -2
  63. package/dist/esm/grid/style.scss +5 -0
  64. package/dist/esm/handoff/index.js +7 -5
  65. package/dist/esm/index.js +5 -1
  66. package/dist/esm/integration-icon/index.js +34 -0
  67. package/dist/esm/integration-icon/style.scss +42 -0
  68. package/dist/esm/integration-icons/index.js +22 -1
  69. package/dist/esm/modal/style.scss +4 -0
  70. package/dist/esm/page/index.js +113 -0
  71. package/dist/esm/page/index.test.js +65 -0
  72. package/dist/esm/page/style.scss +86 -0
  73. package/dist/esm/route-match.js +51 -0
  74. package/dist/esm/route-match.test.js +74 -0
  75. package/dist/esm/section-header/index.js +8 -1
  76. package/dist/esm/section-header/style.scss +11 -11
  77. package/dist/esm/select-control/GroupedSelectControl.js +1 -1
  78. package/dist/esm/select-control/style.scss +4 -0
  79. package/dist/esm/style.scss +1 -4
  80. package/dist/esm/tabbed-navigation/index.js +192 -38
  81. package/dist/esm/tabbed-navigation/index.test.js +296 -0
  82. package/dist/esm/tabbed-navigation/style.scss +77 -57
  83. package/dist/esm/with-wizard/index.js +33 -45
  84. package/dist/esm/with-wizard-screen/index.js +78 -41
  85. package/dist/esm/with-wizard-screen/index.test.js +46 -0
  86. package/dist/esm/with-wizard-screen/style.scss +15 -118
  87. package/dist/esm/wizard/breadcrumbs-select.js +63 -0
  88. package/dist/esm/wizard/breadcrumbs-select.test.js +197 -0
  89. package/dist/esm/wizard/components/WizardError.js +7 -5
  90. package/dist/esm/wizard/index.js +165 -144
  91. package/package.json +10 -2
  92. package/src/accordion/index.js +27 -15
  93. package/src/accordion/index.test.js +62 -0
  94. package/src/accordion/style.scss +25 -21
  95. package/src/action-card/style.scss +0 -22
  96. package/src/breadcrumbs/index.js +63 -0
  97. package/src/breadcrumbs/index.test.js +59 -0
  98. package/src/breadcrumbs/style.scss +41 -0
  99. package/src/card/core-card.js +11 -6
  100. package/src/card/index.js +0 -1
  101. package/src/card/style-core.scss +14 -0
  102. package/src/card/style.scss +0 -9
  103. package/src/card-feature/README.md +3 -3
  104. package/src/card-feature/index.tsx +37 -21
  105. package/src/card-settings-group/index.tsx +4 -0
  106. package/src/card-sortable-list/index.tsx +1 -1
  107. package/src/confirm-dialog/index.test.js +142 -0
  108. package/src/confirm-dialog/index.tsx +34 -7
  109. package/src/divider/style.scss +4 -2
  110. package/src/grid/style.scss +5 -0
  111. package/src/handoff/index.js +4 -3
  112. package/src/index.js +5 -1
  113. package/src/integration-icon/index.tsx +35 -0
  114. package/src/integration-icon/style.scss +42 -0
  115. package/src/integration-icons/index.js +23 -0
  116. package/src/modal/style.scss +4 -0
  117. package/src/page/index.js +89 -0
  118. package/src/page/index.test.js +43 -0
  119. package/src/page/style.scss +86 -0
  120. package/src/route-match.js +47 -0
  121. package/src/route-match.test.js +50 -0
  122. package/src/section-header/index.js +7 -3
  123. package/src/section-header/style.scss +11 -11
  124. package/src/select-control/GroupedSelectControl.js +1 -1
  125. package/src/select-control/style.scss +4 -0
  126. package/src/style.scss +1 -4
  127. package/src/tabbed-navigation/index.js +181 -34
  128. package/src/tabbed-navigation/index.test.js +228 -0
  129. package/src/tabbed-navigation/style.scss +77 -57
  130. package/src/with-wizard/index.js +18 -39
  131. package/src/with-wizard-screen/index.js +70 -35
  132. package/src/with-wizard-screen/index.test.js +36 -0
  133. package/src/with-wizard-screen/style.scss +15 -118
  134. package/src/wizard/breadcrumbs-select.js +51 -0
  135. package/src/wizard/breadcrumbs-select.test.js +124 -0
  136. package/src/wizard/components/WizardError.js +4 -3
  137. package/src/wizard/index.js +160 -146
  138. package/tsconfig.json +7 -0
@@ -4,70 +4,218 @@ 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.isItemActive = exports["default"] = void 0;
8
+ var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty"));
9
+ var _i18n = require("@wordpress/i18n");
10
+ var _ui = require("@wordpress/ui");
8
11
  var _classnames = _interopRequireDefault(require("classnames"));
9
- var _findIndex = _interopRequireDefault(require("lodash/findIndex"));
10
12
  var _router = _interopRequireDefault(require("../proxied-imports/router"));
13
+ var _routeMatch = require("../route-match");
11
14
  require("./style.scss");
12
15
  var _jsxRuntime = require("react/jsx-runtime");
13
- /**
16
+ 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; }
17
+ 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; } /**
18
+ * WordPress dependencies.
19
+ */ /**
14
20
  * External dependencies.
21
+ */ /**
22
+ * Internal dependencies.
15
23
  */
24
+ var useHistory = _router["default"].useHistory,
25
+ useLocation = _router["default"].useLocation;
26
+ var getItemValue = function getItemValue(item, index) {
27
+ return item.path || item.href || "item-".concat(index);
28
+ };
29
+ var isItemActive = exports.isItemActive = function isItemActive(item, pathname) {
30
+ if (item.selected) {
31
+ return true;
32
+ }
33
+ if (null === pathname) {
34
+ return Boolean(item.href) && window.location.href === item.href;
35
+ }
36
+ return (0, _routeMatch.matchesRoute)(item, pathname);
37
+ };
16
38
 
17
39
  /**
18
- * Internal dependencies.
40
+ * Default layout when `Page` doesn't inject one: bar above content.
19
41
  */
42
+ var defaultRenderShell = function defaultRenderShell(bar, content) {
43
+ return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_jsxRuntime.Fragment, {
44
+ children: [bar, content]
45
+ });
46
+ };
20
47
 
21
- var NavLink = _router["default"].NavLink,
22
- useHistory = _router["default"].useHistory;
23
- var TabbedNavigation = function TabbedNavigation(_ref) {
48
+ /**
49
+ * Router-driven tabs: a real WAI-ARIA tabs widget. The routed page content
50
+ * renders inside the active tab's `Tabs.Panel`, so each tab's `aria-controls`
51
+ * points at the panel that actually contains its content — or, when no visible
52
+ * tab owns the route, as a sibling of the panels (see `unownedContent` below).
53
+ */
54
+ var RoutedTabbedNavigation = function RoutedTabbedNavigation(_ref) {
24
55
  var items = _ref.items,
25
56
  className = _ref.className,
26
57
  disableUpcoming = _ref.disableUpcoming,
58
+ _ref$content = _ref.content,
59
+ content = _ref$content === void 0 ? null : _ref$content,
60
+ _ref$renderShell = _ref.renderShell,
61
+ renderShell = _ref$renderShell === void 0 ? defaultRenderShell : _ref$renderShell,
27
62
  _ref$children = _ref.children,
28
63
  children = _ref$children === void 0 ? null : _ref$children;
64
+ var history = useHistory();
65
+ var _useLocation = useLocation(),
66
+ pathname = _useLocation.pathname;
29
67
  var displayedItems = items.filter(function (item) {
30
68
  return !item.isHiddenInTabbedNavigation;
31
69
  });
32
- var _useHistory = useHistory(),
33
- location = _useHistory.location;
34
- var currentIndex = (0, _findIndex["default"])(displayedItems, ['path', location.pathname]);
35
- function _isActive(item, match, pathname) {
36
- if (item.path === pathname) {
37
- return true;
70
+
71
+ // Most-specific match wins, so a tab is never outranked by a sibling that
72
+ // happens to prefix-match the same pathname.
73
+ var activeIndex = displayedItems.reduce(function (best, item, index) {
74
+ var _item$path, _displayedItems$best$;
75
+ if (!isItemActive(item, pathname)) {
76
+ return best;
38
77
  }
39
- if (Array.isArray(item === null || item === void 0 ? void 0 : item.activeTabPaths)) {
40
- return item.activeTabPaths.some(function (path) {
41
- if (path.endsWith('*')) {
42
- var basePath = path.slice(0, -1);
43
- return pathname.startsWith(basePath);
44
- }
45
- return item.activeTabPaths.includes(pathname);
46
- });
78
+ if (best === -1 || (((_item$path = item.path) === null || _item$path === void 0 ? void 0 : _item$path.length) || 0) > (((_displayedItems$best$ = displayedItems[best].path) === null || _displayedItems$best$ === void 0 ? void 0 : _displayedItems$best$.length) || 0)) {
79
+ return index;
47
80
  }
48
- return match;
49
- }
50
- return /*#__PURE__*/(0, _jsxRuntime.jsxs)("div", {
81
+ return best;
82
+ }, -1);
83
+ var activeValue = activeIndex > -1 ? getItemValue(displayedItems[activeIndex], activeIndex) : null;
84
+ var onTabClick = function onTabClick(event, item) {
85
+ // Modified clicks (new tab/window) keep native anchor behavior. Middle
86
+ // clicks never reach onClick (they dispatch auxclick), so the anchor's
87
+ // href handles those natively too.
88
+ if (event.defaultPrevented || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) {
89
+ return;
90
+ }
91
+ // Route through the history object so navigation guards registered via
92
+ // history.block() (unsaved-changes prompts) are consulted.
93
+ event.preventDefault();
94
+ if (item.path !== pathname) {
95
+ history.push(item.path);
96
+ }
97
+ };
98
+ var bar = /*#__PURE__*/(0, _jsxRuntime.jsxs)("div", {
51
99
  className: (0, _classnames["default"])('newspack-tabbed-navigation', className),
52
- children: [/*#__PURE__*/(0, _jsxRuntime.jsx)("ul", {
100
+ children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_ui.Tabs.List, {
101
+ activateOnFocus: false,
53
102
  children: displayedItems.map(function (item, index) {
54
- return /*#__PURE__*/(0, _jsxRuntime.jsx)("li", {
55
- children: /*#__PURE__*/(0, _jsxRuntime.jsx)(NavLink, {
56
- to: item.path,
57
- isActive: function isActive(match, _ref2) {
58
- var pathname = _ref2.pathname;
59
- return _isActive(item, match, pathname);
60
- },
61
- exact: item.hasOwnProperty('exact') ? item.exact : true,
62
- activeClassName: 'selected',
63
- className: (0, _classnames["default"])({
64
- disabled: disableUpcoming && index > currentIndex
65
- }),
66
- children: item.label
67
- })
68
- }, index);
103
+ var isDisabled = disableUpcoming && index > activeIndex;
104
+ var href = item.path ? "#".concat(item.path) : item.href;
105
+ return /*#__PURE__*/(0, _jsxRuntime.jsx)(_ui.Tabs.Tab, {
106
+ value: getItemValue(item, index),
107
+ disabled: isDisabled,
108
+ nativeButton: false,
109
+ render:
110
+ /*#__PURE__*/
111
+ // eslint-disable-next-line jsx-a11y/anchor-has-content, jsx-a11y/anchor-is-valid -- content is supplied via the Tab children through @wordpress/ui's render prop, and disabled tabs intentionally drop the href.
112
+ (0, _jsxRuntime.jsx)("a", {
113
+ href: isDisabled ? undefined : href,
114
+ onClick: isDisabled || !item.path ? undefined : function (event) {
115
+ return onTabClick(event, item);
116
+ }
117
+ }),
118
+ children: item.label
119
+ }, getItemValue(item, index));
69
120
  })
70
121
  }), children]
71
122
  });
123
+
124
+ // Panels must stay 1:1 with the tabs above (@wordpress/ui validates the
125
+ // pairing in development builds); only the active panel holds the content.
126
+ var panels = displayedItems.map(function (item, index) {
127
+ var value = getItemValue(item, index);
128
+ return /*#__PURE__*/(0, _jsxRuntime.jsx)(_ui.Tabs.Panel, {
129
+ value: value,
130
+ tabIndex: -1,
131
+ className: "newspack-tabbed-navigation__panel",
132
+ children: value === activeValue ? content : null
133
+ }, value);
134
+ });
135
+
136
+ // A route no visible tab owns — a hidden sub-view (an edit screen), or a path
137
+ // with no matching tab at all — still has to render its content, outside the
138
+ // panels: with no active tab there is no panel entitled to hold it. Without
139
+ // this the page body is blank, and because the router's own fallback (the
140
+ // wizard's trailing `<Redirect>`) lives inside that content, it never mounts
141
+ // to correct the route either.
142
+ //
143
+ // This renders without the panels' `__panel` class, which is styleless today.
144
+ // Keep it that way: styling `__panel` would silently indent/pad owned routes
145
+ // only, and the divergence would be hard to trace back to here.
146
+ var unownedContent = null === activeValue ? content : null;
147
+ return /*#__PURE__*/(0, _jsxRuntime.jsx)(_ui.Tabs.Root, {
148
+ value: activeValue,
149
+ className: "newspack-tabbed-navigation__root",
150
+ children: renderShell(bar, /*#__PURE__*/(0, _jsxRuntime.jsxs)(_jsxRuntime.Fragment, {
151
+ children: [panels, unownedContent]
152
+ }))
153
+ });
154
+ };
155
+
156
+ /**
157
+ * Link-driven "tabs": plain navigation for items that trigger full page loads
158
+ * (e.g. the PHP-rendered wizards admin header). These are not tabs in the ARIA
159
+ * sense — there is no in-page panel to control — so they render as a `nav` of
160
+ * links with `aria-current`, styled to match the tabs widget.
161
+ */
162
+ var LinkTabbedNavigation = function LinkTabbedNavigation(_ref2) {
163
+ var items = _ref2.items,
164
+ className = _ref2.className,
165
+ _ref2$content = _ref2.content,
166
+ content = _ref2$content === void 0 ? null : _ref2$content,
167
+ _ref2$renderShell = _ref2.renderShell,
168
+ renderShell = _ref2$renderShell === void 0 ? defaultRenderShell : _ref2$renderShell,
169
+ _ref2$children = _ref2.children,
170
+ children = _ref2$children === void 0 ? null : _ref2$children;
171
+ var displayedItems = items.filter(function (item) {
172
+ return !item.isHiddenInTabbedNavigation;
173
+ });
174
+ var bar = /*#__PURE__*/(0, _jsxRuntime.jsxs)("nav", {
175
+ className: (0, _classnames["default"])('newspack-tabbed-navigation', 'newspack-tabbed-navigation--links', className),
176
+ "aria-label": (0, _i18n.__)('Secondary navigation', 'newspack-plugin'),
177
+ children: [displayedItems.map(function (item, index) {
178
+ return /*#__PURE__*/(0, _jsxRuntime.jsx)("a", {
179
+ href: item.href,
180
+ className: "newspack-tabbed-navigation__link",
181
+ "aria-current": isItemActive(item, null) ? 'page' : undefined,
182
+ children: item.label
183
+ }, getItemValue(item, index));
184
+ }), children]
185
+ });
186
+ return renderShell(bar, content);
187
+ };
188
+
189
+ /**
190
+ * Tabbed navigation over `{ label, path? | href?, exact?, activeTabPaths?, selected?, isHiddenInTabbedNavigation? }` items.
191
+ *
192
+ * Items with router `path`s render as a real tabs widget whose active panel
193
+ * contains `content`; href-only items render as plain navigation links.
194
+ *
195
+ * When no visible tab owns the current route — a hidden sub-view, or an unknown
196
+ * URL — `content` renders as a sibling of the panels with no tab selected. That
197
+ * is a safety net, not the preferred shape: for a route that is conceptually a
198
+ * sub-view of a tab, give that tab an `activeTabPaths` entry (e.g.
199
+ * `activeTabPaths: [ '/edit/*' ]`) so it stays selected and its panel keeps
200
+ * owning the content.
201
+ *
202
+ * `renderShell( bar, content )` is a layout injection point used by `Page` to
203
+ * place the bar inside its sticky header region while the content flows below.
204
+ *
205
+ * @param {Object} props
206
+ * @param {Array} props.items Navigation items.
207
+ * @param {boolean} [props.disableUpcoming] Disable tabs after the active one (setup flows).
208
+ * @param {*} [props.content] Page content, rendered inside the active tab's panel — or
209
+ * as a sibling of the panels when no visible tab owns the route.
210
+ * @param {*} [props.renderShell] Layout callback `( bar, content ) => element`.
211
+ * @param {string} [props.className]
212
+ * @param {*} [props.children] Extra elements rendered inside the bar (e.g. error notices).
213
+ * @return {JSX.Element} Tabbed navigation.
214
+ */
215
+ var TabbedNavigation = function TabbedNavigation(props) {
216
+ var hasRoutedItems = props.items.some(function (item) {
217
+ return item.path;
218
+ });
219
+ return hasRoutedItems ? /*#__PURE__*/(0, _jsxRuntime.jsx)(RoutedTabbedNavigation, _objectSpread({}, props)) : /*#__PURE__*/(0, _jsxRuntime.jsx)(LinkTabbedNavigation, _objectSpread({}, props));
72
220
  };
73
221
  var _default = exports["default"] = TabbedNavigation;
@@ -0,0 +1,299 @@
1
+ "use strict";
2
+
3
+ var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
4
+ var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty"));
5
+ var _objectWithoutProperties2 = _interopRequireDefault(require("@babel/runtime/helpers/objectWithoutProperties"));
6
+ var _react = require("@testing-library/react");
7
+ var _reactRouterDom = require("react-router-dom");
8
+ var _index = _interopRequireWildcard(require("./index"));
9
+ var _jsxRuntime = require("react/jsx-runtime");
10
+ var _excluded = ["initialEntries"];
11
+ /**
12
+ * External dependencies.
13
+ */
14
+ /**
15
+ * Internal dependencies.
16
+ */
17
+ function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function _interopRequireWildcard(e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, "default": e }; if (null === e || "object" != typeof e && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (var _t in e) "default" !== _t && {}.hasOwnProperty.call(e, _t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, _t)) && (i.get || i.set) ? o(f, _t, i) : f[_t] = e[_t]); return f; })(e, t); }
18
+ 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; }
19
+ 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; }
20
+ var HistoryGrabber = function HistoryGrabber(_ref) {
21
+ var historyRef = _ref.historyRef;
22
+ historyRef.current = (0, _reactRouterDom.useHistory)();
23
+ return null;
24
+ };
25
+ var ITEMS = [{
26
+ label: 'Stories',
27
+ path: '/stories'
28
+ }, {
29
+ label: 'Budgets',
30
+ path: '/budgets'
31
+ }, {
32
+ label: 'Sites',
33
+ path: '/sites'
34
+ }];
35
+ var renderTabs = function renderTabs() {
36
+ var _ref2 = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {},
37
+ _ref2$initialEntries = _ref2.initialEntries,
38
+ initialEntries = _ref2$initialEntries === void 0 ? ['/stories'] : _ref2$initialEntries,
39
+ props = (0, _objectWithoutProperties2["default"])(_ref2, _excluded);
40
+ var historyRef = {
41
+ current: null
42
+ };
43
+ (0, _react.render)(/*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactRouterDom.MemoryRouter, {
44
+ initialEntries: initialEntries,
45
+ children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(HistoryGrabber, {
46
+ historyRef: historyRef
47
+ }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_index["default"], _objectSpread({
48
+ items: ITEMS,
49
+ content: /*#__PURE__*/(0, _jsxRuntime.jsx)("div", {
50
+ children: "Routed content"
51
+ })
52
+ }, props))]
53
+ }));
54
+ return historyRef.current;
55
+ };
56
+ var getTab = function getTab(name) {
57
+ return _react.screen.getByRole('tab', {
58
+ name: name
59
+ });
60
+ };
61
+ describe('isItemActive', function () {
62
+ it('treats an explicitly selected item as active regardless of pathname', function () {
63
+ expect((0, _index.isItemActive)({
64
+ selected: true,
65
+ path: '/other'
66
+ }, '/current')).toBe(true);
67
+ expect((0, _index.isItemActive)({
68
+ selected: true
69
+ }, null)).toBe(true);
70
+ });
71
+ describe('outside a router (pathname is null)', function () {
72
+ afterEach(function () {
73
+ delete window.location;
74
+ window.location = new URL('http://localhost/');
75
+ });
76
+ it('is active when the href matches the current URL', function () {
77
+ delete window.location;
78
+ window.location = new URL('http://example.com/wp-admin/admin.php?page=ads');
79
+ expect((0, _index.isItemActive)({
80
+ href: 'http://example.com/wp-admin/admin.php?page=ads'
81
+ }, null)).toBe(true);
82
+ });
83
+ it('is inactive when the href does not match', function () {
84
+ delete window.location;
85
+ window.location = new URL('http://example.com/wp-admin/admin.php?page=ads');
86
+ expect((0, _index.isItemActive)({
87
+ href: 'http://example.com/wp-admin/admin.php?page=other'
88
+ }, null)).toBe(false);
89
+ });
90
+ it('is inactive when the item has no href', function () {
91
+ expect((0, _index.isItemActive)({
92
+ path: '/ads'
93
+ }, null)).toBe(false);
94
+ });
95
+ });
96
+ describe('inside a router (delegates to the shared route matcher)', function () {
97
+ it('matches a path as a prefix by default', function () {
98
+ expect((0, _index.isItemActive)({
99
+ path: '/stories'
100
+ }, '/stories')).toBe(true);
101
+ expect((0, _index.isItemActive)({
102
+ path: '/stories'
103
+ }, '/stories/new')).toBe(true);
104
+ expect((0, _index.isItemActive)({
105
+ path: '/stories'
106
+ }, '/budgets')).toBe(false);
107
+ });
108
+ it('matches exactly when the item opts in via exact', function () {
109
+ expect((0, _index.isItemActive)({
110
+ path: '/stories',
111
+ exact: true
112
+ }, '/stories/new')).toBe(false);
113
+ });
114
+ it('keeps the parent tab active on a hidden subpage via wildcard', function () {
115
+ var item = {
116
+ path: '/additional-brands',
117
+ activeTabPaths: ['/additional-brands/*']
118
+ };
119
+ expect((0, _index.isItemActive)(item, '/additional-brands/new')).toBe(true);
120
+ });
121
+ });
122
+ });
123
+ describe('TabbedNavigation with routed items', function () {
124
+ it('renders the routed content inside the active tab panel', function () {
125
+ renderTabs({
126
+ initialEntries: ['/budgets']
127
+ });
128
+ expect(getTab('Budgets')).toHaveAttribute('aria-selected', 'true');
129
+ var content = _react.screen.getByText('Routed content');
130
+ var panel = content.closest('[role="tabpanel"]');
131
+ expect(panel).not.toBeNull();
132
+ expect(getTab('Budgets')).toHaveAttribute('aria-controls', panel.id);
133
+ });
134
+ it('keeps the tab active and the content in its panel on a nested route', function () {
135
+ renderTabs({
136
+ initialEntries: ['/stories/new']
137
+ });
138
+ expect(getTab('Stories')).toHaveAttribute('aria-selected', 'true');
139
+ expect(_react.screen.getByText('Routed content').closest('[role="tabpanel"]')).not.toBeNull();
140
+ });
141
+ it('prefers the most specific tab when paths nest', function () {
142
+ (0, _react.render)(/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactRouterDom.MemoryRouter, {
143
+ initialEntries: ['/stories/new'],
144
+ children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_index["default"], {
145
+ items: [{
146
+ label: 'Stories',
147
+ path: '/stories'
148
+ }, {
149
+ label: 'New story',
150
+ path: '/stories/new'
151
+ }]
152
+ })
153
+ }));
154
+ expect(getTab('New story')).toHaveAttribute('aria-selected', 'true');
155
+ expect(getTab('Stories')).toHaveAttribute('aria-selected', 'false');
156
+ });
157
+ it('navigates through history.push on click', function () {
158
+ var history = renderTabs();
159
+ _react.fireEvent.click(getTab('Sites'));
160
+ expect(history.location.pathname).toBe('/sites');
161
+ expect(getTab('Sites')).toHaveAttribute('aria-selected', 'true');
162
+ });
163
+ it('leaves modified clicks to the browser', function () {
164
+ var history = renderTabs();
165
+ _react.fireEvent.click(getTab('Sites'), {
166
+ metaKey: true
167
+ });
168
+ expect(history.location.pathname).toBe('/stories');
169
+ });
170
+ it('consults history.block guards before navigating', function () {
171
+ var history = renderTabs();
172
+ var unblock = history.block(function () {
173
+ return false;
174
+ });
175
+ _react.fireEvent.click(getTab('Sites'));
176
+ expect(history.location.pathname).toBe('/stories');
177
+ expect(getTab('Stories')).toHaveAttribute('aria-selected', 'true');
178
+ unblock();
179
+ });
180
+ it('disables tabs after the active one with disableUpcoming', function () {
181
+ renderTabs({
182
+ initialEntries: ['/budgets'],
183
+ disableUpcoming: true
184
+ });
185
+ expect(getTab('Stories')).toHaveAttribute('href');
186
+ expect(getTab('Sites')).not.toHaveAttribute('href');
187
+ expect(getTab('Sites')).toHaveAttribute('aria-disabled', 'true');
188
+ _react.fireEvent.click(getTab('Sites'));
189
+ expect(getTab('Budgets')).toHaveAttribute('aria-selected', 'true');
190
+ });
191
+ it('disables every tab with disableUpcoming when no route matches', function () {
192
+ renderTabs({
193
+ initialEntries: ['/unknown'],
194
+ disableUpcoming: true
195
+ });
196
+ ITEMS.forEach(function (_ref3) {
197
+ var label = _ref3.label;
198
+ expect(getTab(label)).toHaveAttribute('aria-disabled', 'true');
199
+ });
200
+ });
201
+ it('renders the content outside the panels when no tab owns the route', function () {
202
+ // A route no tab matches — an edit screen registered as hidden, or a stale
203
+ // URL. The content must still mount, or the page body is blank.
204
+ renderTabs({
205
+ initialEntries: ['/edit/123']
206
+ });
207
+
208
+ // Assert positively: with nothing selected the panels unmount entirely, so
209
+ // a `closest( '[role=tabpanel]' )` check alone would pass even if the
210
+ // content rendered somewhere wrong. Exactly one copy also guards the mutual
211
+ // exclusivity with the in-panel render.
212
+ var matches = _react.screen.getAllByText('Routed content');
213
+ expect(matches).toHaveLength(1);
214
+ expect(matches[0].closest('.newspack-tabbed-navigation__root')).not.toBeNull();
215
+ expect(matches[0].closest('[role="tabpanel"]')).toBeNull();
216
+ ITEMS.forEach(function (_ref4) {
217
+ var label = _ref4.label;
218
+ expect(getTab(label)).toHaveAttribute('aria-selected', 'false');
219
+ });
220
+ });
221
+ it('renders the content when the only matching item is hidden from the bar', function () {
222
+ // The wizard routes hidden sections but filters them out of the bar, so the
223
+ // item exists and yet can never own the route.
224
+ (0, _react.render)(/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactRouterDom.MemoryRouter, {
225
+ initialEntries: ['/edit/123'],
226
+ children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_index["default"], {
227
+ items: [].concat(ITEMS, [{
228
+ label: 'Edit',
229
+ path: '/edit/:id',
230
+ isHiddenInTabbedNavigation: true
231
+ }]),
232
+ content: /*#__PURE__*/(0, _jsxRuntime.jsx)("div", {
233
+ children: "Routed content"
234
+ })
235
+ })
236
+ }));
237
+ expect(_react.screen.getAllByText('Routed content')).toHaveLength(1);
238
+ expect(_react.screen.queryByRole('tab', {
239
+ name: 'Edit'
240
+ })).not.toBeInTheDocument();
241
+ });
242
+ it('renders unowned content even when disableUpcoming disables every tab', function () {
243
+ // The setup wizard's Welcome and Completed screens are hidden from the bar
244
+ // AND use disableUpcoming, so `index > activeIndex` disables every tab. The
245
+ // content still has to render — this is the case that left onboarding blank.
246
+ renderTabs({
247
+ initialEntries: ['/unowned'],
248
+ disableUpcoming: true
249
+ });
250
+ expect(_react.screen.getAllByText('Routed content')).toHaveLength(1);
251
+ ITEMS.forEach(function (_ref5) {
252
+ var label = _ref5.label;
253
+ expect(getTab(label)).toHaveAttribute('aria-disabled', 'true');
254
+ });
255
+ });
256
+ it('mounts unowned content so a router fallback inside it still runs', function () {
257
+ // The wizard's trailing <Redirect> lives inside this content, so dropping
258
+ // it would strand the route instead of correcting it.
259
+ renderTabs({
260
+ initialEntries: ['/unknown'],
261
+ content: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactRouterDom.Redirect, {
262
+ to: "/stories"
263
+ })
264
+ });
265
+ expect(getTab('Stories')).toHaveAttribute('aria-selected', 'true');
266
+ });
267
+ it('navigates when the active tab changes so panels follow the route', function () {
268
+ var history = renderTabs();
269
+ (0, _react.act)(function () {
270
+ return history.push('/budgets');
271
+ });
272
+ expect(getTab('Budgets')).toHaveAttribute('aria-selected', 'true');
273
+ expect(_react.screen.getByText('Routed content').closest('[role="tabpanel"]').id).toBe(getTab('Budgets').getAttribute('aria-controls'));
274
+ });
275
+ });
276
+ describe('TabbedNavigation with href-only items', function () {
277
+ var LINK_ITEMS = [{
278
+ label: 'Newsletters',
279
+ href: 'http://example.com/wp-admin/admin.php?page=newsletters',
280
+ selected: true
281
+ }, {
282
+ label: 'Ads',
283
+ href: 'http://example.com/wp-admin/admin.php?page=ads'
284
+ }];
285
+ it('renders plain navigation links instead of a tabs widget', function () {
286
+ (0, _react.render)(/*#__PURE__*/(0, _jsxRuntime.jsx)(_index["default"], {
287
+ items: LINK_ITEMS
288
+ }));
289
+ expect(_react.screen.queryByRole('tab')).not.toBeInTheDocument();
290
+ expect(_react.screen.getByRole('navigation')).toBeInTheDocument();
291
+ var active = _react.screen.getByRole('link', {
292
+ name: 'Newsletters'
293
+ });
294
+ expect(active).toHaveAttribute('aria-current', 'page');
295
+ expect(_react.screen.getByRole('link', {
296
+ name: 'Ads'
297
+ })).not.toHaveAttribute('aria-current');
298
+ });
299
+ });