newspack-components 4.4.0 → 4.5.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.
- package/CHANGELOG.md +14 -0
- package/dist/cjs/card/core-card.js +9 -4
- package/dist/cjs/card/core-card.test.js +69 -0
- package/dist/cjs/card-settings-group/index.js +1 -2
- package/dist/cjs/divider/style.scss +1 -1
- package/dist/cjs/web-preview/style.scss +2 -3
- package/dist/cjs/wizard/components/WizardSnackbar.js +18 -13
- package/dist/cjs/wizard/components/style.scss +9 -34
- package/dist/cjs/wizard/index.js +35 -18
- package/dist/esm/card/core-card.js +9 -4
- package/dist/esm/card/core-card.test.js +65 -0
- package/dist/esm/card-settings-group/index.js +1 -2
- package/dist/esm/divider/style.scss +1 -1
- package/dist/esm/web-preview/style.scss +2 -3
- package/dist/esm/wizard/components/WizardSnackbar.js +18 -13
- package/dist/esm/wizard/components/style.scss +9 -34
- package/dist/esm/wizard/index.js +36 -19
- package/package.json +1 -1
- package/shared/js/cta-attribution.js +77 -0
- package/src/card/core-card.js +9 -4
- package/src/card/core-card.test.js +42 -0
- package/src/card-settings-group/index.tsx +1 -1
- package/src/card-sortable-list/index.tsx +1 -1
- package/src/divider/README.md +1 -1
- package/src/divider/style.scss +1 -1
- package/src/web-preview/style.scss +2 -3
- package/src/wizard/components/WizardSnackbar.js +15 -12
- package/src/wizard/components/style.scss +9 -34
- package/src/wizard/index.js +40 -23
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,17 @@
|
|
|
1
|
+
# newspack-components [4.5.0-alpha.1](https://github.com/Automattic/newspack-workspace/compare/newspack-components@4.4.0...newspack-components@4.5.0-alpha.1) (2026-07-10)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Bug Fixes
|
|
5
|
+
|
|
6
|
+
* **audience:** avoid [object Object] in segment summary ([#541](https://github.com/Automattic/newspack-workspace/issues/541)) ([336e382](https://github.com/Automattic/newspack-workspace/commit/336e382e489ee65f98fb75e33dfe2517da76e78b))
|
|
7
|
+
* **components:** avoid interactive nesting in CoreCard header ([#396](https://github.com/Automattic/newspack-workspace/issues/396)) ([dca53d4](https://github.com/Automattic/newspack-workspace/commit/dca53d4f1539769302158897f54d3666b6787e7a))
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
### Features
|
|
11
|
+
|
|
12
|
+
* **components:** standardize wp-admin snackbars, bottom-centered ([#447](https://github.com/Automattic/newspack-workspace/issues/447)) ([8c09d8b](https://github.com/Automattic/newspack-workspace/commit/8c09d8bc005d95032f04267b1bdd71df029708c5))
|
|
13
|
+
* **content-gate:** add pre-save checklist and gate preferences ([#408](https://github.com/Automattic/newspack-workspace/issues/408)) ([5ef856b](https://github.com/Automattic/newspack-workspace/commit/5ef856b5355f0929aea1a83ca744a98a64b947de))
|
|
14
|
+
|
|
1
15
|
# newspack-components [4.4.0](https://github.com/Automattic/newspack-workspace/compare/newspack-components@4.3.0...newspack-components@4.4.0) (2026-06-29)
|
|
2
16
|
|
|
3
17
|
|
|
@@ -77,19 +77,24 @@ var CoreCard = function CoreCard(_ref) {
|
|
|
77
77
|
if (noBorder) {
|
|
78
78
|
otherProps.isBorderless = true;
|
|
79
79
|
}
|
|
80
|
+
// A button header would nest its interactive children (toggle, header action, actions menu, or
|
|
81
|
+
// drag controls) inside a <button>, which is invalid. Only render the header as a button when a
|
|
82
|
+
// real click handler is supplied and the header has no interactive children of its own.
|
|
83
|
+
var hasInteractiveHeaderChildren = actionType === 'toggle' || !!headerAction || (actions === null || actions === void 0 ? void 0 : actions.length) > 0 || isDraggable;
|
|
84
|
+
var headerIsButton = !!onHeaderClick && !hasInteractiveHeaderChildren;
|
|
80
85
|
return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_components.Card, _objectSpread(_objectSpread({
|
|
81
86
|
as: as,
|
|
82
87
|
className: classes
|
|
83
88
|
}, otherProps), {}, {
|
|
84
89
|
children: [(header || icon) && /*#__PURE__*/(0, _jsxRuntime.jsxs)(_components.CardHeader, {
|
|
85
|
-
as:
|
|
90
|
+
as: headerIsButton ? 'button' : undefined,
|
|
86
91
|
className: (0, _classnames["default"])('newspack-card--core__header', isDraggable && 'newspack-card--core__header--is-draggable', !hasHeaderBorder && 'newspack-card--core__header--no-border'),
|
|
87
92
|
style: headerStyle,
|
|
88
93
|
size: sizeProps,
|
|
89
94
|
gap: 4,
|
|
90
|
-
onClick: disabled ?
|
|
91
|
-
disabled:
|
|
92
|
-
"aria-disabled":
|
|
95
|
+
onClick: headerIsButton && !disabled ? onHeaderClick : undefined,
|
|
96
|
+
disabled: headerIsButton && disabled ? true : undefined,
|
|
97
|
+
"aria-disabled": headerIsButton && disabled ? true : undefined,
|
|
93
98
|
children: [isDraggable && /*#__PURE__*/(0, _jsxRuntime.jsxs)("div", {
|
|
94
99
|
className: "newspack-card--core__header__draggable-controls",
|
|
95
100
|
children: [/*#__PURE__*/(0, _jsxRuntime.jsx)("div", {
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
|
|
4
|
+
var _react = require("@testing-library/react");
|
|
5
|
+
var _coreCard = _interopRequireDefault(require("./core-card"));
|
|
6
|
+
var _jsxRuntime = require("react/jsx-runtime");
|
|
7
|
+
/**
|
|
8
|
+
* External dependencies.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Internal dependencies.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
var getHeader = function getHeader(container) {
|
|
16
|
+
return container.querySelector('.newspack-card--core__header');
|
|
17
|
+
};
|
|
18
|
+
describe('CoreCard', function () {
|
|
19
|
+
it('renders the header as a <button> when onHeaderClick is supplied and it has no interactive children', function () {
|
|
20
|
+
var _render = (0, _react.render)(/*#__PURE__*/(0, _jsxRuntime.jsx)(_coreCard["default"], {
|
|
21
|
+
header: "Settings",
|
|
22
|
+
onHeaderClick: function onHeaderClick() {}
|
|
23
|
+
})),
|
|
24
|
+
container = _render.container;
|
|
25
|
+
expect(getHeader(container).tagName).toBe('BUTTON');
|
|
26
|
+
});
|
|
27
|
+
it('renders the header as a non-button when it also has a toggle', function () {
|
|
28
|
+
var _render2 = (0, _react.render)(/*#__PURE__*/(0, _jsxRuntime.jsx)(_coreCard["default"], {
|
|
29
|
+
header: "Settings",
|
|
30
|
+
actionType: "toggle",
|
|
31
|
+
onHeaderClick: function onHeaderClick() {}
|
|
32
|
+
})),
|
|
33
|
+
container = _render2.container;
|
|
34
|
+
expect(getHeader(container).tagName).not.toBe('BUTTON');
|
|
35
|
+
});
|
|
36
|
+
it('renders the header as a non-button when it also has a header action', function () {
|
|
37
|
+
var _render3 = (0, _react.render)(/*#__PURE__*/(0, _jsxRuntime.jsx)(_coreCard["default"], {
|
|
38
|
+
header: "Settings",
|
|
39
|
+
headerAction: {
|
|
40
|
+
label: 'Edit',
|
|
41
|
+
onClick: function onClick() {}
|
|
42
|
+
},
|
|
43
|
+
onHeaderClick: function onHeaderClick() {}
|
|
44
|
+
})),
|
|
45
|
+
container = _render3.container;
|
|
46
|
+
expect(getHeader(container).tagName).not.toBe('BUTTON');
|
|
47
|
+
});
|
|
48
|
+
it('renders the header as a non-button when it also has an actions menu', function () {
|
|
49
|
+
var _render4 = (0, _react.render)(/*#__PURE__*/(0, _jsxRuntime.jsx)(_coreCard["default"], {
|
|
50
|
+
header: "Settings",
|
|
51
|
+
actions: [{
|
|
52
|
+
label: 'Delete',
|
|
53
|
+
action: function action() {}
|
|
54
|
+
}],
|
|
55
|
+
onHeaderClick: function onHeaderClick() {}
|
|
56
|
+
})),
|
|
57
|
+
container = _render4.container;
|
|
58
|
+
expect(getHeader(container).tagName).not.toBe('BUTTON');
|
|
59
|
+
});
|
|
60
|
+
it('renders the header as a non-button when it is also draggable', function () {
|
|
61
|
+
var _render5 = (0, _react.render)(/*#__PURE__*/(0, _jsxRuntime.jsx)(_coreCard["default"], {
|
|
62
|
+
header: "Settings",
|
|
63
|
+
isDraggable: true,
|
|
64
|
+
onHeaderClick: function onHeaderClick() {}
|
|
65
|
+
})),
|
|
66
|
+
container = _render5.container;
|
|
67
|
+
expect(getHeader(container).tagName).not.toBe('BUTTON');
|
|
68
|
+
});
|
|
69
|
+
});
|
|
@@ -39,8 +39,7 @@ var CardSettingsGroup = function CardSettingsGroup(_ref) {
|
|
|
39
39
|
isActive = _ref$isActive === void 0 ? false : _ref$isActive,
|
|
40
40
|
_ref$onEnable = _ref.onEnable,
|
|
41
41
|
onEnable = _ref$onEnable === void 0 ? function () {} : _ref$onEnable,
|
|
42
|
-
|
|
43
|
-
onHeaderClick = _ref$onHeaderClick === void 0 ? function () {} : _ref$onHeaderClick;
|
|
42
|
+
onHeaderClick = _ref.onHeaderClick;
|
|
44
43
|
return /*#__PURE__*/(0, _jsxRuntime.jsx)(_.Card, {
|
|
45
44
|
className: (0, _classnames["default"])('newspack-card--core--settings-group', className),
|
|
46
45
|
actionType: actionType,
|
|
@@ -13,7 +13,7 @@ var _store = require("../store");
|
|
|
13
13
|
require("./style.scss");
|
|
14
14
|
var _classnames = _interopRequireDefault(require("classnames"));
|
|
15
15
|
var _jsxRuntime = require("react/jsx-runtime");
|
|
16
|
-
var _excluded = ["children", "
|
|
16
|
+
var _excluded = ["children", "type", "actions"];
|
|
17
17
|
/**
|
|
18
18
|
* Snackbar.
|
|
19
19
|
*/
|
|
@@ -29,26 +29,31 @@ var _excluded = ["children", "position", "type", "actions"];
|
|
|
29
29
|
/**
|
|
30
30
|
* WizardSnackbar component.
|
|
31
31
|
*
|
|
32
|
+
* Wraps core's Snackbar with the wizard-store `onRemove` glue. Positioning and
|
|
33
|
+
* styling are neutral (bottom-centered via the snackbar list container). The
|
|
34
|
+
* notice `type` no longer drives any visual styling, but it still maps to the
|
|
35
|
+
* screen-reader announcement politeness: only `error` announces assertively,
|
|
36
|
+
* every other severity announces politely.
|
|
37
|
+
*
|
|
38
|
+
* Remaining props are spread onto core's Snackbar. See:
|
|
39
|
+
* https://wordpress.github.io/gutenberg/?path=/docs/components-snackbar--docs
|
|
40
|
+
*
|
|
32
41
|
* @param {Object} props - The component props.
|
|
33
|
-
* @param {Object[]} props.actions - The actions to display in the snackbar.
|
|
34
42
|
* @param {JSX.Element} props.children - The component children.
|
|
35
|
-
* @param {
|
|
36
|
-
* @param {
|
|
37
|
-
* @param {string} props.type - The snackbar type: 'info', 'success', 'warning', or 'error'.
|
|
43
|
+
* @param {string} props.type - The notice severity ('error' announces assertively, anything else politely).
|
|
44
|
+
* @param {Object[]} props.actions - The actions to display in the snackbar.
|
|
38
45
|
* @return {JSX.Element} The component.
|
|
39
46
|
*/
|
|
40
47
|
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; }
|
|
41
48
|
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; }
|
|
42
49
|
var WizardSnackbar = function WizardSnackbar(_ref) {
|
|
43
50
|
var children = _ref.children,
|
|
44
|
-
|
|
45
|
-
position = _ref$position === void 0 ? 'bottom-left' : _ref$position,
|
|
46
|
-
_ref$type = _ref.type,
|
|
47
|
-
type = _ref$type === void 0 ? 'info' : _ref$type,
|
|
51
|
+
type = _ref.type,
|
|
48
52
|
_ref$actions = _ref.actions,
|
|
49
53
|
actions = _ref$actions === void 0 ? [] : _ref$actions,
|
|
50
54
|
props = (0, _objectWithoutProperties2["default"])(_ref, _excluded);
|
|
51
|
-
var className = (0, _classnames["default"])('newspack-wizard__snackbar', props.className
|
|
55
|
+
var className = (0, _classnames["default"])('newspack-wizard__snackbar', props.className);
|
|
56
|
+
var politeness = 'error' === type ? 'assertive' : 'polite';
|
|
52
57
|
var _useDispatch = (0, _data.useDispatch)(_store.WIZARD_STORE_NAMESPACE),
|
|
53
58
|
removeNotice = _useDispatch.removeNotice,
|
|
54
59
|
resetNotices = _useDispatch.resetNotices;
|
|
@@ -62,9 +67,9 @@ var WizardSnackbar = function WizardSnackbar(_ref) {
|
|
|
62
67
|
resetNotices();
|
|
63
68
|
}
|
|
64
69
|
};
|
|
65
|
-
return /*#__PURE__*/(0, _jsxRuntime.jsx)(_components.Snackbar, _objectSpread(_objectSpread({
|
|
66
|
-
className: className
|
|
67
|
-
|
|
70
|
+
return /*#__PURE__*/(0, _jsxRuntime.jsx)(_components.Snackbar, _objectSpread(_objectSpread({}, props), {}, {
|
|
71
|
+
className: className,
|
|
72
|
+
politeness: politeness,
|
|
68
73
|
onRemove: onRemove,
|
|
69
74
|
actions: actions,
|
|
70
75
|
children: children
|
|
@@ -1,39 +1,14 @@
|
|
|
1
|
+
@use "~@wordpress/base-styles/mixins" as wp-mixins;
|
|
2
|
+
@use "~@wordpress/base-styles/variables" as wp-vars;
|
|
3
|
+
|
|
1
4
|
.newspack-wizard {
|
|
2
|
-
&__snackbar {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
left: 16px;
|
|
6
|
-
max-width: calc(100vw - 32px);
|
|
7
|
-
position: fixed;
|
|
5
|
+
&__snackbar-list {
|
|
6
|
+
@include wp-mixins.snackbar-container();
|
|
7
|
+
gap: wp-vars.$grid-unit-10;
|
|
8
8
|
z-index: 99999;
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
&--error {
|
|
13
|
-
border-color: var(--newspack-ui-color-error-60);
|
|
14
|
-
}
|
|
15
|
-
&--warning {
|
|
16
|
-
border-color: var(--newspack-ui-color-warning-40);
|
|
17
|
-
}
|
|
18
|
-
@media screen and ( min-width: 783px ) {
|
|
19
|
-
left: calc(36px + 16px); /* 36px for admin menu bar */
|
|
20
|
-
}
|
|
21
|
-
@media screen and ( min-width: 960px ) {
|
|
22
|
-
left: calc(160px + 16px); /* 160px for admin menu bar */
|
|
23
|
-
}
|
|
24
|
-
&.newspack-wizard__snackbar--bottom-right,
|
|
25
|
-
&.newspack-wizard__snackbar--top-right {
|
|
26
|
-
left: auto;
|
|
27
|
-
right: calc(16px);
|
|
28
|
-
}
|
|
29
|
-
&.newspack-wizard__snackbar--top-left,
|
|
30
|
-
&.newspack-wizard__snackbar--top-right {
|
|
31
|
-
bottom: auto;
|
|
32
|
-
top: calc(16px + 46px); /* 46px for admin bar */
|
|
33
|
-
@media screen and ( min-width: 783px ) {
|
|
34
|
-
top: calc(16px + 32px); /* 32px for admin bar */
|
|
35
|
-
}
|
|
36
|
-
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
&__snackbar {
|
|
37
12
|
a,
|
|
38
13
|
.components-button.is-link {
|
|
39
14
|
color: var(--newspack-ui-color-neutral-0);
|
package/dist/cjs/wizard/index.js
CHANGED
|
@@ -229,17 +229,31 @@ var Wizard = function Wizard(_ref, ref) {
|
|
|
229
229
|
className: 'newspack-wizard__header__actions__more'
|
|
230
230
|
},
|
|
231
231
|
children: function children() {
|
|
232
|
-
return
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
},
|
|
242
|
-
|
|
232
|
+
return (
|
|
233
|
+
// Split actions into groups whenever an action opts in via `separator: true`.
|
|
234
|
+
// Consecutive MenuGroups render the WordPress-standard divider between them.
|
|
235
|
+
actions.reduce(function (groups, action) {
|
|
236
|
+
if (action.separator || groups.length === 0) {
|
|
237
|
+
groups.push([]);
|
|
238
|
+
}
|
|
239
|
+
groups[groups.length - 1].push(action);
|
|
240
|
+
return groups;
|
|
241
|
+
}, []).map(function (group, groupIndex) {
|
|
242
|
+
return /*#__PURE__*/(0, _jsxRuntime.jsx)(_components.MenuGroup, {
|
|
243
|
+
children: group.map(function (action, index) {
|
|
244
|
+
return /*#__PURE__*/(0, _jsxRuntime.jsx)(_components.MenuItem, {
|
|
245
|
+
className: action.type === 'primary' || action.type === 'secondary' ? 'newspack-wizard__header__actions__more__main' : 'newspack-wizard__header__actions__more__more',
|
|
246
|
+
icon: action.icon,
|
|
247
|
+
href: action.href,
|
|
248
|
+
onClick: action.action,
|
|
249
|
+
disabled: action.disabled || false,
|
|
250
|
+
isDestructive: action.destructive || false,
|
|
251
|
+
children: action.label
|
|
252
|
+
}, index);
|
|
253
|
+
})
|
|
254
|
+
}, groupIndex);
|
|
255
|
+
})
|
|
256
|
+
);
|
|
243
257
|
}
|
|
244
258
|
})]
|
|
245
259
|
})]
|
|
@@ -281,13 +295,16 @@ var Wizard = function Wizard(_ref, ref) {
|
|
|
281
295
|
})]
|
|
282
296
|
})
|
|
283
297
|
})]
|
|
284
|
-
}), (notices === null || notices === void 0 ? void 0 : notices.length) > 0 &&
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
298
|
+
}), (notices === null || notices === void 0 ? void 0 : notices.length) > 0 && /*#__PURE__*/(0, _jsxRuntime.jsx)("div", {
|
|
299
|
+
className: "newspack-wizard__snackbar-list",
|
|
300
|
+
children: notices.map(function (notice, index) {
|
|
301
|
+
return /*#__PURE__*/(0, _jsxRuntime.jsx)(_WizardSnackbar["default"], {
|
|
302
|
+
id: notice.id,
|
|
303
|
+
type: notice.type,
|
|
304
|
+
actions: notice.actions,
|
|
305
|
+
children: notice.message
|
|
306
|
+
}, notice.id || index);
|
|
307
|
+
})
|
|
291
308
|
})]
|
|
292
309
|
}), !isLoading && /*#__PURE__*/(0, _jsxRuntime.jsx)(_.Footer, {
|
|
293
310
|
simple: hasSimpleFooter
|
|
@@ -73,19 +73,24 @@ var CoreCard = function CoreCard(_ref) {
|
|
|
73
73
|
if (noBorder) {
|
|
74
74
|
otherProps.isBorderless = true;
|
|
75
75
|
}
|
|
76
|
+
// A button header would nest its interactive children (toggle, header action, actions menu, or
|
|
77
|
+
// drag controls) inside a <button>, which is invalid. Only render the header as a button when a
|
|
78
|
+
// real click handler is supplied and the header has no interactive children of its own.
|
|
79
|
+
var hasInteractiveHeaderChildren = actionType === 'toggle' || !!headerAction || (actions === null || actions === void 0 ? void 0 : actions.length) > 0 || isDraggable;
|
|
80
|
+
var headerIsButton = !!onHeaderClick && !hasInteractiveHeaderChildren;
|
|
76
81
|
return /*#__PURE__*/_jsxs(CardWrapper, _objectSpread(_objectSpread({
|
|
77
82
|
as: as,
|
|
78
83
|
className: classes
|
|
79
84
|
}, otherProps), {}, {
|
|
80
85
|
children: [(header || icon) && /*#__PURE__*/_jsxs(CardHeader, {
|
|
81
|
-
as:
|
|
86
|
+
as: headerIsButton ? 'button' : undefined,
|
|
82
87
|
className: classNames('newspack-card--core__header', isDraggable && 'newspack-card--core__header--is-draggable', !hasHeaderBorder && 'newspack-card--core__header--no-border'),
|
|
83
88
|
style: headerStyle,
|
|
84
89
|
size: sizeProps,
|
|
85
90
|
gap: 4,
|
|
86
|
-
onClick: disabled ?
|
|
87
|
-
disabled:
|
|
88
|
-
"aria-disabled":
|
|
91
|
+
onClick: headerIsButton && !disabled ? onHeaderClick : undefined,
|
|
92
|
+
disabled: headerIsButton && disabled ? true : undefined,
|
|
93
|
+
"aria-disabled": headerIsButton && disabled ? true : undefined,
|
|
89
94
|
children: [isDraggable && /*#__PURE__*/_jsxs("div", {
|
|
90
95
|
className: "newspack-card--core__header__draggable-controls",
|
|
91
96
|
children: [/*#__PURE__*/_jsx("div", {
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* External dependencies.
|
|
3
|
+
*/
|
|
4
|
+
import { render } from '@testing-library/react';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Internal dependencies.
|
|
8
|
+
*/
|
|
9
|
+
import CoreCard from './core-card';
|
|
10
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
11
|
+
var getHeader = function getHeader(container) {
|
|
12
|
+
return container.querySelector('.newspack-card--core__header');
|
|
13
|
+
};
|
|
14
|
+
describe('CoreCard', function () {
|
|
15
|
+
it('renders the header as a <button> when onHeaderClick is supplied and it has no interactive children', function () {
|
|
16
|
+
var _render = render(/*#__PURE__*/_jsx(CoreCard, {
|
|
17
|
+
header: "Settings",
|
|
18
|
+
onHeaderClick: function onHeaderClick() {}
|
|
19
|
+
})),
|
|
20
|
+
container = _render.container;
|
|
21
|
+
expect(getHeader(container).tagName).toBe('BUTTON');
|
|
22
|
+
});
|
|
23
|
+
it('renders the header as a non-button when it also has a toggle', function () {
|
|
24
|
+
var _render2 = render(/*#__PURE__*/_jsx(CoreCard, {
|
|
25
|
+
header: "Settings",
|
|
26
|
+
actionType: "toggle",
|
|
27
|
+
onHeaderClick: function onHeaderClick() {}
|
|
28
|
+
})),
|
|
29
|
+
container = _render2.container;
|
|
30
|
+
expect(getHeader(container).tagName).not.toBe('BUTTON');
|
|
31
|
+
});
|
|
32
|
+
it('renders the header as a non-button when it also has a header action', function () {
|
|
33
|
+
var _render3 = render(/*#__PURE__*/_jsx(CoreCard, {
|
|
34
|
+
header: "Settings",
|
|
35
|
+
headerAction: {
|
|
36
|
+
label: 'Edit',
|
|
37
|
+
onClick: function onClick() {}
|
|
38
|
+
},
|
|
39
|
+
onHeaderClick: function onHeaderClick() {}
|
|
40
|
+
})),
|
|
41
|
+
container = _render3.container;
|
|
42
|
+
expect(getHeader(container).tagName).not.toBe('BUTTON');
|
|
43
|
+
});
|
|
44
|
+
it('renders the header as a non-button when it also has an actions menu', function () {
|
|
45
|
+
var _render4 = render(/*#__PURE__*/_jsx(CoreCard, {
|
|
46
|
+
header: "Settings",
|
|
47
|
+
actions: [{
|
|
48
|
+
label: 'Delete',
|
|
49
|
+
action: function action() {}
|
|
50
|
+
}],
|
|
51
|
+
onHeaderClick: function onHeaderClick() {}
|
|
52
|
+
})),
|
|
53
|
+
container = _render4.container;
|
|
54
|
+
expect(getHeader(container).tagName).not.toBe('BUTTON');
|
|
55
|
+
});
|
|
56
|
+
it('renders the header as a non-button when it is also draggable', function () {
|
|
57
|
+
var _render5 = render(/*#__PURE__*/_jsx(CoreCard, {
|
|
58
|
+
header: "Settings",
|
|
59
|
+
isDraggable: true,
|
|
60
|
+
onHeaderClick: function onHeaderClick() {}
|
|
61
|
+
})),
|
|
62
|
+
container = _render5.container;
|
|
63
|
+
expect(getHeader(container).tagName).not.toBe('BUTTON');
|
|
64
|
+
});
|
|
65
|
+
});
|
|
@@ -31,8 +31,7 @@ var CardSettingsGroup = function CardSettingsGroup(_ref) {
|
|
|
31
31
|
isActive = _ref$isActive === void 0 ? false : _ref$isActive,
|
|
32
32
|
_ref$onEnable = _ref.onEnable,
|
|
33
33
|
onEnable = _ref$onEnable === void 0 ? function () {} : _ref$onEnable,
|
|
34
|
-
|
|
35
|
-
onHeaderClick = _ref$onHeaderClick === void 0 ? function () {} : _ref$onHeaderClick;
|
|
34
|
+
onHeaderClick = _ref.onHeaderClick;
|
|
36
35
|
return /*#__PURE__*/_jsx(Card, {
|
|
37
36
|
className: classNames('newspack-card--core--settings-group', className),
|
|
38
37
|
actionType: actionType,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import _defineProperty from "@babel/runtime/helpers/defineProperty";
|
|
2
2
|
import _objectWithoutProperties from "@babel/runtime/helpers/objectWithoutProperties";
|
|
3
|
-
var _excluded = ["children", "
|
|
3
|
+
var _excluded = ["children", "type", "actions"];
|
|
4
4
|
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; }
|
|
5
5
|
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; }
|
|
6
6
|
/**
|
|
@@ -27,25 +27,30 @@ import classnames from 'classnames';
|
|
|
27
27
|
/**
|
|
28
28
|
* WizardSnackbar component.
|
|
29
29
|
*
|
|
30
|
+
* Wraps core's Snackbar with the wizard-store `onRemove` glue. Positioning and
|
|
31
|
+
* styling are neutral (bottom-centered via the snackbar list container). The
|
|
32
|
+
* notice `type` no longer drives any visual styling, but it still maps to the
|
|
33
|
+
* screen-reader announcement politeness: only `error` announces assertively,
|
|
34
|
+
* every other severity announces politely.
|
|
35
|
+
*
|
|
36
|
+
* Remaining props are spread onto core's Snackbar. See:
|
|
37
|
+
* https://wordpress.github.io/gutenberg/?path=/docs/components-snackbar--docs
|
|
38
|
+
*
|
|
30
39
|
* @param {Object} props - The component props.
|
|
31
|
-
* @param {Object[]} props.actions - The actions to display in the snackbar.
|
|
32
40
|
* @param {JSX.Element} props.children - The component children.
|
|
33
|
-
* @param {
|
|
34
|
-
* @param {
|
|
35
|
-
* @param {string} props.type - The snackbar type: 'info', 'success', 'warning', or 'error'.
|
|
41
|
+
* @param {string} props.type - The notice severity ('error' announces assertively, anything else politely).
|
|
42
|
+
* @param {Object[]} props.actions - The actions to display in the snackbar.
|
|
36
43
|
* @return {JSX.Element} The component.
|
|
37
44
|
*/
|
|
38
45
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
39
46
|
var WizardSnackbar = function WizardSnackbar(_ref) {
|
|
40
47
|
var children = _ref.children,
|
|
41
|
-
|
|
42
|
-
position = _ref$position === void 0 ? 'bottom-left' : _ref$position,
|
|
43
|
-
_ref$type = _ref.type,
|
|
44
|
-
type = _ref$type === void 0 ? 'info' : _ref$type,
|
|
48
|
+
type = _ref.type,
|
|
45
49
|
_ref$actions = _ref.actions,
|
|
46
50
|
actions = _ref$actions === void 0 ? [] : _ref$actions,
|
|
47
51
|
props = _objectWithoutProperties(_ref, _excluded);
|
|
48
|
-
var className = classnames('newspack-wizard__snackbar', props.className
|
|
52
|
+
var className = classnames('newspack-wizard__snackbar', props.className);
|
|
53
|
+
var politeness = 'error' === type ? 'assertive' : 'polite';
|
|
49
54
|
var _useDispatch = useDispatch(WIZARD_STORE_NAMESPACE),
|
|
50
55
|
removeNotice = _useDispatch.removeNotice,
|
|
51
56
|
resetNotices = _useDispatch.resetNotices;
|
|
@@ -59,9 +64,9 @@ var WizardSnackbar = function WizardSnackbar(_ref) {
|
|
|
59
64
|
resetNotices();
|
|
60
65
|
}
|
|
61
66
|
};
|
|
62
|
-
return /*#__PURE__*/_jsx(BaseComponent, _objectSpread(_objectSpread({
|
|
63
|
-
className: className
|
|
64
|
-
|
|
67
|
+
return /*#__PURE__*/_jsx(BaseComponent, _objectSpread(_objectSpread({}, props), {}, {
|
|
68
|
+
className: className,
|
|
69
|
+
politeness: politeness,
|
|
65
70
|
onRemove: onRemove,
|
|
66
71
|
actions: actions,
|
|
67
72
|
children: children
|
|
@@ -1,39 +1,14 @@
|
|
|
1
|
+
@use "~@wordpress/base-styles/mixins" as wp-mixins;
|
|
2
|
+
@use "~@wordpress/base-styles/variables" as wp-vars;
|
|
3
|
+
|
|
1
4
|
.newspack-wizard {
|
|
2
|
-
&__snackbar {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
left: 16px;
|
|
6
|
-
max-width: calc(100vw - 32px);
|
|
7
|
-
position: fixed;
|
|
5
|
+
&__snackbar-list {
|
|
6
|
+
@include wp-mixins.snackbar-container();
|
|
7
|
+
gap: wp-vars.$grid-unit-10;
|
|
8
8
|
z-index: 99999;
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
&--error {
|
|
13
|
-
border-color: var(--newspack-ui-color-error-60);
|
|
14
|
-
}
|
|
15
|
-
&--warning {
|
|
16
|
-
border-color: var(--newspack-ui-color-warning-40);
|
|
17
|
-
}
|
|
18
|
-
@media screen and ( min-width: 783px ) {
|
|
19
|
-
left: calc(36px + 16px); /* 36px for admin menu bar */
|
|
20
|
-
}
|
|
21
|
-
@media screen and ( min-width: 960px ) {
|
|
22
|
-
left: calc(160px + 16px); /* 160px for admin menu bar */
|
|
23
|
-
}
|
|
24
|
-
&.newspack-wizard__snackbar--bottom-right,
|
|
25
|
-
&.newspack-wizard__snackbar--top-right {
|
|
26
|
-
left: auto;
|
|
27
|
-
right: calc(16px);
|
|
28
|
-
}
|
|
29
|
-
&.newspack-wizard__snackbar--top-left,
|
|
30
|
-
&.newspack-wizard__snackbar--top-right {
|
|
31
|
-
bottom: auto;
|
|
32
|
-
top: calc(16px + 46px); /* 46px for admin bar */
|
|
33
|
-
@media screen and ( min-width: 783px ) {
|
|
34
|
-
top: calc(16px + 32px); /* 32px for admin bar */
|
|
35
|
-
}
|
|
36
|
-
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
&__snackbar {
|
|
37
12
|
a,
|
|
38
13
|
.components-button.is-link {
|
|
39
14
|
color: var(--newspack-ui-color-neutral-0);
|
package/dist/esm/wizard/index.js
CHANGED
|
@@ -10,7 +10,7 @@ import classnames from 'classnames';
|
|
|
10
10
|
/**
|
|
11
11
|
* WordPress dependencies.
|
|
12
12
|
*/
|
|
13
|
-
import { DropdownMenu, MenuItem } from '@wordpress/components';
|
|
13
|
+
import { DropdownMenu, MenuGroup, MenuItem } from '@wordpress/components';
|
|
14
14
|
import { useDispatch, useSelect } from '@wordpress/data';
|
|
15
15
|
import { useEffect, useState, forwardRef } from '@wordpress/element';
|
|
16
16
|
import { __ } from '@wordpress/i18n';
|
|
@@ -226,17 +226,31 @@ var Wizard = function Wizard(_ref, ref) {
|
|
|
226
226
|
className: 'newspack-wizard__header__actions__more'
|
|
227
227
|
},
|
|
228
228
|
children: function children() {
|
|
229
|
-
return
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
},
|
|
239
|
-
|
|
229
|
+
return (
|
|
230
|
+
// Split actions into groups whenever an action opts in via `separator: true`.
|
|
231
|
+
// Consecutive MenuGroups render the WordPress-standard divider between them.
|
|
232
|
+
actions.reduce(function (groups, action) {
|
|
233
|
+
if (action.separator || groups.length === 0) {
|
|
234
|
+
groups.push([]);
|
|
235
|
+
}
|
|
236
|
+
groups[groups.length - 1].push(action);
|
|
237
|
+
return groups;
|
|
238
|
+
}, []).map(function (group, groupIndex) {
|
|
239
|
+
return /*#__PURE__*/_jsx(MenuGroup, {
|
|
240
|
+
children: group.map(function (action, index) {
|
|
241
|
+
return /*#__PURE__*/_jsx(MenuItem, {
|
|
242
|
+
className: action.type === 'primary' || action.type === 'secondary' ? 'newspack-wizard__header__actions__more__main' : 'newspack-wizard__header__actions__more__more',
|
|
243
|
+
icon: action.icon,
|
|
244
|
+
href: action.href,
|
|
245
|
+
onClick: action.action,
|
|
246
|
+
disabled: action.disabled || false,
|
|
247
|
+
isDestructive: action.destructive || false,
|
|
248
|
+
children: action.label
|
|
249
|
+
}, index);
|
|
250
|
+
})
|
|
251
|
+
}, groupIndex);
|
|
252
|
+
})
|
|
253
|
+
);
|
|
240
254
|
}
|
|
241
255
|
})]
|
|
242
256
|
})]
|
|
@@ -278,13 +292,16 @@ var Wizard = function Wizard(_ref, ref) {
|
|
|
278
292
|
})]
|
|
279
293
|
})
|
|
280
294
|
})]
|
|
281
|
-
}), (notices === null || notices === void 0 ? void 0 : notices.length) > 0 &&
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
295
|
+
}), (notices === null || notices === void 0 ? void 0 : notices.length) > 0 && /*#__PURE__*/_jsx("div", {
|
|
296
|
+
className: "newspack-wizard__snackbar-list",
|
|
297
|
+
children: notices.map(function (notice, index) {
|
|
298
|
+
return /*#__PURE__*/_jsx(WizardSnackbar, {
|
|
299
|
+
id: notice.id,
|
|
300
|
+
type: notice.type,
|
|
301
|
+
actions: notice.actions,
|
|
302
|
+
children: notice.message
|
|
303
|
+
}, notice.id || index);
|
|
304
|
+
})
|
|
288
305
|
})]
|
|
289
306
|
}), !isLoading && /*#__PURE__*/_jsx(Footer, {
|
|
290
307
|
simple: hasSimpleFooter
|
package/package.json
CHANGED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CTA attribution hand-off (NPPD-1887).
|
|
3
|
+
*
|
|
4
|
+
* When a reader clicks a conversion-intent CTA inside a gate or prompt and is sent
|
|
5
|
+
* to a landing page, the surface that caused the conversion is left behind. This
|
|
6
|
+
* module persists the surface's identity so the landing page's checkout/donate form
|
|
7
|
+
* can replay it as a hidden field, letting the existing server chain
|
|
8
|
+
* (`INPUT_GET` -> cart item data -> `_gate_post_id` / `_newspack_popup_id` order meta)
|
|
9
|
+
* fire exactly as it does for a form rendered inside the gate itself.
|
|
10
|
+
*
|
|
11
|
+
* ── Storage contract ────────────────────────────────────────────────────────────
|
|
12
|
+
* This contract is DUPLICATED, by necessity, in newspack-blocks
|
|
13
|
+
* (`src/shared/js/cta-attribution.js`), which owns the checkout and donate forms and
|
|
14
|
+
* therefore owns the replay. Both plugins ship independently; there is no shared JS
|
|
15
|
+
* package between them. Change one, change the other.
|
|
16
|
+
*
|
|
17
|
+
* sessionStorage[ 'newspack_cta_attribution' ] = JSON.stringify( {
|
|
18
|
+
* v: 1, // SCHEMA — reject records written by a drifted shape
|
|
19
|
+
* type: 'gate' | 'prompt', // which surface
|
|
20
|
+
* id: '123', // gate_post_id or newspack_popup_id
|
|
21
|
+
* ts: 1751932800000, // Date.now() at click
|
|
22
|
+
* } )
|
|
23
|
+
*
|
|
24
|
+
* `sessionStorage`, not a cookie, on purpose:
|
|
25
|
+
* - survives multi-hop navigation (gate -> /subscribe -> /plans -> convert)
|
|
26
|
+
* - is copied into a new tab opened from `target="_blank"`
|
|
27
|
+
* - dies with the tab, which is a naturally conservative attribution window
|
|
28
|
+
* - cannot vary a page-cache key
|
|
29
|
+
*
|
|
30
|
+
* Last touch wins: a second CTA click overwrites the first.
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
/** Storage key. Shared with newspack-blocks. */
|
|
34
|
+
export const STORAGE_KEY = 'newspack_cta_attribution';
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Storage-record schema version. The contract is duplicated across newspack-plugin and
|
|
38
|
+
* newspack-blocks (no shared JS package), so a mismatch is a real failure mode if the
|
|
39
|
+
* two ever drift — one side bumps TTL or the record shape, the other silently honors a
|
|
40
|
+
* stale record. Stamping the version and checking it on read makes drift fail safe: a
|
|
41
|
+
* record from a different version is ignored rather than trusted. Bump on any change to
|
|
42
|
+
* the record fields, the TTL semantics, or the storage key. (dkoo review, #575.)
|
|
43
|
+
*/
|
|
44
|
+
export const SCHEMA = 1;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Attribution lifetime, in milliseconds.
|
|
48
|
+
*
|
|
49
|
+
* Session-scoped AND time-bounded (product decision, NPPD-1887): a reader who clicks
|
|
50
|
+
* the CTA, wanders off, and converts two hours later in the same tab gets no credit.
|
|
51
|
+
* Kept in sync with the same constant in newspack-blocks.
|
|
52
|
+
*/
|
|
53
|
+
export const TTL_MS = 30 * 60 * 1000;
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Persist the surface that owns this click.
|
|
57
|
+
*
|
|
58
|
+
* Never throws: Safari private mode and storage-disabled browsers make
|
|
59
|
+
* `sessionStorage.setItem` raise. Attribution is best-effort — losing it must never
|
|
60
|
+
* break the navigation the reader actually asked for.
|
|
61
|
+
*
|
|
62
|
+
* @param {string} type Surface type: 'gate' or 'prompt'.
|
|
63
|
+
* @param {string|number} id The gate_post_id or newspack_popup_id.
|
|
64
|
+
*
|
|
65
|
+
* @return {boolean} Whether the value was stored.
|
|
66
|
+
*/
|
|
67
|
+
export function persistCtaAttribution( type, id ) {
|
|
68
|
+
if ( ! id || ( 'gate' !== type && 'prompt' !== type ) ) {
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
try {
|
|
72
|
+
window.sessionStorage.setItem( STORAGE_KEY, JSON.stringify( { v: SCHEMA, type, id: String( id ), ts: Date.now() } ) );
|
|
73
|
+
return true;
|
|
74
|
+
} catch ( e ) {
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
}
|
package/src/card/core-card.js
CHANGED
|
@@ -80,11 +80,16 @@ const CoreCard = ( {
|
|
|
80
80
|
if ( noBorder ) {
|
|
81
81
|
otherProps.isBorderless = true;
|
|
82
82
|
}
|
|
83
|
+
// A button header would nest its interactive children (toggle, header action, actions menu, or
|
|
84
|
+
// drag controls) inside a <button>, which is invalid. Only render the header as a button when a
|
|
85
|
+
// real click handler is supplied and the header has no interactive children of its own.
|
|
86
|
+
const hasInteractiveHeaderChildren = actionType === 'toggle' || !! headerAction || actions?.length > 0 || isDraggable;
|
|
87
|
+
const headerIsButton = !! onHeaderClick && ! hasInteractiveHeaderChildren;
|
|
83
88
|
return (
|
|
84
89
|
<CardWrapper as={ as } className={ classes } { ...otherProps }>
|
|
85
90
|
{ ( header || icon ) && (
|
|
86
91
|
<CardHeader
|
|
87
|
-
as={
|
|
92
|
+
as={ headerIsButton ? 'button' : undefined }
|
|
88
93
|
className={ classNames(
|
|
89
94
|
'newspack-card--core__header',
|
|
90
95
|
isDraggable && 'newspack-card--core__header--is-draggable',
|
|
@@ -93,9 +98,9 @@ const CoreCard = ( {
|
|
|
93
98
|
style={ headerStyle }
|
|
94
99
|
size={ sizeProps }
|
|
95
100
|
gap={ 4 }
|
|
96
|
-
onClick={ disabled ?
|
|
97
|
-
disabled={
|
|
98
|
-
aria-disabled={
|
|
101
|
+
onClick={ headerIsButton && ! disabled ? onHeaderClick : undefined }
|
|
102
|
+
disabled={ headerIsButton && disabled ? true : undefined }
|
|
103
|
+
aria-disabled={ headerIsButton && disabled ? true : undefined }
|
|
99
104
|
>
|
|
100
105
|
{ isDraggable && (
|
|
101
106
|
<div className="newspack-card--core__header__draggable-controls">
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* External dependencies.
|
|
3
|
+
*/
|
|
4
|
+
import { render } from '@testing-library/react';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Internal dependencies.
|
|
8
|
+
*/
|
|
9
|
+
import CoreCard from './core-card';
|
|
10
|
+
|
|
11
|
+
const getHeader = container => container.querySelector( '.newspack-card--core__header' );
|
|
12
|
+
|
|
13
|
+
describe( 'CoreCard', () => {
|
|
14
|
+
it( 'renders the header as a <button> when onHeaderClick is supplied and it has no interactive children', () => {
|
|
15
|
+
const { container } = render( <CoreCard header="Settings" onHeaderClick={ () => {} } /> );
|
|
16
|
+
expect( getHeader( container ).tagName ).toBe( 'BUTTON' );
|
|
17
|
+
} );
|
|
18
|
+
|
|
19
|
+
it( 'renders the header as a non-button when it also has a toggle', () => {
|
|
20
|
+
const { container } = render( <CoreCard header="Settings" actionType="toggle" onHeaderClick={ () => {} } /> );
|
|
21
|
+
expect( getHeader( container ).tagName ).not.toBe( 'BUTTON' );
|
|
22
|
+
} );
|
|
23
|
+
|
|
24
|
+
it( 'renders the header as a non-button when it also has a header action', () => {
|
|
25
|
+
const { container } = render(
|
|
26
|
+
<CoreCard header="Settings" headerAction={ { label: 'Edit', onClick: () => {} } } onHeaderClick={ () => {} } />
|
|
27
|
+
);
|
|
28
|
+
expect( getHeader( container ).tagName ).not.toBe( 'BUTTON' );
|
|
29
|
+
} );
|
|
30
|
+
|
|
31
|
+
it( 'renders the header as a non-button when it also has an actions menu', () => {
|
|
32
|
+
const { container } = render(
|
|
33
|
+
<CoreCard header="Settings" actions={ [ { label: 'Delete', action: () => {} } ] } onHeaderClick={ () => {} } />
|
|
34
|
+
);
|
|
35
|
+
expect( getHeader( container ).tagName ).not.toBe( 'BUTTON' );
|
|
36
|
+
} );
|
|
37
|
+
|
|
38
|
+
it( 'renders the header as a non-button when it is also draggable', () => {
|
|
39
|
+
const { container } = render( <CoreCard header="Settings" isDraggable onHeaderClick={ () => {} } /> );
|
|
40
|
+
expect( getHeader( container ).tagName ).not.toBe( 'BUTTON' );
|
|
41
|
+
} );
|
|
42
|
+
} );
|
|
@@ -33,7 +33,7 @@ type DraggableItemAction = {
|
|
|
33
33
|
type DraggableItem = {
|
|
34
34
|
id: string | number;
|
|
35
35
|
title: string;
|
|
36
|
-
description?:
|
|
36
|
+
description?: React.ReactNode;
|
|
37
37
|
badgeLevel: 'default' | 'success' | 'info' | 'warning' | 'error';
|
|
38
38
|
badgeText: string;
|
|
39
39
|
toggleChecked?: boolean;
|
package/src/divider/README.md
CHANGED
|
@@ -10,7 +10,7 @@ Horizontal rule component with alignment, variant, and margin options.
|
|
|
10
10
|
| `className` | `string` | — | Additional CSS class. |
|
|
11
11
|
| `marginBottom` | `number` \| `string` | `64` | Bottom margin (e.g. `64` or `"2rem"`). Capped at 32px on viewports < 783px. |
|
|
12
12
|
| `marginTop` | `number` \| `string` | `64` | Top margin (e.g. `64` or `"2rem"`). Capped at 32px on viewports < 783px. |
|
|
13
|
-
| `variant` | `'default'` \| `'primary'` \| `'secondary'` \| `'tertiary'` | `'default'` | Line color: `default` uses `$gray-300`; `primary` uses the admin theme color (`--wp-admin-theme-color`); `secondary` uses `$gray-
|
|
13
|
+
| `variant` | `'default'` \| `'primary'` \| `'secondary'` \| `'tertiary'` | `'default'` | Line color: `default` uses `$gray-300`; `primary` uses the admin theme color (`--wp-admin-theme-color`); `secondary` uses `$gray-200`; `tertiary` uses `$gray-100`. |
|
|
14
14
|
|
|
15
15
|
## Usage
|
|
16
16
|
|
package/src/divider/style.scss
CHANGED
|
@@ -22,21 +22,24 @@ import classnames from 'classnames';
|
|
|
22
22
|
/**
|
|
23
23
|
* WizardSnackbar component.
|
|
24
24
|
*
|
|
25
|
+
* Wraps core's Snackbar with the wizard-store `onRemove` glue. Positioning and
|
|
26
|
+
* styling are neutral (bottom-centered via the snackbar list container). The
|
|
27
|
+
* notice `type` no longer drives any visual styling, but it still maps to the
|
|
28
|
+
* screen-reader announcement politeness: only `error` announces assertively,
|
|
29
|
+
* every other severity announces politely.
|
|
30
|
+
*
|
|
31
|
+
* Remaining props are spread onto core's Snackbar. See:
|
|
32
|
+
* https://wordpress.github.io/gutenberg/?path=/docs/components-snackbar--docs
|
|
33
|
+
*
|
|
25
34
|
* @param {Object} props - The component props.
|
|
26
|
-
* @param {Object[]} props.actions - The actions to display in the snackbar.
|
|
27
35
|
* @param {JSX.Element} props.children - The component children.
|
|
28
|
-
* @param {
|
|
29
|
-
* @param {
|
|
30
|
-
* @param {string} props.type - The snackbar type: 'info', 'success', 'warning', or 'error'.
|
|
36
|
+
* @param {string} props.type - The notice severity ('error' announces assertively, anything else politely).
|
|
37
|
+
* @param {Object[]} props.actions - The actions to display in the snackbar.
|
|
31
38
|
* @return {JSX.Element} The component.
|
|
32
39
|
*/
|
|
33
|
-
const WizardSnackbar = ( { children,
|
|
34
|
-
const className = classnames(
|
|
35
|
-
|
|
36
|
-
props.className,
|
|
37
|
-
`newspack-wizard__snackbar--${ position }`,
|
|
38
|
-
`newspack-wizard__snackbar--${ type }`
|
|
39
|
-
);
|
|
40
|
+
const WizardSnackbar = ( { children, type, actions = [], ...props } ) => {
|
|
41
|
+
const className = classnames( 'newspack-wizard__snackbar', props.className );
|
|
42
|
+
const politeness = 'error' === type ? 'assertive' : 'polite';
|
|
40
43
|
const { removeNotice, resetNotices } = useDispatch( WIZARD_STORE_NAMESPACE );
|
|
41
44
|
const onRemove = () => {
|
|
42
45
|
if ( props.onRemove ) {
|
|
@@ -49,7 +52,7 @@ const WizardSnackbar = ( { children, position = 'bottom-left', type = 'info', ac
|
|
|
49
52
|
}
|
|
50
53
|
};
|
|
51
54
|
return (
|
|
52
|
-
<BaseComponent className={ className } {
|
|
55
|
+
<BaseComponent { ...props } className={ className } politeness={ politeness } onRemove={ onRemove } actions={ actions }>
|
|
53
56
|
{ children }
|
|
54
57
|
</BaseComponent>
|
|
55
58
|
);
|
|
@@ -1,39 +1,14 @@
|
|
|
1
|
+
@use "~@wordpress/base-styles/mixins" as wp-mixins;
|
|
2
|
+
@use "~@wordpress/base-styles/variables" as wp-vars;
|
|
3
|
+
|
|
1
4
|
.newspack-wizard {
|
|
2
|
-
&__snackbar {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
left: 16px;
|
|
6
|
-
max-width: calc(100vw - 32px);
|
|
7
|
-
position: fixed;
|
|
5
|
+
&__snackbar-list {
|
|
6
|
+
@include wp-mixins.snackbar-container();
|
|
7
|
+
gap: wp-vars.$grid-unit-10;
|
|
8
8
|
z-index: 99999;
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
&--error {
|
|
13
|
-
border-color: var(--newspack-ui-color-error-60);
|
|
14
|
-
}
|
|
15
|
-
&--warning {
|
|
16
|
-
border-color: var(--newspack-ui-color-warning-40);
|
|
17
|
-
}
|
|
18
|
-
@media screen and ( min-width: 783px ) {
|
|
19
|
-
left: calc(36px + 16px); /* 36px for admin menu bar */
|
|
20
|
-
}
|
|
21
|
-
@media screen and ( min-width: 960px ) {
|
|
22
|
-
left: calc(160px + 16px); /* 160px for admin menu bar */
|
|
23
|
-
}
|
|
24
|
-
&.newspack-wizard__snackbar--bottom-right,
|
|
25
|
-
&.newspack-wizard__snackbar--top-right {
|
|
26
|
-
left: auto;
|
|
27
|
-
right: calc(16px);
|
|
28
|
-
}
|
|
29
|
-
&.newspack-wizard__snackbar--top-left,
|
|
30
|
-
&.newspack-wizard__snackbar--top-right {
|
|
31
|
-
bottom: auto;
|
|
32
|
-
top: calc(16px + 46px); /* 46px for admin bar */
|
|
33
|
-
@media screen and ( min-width: 783px ) {
|
|
34
|
-
top: calc(16px + 32px); /* 32px for admin bar */
|
|
35
|
-
}
|
|
36
|
-
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
&__snackbar {
|
|
37
12
|
a,
|
|
38
13
|
.components-button.is-link {
|
|
39
14
|
color: var(--newspack-ui-color-neutral-0);
|
package/src/wizard/index.js
CHANGED
|
@@ -6,7 +6,7 @@ import classnames from 'classnames';
|
|
|
6
6
|
/**
|
|
7
7
|
* WordPress dependencies.
|
|
8
8
|
*/
|
|
9
|
-
import { DropdownMenu, MenuItem } from '@wordpress/components';
|
|
9
|
+
import { DropdownMenu, MenuGroup, MenuItem } from '@wordpress/components';
|
|
10
10
|
import { useDispatch, useSelect } from '@wordpress/data';
|
|
11
11
|
import { useEffect, useState, forwardRef } from '@wordpress/element';
|
|
12
12
|
import { __ } from '@wordpress/i18n';
|
|
@@ -190,23 +190,37 @@ const Wizard = (
|
|
|
190
190
|
popoverProps={ { className: 'newspack-wizard__header__actions__more' } }
|
|
191
191
|
>
|
|
192
192
|
{ () =>
|
|
193
|
-
actions
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
: 'newspack-wizard__header__actions__more__more'
|
|
193
|
+
// Split actions into groups whenever an action opts in via `separator: true`.
|
|
194
|
+
// Consecutive MenuGroups render the WordPress-standard divider between them.
|
|
195
|
+
actions
|
|
196
|
+
.reduce( ( groups, action ) => {
|
|
197
|
+
if ( action.separator || groups.length === 0 ) {
|
|
198
|
+
groups.push( [] );
|
|
200
199
|
}
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
200
|
+
groups[ groups.length - 1 ].push( action );
|
|
201
|
+
return groups;
|
|
202
|
+
}, [] )
|
|
203
|
+
.map( ( group, groupIndex ) => (
|
|
204
|
+
<MenuGroup key={ groupIndex }>
|
|
205
|
+
{ group.map( ( action, index ) => (
|
|
206
|
+
<MenuItem
|
|
207
|
+
key={ index }
|
|
208
|
+
className={
|
|
209
|
+
action.type === 'primary' || action.type === 'secondary'
|
|
210
|
+
? 'newspack-wizard__header__actions__more__main'
|
|
211
|
+
: 'newspack-wizard__header__actions__more__more'
|
|
212
|
+
}
|
|
213
|
+
icon={ action.icon }
|
|
214
|
+
href={ action.href }
|
|
215
|
+
onClick={ action.action }
|
|
216
|
+
disabled={ action.disabled || false }
|
|
217
|
+
isDestructive={ action.destructive || false }
|
|
218
|
+
>
|
|
219
|
+
{ action.label }
|
|
220
|
+
</MenuItem>
|
|
221
|
+
) ) }
|
|
222
|
+
</MenuGroup>
|
|
223
|
+
) )
|
|
210
224
|
}
|
|
211
225
|
</DropdownMenu>
|
|
212
226
|
</div>
|
|
@@ -263,12 +277,15 @@ const Wizard = (
|
|
|
263
277
|
</Switch>
|
|
264
278
|
</div>
|
|
265
279
|
</HashRouter>
|
|
266
|
-
{ notices?.length > 0 &&
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
{ notice.
|
|
270
|
-
|
|
271
|
-
|
|
280
|
+
{ notices?.length > 0 && (
|
|
281
|
+
<div className="newspack-wizard__snackbar-list">
|
|
282
|
+
{ notices.map( ( notice, index ) => (
|
|
283
|
+
<WizardSnackbar key={ notice.id || index } id={ notice.id } type={ notice.type } actions={ notice.actions }>
|
|
284
|
+
{ notice.message }
|
|
285
|
+
</WizardSnackbar>
|
|
286
|
+
) ) }
|
|
287
|
+
</div>
|
|
288
|
+
) }
|
|
272
289
|
</div>
|
|
273
290
|
{ ! isLoading && <Footer simple={ hasSimpleFooter } /> }
|
|
274
291
|
</div>
|