newspack-components 4.11.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 (66) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/DEVELOPMENT.md +79 -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 +17 -9
  7. package/dist/cjs/drawer/body.js +4 -6
  8. package/dist/cjs/drawer/content.js +7 -37
  9. package/dist/cjs/drawer/footer.js +10 -1
  10. package/dist/cjs/drawer/header.js +9 -1
  11. package/dist/cjs/drawer/index.test.js +3 -4
  12. package/dist/cjs/drawer/slots.test.js +25 -67
  13. package/dist/cjs/drawer/style.scss +0 -18
  14. package/dist/cjs/index.js +7 -0
  15. package/dist/cjs/page/index.js +10 -2
  16. package/dist/cjs/page/style.scss +24 -2
  17. package/dist/cjs/tabbed-navigation/style.scss +5 -0
  18. package/dist/cjs/tooltip-badge/index.js +50 -0
  19. package/dist/cjs/tooltip-badge/index.test.js +30 -0
  20. package/dist/cjs/wizard/index.js +21 -8
  21. package/dist/cjs/wizard/index.test.js +78 -0
  22. package/dist/cjs/wizard/store/index.js +2 -1
  23. package/dist/esm/breadcrumbs/index.js +1 -0
  24. package/dist/esm/breadcrumbs/style.scss +3 -10
  25. package/dist/esm/card/style-core.scss +1 -1
  26. package/dist/esm/card-sortable-list/index.js +20 -11
  27. package/dist/esm/drawer/body.js +4 -5
  28. package/dist/esm/drawer/content.js +7 -36
  29. package/dist/esm/drawer/footer.js +10 -1
  30. package/dist/esm/drawer/header.js +9 -1
  31. package/dist/esm/drawer/index.test.js +3 -4
  32. package/dist/esm/drawer/slots.test.js +25 -67
  33. package/dist/esm/drawer/style.scss +0 -18
  34. package/dist/esm/index.js +1 -0
  35. package/dist/esm/page/index.js +10 -2
  36. package/dist/esm/page/style.scss +24 -2
  37. package/dist/esm/tabbed-navigation/style.scss +5 -0
  38. package/dist/esm/tooltip-badge/index.js +43 -0
  39. package/dist/esm/tooltip-badge/index.test.js +26 -0
  40. package/dist/esm/wizard/index.js +23 -12
  41. package/dist/esm/wizard/index.test.js +78 -0
  42. package/dist/esm/wizard/store/index.js +2 -1
  43. package/package.json +2 -2
  44. package/src/breadcrumbs/index.js +1 -1
  45. package/src/breadcrumbs/style.scss +3 -10
  46. package/src/card/style-core.scss +1 -1
  47. package/src/card-form/README.md +2 -2
  48. package/src/card-sortable-list/index.tsx +27 -11
  49. package/src/drawer/README.md +10 -16
  50. package/src/drawer/body.tsx +4 -5
  51. package/src/drawer/content.tsx +7 -33
  52. package/src/drawer/footer.tsx +8 -1
  53. package/src/drawer/header.tsx +8 -1
  54. package/src/drawer/index.test.js +3 -4
  55. package/src/drawer/slots.test.js +14 -48
  56. package/src/drawer/style.scss +0 -18
  57. package/src/drawer/types.ts +8 -3
  58. package/src/index.js +1 -0
  59. package/src/page/index.js +7 -3
  60. package/src/page/style.scss +24 -2
  61. package/src/tabbed-navigation/style.scss +5 -0
  62. package/src/tooltip-badge/index.test.js +24 -0
  63. package/src/tooltip-badge/index.tsx +40 -0
  64. package/src/wizard/index.js +25 -14
  65. package/src/wizard/index.test.js +38 -0
  66. package/src/wizard/store/index.js +1 -0
@@ -80,10 +80,7 @@ $drawer-size-x-large: 640px;
80
80
  }
81
81
 
82
82
  .newspack-drawer__header {
83
- align-items: center;
84
- display: flex;
85
83
  flex: 0 0 auto;
86
- gap: wp-vars.$grid-unit-10;
87
84
  padding: wp-vars.$grid-unit-30 wp-vars.$grid-unit-30 wp-vars.$grid-unit-20;
88
85
  }
89
86
 
@@ -139,23 +136,8 @@ $drawer-size-x-large: 640px;
139
136
 
140
137
  .newspack-drawer__footer {
141
138
  background: wp-colors.$white;
142
- display: flex;
143
139
  flex: 0 0 auto;
144
- gap: wp-vars.$grid-unit-10;
145
140
  padding: wp-vars.$grid-unit-20 wp-vars.$grid-unit-30 wp-vars.$grid-unit-30;
146
-
147
- > .components-button {
148
- flex: 1;
149
- justify-content: center;
150
- }
151
-
152
- &:has(> :nth-child(3)) {
153
- flex-direction: column;
154
-
155
- > .components-button {
156
- flex: 0 0 auto;
157
- }
158
- }
159
141
  }
