newspack-components 4.6.3 → 4.7.0-alpha.2

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 (148) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/colors/colors.module.scss +13 -7
  3. package/dist/cjs/autocomplete-tokenfield/style.scss +4 -2
  4. package/dist/cjs/breadcrumbs/format-count.js +33 -0
  5. package/dist/cjs/breadcrumbs/index.js +78 -20
  6. package/dist/cjs/breadcrumbs/index.test.js +110 -0
  7. package/dist/cjs/breadcrumbs/style.scss +6 -0
  8. package/dist/cjs/button/index.js +41 -17
  9. package/dist/cjs/button/index.test.js +273 -0
  10. package/dist/cjs/card/core-card.js +9 -4
  11. package/dist/cjs/card/core-card.test.js +29 -0
  12. package/dist/cjs/card/index.js +4 -2
  13. package/dist/cjs/card/style-core.scss +59 -0
  14. package/dist/cjs/card-feature/index.js +7 -1
  15. package/dist/cjs/card-feature/index.test.js +92 -0
  16. package/dist/cjs/color-picker/index.js +1 -1
  17. package/dist/cjs/confirm-dialog/index.js +26 -9
  18. package/dist/cjs/confirm-dialog/index.test.js +176 -0
  19. package/dist/cjs/dataviews/index.js +11 -2
  20. package/dist/cjs/divider/index.js +13 -0
  21. package/dist/cjs/drawer/action.js +74 -0
  22. package/dist/cjs/drawer/body.js +79 -0
  23. package/dist/cjs/drawer/close-icon.js +45 -0
  24. package/dist/cjs/drawer/content.js +72 -0
  25. package/dist/cjs/drawer/context.js +23 -0
  26. package/dist/cjs/drawer/context.test.js +43 -0
  27. package/dist/cjs/drawer/divider.js +24 -0
  28. package/dist/cjs/drawer/footer.js +26 -0
  29. package/dist/cjs/drawer/header.js +26 -0
  30. package/dist/cjs/drawer/index.js +42 -0
  31. package/dist/cjs/drawer/index.test.js +1287 -0
  32. package/dist/cjs/drawer/root.js +350 -0
  33. package/dist/cjs/drawer/slots.test.js +422 -0
  34. package/dist/cjs/drawer/style.scss +205 -0
  35. package/dist/cjs/drawer/title.js +54 -0
  36. package/dist/cjs/drawer/types.js +5 -0
  37. package/dist/cjs/drawer/use-exit-animation.js +59 -0
  38. package/dist/cjs/hooks/use-confirm-dialog.js +15 -3
  39. package/dist/cjs/hooks/use-unsaved-changes-dialog.js +19 -4
  40. package/dist/cjs/index.js +28 -1
  41. package/dist/cjs/page/index.js +1 -1
  42. package/dist/cjs/page-control/index.js +82 -0
  43. package/dist/cjs/plugin-settings/SettingsSection.js +7 -1
  44. package/dist/cjs/section-header/index.js +5 -2
  45. package/dist/cjs/section-header/style.scss +25 -12
  46. package/dist/cjs/table-card/index.js +67 -0
  47. package/dist/cjs/table-card/index.test.js +153 -0
  48. package/dist/cjs/with-wizard-screen/style.scss +8 -0
  49. package/dist/cjs/wizard/index.js +102 -38
  50. package/dist/esm/autocomplete-tokenfield/style.scss +4 -2
  51. package/dist/esm/breadcrumbs/format-count.js +27 -0
  52. package/dist/esm/breadcrumbs/index.js +81 -20
  53. package/dist/esm/breadcrumbs/index.test.js +110 -0
  54. package/dist/esm/breadcrumbs/style.scss +6 -0
  55. package/dist/esm/button/index.js +42 -18
  56. package/dist/esm/button/index.test.js +273 -0
  57. package/dist/esm/card/core-card.js +9 -4
  58. package/dist/esm/card/core-card.test.js +29 -0
  59. package/dist/esm/card/index.js +4 -2
  60. package/dist/esm/card/style-core.scss +59 -0
  61. package/dist/esm/card-feature/index.js +8 -2
  62. package/dist/esm/card-feature/index.test.js +96 -0
  63. package/dist/esm/color-picker/index.js +1 -1
  64. package/dist/esm/confirm-dialog/index.js +26 -9
  65. package/dist/esm/confirm-dialog/index.test.js +176 -0
  66. package/dist/esm/dataviews/index.js +11 -2
  67. package/dist/esm/divider/index.js +14 -0
  68. package/dist/esm/drawer/action.js +68 -0
  69. package/dist/esm/drawer/body.js +70 -0
  70. package/dist/esm/drawer/close-icon.js +37 -0
  71. package/dist/esm/drawer/content.js +63 -0
  72. package/dist/esm/drawer/context.js +17 -0
  73. package/dist/esm/drawer/context.test.js +40 -0
  74. package/dist/esm/drawer/divider.js +16 -0
  75. package/dist/esm/drawer/footer.js +18 -0
  76. package/dist/esm/drawer/header.js +18 -0
  77. package/dist/esm/drawer/index.js +35 -0
  78. package/dist/esm/drawer/index.test.js +1285 -0
  79. package/dist/esm/drawer/root.js +342 -0
  80. package/dist/esm/drawer/slots.test.js +422 -0
  81. package/dist/esm/drawer/style.scss +205 -0
  82. package/dist/esm/drawer/title.js +46 -0
  83. package/dist/esm/drawer/types.js +1 -0
  84. package/dist/esm/drawer/use-exit-animation.js +52 -0
  85. package/dist/esm/hooks/use-confirm-dialog.js +15 -3
  86. package/dist/esm/hooks/use-unsaved-changes-dialog.js +20 -5
  87. package/dist/esm/index.js +4 -0
  88. package/dist/esm/page/index.js +1 -1
  89. package/dist/esm/page-control/index.js +82 -0
  90. package/dist/esm/plugin-settings/SettingsSection.js +8 -2
  91. package/dist/esm/section-header/index.js +5 -2
  92. package/dist/esm/section-header/style.scss +25 -12
  93. package/dist/esm/table-card/index.js +60 -0
  94. package/dist/esm/table-card/index.test.js +150 -0
  95. package/dist/esm/with-wizard-screen/style.scss +8 -0
  96. package/dist/esm/wizard/index.js +104 -38
  97. package/package.json +2 -2
  98. package/src/autocomplete-tokenfield/style.scss +4 -2
  99. package/src/breadcrumbs/format-count.js +27 -0
  100. package/src/breadcrumbs/index.js +68 -8
  101. package/src/breadcrumbs/index.test.js +57 -0
  102. package/src/breadcrumbs/style.scss +6 -0
  103. package/src/button/index.test.js +143 -0
  104. package/src/button/index.tsx +32 -7
  105. package/src/card/core-card.js +15 -2
  106. package/src/card/core-card.test.js +19 -0
  107. package/src/card/index.js +3 -0
  108. package/src/card/style-core.scss +59 -0
  109. package/src/card-feature/README.md +4 -0
  110. package/src/card-feature/index.test.js +65 -0
  111. package/src/card-feature/index.tsx +13 -2
  112. package/src/color-picker/index.js +1 -1
  113. package/src/confirm-dialog/README.md +3 -2
  114. package/src/confirm-dialog/index.test.js +146 -0
  115. package/src/confirm-dialog/index.tsx +23 -9
  116. package/src/dataviews/index.tsx +11 -2
  117. package/src/divider/index.js +13 -0
  118. package/src/drawer/README.md +454 -0
  119. package/src/drawer/action.tsx +67 -0
  120. package/src/drawer/body.tsx +60 -0
  121. package/src/drawer/close-icon.tsx +37 -0
  122. package/src/drawer/content.tsx +54 -0
  123. package/src/drawer/context.test.js +31 -0
  124. package/src/drawer/context.ts +25 -0
  125. package/src/drawer/divider.tsx +13 -0
  126. package/src/drawer/footer.tsx +15 -0
  127. package/src/drawer/header.tsx +15 -0
  128. package/src/drawer/index.test.js +1012 -0
  129. package/src/drawer/index.tsx +33 -0
  130. package/src/drawer/root.tsx +328 -0
  131. package/src/drawer/slots.test.js +314 -0
  132. package/src/drawer/style.scss +205 -0
  133. package/src/drawer/title.tsx +40 -0
  134. package/src/drawer/types.ts +79 -0
  135. package/src/drawer/use-exit-animation.ts +46 -0
  136. package/src/hooks/use-confirm-dialog.tsx +14 -3
  137. package/src/hooks/use-unsaved-changes-dialog.tsx +17 -5
  138. package/src/index.js +4 -0
  139. package/src/page/index.js +1 -1
  140. package/src/page-control/index.js +63 -0
  141. package/src/plugin-settings/SettingsSection.js +8 -2
  142. package/src/section-header/index.js +5 -2
  143. package/src/section-header/style.scss +25 -12
  144. package/src/table-card/README.md +100 -0
  145. package/src/table-card/index.test.js +112 -0
  146. package/src/table-card/index.tsx +56 -0
  147. package/src/with-wizard-screen/style.scss +8 -0
  148. package/src/wizard/index.js +95 -34
