newspack-components 4.10.0 → 4.12.0-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/DEVELOPMENT.md +80 -38
  3. package/dist/cjs/breadcrumbs/index.js +1 -0
  4. package/dist/cjs/breadcrumbs/style.scss +3 -10
  5. package/dist/cjs/card/style-core.scss +1 -1
  6. package/dist/cjs/card-sortable-list/index.js +23 -12
  7. package/dist/cjs/card-sortable-list/index.test.js +21 -0
  8. package/dist/cjs/card-sortable-list/style.scss +8 -0
  9. package/dist/cjs/drawer/body.js +4 -6
  10. package/dist/cjs/drawer/content.js +7 -37
  11. package/dist/cjs/drawer/footer.js +10 -1
  12. package/dist/cjs/drawer/header.js +9 -1
  13. package/dist/cjs/drawer/index.test.js +3 -4
  14. package/dist/cjs/drawer/slots.test.js +25 -67
  15. package/dist/cjs/drawer/style.scss +0 -18
  16. package/dist/cjs/index.js +14 -0
  17. package/dist/cjs/page/index.js +10 -2
  18. package/dist/cjs/page/style.scss +24 -2
  19. package/dist/cjs/reorder-modal/index.js +521 -0
  20. package/dist/cjs/reorder-modal/index.test.js +1223 -0
  21. package/dist/cjs/reorder-modal/style.scss +114 -0
  22. package/dist/cjs/tabbed-navigation/style.scss +5 -0
  23. package/dist/cjs/tooltip-badge/index.js +50 -0
  24. package/dist/cjs/tooltip-badge/index.test.js +30 -0
  25. package/dist/cjs/wizard/index.js +21 -8
  26. package/dist/cjs/wizard/index.test.js +78 -0
  27. package/dist/cjs/wizard/store/index.js +2 -1
  28. package/dist/esm/breadcrumbs/index.js +1 -0
  29. package/dist/esm/breadcrumbs/style.scss +3 -10
  30. package/dist/esm/card/style-core.scss +1 -1
  31. package/dist/esm/card-sortable-list/index.js +25 -13
  32. package/dist/esm/card-sortable-list/index.test.js +21 -0
  33. package/dist/esm/card-sortable-list/style.scss +8 -0
  34. package/dist/esm/drawer/body.js +4 -5
  35. package/dist/esm/drawer/content.js +7 -36
  36. package/dist/esm/drawer/footer.js +10 -1
  37. package/dist/esm/drawer/header.js +9 -1
  38. package/dist/esm/drawer/index.test.js +3 -4
  39. package/dist/esm/drawer/slots.test.js +25 -67
  40. package/dist/esm/drawer/style.scss +0 -18
  41. package/dist/esm/index.js +2 -0
  42. package/dist/esm/page/index.js +10 -2
  43. package/dist/esm/page/style.scss +24 -2
  44. package/dist/esm/reorder-modal/index.js +514 -0
  45. package/dist/esm/reorder-modal/index.test.js +1224 -0
  46. package/dist/esm/reorder-modal/style.scss +114 -0
  47. package/dist/esm/tabbed-navigation/style.scss +5 -0
  48. package/dist/esm/tooltip-badge/index.js +43 -0
  49. package/dist/esm/tooltip-badge/index.test.js +26 -0
  50. package/dist/esm/wizard/index.js +23 -12
  51. package/dist/esm/wizard/index.test.js +78 -0
  52. package/dist/esm/wizard/store/index.js +2 -1
  53. package/package.json +2 -2
  54. package/src/breadcrumbs/index.js +1 -1
  55. package/src/breadcrumbs/style.scss +3 -10
  56. package/src/card/style-core.scss +1 -1
  57. package/src/card-form/README.md +2 -2
  58. package/src/card-sortable-list/index.test.js +13 -0
  59. package/src/card-sortable-list/index.tsx +31 -12
  60. package/src/card-sortable-list/style.scss +8 -0
  61. package/src/drawer/README.md +10 -16
  62. package/src/drawer/body.tsx +4 -5
  63. package/src/drawer/content.tsx +7 -33
  64. package/src/drawer/footer.tsx +8 -1
  65. package/src/drawer/header.tsx +8 -1
  66. package/src/drawer/index.test.js +3 -4
  67. package/src/drawer/slots.test.js +14 -48
  68. package/src/drawer/style.scss +0 -18
  69. package/src/drawer/types.ts +8 -3
  70. package/src/index.js +2 -0
  71. package/src/page/index.js +7 -3
  72. package/src/page/style.scss +24 -2
  73. package/src/reorder-modal/index.js +484 -0
  74. package/src/reorder-modal/index.test.js +566 -0
  75. package/src/reorder-modal/style.scss +114 -0
  76. package/src/tabbed-navigation/style.scss +5 -0
  77. package/src/tooltip-badge/index.test.js +24 -0
  78. package/src/tooltip-badge/index.tsx +40 -0
  79. package/src/wizard/index.js +25 -14
  80. package/src/wizard/index.test.js +38 -0
  81. package/src/wizard/store/index.js +1 -0