160
142
 
161
143
  @keyframes newspack-drawer-slide-in {
@@ -1,3 +1,8 @@
1
+ /**
2
+ * WordPress dependencies.
3
+ */
4
+ import type { Stack } from '@wordpress/ui';
5
+
1
6
  export type DrawerSize = 'small' | 'medium' | 'large' | 'x-large' | 'full';
2
7
 
3
8
  /** `text` is null unless the title's children are a plain string. */
@@ -39,10 +44,10 @@ export type DrawerCloseIconProps = {
39
44
  };
40
45
 
41
46
  export type DrawerContentProps = {
42
- /** 4px scale, as VStack's `spacing`. `0` for a flush section. */
47
+ /** 4px scale: `6` is 24px, `0` a flush section. */
43
48
  padding?: number;
44
- /** 4px scale, as VStack's `spacing`. */
45
- gap?: number;
49
+ /** A `Stack` gap token from `@wordpress/ui`. */
50
+ gap?: React.ComponentProps< typeof Stack >[ 'gap' ];
46
51
  className?: string;
47
52
  children?: React.ReactNode;
48
53
  };
package/src/index.js CHANGED
@@ -51,6 +51,7 @@ export { default as StyleCard } from './style-card';
51
51
  export { default as TabbedNavigation } from './tabbed-navigation';
52
52
  export { default as TableCard } from './table-card';
53
53
  export { default as TextControl } from './text-control';
54
+ export { default as TooltipBadge } from './tooltip-badge';
54
55
  export { default as Waiting } from './waiting';
55
56
  export { default as WebPreview } from './web-preview';
56
57
  export { default as Wizard } from './wizard';
package/src/page/index.js CHANGED
@@ -49,15 +49,19 @@ const Page = ( { breadcrumbItems = [], badges, subTitle, actions, tabbedNavigati
49
49
  <>
50
50
  <Stack direction="column" className="newspack-page__header-region">
51
51
  <Stack direction="column" className="newspack-page__header">
52
- <Stack direction="row" gap="sm" justify="space-between">
53
- <Stack direction="row" gap="sm" align="center" justify="start">
52
+ <Stack direction="row" gap="sm" justify="space-between" wrap="wrap">
53
+ <Stack direction="row" gap="sm" align="start" justify="start" className="newspack-page__header-title">
54
54
  <div className="newspack-page__header-visual" aria-hidden="true">
55
55
  <Icon icon={ newspack } />
56
56
  </div>
57
57
  <HStack className="newspack-page__breadcrumbs" justify="flex-start">
58
58
  <Breadcrumbs items={ breadcrumbItems } />
59
59
  </HStack>
60
- { badges }
60
+ { badges && (
61
+ <Stack direction="row" gap="sm" align="center" className="newspack-page__header-badges">
62
+ { badges }
63
+ </Stack>
64
+ ) }
61
65
  </Stack>
62
66
  { actions && (
63
67
  <Stack direction="row" gap="sm" align="center" className="newspack-page__header-actions">
@@ -56,8 +56,29 @@
56
56
  padding: var(--wpds-dimension-padding-lg) var(--wpds-dimension-padding-2xl);
57
57
  }
58
58
 
59
+ .newspack-page__header-title {
60
+ min-width: 0;
61
+ }
62
+
59
63
  .newspack-page__header-actions {
64
+ flex-basis: 100%;
65
+ flex-shrink: 0;
66
+ justify-content: flex-end;
67
+ margin-inline-start: auto;
68
+
69
+ @include wp-mixins.break-medium {
70
+ flex-basis: auto;
71
+ }
72
+
73
+ &:empty,
74
+ &:has(> :only-child:empty) {
75
+ display: none;
76
+ }
77
+ }
78
+
79
+ .newspack-page__header-badges {
60
80
  flex-shrink: 0;
81
+ min-height: wp-vars.$button-size;
61
82
  }
62
83
 
63
84
  .newspack-page__header-subtitle {
@@ -68,7 +89,7 @@
68
89
  .newspack-page__header-visual {
69
90
  display: grid;
70
91
  flex-shrink: 0;
71
- height: 24px;
92
+ height: wp-vars.$button-size;
72
93
  place-items: center;
73
94
  width: 24px;
74
95
 
@@ -80,5 +101,6 @@
80
101
  }
81
102
 
82
103
  .newspack-page__breadcrumbs {
83
- min-height: wp-vars.$button-size;
104
+ min-width: 0;
105
+ padding-block: calc((#{wp-vars.$button-size} - var(--wpds-typography-line-height-md)) / 2);
84
106
  }
@@ -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,24 @@
1
+ /**
2
+ * External dependencies
3
+ */
4
+ import { render, screen } from '@testing-library/react';
5
+
6
+ /**
7
+ * Internal dependencies
8
+ */
9
+ import TooltipBadge from './';
10
+
11
+ describe( 'TooltipBadge', () => {
12
+ it( 'describes its focusable trigger with the tooltip text, keeping it out of a surrounding heading name', () => {
13
+ render(
14
+ <h3>
15
+ Registration wall
16
+ <TooltipBadge label="Overrides paid access" tooltip="This grants registered readers access." intent="low" />
17
+ </h3>
18
+ );
19
+
20
+ expect( screen.getByRole( 'heading', { name: 'Registration wall Overrides paid access' } ) ).toBeInTheDocument();
21
+ const trigger = screen.getByText( 'Overrides paid access' ).closest( '[tabindex="0"]' );
22
+ expect( trigger ).toHaveAccessibleDescription( 'This grants registered readers access.' );
23
+ } );
24
+ } );
@@ -0,0 +1,40 @@
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 type { BadgeIntent } from '../types';
11
+
12
+ type TooltipBadgeProps = {
13
+ label: string;
14
+ tooltip: string;
15
+ intent?: BadgeIntent;
16
+ };
17
+
18
+ /**
19
+ * A badge whose tooltip explains it. The tooltip serves mouse and keyboard users;
20
+ * screen readers get the same text as the trigger's description, which keeps it
21
+ * out of the accessible name of a heading the badge sits in.
22
+ */
23
+ const TooltipBadge = ( { label, tooltip, intent = 'none' }: TooltipBadgeProps ) => {
24
+ const descriptionId = useInstanceId( TooltipBadge, 'newspack-tooltip-badge' );
25
+ return (
26
+ <>
27
+ <Tooltip.Root>
28
+ <Tooltip.Trigger render={ <span tabIndex={ 0 } aria-describedby={ descriptionId } /> }>
29
+ <Badge intent={ intent }>{ label }</Badge>
30
+ </Tooltip.Trigger>
31
+ <Tooltip.Popup>{ tooltip }</Tooltip.Popup>
32
+ </Tooltip.Root>
33
+ <span id={ descriptionId } hidden>
34
+ { tooltip }
35
+ </span>
36
+ </>
37
+ );
38
+ };
39
+
40
+ export default TooltipBadge;
@@ -6,16 +6,8 @@ import classnames from 'classnames';
6
6
  /**
7
7
  * WordPress dependencies.
8
8
  */
9
- import {
10
- DropdownMenu,
11
- MenuGroup,
12
- MenuItem,
13
- Notice,
14
- SlotFillProvider,
15
- createSlotFill,
16
- // eslint-disable-next-line @wordpress/no-unsafe-wp-apis
17
- __experimentalVStack as VStack,
18
- } from '@wordpress/components';
9
+ import { DropdownMenu, MenuGroup, MenuItem, Notice, SlotFillProvider, createSlotFill } from '@wordpress/components';
10
+ import { Stack } from '@wordpress/ui';
19
11
  import { useDispatch, useSelect } from '@wordpress/data';
20
12
  import { cloneElement, createInterpolateElement, isValidElement, useLayoutEffect, useRef, useState, forwardRef } from '@wordpress/element';
21
13
  import { __ } from '@wordpress/i18n';
@@ -111,6 +103,7 @@ const WizardHeaderRegion = ( {
111
103
  sections,
112
104
  sectionName,
113
105
  subTitle,
106
+ headerSubTitle,
114
107
  actions,
115
108
  tabbedNavigation: wizardTabbedNavigation,
116
109
  children,
@@ -134,6 +127,17 @@ const WizardHeaderRegion = ( {
134
127
  }
135
128
  }
136
129
 
130
+ // WizardError normally mounts inside the tab bar, so a route without one renders it itself.
131
+ if ( activeSection( sections, pathname )?.hideTabbedNavigation && tabbedNavigation ) {
132
+ tabbedNavigation = null;
133
+ children = (
134
+ <>
135
+ <WizardError />
136
+ { children }
137
+ </>
138
+ );
139
+ }
140
+
137
141
  if ( hideHeader ) {
138
142
  // Without the Page shell the tabs still own the content: it renders
139
143
  // inside the active tab's panel.
@@ -154,7 +158,12 @@ const WizardHeaderRegion = ( {
154
158
  const sectionSubTitle = activeSection( sections, pathname )?.subHeaderText;
155
159
 
156
160
  return (
157
- <Page breadcrumbItems={ breadcrumbItems } subTitle={ sectionSubTitle ?? subTitle } actions={ actions } tabbedNavigation={ tabbedNavigation }>
161
+ <Page
162
+ breadcrumbItems={ breadcrumbItems }
163
+ subTitle={ headerSubTitle || ( sectionSubTitle ?? subTitle ) }
164
+ actions={ actions }
165
+ tabbedNavigation={ tabbedNavigation }
166
+ >
158
167
  { children }
159
168
  </Page>
160
169
  );
@@ -166,7 +175,7 @@ const WizardHeaderRegion = ( {
166
175
  * @property {string} [subHeaderText] The sub-header text, optional.
167
176
  * @property {string} [apiSlug] The API slug, optional.
168
177
  * @property {string} [className] CSS classes, optional.
169
- * @property {any[]} sections Array of sections. A section's own `subHeaderText` replaces the wizard's while it is active.
178
+ * @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.
170
179
  * Its optional `tabbedNavigation( params )` returns the tab items shown while its route matches.
171
180
  * @property {boolean} [hasSimpleFooter] Indicates if a simple footer is used, optional.
172
181
  * @property {() => void} [renderAboveSections] Function to render content above sections, optional.
@@ -215,6 +224,7 @@ const Wizard = (
215
224
  sectionTitle,
216
225
  sectionPrimaryAction,
217
226
  sectionSecondaryAction,
227
+ subTitle: headerSubTitle,
218
228
  } = headerData;
219
229
 
220
230
  const mainActions = actions?.filter( action => action.type === 'primary' || action.type === 'secondary' );
@@ -268,10 +278,10 @@ const Wizard = (
268
278
  <>
269
279
  { ! pluginRequirementsKnown && (
270
280
  <div className="newspack-wizard__loader">
271
- <VStack alignment="center" spacing={ 2 }>
281
+ <Stack direction="column" align="center" gap="sm">
272
282
  <Waiting noMargin />
273
283
  <strong>{ __( 'Fetching…', 'newspack-plugin' ) }</strong>
274
- </VStack>
284
+ </Stack>
275
285
  </div>
276
286
  ) }
277
287
  <div hidden={ ! pluginRequirementsKnown }>
@@ -443,6 +453,7 @@ const Wizard = (
443
453
  sections={ routedSections }
444
454
  sectionName={ sectionName }
445
455
  subTitle={ subHeaderText }
456
+ headerSubTitle={ headerSubTitle }
446
457
  actions={ headerActions }
447
458
  tabbedNavigation={ tabbedNavigation }
448
459
  >
@@ -320,3 +320,41 @@ describe( 'Wizard section subtitle', () => {
320
320
  expect( subtitle() ).toBe( 'Wizard intro' );
321
321
  } );
322
322
  } );
323
+
324
+ describe( 'Wizard tab bar on hidden routes', () => {
325
+ beforeEach( () => {
326
+ apiFetch.mockReset();
327
+ apiFetch.mockResolvedValue( {} );
328
+ } );
329
+
330
+ it( 'hides the tab bar on a hideTabbedNavigation route and restores it on a tab route', async () => {
331
+ window.location.hash = '#/list';
332
+ render(
333
+ <Wizard
334
+ headerText="Test wizard"
335
+ sections={ [
336
+ { label: 'List', path: '/list', exact: true, render: () => <div>List view</div> },
337
+ { label: 'Other', path: '/other', exact: true, render: () => <div>Other view</div> },
338
+ { path: '/list/:id', isHidden: true, hideTabbedNavigation: true, render: () => <div>Item view</div> },
339
+ ] }
340
+ />
341
+ );
342
+
343
+ expect( await screen.findByText( 'List view' ) ).toBeInTheDocument();
344
+ expect( screen.getByRole( 'tab', { name: 'Other' } ) ).toBeInTheDocument();
345
+
346
+ act( () => {
347
+ window.location.hash = '#/list/esp';
348
+ window.dispatchEvent( new HashChangeEvent( 'hashchange' ) );
349
+ } );
350
+ expect( await screen.findByText( 'Item view' ) ).toBeInTheDocument();
351
+ expect( screen.queryByRole( 'tab' ) ).not.toBeInTheDocument();
352
+
353
+ act( () => {
354
+ window.location.hash = '#/list';
355
+ window.dispatchEvent( new HashChangeEvent( 'hashchange' ) );
356
+ } );
357
+ expect( await screen.findByText( 'List view' ) ).toBeInTheDocument();
358
+ expect( screen.getByRole( 'tab', { name: 'Other' } ) ).toBeInTheDocument();
359
+ } );
360
+ } );
@@ -25,6 +25,7 @@ const DEFAULT_STATE = {
25
25
  sectionDescription: '',
26
26
  sectionName: '',
27
27
  sectionTitle: '',
28
+ subTitle: '',
28
29
  },
29
30
  isLoading: false,
30
31
  isQuietLoading: false,