@@ -0,0 +1,100 @@
1
+ # TableCard
2
+
3
+ A card frame for a table: an optional heading, the table shown edge-to-edge,
4
+ and padded slots above and below it. It pins the composition the Gutenberg
5
+ DataViews docs demonstrate as "With Card" — `@wordpress/ui`'s compound `Card`
6
+ with the table in `Card.FullBleed` — so screens don't each reassemble the
7
+ spacing and slot order by hand.
8
+
9
+ Nothing in it is DataViews-specific. Any content that should run to the card's
10
+ edges works: a DataViews table, a plain `<table>`, an embed.
11
+
12
+ ```jsx
13
+ import { TableCard } from 'newspack-components';
14
+ ```
15
+
16
+ ## Usage
17
+
18
+ ```jsx
19
+ import { __ } from '@wordpress/i18n';
20
+ import { useId } from '@wordpress/element';
21
+ import { TableCard } from 'newspack-components';
22
+
23
+ const titleId = useId();
24
+
25
+ <TableCard
26
+ title={ __( 'Price Schedule', 'newspack-plugin' ) }
27
+ titleId={ titleId }
28
+ actions={ <Button variant="secondary">{ __( 'Add Price', 'newspack-plugin' ) }</Button> }
29
+ >
30
+ <div role="region" aria-labelledby={ titleId }>
31
+ <DataViews … />
32
+ </div>
33
+ </TableCard>
34
+ ```
35
+
36
+ ## Props
37
+
38
+ - `title` — optional heading, rendered in a `Card.Header` as a real heading
39
+ element (`h3` by default; see `heading`). `0` renders; `''`/`null`/
40
+ `undefined`/booleans (the leftovers of `cond && '…'`) render no heading.
41
+ - `titleId` — id placed on the heading element, so a region inside the card can
42
+ name itself with `aria-labelledby` instead of duplicating the visible title
43
+ in an `aria-label`. Only meaningful together with `title`: with no heading
44
+ rendered, an `aria-labelledby` pointing at it names nothing.
45
+ - `heading` — heading level for the title, `1`–`6`. Defaults to `3`, one level
46
+ under the `SectionHeader` (`h2`) most wizard screens use.
47
+ - `actions` — controls rendered in the card header: opposite the title when
48
+ there is one, end-aligned on their own otherwise. A header renders when
49
+ either `title` or `actions` is renderable (same falsy rules as `title`).
50
+ - `before` / `after` — content rendered inside the card padding, above and
51
+ below the table: stats rows, notes, footer actions. Slot children stretch to
52
+ the card's full content width; wrap a lone control in an `HStack` to keep its
53
+ natural width.
54
+ - `className` — forwarded to `Card.Root`, the card's outermost element.
55
+ - `children` — the table itself, rendered edge-to-edge via `Card.FullBleed`.
56
+ The card supplies no cell padding: pad the table's first and last columns to
57
+ the card's own padding (24px — `$grid-unit-30`, matching upstream's
58
+ `--wpds-dimension-padding-2xl`) or the outer columns land against the card
59
+ border.
60
+
61
+ ## How the edges behave
62
+
63
+ Whether the table touches the card's top and bottom edges is decided by
64
+ upstream `:first-child`/`:last-child` rules, and absent slots render no DOM
65
+ node, so the shape follows from which props are passed:
66
+
67
+ - No `title`, no `actions`, no `before`, no `after` — the table is flush to
68
+ all four card edges.
69
+ - `title` or `actions` present — a header sits above; the table is inset from
70
+ the top by the card's header→content gap token (upstream's
71
+ `.header + .content` rule zeroes the content's own top padding and reconciles
72
+ the header's bottom padding to that gap with a compensating margin).
73
+ - `before` / `after` present — the slot sits in the padding and the table no
74
+ longer touches that edge.
75
+
76
+ A slot that appears conditionally (a See More button only when the table is
77
+ long) therefore changes the card's shape with the condition. That can be the
78
+ right call — a card with a footer control legitimately looks different from one
79
+ without — but make it deliberately, knowing a data-size threshold can flip it.
80
+
81
+ ## On a `@wordpress/ui` upgrade
82
+
83
+ The component is a thin composition over `@wordpress/ui` internals that are not
84
+ a stable contract. The package pins `^0.16.0` (0.x, so 0.16.x only); when
85
+ bumping past it, re-check:
86
+
87
+ - `Card.FullBleed`'s placement rules — the edge behaviour above comes from its
88
+ stylesheet, not from this component.
89
+ - `Card.Content` composed with `Stack` via the `render` prop — the documented
90
+ way to add inter-slot spacing while keeping `FullBleed` a direct child.
91
+ - `Card.Title`'s default rendering (a `div`; this component swaps in a heading
92
+ via `render`) and the global-CSS-defense rules that keep wp-admin's heading
93
+ styles from restyling it.
94
+ - That the card padding is still 24px — callers pad their outer table columns
95
+ with `$grid-unit-30` to match it, and the two values drift independently.
96
+ - Every consumer, visually: the Price Schedule card (the one product surface
97
+ with a header — title, heading render, and the actions row), the impact table
98
+ (headerless; rendered by both the rule editor and the list page's catalog
99
+ panel), and the components-demo section, which is the cheapest place to
100
+ eyeball a bump.
@@ -0,0 +1,112 @@
1
+ /**
2
+ * External dependencies
3
+ */
4
+ import { render, screen } from '@testing-library/react';
5
+
6
+ /**
7
+ * Internal dependencies
8
+ */
9
+ import TableCard from '.';
10
+
11
+ // @wordpress/ui injects no CSS under Jest, so the layout this component exists
12
+ // for (full bleed, edge bumps, the content gap) is invisible here; slot order
13
+ // and heading semantics are the assertable contract.
14
+ describe( 'TableCard', () => {
15
+ it( 'renders the table children inside the card', () => {
16
+ render(
17
+ <TableCard>
18
+ <table aria-label="Prices" />
19
+ </TableCard>
20
+ );
21
+ expect( screen.getByRole( 'table', { name: 'Prices' } ) ).toBeInTheDocument();
22
+ } );
23
+
24
+ it( 'renders the title as a real heading carrying the given id', () => {
25
+ const { rerender } = render(
26
+ <TableCard title="Price schedule" titleId="tc-title">
27
+ <table aria-label="Prices" />
28
+ </TableCard>
29
+ );
30
+ expect( screen.getByRole( 'heading', { level: 3, name: 'Price schedule' } ) ).toHaveAttribute( 'id', 'tc-title' );
31
+
32
+ rerender(
33
+ <TableCard title="Price schedule" heading={ 2 }>
34
+ <table aria-label="Prices" />
35
+ </TableCard>
36
+ );
37
+ expect( screen.getByRole( 'heading', { level: 2, name: 'Price schedule' } ) ).toBeInTheDocument();
38
+
39
+ rerender(
40
+ <TableCard>
41
+ <table aria-label="Prices" />
42
+ </TableCard>
43
+ );
44
+ expect( screen.queryByRole( 'heading' ) ).not.toBeInTheDocument();
45
+ } );
46
+
47
+ it( 'treats 0 as a renderable title and the empty string as none', () => {
48
+ const { rerender } = render(
49
+ <TableCard title={ 0 }>
50
+ <table aria-label="Prices" />
51
+ </TableCard>
52
+ );
53
+ expect( screen.getByRole( 'heading', { level: 3, name: '0' } ) ).toBeInTheDocument();
54
+
55
+ rerender(
56
+ <TableCard title="">
57
+ <table aria-label="Prices" />
58
+ </TableCard>
59
+ );
60
+ expect( screen.queryByRole( 'heading' ) ).not.toBeInTheDocument();
61
+ } );
62
+
63
+ it( 'renders header actions alongside the title', () => {
64
+ render(
65
+ <TableCard title="Price Schedule" actions={ <button>Add Price</button> }>
66
+ <table aria-label="Prices" />
67
+ </TableCard>
68
+ );
69
+ const heading = screen.getByRole( 'heading', { level: 3, name: 'Price Schedule' } );
70
+ const action = screen.getByRole( 'button', { name: 'Add Price' } );
71
+ expect( heading.parentElement ).toContainElement( action );
72
+ } );
73
+
74
+ it( 'renders a header for actions alone, and none for boolean slot leftovers', () => {
75
+ const { rerender } = render(
76
+ <TableCard actions={ <button>Add Price</button> }>
77
+ <table aria-label="Prices" />
78
+ </TableCard>
79
+ );
80
+ expect( screen.getByRole( 'button', { name: 'Add Price' } ) ).toBeInTheDocument();
81
+ expect( screen.queryByRole( 'heading' ) ).not.toBeInTheDocument();
82
+
83
+ rerender(
84
+ <TableCard title={ false } actions={ false }>
85
+ <table aria-label="Prices" />
86
+ </TableCard>
87
+ );
88
+ expect( screen.queryByRole( 'heading' ) ).not.toBeInTheDocument();
89
+ expect( screen.queryByRole( 'button' ) ).not.toBeInTheDocument();
90
+ } );
91
+
92
+ it( 'orders before, table, after within the card body', () => {
93
+ render(
94
+ <TableCard before={ <p>Stats row</p> } after={ <button>See More</button> }>
95
+ <table aria-label="Prices" />
96
+ </TableCard>
97
+ );
98
+ const before = screen.getByText( 'Stats row' );
99
+ const after = screen.getByRole( 'button', { name: 'See More' } );
100
+ const table = screen.getByRole( 'table', { name: 'Prices' } );
101
+ // Walk the table up to the card body's direct child, so extra upstream
102
+ // wrappers cannot silently repoint the comparison.
103
+ const body = before.parentElement;
104
+ let tableWrap = table;
105
+ while ( tableWrap.parentElement !== body ) {
106
+ tableWrap = tableWrap.parentElement;
107
+ }
108
+ const order = Array.from( body.children );
109
+ expect( order.indexOf( before ) ).toBeLessThan( order.indexOf( tableWrap ) );
110
+ expect( order.indexOf( tableWrap ) ).toBeLessThan( order.indexOf( after ) );
111
+ } );
112
+ } );
@@ -0,0 +1,56 @@
1
+ /**
2
+ * TableCard
3
+ *
4
+ * The DataViews-in-a-card composition from @wordpress/ui. Which card edges the
5
+ * table touches follows upstream first/last-child rules, so it changes with the
6
+ * slots present — the README spells the combinations out.
7
+ */
8
+
9
+ /**
10
+ * WordPress dependencies
11
+ */
12
+ import { Card, Stack } from '@wordpress/ui';
13
+
14
+ interface TableCardProps {
15
+ title?: React.ReactNode;
16
+ /** Id for the heading, so a caller's region can name itself via `aria-labelledby`. */
17
+ titleId?: string;
18
+ heading?: 1 | 2 | 3 | 4 | 5 | 6;
19
+ actions?: React.ReactNode;
20
+ before?: React.ReactNode;
21
+ after?: React.ReactNode;
22
+ className?: string;
23
+ /** Rendered edge-to-edge via `Card.FullBleed`. */
24
+ children: React.ReactNode;
25
+ }
26
+
27
+ /** `0` is a renderable slot value; the falsy leftovers of `cond && <X />` are not. */
28
+ const isRenderable = ( node: React.ReactNode ) => node !== undefined && node !== null && node !== '' && typeof node !== 'boolean';
29
+
30
+ export default function TableCard( { title, titleId, heading = 3, actions, before, after, className, children }: TableCardProps ) {
31
+ const TitleTag = `h${ heading }` as const;
32
+ const hasTitle = isRenderable( title );
33
+ const hasActions = isRenderable( actions );
34
+ return (
35
+ <Card.Root className={ className }>
36
+ { ( hasTitle || hasActions ) && (
37
+ <Card.Header
38
+ render={ hasActions ? <Stack direction="row" justify={ hasTitle ? 'space-between' : 'end' } align="center" /> : undefined }
39
+ >
40
+ { hasTitle && <Card.Title render={ <TitleTag id={ titleId } /> }>{ title }</Card.Title> }
41
+ { /* Its own row, so several actions don't scatter across the header's space-between. */ }
42
+ { hasActions && (
43
+ <Stack direction="row" gap="sm" align="center">
44
+ { actions }
45
+ </Stack>
46
+ ) }
47
+ </Card.Header>
48
+ ) }
49
+ <Card.Content render={ <Stack direction="column" gap="lg" /> }>
50
+ { before }
51
+ <Card.FullBleed>{ children }</Card.FullBleed>
52
+ { after }
53
+ </Card.Content>
54
+ </Card.Root>
55
+ );
56
+ }
@@ -18,6 +18,14 @@
18
18
 