@@ -0,0 +1,114 @@
1
+ @use "~@wordpress/base-styles/colors" as wp-colors;
2
+ @use "~@wordpress/base-styles/mixins" as wp-mixins;
3
+ @use "~@wordpress/base-styles/variables" as wp-vars;
4
+
5
+ .newspack-reorder-modal {
6
+ &__body {
7
+ display: flex;
8
+ flex-direction: column;
9
+ min-height: calc(#{wp-vars.$grid-unit} * 23);
10
+ }
11
+
12
+ &__loading {
13
+ align-items: center;
14
+ display: flex;
15
+ flex: 1;
16
+ justify-content: center;
17
+ }
18
+
19
+ &__notice {
20
+ margin: 0 0 wp-vars.$grid-unit-10;
21
+ }
22
+
23
+ &__list {
24
+ display: flex;
25
+ flex-direction: column;
26
+ gap: wp-vars.$grid-unit-10;
27
+ list-style: none;
28
+ margin: 0;
29
+ padding: 0;
30
+ }
31
+
32
+ &__item {
33
+ --newspack-reorder-modal-divider: #{wp-colors.$gray-100};
34
+ --wp-ui-card-padding: #{wp-vars.$grid-unit-15};
35
+
36
+ align-items: stretch;
37
+ cursor: grab;
38
+ flex-direction: row;
39
+ margin: 0;
40
+
41
+ &:focus-within,
42
+ &:hover {
43
+ --newspack-reorder-modal-divider: #{wp-colors.$gray-300};
44
+
45
+ border-color: wp-colors.$gray-300;
46
+ }
47
+
48
+ &.is-dragging {
49
+ cursor: grabbing;
50
+ opacity: 0.5;
51
+ }
52
+ }
53
+
54
+ &__grip {
55
+ align-items: center;
56
+ color: wp-colors.$gray-700;
57
+ display: flex;
58
+ padding-left: wp-vars.$grid-unit-05;
59
+ }
60
+
61
+ &__move {
62
+ align-items: center;
63
+ border-right: 1px solid var(--newspack-reorder-modal-divider);
64
+ display: flex;
65
+ flex-direction: column;
66
+ padding: wp-vars.$grid-unit-05;
67
+ }
68
+
69
+ &__content {
70
+ align-items: center;
71
+ align-self: center;
72
+ display: flex;
73
+ flex: 1;
74
+ gap: wp-vars.$grid-unit-10;
75
+ min-width: 0;
76
+ padding-block-end: var(--wp-ui-card-padding);
77
+ }
78
+
79
+ &__title {
80
+ @include wp-mixins.heading-large();
81
+
82
+ min-width: 0;
83
+ overflow: hidden;
84
+ text-overflow: ellipsis;
85
+ white-space: nowrap;
86
+ }
87
+
88
+ &__badge {
89
+ display: flex;
90
+ flex-shrink: 0;
91
+ }
92
+
93
+ &__toggle {
94
+ align-items: center;
95
+ display: flex;
96
+ padding-inline-end: wp-vars.$grid-unit-15;
97
+ }
98
+
99
+ &__description {
100
+ margin: 0 0 wp-vars.$grid-unit-20;
101
+ }
102
+
103
+ &__item.is-off &__title {
104
+ color: wp-colors.$gray-700;
105
+ }
106
+
107
+ &__footer {
108
+ display: flex;
109
+ gap: wp-vars.$grid-unit-10;
110
+ justify-content: flex-end;
111
+ margin-top: auto;
112
+ padding-top: wp-vars.$grid-unit-30;
113
+ }
114
+ }
@@ -21,6 +21,11 @@
21
21
  /* stylelint-disable-next-line plugin-wpds/no-setting-wpds-custom-properties -- deliberate Newspack brand override of the indicator token. */
22
22
  --wpds-color-stroke-interactive-neutral-strong: #{np-colors.$primary-600};
23
23
 
24
+ [role="tablist"] {
25
+ max-width: 100%;
26
+ overflow-x: auto;
27
+ }
28
+
24
29
  a[role="tab"] {
25
30
  text-decoration: none;
26
31
 
@@ -0,0 +1,43 @@
1
+ /**
2
+ * WordPress dependencies
3
+ */
4
+ import { useInstanceId } from '@wordpress/compose';
5
+ import { Badge, Tooltip } from '@wordpress/ui';
6
+
7
+ /**
8
+ * Internal dependencies
9
+ */
10
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
11
+ /**
12
+ * A badge whose tooltip explains it. The tooltip serves mouse and keyboard users;
13
+ * screen readers get the same text as the trigger's description, which keeps it
14
+ * out of the accessible name of a heading the badge sits in.
15
+ */
16
+ var _TooltipBadge = function TooltipBadge(_ref) {
17
+ var label = _ref.label,
18
+ tooltip = _ref.tooltip,
19
+ _ref$intent = _ref.intent,
20
+ intent = _ref$intent === void 0 ? 'none' : _ref$intent;
21
+ var descriptionId = useInstanceId(_TooltipBadge, 'newspack-tooltip-badge');
22
+ return /*#__PURE__*/_jsxs(_Fragment, {
23
+ children: [/*#__PURE__*/_jsxs(Tooltip.Root, {
24
+ children: [/*#__PURE__*/_jsx(Tooltip.Trigger, {
25
+ render: /*#__PURE__*/_jsx("span", {
26
+ tabIndex: 0,
27
+ "aria-describedby": descriptionId
28
+ }),
29
+ children: /*#__PURE__*/_jsx(Badge, {
30
+ intent: intent,
31
+ children: label
32
+ })
33
+ }), /*#__PURE__*/_jsx(Tooltip.Popup, {
34
+ children: tooltip
35
+ })]
36
+ }), /*#__PURE__*/_jsx("span", {
37
+ id: descriptionId,
38
+ hidden: true,
39
+ children: tooltip
40
+ })]
41
+ });
42
+ };
43
+ export default _TooltipBadge;
@@ -0,0 +1,26 @@
1
+ /**
2
+ * External dependencies
3
+ */
4
+ import { render, screen } from '@testing-library/react';
5
+
6
+ /**
7
+ * Internal dependencies
8
+ */
9
+ import TooltipBadge from './';
10
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
11
+ describe('TooltipBadge', function () {
12
+ it('describes its focusable trigger with the tooltip text, keeping it out of a surrounding heading name', function () {
13
+ render(/*#__PURE__*/_jsxs("h3", {
14
+ children: ["Registration wall", /*#__PURE__*/_jsx(TooltipBadge, {
15
+ label: "Overrides paid access",
16
+ tooltip: "This grants registered readers access.",
17
+ intent: "low"
18
+ })]
19
+ }));
20
+ expect(screen.getByRole('heading', {
21
+ name: 'Registration wall Overrides paid access'
22
+ })).toBeInTheDocument();
23
+ var trigger = screen.getByText('Overrides paid access').closest('[tabindex="0"]');
24
+ expect(trigger).toHaveAccessibleDescription('This grants registered readers access.');
25
+ });
26
+ });
@@ -13,9 +13,8 @@ import classnames from 'classnames';
13
13
  /**
14
14
  * WordPress dependencies.
15
15
  */
16
- import { DropdownMenu, MenuGroup, MenuItem, Notice, SlotFillProvider, createSlotFill,
17
- // eslint-disable-next-line @wordpress/no-unsafe-wp-apis
18
- __experimentalVStack as VStack } from '@wordpress/components';
16
+ import { DropdownMenu, MenuGroup, MenuItem, Notice, SlotFillProvider, createSlotFill } from '@wordpress/components';
17
+ import { Stack } from '@wordpress/ui';
19
18
  import { useDispatch, useSelect } from '@wordpress/data';
20
19
  import { cloneElement, createInterpolateElement, isValidElement, useLayoutEffect, useRef, useState, forwardRef } from '@wordpress/element';
21
20
  import { __ } from '@wordpress/i18n';
@@ -30,7 +29,7 @@ import Router from '../proxied-imports/router';
30
29
  import registerStore, { WIZARD_STORE_NAMESPACE } from './store';
31
30
  import WizardSnackbar from './components/WizardSnackbar';
32
31
  import WizardError from './components/WizardError';
33
- import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
32
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
34
33
  registerStore();
35
34
 
36
35
  /**
@@ -115,12 +114,13 @@ var ResetHeaderData = function ResetHeaderData() {
115
114
  * can read the current route and derive the active-tab breadcrumb.
116
115
  */
117
116
  var WizardHeaderRegion = function WizardHeaderRegion(_ref) {
118
- var _activeSection;
117
+ var _activeSection, _activeSection2;
119
118
  var hideHeader = _ref.hideHeader,
120
119
  headerText = _ref.headerText,
121
120
  sections = _ref.sections,
122
121
  sectionName = _ref.sectionName,
123
122
  subTitle = _ref.subTitle,
123
+ headerSubTitle = _ref.headerSubTitle,
124
124
  actions = _ref.actions,
125
125
  wizardTabbedNavigation = _ref.tabbedNavigation,
126
126
  children = _ref.children;
@@ -150,11 +150,19 @@ var WizardHeaderRegion = function WizardHeaderRegion(_ref) {
150
150
  break;
151
151
  }
152
152
  }
153
+
154
+ // WizardError normally mounts inside the tab bar, so a route without one renders it itself.
153
155
  } catch (err) {
154
156
  _iterator.e(err);
155
157
  } finally {
156
158
  _iterator.f();
157
159
  }
160
+ if ((_activeSection = activeSection(sections, pathname)) !== null && _activeSection !== void 0 && _activeSection.hideTabbedNavigation && tabbedNavigation) {
161
+ tabbedNavigation = null;
162
+ children = /*#__PURE__*/_jsxs(_Fragment, {
163
+ children: [/*#__PURE__*/_jsx(WizardError, {}), children]
164
+ });
165
+ }
158
166
  if (hideHeader) {
159
167
  // Without the Page shell the tabs still own the content: it renders
160
168
  // inside the active tab's panel.
@@ -174,10 +182,10 @@ var WizardHeaderRegion = function WizardHeaderRegion(_ref) {
174
182
  // Append any render-time leaf crumb(s) the section supplied via
175
183
  // headerData.sectionName (deduped against the current trailing label).
176
184
  breadcrumbItems = appendSectionName(breadcrumbItems, sectionName);
177
- var sectionSubTitle = (_activeSection = activeSection(sections, pathname)) === null || _activeSection === void 0 ? void 0 : _activeSection.subHeaderText;
185
+ var sectionSubTitle = (_activeSection2 = activeSection(sections, pathname)) === null || _activeSection2 === void 0 ? void 0 : _activeSection2.subHeaderText;
178
186
  return /*#__PURE__*/_jsx(Page, {
179
187
  breadcrumbItems: breadcrumbItems,
180
- subTitle: sectionSubTitle !== null && sectionSubTitle !== void 0 ? sectionSubTitle : subTitle,
188
+ subTitle: headerSubTitle || (sectionSubTitle !== null && sectionSubTitle !== void 0 ? sectionSubTitle : subTitle),
181
189
  actions: actions,
182
190
  tabbedNavigation: tabbedNavigation,
183
191
  children: children
@@ -190,7 +198,7 @@ var WizardHeaderRegion = function WizardHeaderRegion(_ref) {
190
198
  * @property {string} [subHeaderText] The sub-header text, optional.
191
199
  * @property {string} [apiSlug] The API slug, optional.
192
200
  * @property {string} [className] CSS classes, optional.
193
- * @property {any[]} sections Array of sections. A section's own `subHeaderText` replaces the wizard's while it is active.
201
+ * @property {any[]} sections Array of sections. A section's own `subHeaderText` replaces the wizard's while it is active, and `hideTabbedNavigation` hides the tab bar on its route.
194
202
  * Its optional `tabbedNavigation( params )` returns the tab items shown while its route matches.
195
203
  * @property {boolean} [hasSimpleFooter] Indicates if a simple footer is used, optional.
196
204
  * @property {() => void} [renderAboveSections] Function to render content above sections, optional.
@@ -248,7 +256,8 @@ var Wizard = function Wizard(_ref2, ref) {
248
256
  sectionSize = headerData.sectionSize,
249
257
  sectionTitle = headerData.sectionTitle,
250
258
  sectionPrimaryAction = headerData.sectionPrimaryAction,
251
- sectionSecondaryAction = headerData.sectionSecondaryAction;
259
+ sectionSecondaryAction = headerData.sectionSecondaryAction,
260
+ headerSubTitle = headerData.subTitle;
252
261
  var mainActions = actions === null || actions === void 0 ? void 0 : actions.filter(function (action) {
253
262
  return action.type === 'primary' || action.type === 'secondary';
254
263
  });
@@ -311,9 +320,10 @@ var Wizard = function Wizard(_ref2, ref) {
311
320
  return /*#__PURE__*/_jsxs(_Fragment, {
312
321
  children: [!pluginRequirementsKnown && /*#__PURE__*/_jsx("div", {
313
322
  className: "newspack-wizard__loader",
314
- children: /*#__PURE__*/_jsxs(VStack, {
315
- alignment: "center",
316
- spacing: 2,
323
+ children: /*#__PURE__*/_jsxs(Stack, {
324
+ direction: "column",
325
+ align: "center",
326
+ gap: "sm",
317
327
  children: [/*#__PURE__*/_jsx(Waiting, {
318
328
  noMargin: true
319
329
  }), /*#__PURE__*/_jsx("strong", {
@@ -467,6 +477,7 @@ var Wizard = function Wizard(_ref2, ref) {
467
477
  sections: routedSections,
468
478
  sectionName: sectionName,
469
479
  subTitle: subHeaderText,
480
+ headerSubTitle: headerSubTitle,
470
481
  actions: headerActions,
471
482
  tabbedNavigation: tabbedNavigation,
472
483
  children: content
@@ -550,4 +550,82 @@ describe('Wizard section subtitle', function () {
550
550
  }
551
551
  }, _callee1);
552
552
  })));
553
+ });
554
+ describe('Wizard tab bar on hidden routes', function () {
555
+ beforeEach(function () {
556
+ apiFetch.mockReset();
557
+ apiFetch.mockResolvedValue({});
558
+ });
559
+ it('hides the tab bar on a hideTabbedNavigation route and restores it on a tab route', /*#__PURE__*/_asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee10() {
560
+ var _t10, _t11, _t12;
561
+ return _regenerator().w(function (_context10) {
562
+ while (1) switch (_context10.n) {
563
+ case 0:
564
+ window.location.hash = '#/list';
565
+ render(/*#__PURE__*/_jsx(Wizard, {
566
+ headerText: "Test wizard",
567
+ sections: [{
568
+ label: 'List',
569
+ path: '/list',
570
+ exact: true,
571
+ render: function render() {
572
+ return /*#__PURE__*/_jsx("div", {
573
+ children: "List view"
574
+ });
575
+ }
576
+ }, {
577
+ label: 'Other',
578
+ path: '/other',
579
+ exact: true,
580
+ render: function render() {
581
+ return /*#__PURE__*/_jsx("div", {
582
+ children: "Other view"
583
+ });
584
+ }
585
+ }, {
586
+ path: '/list/:id',
587
+ isHidden: true,
588
+ hideTabbedNavigation: true,
589
+ render: function render() {
590
+ return /*#__PURE__*/_jsx("div", {
591
+ children: "Item view"
592
+ });
593
+ }
594
+ }]
595
+ }));
596
+ _t10 = expect;
597
+ _context10.n = 1;
598
+ return screen.findByText('List view');
599
+ case 1:
600
+ _t10(_context10.v).toBeInTheDocument();
601
+ expect(screen.getByRole('tab', {
602
+ name: 'Other'
603
+ })).toBeInTheDocument();
604
+ act(function () {
605
+ window.location.hash = '#/list/esp';
606
+ window.dispatchEvent(new HashChangeEvent('hashchange'));
607
+ });
608
+ _t11 = expect;
609
+ _context10.n = 2;
610
+ return screen.findByText('Item view');
611
+ case 2:
612
+ _t11(_context10.v).toBeInTheDocument();
613
+ expect(screen.queryByRole('tab')).not.toBeInTheDocument();
614
+ act(function () {
615
+ window.location.hash = '#/list';
616
+ window.dispatchEvent(new HashChangeEvent('hashchange'));
617
+ });
618
+ _t12 = expect;
619
+ _context10.n = 3;
620
+ return screen.findByText('List view');
621
+ case 3:
622
+ _t12(_context10.v).toBeInTheDocument();
623
+ expect(screen.getByRole('tab', {
624
+ name: 'Other'
625
+ })).toBeInTheDocument();
626
+ case 4:
627
+ return _context10.a(2);
628
+ }
629
+ }, _callee10);
630
+ })));
553
631
  });
@@ -27,7 +27,8 @@ var DEFAULT_STATE = {
27
27
  badges: [],
28
28
  sectionDescription: '',
29
29
  sectionName: '',
30
- sectionTitle: ''
30
+ sectionTitle: '',
31
+ subTitle: ''
31
32
  },
32
33
  isLoading: false,
33
34
  isQuietLoading: false,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "newspack-components",
3
- "version": "4.10.0",
3
+ "version": "4.12.0-alpha.1",
4
4
  "description": "Newspack design system components",
5
5
  "author": "Automattic",
6
6
  "license": "GPL-2.0-or-later",
@@ -30,7 +30,7 @@
30
30
  "classnames": "^2.3.1",
31
31
  "lodash": "^4.17.21",
32
32
  "moment-range": "^3.1.1",
33
- "newspack-icons": "1.2.0",
33
+ "newspack-icons": "1.3.0-alpha.1",
34
34
  "qs": "^6.15.2",
35
35
  "react-router-dom": "^5.3.4",
36
36
  "@babel/runtime": "^7.29.7",
@@ -80,7 +80,7 @@ const Breadcrumbs = ( { items = [] } ) => {
80
80
 
81
81
  return (
82
82
  <nav aria-label={ __( 'Breadcrumbs', 'newspack-plugin' ) }>
83
- <Stack render={ <ul /> } direction="row" align="center" className="newspack-breadcrumbs__list">
83
+ <Stack render={ <ul /> } direction="row" align="center" wrap="wrap" className="newspack-breadcrumbs__list">
84
84
  { preceding.map( ( item, index ) => (
85
85
  <li key={ item.url || index }>
86
86
  { item.url ? (
@@ -7,8 +7,8 @@
7
7
  padding: 0;
8
8
 
9
9
  > li {
10
- flex-shrink: 0;
11
10
  margin: 0;
11
+ min-width: 0;
12
12
 
13
13
  a {
14
14
  color: wp-colors.$gray-900;
@@ -20,11 +20,6 @@
20
20
  }
21
21
  }
22
22
  }
23
-
24
- > li:last-child {
25
- flex-shrink: 1;
26
- min-width: 0;
27
- }
28
23
  }
29
24
 
30
25
  .newspack-breadcrumbs__count {
@@ -39,9 +34,7 @@
39
34
  }
40
35
 
41
36
  .newspack-breadcrumbs__list h1.newspack-breadcrumbs__current {
42
- overflow: hidden;
43
- text-overflow: ellipsis;
44
- white-space: nowrap;
37
+ overflow-wrap: break-word;
45
38
  padding: 0;
46
- line-height: normal;
39
+ line-height: var(--wpds-typography-line-height-md, #{wp-vars.$grid-unit-30});
47
40
  }
@@ -221,7 +221,7 @@
221
221
  color: inherit;
222
222
  text-decoration: none;
223
223
  &:hover {
224
- text-decoration: underline;
224
+ color: var(--wp-admin-theme-color);
225
225
  }
226
226
  }
227
227
  }
@@ -4,8 +4,8 @@ A card component for presenting a named setting or feature with an expandable in
4
4
 
5
5
  ## Layout rules
6
6
 
7
- - Stack multiple `CardForm` cards inside a `<VStack>` — they are designed to appear as a list.
8
- - The `actions` slot sits to the right of the badge. Keep it to one button; if you need multiple actions, use an `HStack` with `expanded={ false }`.
7
+ - Stack multiple `CardForm` cards inside a `<Stack direction="column" gap="lg">` from `@wordpress/ui`; they are designed to appear as a list.
8
+ - The `actions` slot sits to the right of the badge. Keep it to one button; if you need multiple actions, wrap them in a `<Stack gap="sm" align="center">` from `@wordpress/ui`.
9
9
  - The form body (`children`) is only mounted when `isOpen` is `true`.
10
10
 
11
11
  ## States
@@ -26,4 +26,17 @@ describe( 'CardSortableList', () => {
26
26
  // An unguarded Badge would still emit an empty span, so assert on element children.
27
27
  expect( container.querySelector( 'h3' ).children ).toHaveLength( 0 );
28
28
  } );
29
+
30
+ it( 'names each toggle after its item', () => {
31
+ render(
32
+ <CardSortableList
33
+ items={ [
34
+ { id: 1, title: 'Homepage', toggleChecked: true, onToggleChange: () => {} },
35
+ { id: 2, title: 'About', toggleChecked: false, onToggleChange: () => {} },
36
+ ] }
37
+ />
38
+ );
39
+ expect( screen.getByRole( 'checkbox', { name: 'Homepage' } ) ).toBeChecked();
40
+ expect( screen.getByRole( 'checkbox', { name: 'About' } ) ).not.toBeChecked();
41
+ } );
29
42
  } );
@@ -2,25 +2,26 @@
2
2
  * Card - Sortable list component.
3
3
  */
4
4
 
5
+ /**
6
+ * External dependencies
7
+ */
8
+ import classNames from 'classnames';
9
+
5
10
  /**
6
11
  * WordPress dependencies.
7
12
  */
8
- import { Disabled, Draggable, __experimentalVStack as VStack } from '@wordpress/components'; // eslint-disable-line @wordpress/no-unsafe-wp-apis
13
+ import { Disabled, Draggable } from '@wordpress/components';
9
14
  import { useEffect, useLayoutEffect, useRef, useState } from '@wordpress/element';
10
- import { Badge } from '@wordpress/ui';
15
+ import { Badge, Stack } from '@wordpress/ui';
11
16
 
12
17
  /**
13
18
  * Internal dependencies
14
19
  */
15
- import { Card } from '../';
20
+ import Card from '../card';
21
+ import TooltipBadge from '../tooltip-badge';
16
22
  import type { CardBadge } from '../types';
17
23
  import './style.scss';
18
24
 
19
- /**
20
- * External dependencies
21
- */
22
- import classNames from 'classnames';
23
-
24
25
  const DROP_ANIMATION_DURATION = 400; // ms — must match $drop-duration in style.scss
25
26
  const BUTTON_MOVE_DURATION = 200; // ms — must match $shift-duration in style.scss
26
27
 
@@ -37,6 +38,8 @@ type DraggableItem = {
37
38
  title: string;
38
39
  description?: React.ReactNode;
39
40
  badge?: CardBadge;
41
+ /** A second badge, e.g. a warning, whose tooltip explains it. */
42
+ secondaryBadge?: CardBadge & { tooltip?: string };
40
43
  toggleChecked?: boolean;
41
44
  onToggleChange?: () => void;
42
45
  actions?: DraggableItemAction[];
@@ -45,7 +48,7 @@ type DraggableItem = {
45
48
  type DragMeasurements = {
46
49
  lockedHeight: number;
47
50
  sourceHeight: number;
48
- // Height of each item (including the VStack gap after it, except the last).
51
+ // Height of each item (including the Stack gap after it, except the last).
49
52
  itemStrides: number[];
50
53
  // Top edge of each item, used for drop position hit-testing in onDragEnd.
51
54
  itemTops: number[];
@@ -357,7 +360,9 @@ const CardSortableList = ( {
357
360
  const isDragging = draggingIndex !== null;
358
361
 
359
362
  return (
360
- <VStack
363
+ <Stack
364
+ direction="column"
365
+ gap="lg"
361
366
  ref={ listRef }
362
367
  className={ classNames(
363
368
  'newspack-card--core--sortable-list',
@@ -365,7 +370,6 @@ const CardSortableList = ( {
365
370
  isDragging && 'newspack-card--core--sortable-list__is-dragging'
366
371
  ) }
367
372
  style={ measurements ? { height: measurements.lockedHeight } : undefined }
368
- spacing={ 4 }
369
373
  >
370
374
  { sortedItems.map( ( item, index ) => {
371
375
  const translateY = getTranslateY( index );
@@ -406,6 +410,18 @@ const CardSortableList = ( {
406
410
  { item.badge?.label && (
407
411
  <Badge intent={ item.badge.intent ?? 'none' }>{ item.badge.label }</Badge>
408
412
  ) }
413
+ { item.secondaryBadge?.label &&
414
+ ( item.secondaryBadge.tooltip ? (
415
+ <TooltipBadge
416
+ label={ item.secondaryBadge.label }
417
+ tooltip={ item.secondaryBadge.tooltip }
418
+ intent={ item.secondaryBadge.intent }
419
+ />
420
+ ) : (
421
+ <Badge intent={ item.secondaryBadge.intent ?? 'none' }>
422
+ { item.secondaryBadge.label }
423
+ </Badge>
424
+ ) ) }
409
425
  </h3>
410
426
  { item.description && <p>{ item.description }</p> }
411
427
  </>
@@ -419,6 +435,9 @@ const CardSortableList = ( {
419
435
  actionType: 'toggle',
420
436
  isActive: item.toggleChecked,
421
437
  onToggle: item.onToggleChange,
438
+ // Names the toggle for assistive tech. The card hides this
439
+ // label, since the header already shows the title.
440
+ title: item.title,
422
441
  } ),
423
442
  ...( item.actions !== undefined && {
424
443
  actions: item.actions,
@@ -431,7 +450,7 @@ const CardSortableList = ( {
431
450
  </Disabled>
432
451
  );
433
452
  } ) }
434
- </VStack>
453
+ </Stack>
435
454
  );
436
455
  };
437
456
 
@@ -6,6 +6,14 @@ $drop-duration: 400ms; /* must match DROP_ANIMATION_DURATION in index.tsx */
6
6
  &__item {
7
7
  cursor: grab;
8
8
 
9
+ /*
10
+ * Keep a card's title in the case it was written in. The block editor
11
+ * sidebar uppercases every h3 (`.interface-complementary-area h3`).
12
+ */
13
+ .newspack-card--core__header-content h3 {
14
+ text-transform: none;
15
+ }
16
+
9
17
  /*
10
18
  * Animate items sliding to fill/make space around the dragged item.
11
19
  */
@@ -148,9 +148,9 @@ unsaved-changes confirmation always applies.
148
148
 
149
149
  ## `Drawer.Content`
150
150
 
151
- The body. Repeatable: each `Drawer.Content` is a section, rendered as a VStack
152
- with a 16px gap by default; `gap` changes it. Children space through the gap
153
- alone; their own top and bottom margins are reset.
151
+ The body. Repeatable: each `Drawer.Content` is a section that stacks its
152
+ children in a column, 16px apart by default; `gap` changes it. Children space
153
+ through the gap alone; their own top and bottom margins are reset.
154
154
 
155
155
  Anything can go in a section: an element, a control, a nested stack, or plain
156
156
  text. Each child becomes a row separated by the gap, and a run of plain text and
@@ -165,8 +165,8 @@ with markup in your own element and hand the section that instead.
165
165
  |------|------|---------|-------------|
166
166
  | `children` | `React.ReactNode` | — | Section content. |
167
167
  | `className` | `string` | — | Additional CSS class. |
168
- | `gap` | `number` | `4` | Space between the section's children, on the 4px scale, as VStack's `spacing`. `4` is 16px. |
169
- | `padding` | `number` | `6` | On the 4px scale, as VStack's `spacing`. `6` is 24px; `0` is a flush section that brings its own padding. |
168
+ | `gap` | `string` | `'lg'` | Space between the section's children, as a `Stack` gap token from `@wordpress/ui`. `'lg'` is 16px. |
169
+ | `padding` | `number` | `6` | On the 4px scale. `6` is 24px; `0` is a flush section that brings its own padding. |
170
170
 
171
171
  Consecutive sections share one scroll container, so they scroll together between
172
172
  the pinned header and footer. Sections are not self-separating: put a
@@ -234,18 +234,12 @@ separator.
234
234
 
235
235
  ## `Drawer.Footer`
236
236
 
237
- A pinned container for `Drawer.Action` elements. The layout is pure CSS on the
238
- child count.
237
+ A pinned container for `Drawer.Action` elements. Whatever the number of
238
+ actions, they sit in one row aligned to the end, each at its natural width. The
239
+ row wraps when the actions don't fit side by side.
239
240
 
240
- | Children | Layout |
241
- |---|---|
242
- | 1 | Row, the button fills it |
243
- | 2 | Row, split 50/50 |
244
- | 3 or more | Column, full-width buttons |
245
-
246
- Convention, not enforced: with two actions pass the secondary first, so the
247
- primary sits on the right; with three pass primary, secondary, tertiary
248
- top-down.
241
+ Convention, not enforced: pass the primary action last, so it sits at the end
242
+ of the row.
249
243
 
250
244
  Takes `className` and `children`.
251
245