newspack-components 4.6.2 → 4.7.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 +23 -0
- package/colors/colors.module.scss +13 -7
- package/dist/cjs/autocomplete-tokenfield/style.scss +4 -2
- package/dist/cjs/breadcrumbs/format-count.js +33 -0
- package/dist/cjs/breadcrumbs/index.js +78 -20
- package/dist/cjs/breadcrumbs/index.test.js +110 -0
- package/dist/cjs/breadcrumbs/style.scss +6 -0
- package/dist/cjs/button/index.js +41 -17
- package/dist/cjs/button/index.test.js +273 -0
- package/dist/cjs/card/core-card.js +9 -4
- package/dist/cjs/card/core-card.test.js +29 -0
- package/dist/cjs/card/index.js +4 -2
- package/dist/cjs/card/style-core.scss +59 -0
- package/dist/cjs/card-feature/index.js +7 -1
- package/dist/cjs/card-feature/index.test.js +92 -0
- package/dist/cjs/color-picker/index.js +1 -1
- package/dist/cjs/confirm-dialog/index.js +26 -9
- package/dist/cjs/confirm-dialog/index.test.js +176 -0
- package/dist/cjs/dataviews/index.js +11 -2
- package/dist/cjs/divider/index.js +13 -0
- package/dist/cjs/drawer/action.js +74 -0
- package/dist/cjs/drawer/body.js +79 -0
- package/dist/cjs/drawer/close-icon.js +45 -0
- package/dist/cjs/drawer/content.js +72 -0
- package/dist/cjs/drawer/context.js +23 -0
- package/dist/cjs/drawer/context.test.js +43 -0
- package/dist/cjs/drawer/divider.js +24 -0
- package/dist/cjs/drawer/footer.js +26 -0
- package/dist/cjs/drawer/header.js +26 -0
- package/dist/cjs/drawer/index.js +42 -0
- package/dist/cjs/drawer/index.test.js +1287 -0
- package/dist/cjs/drawer/root.js +350 -0
- package/dist/cjs/drawer/slots.test.js +422 -0
- package/dist/cjs/drawer/style.scss +205 -0
- package/dist/cjs/drawer/title.js +54 -0
- package/dist/cjs/drawer/types.js +5 -0
- package/dist/cjs/drawer/use-exit-animation.js +59 -0
- package/dist/cjs/hooks/use-confirm-dialog.js +15 -3
- package/dist/cjs/hooks/use-unsaved-changes-dialog.js +19 -4
- package/dist/cjs/index.js +28 -1
- package/dist/cjs/page/index.js +1 -1
- package/dist/cjs/page-control/index.js +82 -0
- package/dist/cjs/plugin-settings/SettingsSection.js +7 -1
- package/dist/cjs/section-header/index.js +5 -2
- package/dist/cjs/section-header/style.scss +25 -12
- package/dist/cjs/table-card/index.js +67 -0
- package/dist/cjs/table-card/index.test.js +153 -0
- package/dist/cjs/with-wizard-screen/style.scss +8 -0
- package/dist/cjs/wizard/index.js +102 -38
- package/dist/esm/autocomplete-tokenfield/style.scss +4 -2
- package/dist/esm/breadcrumbs/format-count.js +27 -0
- package/dist/esm/breadcrumbs/index.js +81 -20
- package/dist/esm/breadcrumbs/index.test.js +110 -0
- package/dist/esm/breadcrumbs/style.scss +6 -0
- package/dist/esm/button/index.js +42 -18
- package/dist/esm/button/index.test.js +273 -0
- package/dist/esm/card/core-card.js +9 -4
- package/dist/esm/card/core-card.test.js +29 -0
- package/dist/esm/card/index.js +4 -2
- package/dist/esm/card/style-core.scss +59 -0
- package/dist/esm/card-feature/index.js +8 -2
- package/dist/esm/card-feature/index.test.js +96 -0
- package/dist/esm/color-picker/index.js +1 -1
- package/dist/esm/confirm-dialog/index.js +26 -9
- package/dist/esm/confirm-dialog/index.test.js +176 -0
- package/dist/esm/dataviews/index.js +11 -2
- package/dist/esm/divider/index.js +14 -0
- package/dist/esm/drawer/action.js +68 -0
- package/dist/esm/drawer/body.js +70 -0
- package/dist/esm/drawer/close-icon.js +37 -0
- package/dist/esm/drawer/content.js +63 -0
- package/dist/esm/drawer/context.js +17 -0
- package/dist/esm/drawer/context.test.js +40 -0
- package/dist/esm/drawer/divider.js +16 -0
- package/dist/esm/drawer/footer.js +18 -0
- package/dist/esm/drawer/header.js +18 -0
- package/dist/esm/drawer/index.js +35 -0
- package/dist/esm/drawer/index.test.js +1285 -0
- package/dist/esm/drawer/root.js +342 -0
- package/dist/esm/drawer/slots.test.js +422 -0
- package/dist/esm/drawer/style.scss +205 -0
- package/dist/esm/drawer/title.js +46 -0
- package/dist/esm/drawer/types.js +1 -0
- package/dist/esm/drawer/use-exit-animation.js +52 -0
- package/dist/esm/hooks/use-confirm-dialog.js +15 -3
- package/dist/esm/hooks/use-unsaved-changes-dialog.js +20 -5
- package/dist/esm/index.js +4 -0
- package/dist/esm/page/index.js +1 -1
- package/dist/esm/page-control/index.js +82 -0
- package/dist/esm/plugin-settings/SettingsSection.js +8 -2
- package/dist/esm/section-header/index.js +5 -2
- package/dist/esm/section-header/style.scss +25 -12
- package/dist/esm/table-card/index.js +60 -0
- package/dist/esm/table-card/index.test.js +150 -0
- package/dist/esm/with-wizard-screen/style.scss +8 -0
- package/dist/esm/wizard/index.js +104 -38
- package/package.json +2 -2
- package/src/autocomplete-tokenfield/style.scss +4 -2
- package/src/breadcrumbs/format-count.js +27 -0
- package/src/breadcrumbs/index.js +68 -8
- package/src/breadcrumbs/index.test.js +57 -0
- package/src/breadcrumbs/style.scss +6 -0
- package/src/button/index.test.js +143 -0
- package/src/button/index.tsx +32 -7
- package/src/card/core-card.js +15 -2
- package/src/card/core-card.test.js +19 -0
- package/src/card/index.js +3 -0
- package/src/card/style-core.scss +59 -0
- package/src/card-feature/README.md +4 -0
- package/src/card-feature/index.test.js +65 -0
- package/src/card-feature/index.tsx +13 -2
- package/src/color-picker/index.js +1 -1
- package/src/confirm-dialog/README.md +3 -2
- package/src/confirm-dialog/index.test.js +146 -0
- package/src/confirm-dialog/index.tsx +23 -9
- package/src/dataviews/index.tsx +11 -2
- package/src/divider/index.js +13 -0
- package/src/drawer/README.md +454 -0
- package/src/drawer/action.tsx +67 -0
- package/src/drawer/body.tsx +60 -0
- package/src/drawer/close-icon.tsx +37 -0
- package/src/drawer/content.tsx +54 -0
- package/src/drawer/context.test.js +31 -0
- package/src/drawer/context.ts +25 -0
- package/src/drawer/divider.tsx +13 -0
- package/src/drawer/footer.tsx +15 -0
- package/src/drawer/header.tsx +15 -0
- package/src/drawer/index.test.js +1012 -0
- package/src/drawer/index.tsx +33 -0
- package/src/drawer/root.tsx +328 -0
- package/src/drawer/slots.test.js +314 -0
- package/src/drawer/style.scss +205 -0
- package/src/drawer/title.tsx +40 -0
- package/src/drawer/types.ts +79 -0
- package/src/drawer/use-exit-animation.ts +46 -0
- package/src/hooks/use-confirm-dialog.tsx +14 -3
- package/src/hooks/use-unsaved-changes-dialog.tsx +17 -5
- package/src/index.js +4 -0
- package/src/page/index.js +1 -1
- package/src/page-control/index.js +63 -0
- package/src/plugin-settings/SettingsSection.js +8 -2
- package/src/section-header/index.js +5 -2
- package/src/section-header/style.scss +25 -12
- package/src/table-card/README.md +100 -0
- package/src/table-card/index.test.js +112 -0
- package/src/table-card/index.tsx +56 -0
- package/src/with-wizard-screen/style.scss +8 -0
- 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,
|
package/src/wizard/index.js
CHANGED
|
@@ -6,9 +6,10 @@ import classnames from 'classnames';
|
|
|
6
6
|
/**
|
|
7
7
|
* WordPress dependencies.
|
|
8
8
|
*/
|
|
9
|
-
|
|
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, useState, forwardRef } from '@wordpress/element';
|
|
12
|
+
import { cloneElement, createInterpolateElement, isValidElement, 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
|
|
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.
|
|
@@ -161,6 +198,26 @@ const Wizard = (
|
|
|
161
198
|
</TabbedNavigation>
|
|
162
199
|
);
|
|
163
200
|
|
|
201
|
+
// Rendered here rather than as a core admin notice, which wizards strip at
|
|
202
|
+
// priority -9999. Sits as the first child of .newspack-wizard__main so it lands
|
|
203
|
+
// flush beneath the header region and spans the full width in every view: this
|
|
204
|
+
// describes the state of the whole site, not of the section below it, so it reads
|
|
205
|
+
// as page chrome rather than as content.
|
|
206
|
+
const inertGating = window.newspack_aux_data?.inert_gating;
|
|
207
|
+
const inertGatingNotice = inertGating?.show && (
|
|
208
|
+
<CoreNotice status="warning" isDismissible={ false } className="newspack-wizard__inert-gating-notice">
|
|
209
|
+
{ /* The conversion map takes childless elements and fills them from the
|
|
210
|
+
translated string, so jsx-a11y can't see the content they end up with. */ }
|
|
211
|
+
{ interpolateOrPlainText( inertGating.message, {
|
|
212
|
+
/* eslint-disable jsx-a11y/anchor-has-content */
|
|
213
|
+
accessControl: <a href={ inertGating.urls.accessControl } />,
|
|
214
|
+
audience: <a href={ inertGating.urls.audience } />,
|
|
215
|
+
/* eslint-enable jsx-a11y/anchor-has-content */
|
|
216
|
+
strong: <strong />,
|
|
217
|
+
} ) }
|
|
218
|
+
</CoreNotice>
|
|
219
|
+
);
|
|
220
|
+
|
|
164
221
|
const content = (
|
|
165
222
|
<>
|
|
166
223
|
<HandoffMessage />
|
|
@@ -168,6 +225,8 @@ const Wizard = (
|
|
|
168
225
|
{ sections.length > 1 && <ResetHeaderData /> }
|
|
169
226
|
|
|
170
227
|
<div className="newspack-wizard__main">
|
|
228
|
+
{ inertGatingNotice }
|
|
229
|
+
<WizardBannerSlot bubblesVirtually />
|
|
171
230
|
<Switch>
|
|
172
231
|
{ routedSections.map( ( section, index ) => {
|
|
173
232
|
const SectionComponent = section.render;
|
|
@@ -271,38 +330,40 @@ const Wizard = (
|
|
|
271
330
|
) : undefined;
|
|
272
331
|
|
|
273
332
|
return (
|
|
274
|
-
<
|
|
275
|
-
<div
|
|
276
|
-
|
|
277
|
-
'newspack-wizard__is-loading
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
{ notice.
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
333
|
+
<SlotFillProvider>
|
|
334
|
+
<div ref={ ref }>
|
|
335
|
+
<div
|
|
336
|
+
className={ classnames( isLoading ? 'newspack-wizard__is-loading' : 'newspack-wizard__is-loaded', {
|
|
337
|
+
'newspack-wizard__is-loading-quiet': isQuietLoading,
|
|
338
|
+
} ) }
|
|
339
|
+
>
|
|
340
|
+
<HashRouter hashType="slash">
|
|
341
|
+
{ newspack_aux_data.is_debug_mode && <Notice debugMode /> }
|
|
342
|
+
<WizardHeaderRegion
|
|
343
|
+
hideHeader={ hideHeader }
|
|
344
|
+
headerText={ headerText }
|
|
345
|
+
sections={ routedSections }
|
|
346
|
+
sectionName={ sectionName }
|
|
347
|
+
subTitle={ subHeaderText }
|
|
348
|
+
actions={ headerActions }
|
|
349
|
+
tabbedNavigation={ tabbedNavigation }
|
|
350
|
+
>
|
|
351
|
+
{ content }
|
|
352
|
+
</WizardHeaderRegion>
|
|
353
|
+
</HashRouter>
|
|
354
|
+
{ notices?.length > 0 && (
|
|
355
|
+
<div className="newspack-wizard__snackbar-list">
|
|
356
|
+
{ notices.map( ( notice, index ) => (
|
|
357
|
+
<WizardSnackbar key={ notice.id || index } id={ notice.id } type={ notice.type } actions={ notice.actions }>
|
|
358
|
+
{ notice.message }
|
|
359
|
+
</WizardSnackbar>
|
|
360
|
+
) ) }
|
|
361
|
+
</div>
|
|
362
|
+
) }
|
|
363
|
+
</div>
|
|
364
|
+
{ ! isLoading && <Footer simple={ hasSimpleFooter } /> }
|
|
303
365
|
</div>
|
|
304
|
-
|
|
305
|
-
</div>
|
|
366
|
+
</SlotFillProvider>
|
|
306
367
|
);
|
|
307
368
|
};
|
|
308
369
|
|