19
19
  color: wp-colors.$gray-900;
20
20
 
21
+ // Full-width dividers span 100vw, which overspills the admin content area
22
+ // (admin menu offset + scrollbar) and triggers horizontal scrolling; clip
23
+ // (not hidden) so no scroll container is created. Scoped off __content,
24
+ // which can be a constrained centered column.
25
+ &:not(.newspack-wizard__content) {
26
+ overflow-x: clip;
27
+ }
28
+
21
29
  // Headings
22
30
  h1,
23
31
  h2,
@@ -6,9 +6,10 @@ import classnames from 'classnames';
6
6
  /**
7
7
  * WordPress dependencies.
8
8
  */
9
- import { DropdownMenu, MenuGroup, MenuItem } from '@wordpress/components';
9
+ // Notice is aliased: `Notice` below is Newspack's own, which this file also uses.
10
+ import { DropdownMenu, MenuGroup, MenuItem, Notice as CoreNotice, SlotFillProvider, createSlotFill } from '@wordpress/components';
10
11
  import { useDispatch, useSelect } from '@wordpress/data';
11
- import { cloneElement, isValidElement, useEffect, useRef, useState, forwardRef } from '@wordpress/element';
12
+ import { cloneElement, createInterpolateElement, isValidElement, useRef, useEffect, useState, forwardRef } from '@wordpress/element';
12
13
  import { __ } from '@wordpress/i18n';
13
14
  import { category, chevronLeft, moreVertical } from '@wordpress/icons';
14
15
 
@@ -24,6 +25,14 @@ import WizardError from './components/WizardError';
24
25
 
25
26
  registerStore();
26
27
 
28
+ /**
29
+ * Renders a view's page-level banner outside the padded content column, so it sits
30
+ * flush beneath the header rather than indented within the section it describes.
31
+ */
32
+ const { Slot: WizardBannerSlot, Fill: WizardBanner } = createSlotFill( 'NewspackWizardBanner' );
33
+
34
+ export { WizardBanner };
35
+
27
36
  /**
28
37
  * Icon registry for resolving icon name strings passed through the data store.
29
38
  * React elements from @wordpress/icons can't cross webpack entry point boundaries
@@ -39,6 +48,34 @@ const resolveIcon = icon => {
39
48
 
40
49
  const { HashRouter, Redirect, Route, Switch, useLocation } = Router;
41
50
 
51
+ /**
52
+ * Interpolate a translated message's named tags, falling back to plain text.
53
+ *
54
+ * The message is translated, so its tags are site-controlled: any .mo file under
55
+ * wp-content/languages/plugins, any GlotPress export, or any `gettext`-filter
56
+ * plugin can supply one. `createInterpolateElement` throws on an unbalanced
57
+ * closing tag whose name is in the conversion map - verified against core's own
58
+ * element.js, which is what runs here since the bundle externalizes wp-element.
59
+ * Nothing above this renders an error boundary, so an uncaught throw would blank
60
+ * the whole wizard, including the screen the inert-gating notice links to as the
61
+ * fix. Degraded copy is recoverable; a blank screen is not.
62
+ *
63
+ * The other malformed cases already degrade on their own and are left alone: an
64
+ * unclosed opener drops that tag and renders the rest as text, and a tag not in
65
+ * the map renders literally.
66
+ *
67
+ * @param {string} message Translated message carrying named tags.
68
+ * @param {Object} conversion Conversion map for createInterpolateElement.
69
+ * @return {JSX.Element|string} The interpolated message, or the message with its tags stripped.
70
+ */
71
+ const interpolateOrPlainText = ( message, conversion ) => {
72
+ try {
73
+ return createInterpolateElement( message, conversion );
74
+ } catch {
75
+ return message.replace( /<\/?[a-zA-Z][a-zA-Z0-9]*\s*\/?>/g, '' );
76
+ }
77
+ };
78
+
42
79
  /**
43
80
  * Reset the header data when a new section is rendered.
44
81
  */
@@ -87,7 +124,7 @@ const WizardHeaderRegion = ( { hideHeader, headerText, sections, sectionName, su
87
124
 
88
125
  /**
89
126
  * @typedef {Object} WizardProps
90
- * @property {string} headerText The header text.
127
+ * @property {string} [headerText] Fallback heading, used only when no section declares breadcrumbs.
91
128
  * @property {string} [subHeaderText] The sub-header text, optional.
92
129
  * @property {string} [apiSlug] The API slug, optional.
93
130
  * @property {string} [className] CSS classes, optional.
@@ -184,6 +221,26 @@ const Wizard = (
184
221
  </TabbedNavigation>
185
222
  );
186
223
 
224
+ // Rendered here rather than as a core admin notice, which wizards strip at
225
+ // priority -9999. Sits as the first child of .newspack-wizard__main so it lands
226
+ // flush beneath the header region and spans the full width in every view: this
227
+ // describes the state of the whole site, not of the section below it, so it reads
228
+ // as page chrome rather than as content.
229
+ const inertGating = window.newspack_aux_data?.inert_gating;
230
+ const inertGatingNotice = inertGating?.show && (
231
+ <CoreNotice status="warning" isDismissible={ false } className="newspack-wizard__inert-gating-notice">
232
+ { /* The conversion map takes childless elements and fills them from the
233
+ translated string, so jsx-a11y can't see the content they end up with. */ }
234
+ { interpolateOrPlainText( inertGating.message, {
235
+ /* eslint-disable jsx-a11y/anchor-has-content */
236
+ accessControl: <a href={ inertGating.urls.accessControl } />,
237
+ audience: <a href={ inertGating.urls.audience } />,
238
+ /* eslint-enable jsx-a11y/anchor-has-content */
239
+ strong: <strong />,
240
+ } ) }
241
+ </CoreNotice>
242
+ );
243
+
187
244
  const content = (
188
245
  <>
189
246
  <HandoffMessage />
@@ -191,6 +248,8 @@ const Wizard = (
191
248
  { sections.length > 1 && <ResetHeaderData /> }
192
249
 
193
250
  <div className="newspack-wizard__main">
251
+ { inertGatingNotice }
252
+ <WizardBannerSlot bubblesVirtually />
194
253
  <Switch>
195
254
  { routedSections.map( ( section, index ) => {
196
255
  const SectionComponent = section.render;
@@ -294,38 +353,40 @@ const Wizard = (
294
353
  ) : undefined;
295
354
 
296
355
  return (
297
- <div ref={ ref }>
298
- <div
299
- className={ classnames( isLoading ? 'newspack-wizard__is-loading' : 'newspack-wizard__is-loaded', {
300
- 'newspack-wizard__is-loading-quiet': isQuietLoading,
301
- } ) }
302
- >
303
- <HashRouter hashType="slash">
304
- { newspack_aux_data.is_debug_mode && <Notice debugMode /> }
305
- <WizardHeaderRegion
306
- hideHeader={ hideHeader }
307
- headerText={ headerText }
308
- sections={ routedSections }
309
- sectionName={ sectionName }
310
- subTitle={ subHeaderText }
311
- actions={ headerActions }
312
- tabbedNavigation={ tabbedNavigation }
313
- >
314
- { content }
315
- </WizardHeaderRegion>
316
- </HashRouter>
317
- { notices?.length > 0 && (
318
- <div className="newspack-wizard__snackbar-list">
319
- { notices.map( ( notice, index ) => (
320
- <WizardSnackbar key={ notice.id || index } id={ notice.id } type={ notice.type } actions={ notice.actions }>
321
- { notice.message }
322
- </WizardSnackbar>
323
- ) ) }
324
- </div>
325
- ) }
356
+ <SlotFillProvider>
357
+ <div ref={ ref }>
358
+ <div
359
+ className={ classnames( isLoading ? 'newspack-wizard__is-loading' : 'newspack-wizard__is-loaded', {
360
+ 'newspack-wizard__is-loading-quiet': isQuietLoading,
361
+ } ) }
362
+ >
363
+ <HashRouter hashType="slash">
364
+ { newspack_aux_data.is_debug_mode && <Notice debugMode /> }
365
+ <WizardHeaderRegion
366
+ hideHeader={ hideHeader }
367
+ headerText={ headerText }
368
+ sections={ routedSections }
369
+ sectionName={ sectionName }
370
+ subTitle={ subHeaderText }
371
+ actions={ headerActions }
372
+ tabbedNavigation={ tabbedNavigation }
373
+ >
374
+ { content }
375
+ </WizardHeaderRegion>
376
+ </HashRouter>
377
+ { notices?.length > 0 && (
378
+ <div className="newspack-wizard__snackbar-list">
379
+ { notices.map( ( notice, index ) => (
380
+ <WizardSnackbar key={ notice.id || index } id={ notice.id } type={ notice.type } actions={ notice.actions }>
381
+ { notice.message }
382
+ </WizardSnackbar>
383
+ ) ) }
384
+ </div>
385
+ ) }
386
+ </div>
387
+ { ! isLoading && <Footer simple={ hasSimpleFooter } /> }
326
388
  </div>
327
- { ! isLoading && <Footer simple={ hasSimpleFooter } /> }
328
- </div>
389
+ </SlotFillProvider>
329
390
  );
330
391
  };
331
392