newspack-components 4.7.0 → 4.8.0
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 +20 -0
- package/DEVELOPMENT.md +42 -21
- package/dist/cjs/_mixins.scss +13 -0
- package/dist/cjs/_variables.scss +12 -0
- package/dist/cjs/action-card/index.js +18 -10
- package/dist/cjs/action-card/index.test.js +91 -0
- package/dist/cjs/action-card/style.scss +2 -40
- package/dist/cjs/button/index.js +8 -3
- package/dist/cjs/button/index.test.js +61 -5
- package/dist/cjs/card/core-card.js +0 -2
- package/dist/cjs/card/style-core.scss +7 -5
- package/dist/cjs/card-feature/index.js +75 -61
- package/dist/cjs/card-feature/index.test.js +396 -54
- package/dist/cjs/card-feature/style.scss +20 -31
- package/dist/cjs/card-form/index.js +5 -5
- package/dist/cjs/card-sortable-list/index.js +5 -3
- package/dist/cjs/card-sortable-list/index.test.js +50 -0
- package/dist/cjs/collapsible-group/context.js +19 -0
- package/dist/cjs/collapsible-group/index.js +24 -0
- package/dist/cjs/collapsible-group/index.test.js +215 -0
- package/dist/cjs/collapsible-group/item.js +61 -0
- package/dist/cjs/collapsible-group/root.js +62 -0
- package/dist/cjs/collapsible-group/style.scss +47 -0
- package/dist/cjs/collapsible-group/types.js +5 -0
- package/dist/cjs/empty-state/actions.js +45 -0
- package/dist/cjs/empty-state/context.js +41 -0
- package/dist/cjs/empty-state/header.js +66 -0
- package/dist/cjs/empty-state/index.js +31 -0
- package/dist/cjs/empty-state/index.test.js +303 -0
- package/dist/cjs/empty-state/root.js +56 -0
- package/dist/cjs/empty-state/style.scss +51 -0
- package/dist/cjs/empty-state/types.js +5 -0
- package/dist/cjs/index.js +34 -20
- package/dist/cjs/info-button/index.js +59 -43
- package/dist/cjs/info-button/index.test.js +199 -0
- package/dist/cjs/info-button/style.scss +41 -9
- package/dist/cjs/modal/style.scss +12 -12
- package/dist/cjs/newspack-icon/style.scss +4 -4
- package/dist/cjs/notice/style.scss +3 -3
- package/dist/cjs/page/style.scss +16 -16
- package/dist/cjs/plugin-installer/index.js +5 -9
- package/dist/cjs/plugin-installer/style.scss +3 -14
- package/dist/cjs/section-header/index.js +31 -18
- package/dist/cjs/section-header/index.test.js +40 -0
- package/dist/cjs/section-header/style.scss +22 -6
- package/dist/cjs/settings/DateRangeSetting.js +309 -0
- package/dist/cjs/settings/SettingsSection.js +19 -5
- package/dist/cjs/settings/SettingsSection.test.js +49 -0
- package/dist/cjs/settings/index.js +3 -1
- package/dist/cjs/settings/style.scss +40 -7
- package/dist/cjs/stat-card/body.js +41 -0
- package/dist/cjs/stat-card/constants.js +11 -0
- package/dist/cjs/stat-card/context.js +54 -0
- package/dist/cjs/stat-card/delta.js +91 -0
- package/dist/cjs/stat-card/footer.js +72 -0
- package/dist/cjs/stat-card/index.js +47 -0
- package/dist/cjs/stat-card/index.test.js +938 -0
- package/dist/cjs/stat-card/label.js +66 -0
- package/dist/cjs/stat-card/root.js +69 -0
- package/dist/cjs/stat-card/secondary.js +38 -0
- package/dist/cjs/stat-card/style.scss +89 -0
- package/dist/cjs/stat-card/types.js +5 -0
- package/dist/cjs/stat-card/value.js +78 -0
- package/dist/cjs/status-indicator/index.js +66 -0
- package/dist/cjs/status-indicator/index.test.js +101 -0
- package/dist/cjs/status-indicator/statuses.js +39 -0
- package/dist/cjs/status-indicator/style.scss +9 -0
- package/dist/cjs/status-indicator/types.js +5 -0
- package/dist/cjs/style.scss +9 -9
- package/dist/cjs/tabbed-navigation/style.scss +6 -4
- package/dist/cjs/types.js +5 -0
- package/dist/cjs/web-preview/style.scss +11 -11
- package/dist/cjs/with-wizard/style.scss +10 -10
- package/dist/cjs/with-wizard-screen/style.scss +9 -9
- package/dist/cjs/wizard/index.js +33 -4
- package/dist/cjs/wizard/store/index.js +23 -1
- package/dist/esm/_mixins.scss +13 -0
- package/dist/esm/_variables.scss +12 -0
- package/dist/esm/action-card/index.js +18 -10
- package/dist/esm/action-card/index.test.js +89 -0
- package/dist/esm/action-card/style.scss +2 -40
- package/dist/esm/button/index.js +9 -4
- package/dist/esm/button/index.test.js +63 -5
- package/dist/esm/card/core-card.js +0 -2
- package/dist/esm/card/style-core.scss +7 -5
- package/dist/esm/card-feature/index.js +78 -62
- package/dist/esm/card-feature/index.test.js +397 -55
- package/dist/esm/card-feature/style.scss +20 -31
- package/dist/esm/card-form/index.js +5 -5
- package/dist/esm/card-sortable-list/index.js +6 -4
- package/dist/esm/card-sortable-list/index.test.js +46 -0
- package/dist/esm/collapsible-group/context.js +13 -0
- package/dist/esm/collapsible-group/index.js +17 -0
- package/dist/esm/collapsible-group/index.test.js +215 -0
- package/dist/esm/collapsible-group/item.js +53 -0
- package/dist/esm/collapsible-group/root.js +54 -0
- package/dist/esm/collapsible-group/style.scss +47 -0
- package/dist/esm/collapsible-group/types.js +1 -0
- package/dist/esm/empty-state/actions.js +37 -0
- package/dist/esm/empty-state/context.js +35 -0
- package/dist/esm/empty-state/header.js +58 -0
- package/dist/esm/empty-state/index.js +23 -0
- package/dist/esm/empty-state/index.test.js +299 -0
- package/dist/esm/empty-state/root.js +56 -0
- package/dist/esm/empty-state/style.scss +51 -0
- package/dist/esm/empty-state/types.js +1 -0
- package/dist/esm/index.js +4 -2
- package/dist/esm/info-button/index.js +54 -43
- package/dist/esm/info-button/index.test.js +195 -0
- package/dist/esm/info-button/style.scss +41 -9
- package/dist/esm/modal/style.scss +12 -12
- package/dist/esm/newspack-icon/style.scss +4 -4
- package/dist/esm/notice/style.scss +3 -3
- package/dist/esm/page/style.scss +16 -16
- package/dist/esm/plugin-installer/index.js +5 -9
- package/dist/esm/plugin-installer/style.scss +3 -14
- package/dist/esm/section-header/index.js +27 -14
- package/dist/esm/section-header/index.test.js +36 -0
- package/dist/esm/section-header/style.scss +22 -6
- package/dist/esm/settings/DateRangeSetting.js +304 -0
- package/dist/esm/settings/SettingsSection.js +16 -3
- package/dist/esm/settings/SettingsSection.test.js +45 -0
- package/dist/esm/settings/index.js +3 -1
- package/dist/esm/settings/style.scss +40 -7
- package/dist/esm/stat-card/body.js +36 -0
- package/dist/esm/stat-card/constants.js +5 -0
- package/dist/esm/stat-card/context.js +48 -0
- package/dist/esm/stat-card/delta.js +86 -0
- package/dist/esm/stat-card/footer.js +67 -0
- package/dist/esm/stat-card/index.js +33 -0
- package/dist/esm/stat-card/index.test.js +939 -0
- package/dist/esm/stat-card/label.js +61 -0
- package/dist/esm/stat-card/root.js +64 -0
- package/dist/esm/stat-card/secondary.js +33 -0
- package/dist/esm/stat-card/style.scss +89 -0
- package/dist/esm/stat-card/types.js +1 -0
- package/dist/esm/stat-card/value.js +73 -0
- package/dist/esm/status-indicator/index.js +52 -0
- package/dist/esm/status-indicator/index.test.js +96 -0
- package/dist/esm/status-indicator/statuses.js +33 -0
- package/dist/esm/status-indicator/style.scss +9 -0
- package/dist/esm/status-indicator/types.js +1 -0
- package/dist/esm/style.scss +9 -9
- package/dist/esm/tabbed-navigation/style.scss +6 -4
- package/dist/esm/types.js +1 -0
- package/dist/esm/web-preview/style.scss +11 -11
- package/dist/esm/with-wizard/style.scss +10 -10
- package/dist/esm/with-wizard-screen/style.scss +9 -9
- package/dist/esm/wizard/index.js +38 -7
- package/dist/esm/wizard/store/index.js +23 -1
- package/package.json +1 -1
- package/src/_mixins.scss +13 -0
- package/src/_variables.scss +12 -0
- package/src/action-card/action-card.d.ts +3 -2
- package/src/action-card/index.js +12 -12
- package/src/action-card/index.test.js +56 -0
- package/src/action-card/style.scss +2 -40
- package/src/button/index.test.js +52 -5
- package/src/button/index.tsx +9 -4
- package/src/card/core-card.js +0 -2
- package/src/card/style-core.scss +7 -5
- package/src/card-feature/README.md +68 -27
- package/src/card-feature/index.test.js +242 -27
- package/src/card-feature/index.tsx +77 -67
- package/src/card-feature/style.scss +20 -31
- package/src/card-form/README.md +10 -10
- package/src/card-form/index.tsx +4 -8
- package/src/card-sortable-list/README.md +4 -5
- package/src/card-sortable-list/index.test.js +29 -0
- package/src/card-sortable-list/index.tsx +7 -4
- package/src/collapsible-group/README.md +50 -0
- package/src/collapsible-group/context.ts +13 -0
- package/src/collapsible-group/index.test.js +168 -0
- package/src/collapsible-group/index.tsx +17 -0
- package/src/collapsible-group/item.tsx +42 -0
- package/src/collapsible-group/root.tsx +42 -0
- package/src/collapsible-group/style.scss +47 -0
- package/src/collapsible-group/types.ts +22 -0
- package/src/drawer/README.md +2 -2
- package/src/empty-state/README.md +165 -0
- package/src/empty-state/actions.tsx +37 -0
- package/src/empty-state/context.ts +40 -0
- package/src/empty-state/header.tsx +48 -0
- package/src/empty-state/index.test.js +247 -0
- package/src/empty-state/index.tsx +22 -0
- package/src/empty-state/root.tsx +39 -0
- package/src/empty-state/style.scss +51 -0
- package/src/empty-state/types.ts +33 -0
- package/src/index.js +4 -2
- package/src/info-button/README.md +115 -0
- package/src/info-button/index.test.js +163 -0
- package/src/info-button/index.tsx +64 -0
- package/src/info-button/style.scss +41 -9
- package/src/modal/style.scss +12 -12
- package/src/newspack-icon/style.scss +4 -4
- package/src/notice/style.scss +3 -3
- package/src/page/style.scss +16 -16
- package/src/plugin-installer/index.js +3 -9
- package/src/plugin-installer/style.scss +3 -14
- package/src/section-header/index.js +27 -14
- package/src/section-header/index.test.js +28 -0
- package/src/section-header/style.scss +22 -6
- package/src/settings/DateRangeSetting.js +266 -0
- package/src/settings/SettingsSection.js +24 -4
- package/src/settings/SettingsSection.test.js +45 -0
- package/src/settings/index.js +2 -0
- package/src/settings/style.scss +40 -7
- package/src/stat-card/README.md +434 -0
- package/src/stat-card/body.tsx +28 -0
- package/src/stat-card/constants.ts +5 -0
- package/src/stat-card/context.ts +53 -0
- package/src/stat-card/delta.tsx +88 -0
- package/src/stat-card/footer.tsx +68 -0
- package/src/stat-card/index.test.js +818 -0
- package/src/stat-card/index.tsx +48 -0
- package/src/stat-card/label.tsx +61 -0
- package/src/stat-card/root.tsx +40 -0
- package/src/stat-card/secondary.tsx +27 -0
- package/src/stat-card/style.scss +89 -0
- package/src/stat-card/types.ts +84 -0
- package/src/stat-card/value.tsx +72 -0
- package/src/status-indicator/README.md +123 -0
- package/src/status-indicator/index.test.js +70 -0
- package/src/status-indicator/index.tsx +39 -0
- package/src/status-indicator/statuses.ts +35 -0
- package/src/status-indicator/style.scss +9 -0
- package/src/status-indicator/types.ts +28 -0
- package/src/style.scss +9 -9
- package/src/tabbed-navigation/style.scss +6 -4
- package/src/types.ts +25 -0
- package/src/web-preview/style.scss +11 -11
- package/src/with-wizard/style.scss +10 -10
- package/src/with-wizard-screen/style.scss +9 -9
- package/src/wizard/index.js +49 -7
- package/src/wizard/store/index.js +26 -1
- package/dist/cjs/accordion/index.js +0 -72
- package/dist/cjs/accordion/index.test.js +0 -80
- package/dist/cjs/accordion/style.scss +0 -35
- package/dist/cjs/badge/index.js +0 -32
- package/dist/cjs/badge/style.scss +0 -36
- package/dist/esm/accordion/index.js +0 -63
- package/dist/esm/accordion/index.test.js +0 -79
- package/dist/esm/accordion/style.scss +0 -35
- package/dist/esm/badge/index.js +0 -24
- package/dist/esm/badge/style.scss +0 -36
- package/src/accordion/index.js +0 -46
- package/src/accordion/index.test.js +0 -62
- package/src/accordion/style.scss +0 -35
- package/src/badge/index.tsx +0 -26
- package/src/badge/style.scss +0 -36
- package/src/info-button/index.js +0 -38
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* External dependencies
|
|
3
|
+
*/
|
|
4
|
+
import { render, screen } from '@testing-library/react';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Internal dependencies
|
|
8
|
+
*/
|
|
9
|
+
import EmptyState from '.';
|
|
10
|
+
|
|
11
|
+
describe( 'EmptyState.Root', () => {
|
|
12
|
+
it( 'renders the four-column grid spine', () => {
|
|
13
|
+
const { container } = render(
|
|
14
|
+
<EmptyState.Root>
|
|
15
|
+
<p>body</p>
|
|
16
|
+
</EmptyState.Root>
|
|
17
|
+
);
|
|
18
|
+
const grid = container.querySelector( '.newspack-empty-state' );
|
|
19
|
+
expect( grid ).toHaveClass( 'newspack-grid' );
|
|
20
|
+
expect( grid ).toHaveClass( 'newspack-grid__columns-4' );
|
|
21
|
+
expect( grid ).toHaveClass( 'newspack-grid--no-margin' );
|
|
22
|
+
} );
|
|
23
|
+
|
|
24
|
+
// grid/style.scss matches on these attributes, so they are a contract.
|
|
25
|
+
it( 'gives the inner stack the data attributes the Grid stylesheet matches on', () => {
|
|
26
|
+
const { container } = render(
|
|
27
|
+
<EmptyState.Root>
|
|
28
|
+
<p>body</p>
|
|
29
|
+
</EmptyState.Root>
|
|
30
|
+
);
|
|
31
|
+
const stack = container.querySelector( '.newspack-empty-state' ).firstElementChild;
|
|
32
|
+
expect( stack ).toHaveAttribute( 'data-start', '2' );
|
|
33
|
+
expect( stack ).toHaveAttribute( 'data-end', '4' );
|
|
34
|
+
} );
|
|
35
|
+
|
|
36
|
+
// The width cap hangs off this class, so losing it silently widens the block.
|
|
37
|
+
it( 'carries the stack class hook', () => {
|
|
38
|
+
const { container } = render(
|
|
39
|
+
<EmptyState.Root>
|
|
40
|
+
<p>body</p>
|
|
41
|
+
</EmptyState.Root>
|
|
42
|
+
);
|
|
43
|
+
const stack = container.querySelector( '.newspack-empty-state' ).firstElementChild;
|
|
44
|
+
expect( stack ).toHaveClass( 'newspack-empty-state__stack' );
|
|
45
|
+
expect( stack ).toHaveStyle( { gap: 'var(--wpds-dimension-gap-2xl, 32px)' } );
|
|
46
|
+
} );
|
|
47
|
+
|
|
48
|
+
// Consumers key `:has()` selectors off this class, so losing it changes their
|
|
49
|
+
// layout without failing anything.
|
|
50
|
+
it( 'merges className onto the grid', () => {
|
|
51
|
+
const { container } = render(
|
|
52
|
+
<EmptyState.Root className="consumer-empty-state">
|
|
53
|
+
<p>body</p>
|
|
54
|
+
</EmptyState.Root>
|
|
55
|
+
);
|
|
56
|
+
expect( container.querySelector( '.newspack-empty-state' ) ).toHaveClass( 'consumer-empty-state' );
|
|
57
|
+
} );
|
|
58
|
+
|
|
59
|
+
// Elements, not a bare string: the stack keeps a lone string but drops one sitting
|
|
60
|
+
// beside an element.
|
|
61
|
+
it( 'renders its children', () => {
|
|
62
|
+
render(
|
|
63
|
+
<EmptyState.Root>
|
|
64
|
+
<p>body</p>
|
|
65
|
+
</EmptyState.Root>
|
|
66
|
+
);
|
|
67
|
+
expect( screen.getByText( 'body' ) ).toBeInTheDocument();
|
|
68
|
+
} );
|
|
69
|
+
} );
|
|
70
|
+
|
|
71
|
+
describe( 'EmptyState.Header', () => {
|
|
72
|
+
it( 'renders the title and description in the header', () => {
|
|
73
|
+
const { container } = render(
|
|
74
|
+
<EmptyState.Root>
|
|
75
|
+
<EmptyState.Header title="Get started with newsletters" description="Compose and send." />
|
|
76
|
+
</EmptyState.Root>
|
|
77
|
+
);
|
|
78
|
+
expect( container.querySelector( '.newspack-empty-state__title' ) ).toHaveTextContent( 'Get started with newsletters' );
|
|
79
|
+
expect( container.querySelector( '.newspack-empty-state__description' ) ).toHaveTextContent( 'Compose and send.' );
|
|
80
|
+
} );
|
|
81
|
+
|
|
82
|
+
// Spacing comes from the stacks, so a margin creeping back onto the heading or
|
|
83
|
+
// paragraph would silently widen every gap.
|
|
84
|
+
it( 'spaces the header from its stacks rather than from margins', () => {
|
|
85
|
+
const { container } = render(
|
|
86
|
+
<EmptyState.Root>
|
|
87
|
+
<EmptyState.Header title="Get started with newsletters" description="Compose and send." />
|
|
88
|
+
</EmptyState.Root>
|
|
89
|
+
);
|
|
90
|
+
const header = container.querySelector( '.newspack-empty-state__header' );
|
|
91
|
+
expect( header ).toHaveStyle( { flexDirection: 'column', alignItems: 'center', gap: 'var(--wpds-dimension-gap-sm, 8px)' } );
|
|
92
|
+
expect( header.firstElementChild ).toHaveStyle( {
|
|
93
|
+
flexDirection: 'column',
|
|
94
|
+
alignItems: 'center',
|
|
95
|
+
gap: 'var(--wpds-dimension-gap-lg, 16px)',
|
|
96
|
+
} );
|
|
97
|
+
} );
|
|
98
|
+
|
|
99
|
+
it( 'wraps the icon in the disc', () => {
|
|
100
|
+
const { container } = render(
|
|
101
|
+
<EmptyState.Root>
|
|
102
|
+
<EmptyState.Header title="Get started with newsletters" icon={ <svg /> } />
|
|
103
|
+
</EmptyState.Root>
|
|
104
|
+
);
|
|
105
|
+
expect( container.querySelector( '.newspack-empty-state__icon svg' ) ).toBeInTheDocument();
|
|
106
|
+
} );
|
|
107
|
+
|
|
108
|
+
it( 'omits the description and the icon when they are not given', () => {
|
|
109
|
+
const { container } = render(
|
|
110
|
+
<EmptyState.Root>
|
|
111
|
+
<EmptyState.Header title="Get started with newsletters" />
|
|
112
|
+
</EmptyState.Root>
|
|
113
|
+
);
|
|
114
|
+
expect( container.querySelector( '.newspack-empty-state__description' ) ).not.toBeInTheDocument();
|
|
115
|
+
expect( container.querySelector( '.newspack-empty-state__icon' ) ).not.toBeInTheDocument();
|
|
116
|
+
} );
|
|
117
|
+
|
|
118
|
+
it( 'renders an h2 and a 48px icon at the default size', () => {
|
|
119
|
+
const { container } = render(
|
|
120
|
+
<EmptyState.Root>
|
|
121
|
+
<EmptyState.Header title="Get started with newsletters" icon={ <svg /> } />
|
|
122
|
+
</EmptyState.Root>
|
|
123
|
+
);
|
|
124
|
+
expect( screen.getByRole( 'heading', { level: 2, name: 'Get started with newsletters' } ) ).toBeInTheDocument();
|
|
125
|
+
expect( container.querySelector( '.newspack-empty-state__header--small' ) ).not.toBeInTheDocument();
|
|
126
|
+
expect( container.querySelector( '.newspack-empty-state__icon svg' ) ).toHaveAttribute( 'width', '48' );
|
|
127
|
+
} );
|
|
128
|
+
|
|
129
|
+
it( 'drops to an h3, the small modifier and a 24px icon when the root is small', () => {
|
|
130
|
+
const { container } = render(
|
|
131
|
+
<EmptyState.Root size="small">
|
|
132
|
+
<EmptyState.Header title="No products match this rule" icon={ <svg /> } />
|
|
133
|
+
</EmptyState.Root>
|
|
134
|
+
);
|
|
135
|
+
expect( container.querySelector( '.newspack-empty-state__header--small' ) ).toBeInTheDocument();
|
|
136
|
+
expect( screen.getByRole( 'heading', { level: 3, name: 'No products match this rule' } ) ).toBeInTheDocument();
|
|
137
|
+
expect( container.querySelector( '.newspack-empty-state__icon svg' ) ).toHaveAttribute( 'width', '24' );
|
|
138
|
+
expect( container.querySelector( '.newspack-empty-state__header' ).firstElementChild ).toHaveStyle( {
|
|
139
|
+
gap: 'var(--wpds-dimension-gap-md, 12px)',
|
|
140
|
+
} );
|
|
141
|
+
} );
|
|
142
|
+
|
|
143
|
+
it( 'lets heading override the level the size implies', () => {
|
|
144
|
+
render(
|
|
145
|
+
<EmptyState.Root size="small">
|
|
146
|
+
<EmptyState.Header title="No products match this rule" heading={ 1 } />
|
|
147
|
+
</EmptyState.Root>
|
|
148
|
+
);
|
|
149
|
+
expect( screen.getByRole( 'heading', { level: 1, name: 'No products match this rule' } ) ).toBeInTheDocument();
|
|
150
|
+
} );
|
|
151
|
+
|
|
152
|
+
it( 'carries its own class hook alongside any className', () => {
|
|
153
|
+
const { container } = render(
|
|
154
|
+
<EmptyState.Root>
|
|
155
|
+
<EmptyState.Header title="Get started with newsletters" className="consumer-header" />
|
|
156
|
+
</EmptyState.Root>
|
|
157
|
+
);
|
|
158
|
+
const header = container.querySelector( '.newspack-empty-state__header' );
|
|
159
|
+
expect( header ).toBeInTheDocument();
|
|
160
|
+
expect( header ).toHaveClass( 'consumer-header' );
|
|
161
|
+
} );
|
|
162
|
+
|
|
163
|
+
it( 'throws outside Root in development', () => {
|
|
164
|
+
const consoleError = jest.spyOn( console, 'error' ).mockImplementation( () => {} );
|
|
165
|
+
try {
|
|
166
|
+
expect( () => render( <EmptyState.Header title="Orphan" /> ) ).toThrow(
|
|
167
|
+
'EmptyState subcomponents must be rendered inside EmptyState.Root.'
|
|
168
|
+
);
|
|
169
|
+
} finally {
|
|
170
|
+
consoleError.mockRestore();
|
|
171
|
+
}
|
|
172
|
+
} );
|
|
173
|
+
|
|
174
|
+
// Header is the visible half, so a misplaced one blanking an admin screen costs more
|
|
175
|
+
// than the default size it falls back to.
|
|
176
|
+
it( 'falls back to the default size outside Root in production', () => {
|
|
177
|
+
const previous = process.env.NODE_ENV;
|
|
178
|
+
process.env.NODE_ENV = 'production';
|
|
179
|
+
try {
|
|
180
|
+
const { container } = render( <EmptyState.Header title="Orphan" icon={ <svg /> } /> );
|
|
181
|
+
expect( screen.getByRole( 'heading', { level: 2, name: 'Orphan' } ) ).toBeInTheDocument();
|
|
182
|
+
expect( container.querySelector( '.newspack-empty-state__header--small' ) ).not.toBeInTheDocument();
|
|
183
|
+
expect( container.querySelector( '.newspack-empty-state__icon svg' ) ).toHaveAttribute( 'width', '48' );
|
|
184
|
+
} finally {
|
|
185
|
+
process.env.NODE_ENV = previous;
|
|
186
|
+
}
|
|
187
|
+
} );
|
|
188
|
+
} );
|
|
189
|
+
|
|
190
|
+
describe( 'EmptyState.Actions', () => {
|
|
191
|
+
it( 'renders its children in a centred row', () => {
|
|
192
|
+
const { container } = render(
|
|
193
|
+
<EmptyState.Root>
|
|
194
|
+
<EmptyState.Actions>
|
|
195
|
+
<button type="button">Add Newsletter</button>
|
|
196
|
+
</EmptyState.Actions>
|
|
197
|
+
</EmptyState.Root>
|
|
198
|
+
);
|
|
199
|
+
expect( container.querySelector( '.newspack-empty-state__actions' ) ).toBeInTheDocument();
|
|
200
|
+
expect( container.querySelector( '.newspack-empty-state__actions' ) ).toHaveStyle( {
|
|
201
|
+
justifyContent: 'center',
|
|
202
|
+
gap: 'var(--wpds-dimension-gap-sm, 8px)',
|
|
203
|
+
} );
|
|
204
|
+
expect( screen.getByRole( 'button', { name: 'Add Newsletter' } ) ).toBeInTheDocument();
|
|
205
|
+
} );
|
|
206
|
+
|
|
207
|
+
it( 'stacks into a column while keeping the hook class', () => {
|
|
208
|
+
const { container } = render(
|
|
209
|
+
<EmptyState.Root>
|
|
210
|
+
<EmptyState.Actions orientation="column">
|
|
211
|
+
<button type="button">Set up Audience Management</button>
|
|
212
|
+
<a href="https://example.com">Learn more</a>
|
|
213
|
+
</EmptyState.Actions>
|
|
214
|
+
</EmptyState.Root>
|
|
215
|
+
);
|
|
216
|
+
const actions = container.querySelector( '.newspack-empty-state__actions' );
|
|
217
|
+
expect( actions ).toBeInTheDocument();
|
|
218
|
+
expect( actions ).toHaveStyle( { flexDirection: 'column' } );
|
|
219
|
+
// A stack's own default is `stretch`, so without align="center" the buttons
|
|
220
|
+
// would go full-bleed while flexDirection stayed correct.
|
|
221
|
+
expect( actions ).toHaveStyle( { alignItems: 'center' } );
|
|
222
|
+
} );
|
|
223
|
+
|
|
224
|
+
it( 'throws outside Root in development', () => {
|
|
225
|
+
const consoleError = jest.spyOn( console, 'error' ).mockImplementation( () => {} );
|
|
226
|
+
try {
|
|
227
|
+
expect( () => render( <EmptyState.Actions>x</EmptyState.Actions> ) ).toThrow(
|
|
228
|
+
'EmptyState subcomponents must be rendered inside EmptyState.Root.'
|
|
229
|
+
);
|
|
230
|
+
} finally {
|
|
231
|
+
consoleError.mockRestore();
|
|
232
|
+
}
|
|
233
|
+
} );
|
|
234
|
+
|
|
235
|
+
// Actions reads nothing from context, so the invariant is a development aid. A
|
|
236
|
+
// stray one must not blank an admin screen in production over a layout hint.
|
|
237
|
+
it( 'renders outside Root in production', () => {
|
|
238
|
+
const previous = process.env.NODE_ENV;
|
|
239
|
+
process.env.NODE_ENV = 'production';
|
|
240
|
+
try {
|
|
241
|
+
const { container } = render( <EmptyState.Actions>x</EmptyState.Actions> );
|
|
242
|
+
expect( container.querySelector( '.newspack-empty-state__actions' ) ).toBeInTheDocument();
|
|
243
|
+
} finally {
|
|
244
|
+
process.env.NODE_ENV = previous;
|
|
245
|
+
}
|
|
246
|
+
} );
|
|
247
|
+
} );
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* EmptyState
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Internal dependencies.
|
|
7
|
+
*/
|
|
8
|
+
import Actions from './actions';
|
|
9
|
+
import Header from './header';
|
|
10
|
+
import Root from './root';
|
|
11
|
+
|
|
12
|
+
export const EmptyState = {
|
|
13
|
+
Root,
|
|
14
|
+
Header,
|
|
15
|
+
Actions,
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
Object.entries( EmptyState ).forEach( ( [ name, part ] ) => {
|
|
19
|
+
( part as { displayName?: string } ).displayName = `EmptyState.${ name }`;
|
|
20
|
+
} );
|
|
21
|
+
|
|
22
|
+
export default EmptyState;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WordPress dependencies.
|
|
3
|
+
*/
|
|
4
|
+
import { useMemo } from '@wordpress/element';
|
|
5
|
+
import { Stack } from '@wordpress/ui';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* External dependencies.
|
|
9
|
+
*/
|
|
10
|
+
import classnames from 'classnames';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Internal dependencies.
|
|
14
|
+
*/
|
|
15
|
+
import Grid from '../grid';
|
|
16
|
+
import { EmptyStateContext } from './context';
|
|
17
|
+
import type { EmptyStateRootProps } from './types';
|
|
18
|
+
import './style.scss';
|
|
19
|
+
|
|
20
|
+
// Positions the stack in columns 2 to 4. `data-` attributes because that is the form
|
|
21
|
+
// grid/style.scss standardised on, after prop filtering was found to drop a bare `end`
|
|
22
|
+
// before it reached the DOM. That file carries the full note.
|
|
23
|
+
const gridColumn = { 'data-start': '2', 'data-end': '4' };
|
|
24
|
+
|
|
25
|
+
const Root = ( { size = 'default', className, children }: EmptyStateRootProps ) => {
|
|
26
|
+
const context = useMemo( () => ( { size } ), [ size ] );
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<EmptyStateContext.Provider value={ context }>
|
|
30
|
+
<Grid className={ classnames( 'newspack-empty-state', className ) } columns={ 4 } noMargin>
|
|
31
|
+
<Stack className="newspack-empty-state__stack" direction="column" gap="2xl" { ...gridColumn }>
|
|
32
|
+
{ children }
|
|
33
|
+
</Stack>
|
|
34
|
+
</Grid>
|
|
35
|
+
</EmptyStateContext.Provider>
|
|
36
|
+
);
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export default Root;
|
|
@@ -0,0 +1,51 @@
|
|
|
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
|
+
@use "../../../colors/colors.module" as np-colors;
|
|
5
|
+
|
|
6
|
+
.newspack-empty-state {
|
|
7
|
+
&__stack {
|
|
8
|
+
margin-inline: auto;
|
|
9
|
+
max-width: var(--wpds-dimension-surface-width-lg, 560px);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
&__header {
|
|
13
|
+
text-align: center;
|
|
14
|
+
text-wrap: balance;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
&__icon {
|
|
18
|
+
background-color: var(--wp-admin-theme-color-lighter-10, #{np-colors.$primary-000});
|
|
19
|
+
border-radius: 100%;
|
|
20
|
+
display: grid;
|
|
21
|
+
height: wp-vars.$grid-unit-50 * 2;
|
|
22
|
+
place-items: center;
|
|
23
|
+
width: wp-vars.$grid-unit-50 * 2;
|
|
24
|
+
|
|
25
|
+
svg {
|
|
26
|
+
fill: var(--wp-admin-theme-color, #{np-colors.$primary-600});
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
&__title {
|
|
31
|
+
@include wp-mixins.heading-x-large();
|
|
32
|
+
color: var(--wpds-color-foreground-content-neutral, #{wp-colors.$gray-900});
|
|
33
|
+
margin: 0;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
&__description {
|
|
37
|
+
color: var(--wpds-color-foreground-content-neutral-weak, #{wp-colors.$gray-700});
|
|
38
|
+
margin: 0;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
&__header--small {
|
|
42
|
+
.newspack-empty-state__icon {
|
|
43
|
+
height: wp-vars.$grid-unit-50;
|
|
44
|
+
width: wp-vars.$grid-unit-50;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.newspack-empty-state__title {
|
|
48
|
+
@include wp-mixins.heading-large();
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { GapSize } from '@wordpress/theme';
|
|
2
|
+
|
|
3
|
+
export type EmptyStateSize = 'default' | 'small';
|
|
4
|
+
|
|
5
|
+
export type EmptyStateActionsOrientation = 'row' | 'column';
|
|
6
|
+
|
|
7
|
+
export type EmptyStateRootProps = {
|
|
8
|
+
/** Read by `EmptyState.Header` through context. */
|
|
9
|
+
size?: EmptyStateSize;
|
|
10
|
+
/** Merged onto the grid, which is the element consumers' `:has()` selectors look for. */
|
|
11
|
+
className?: string;
|
|
12
|
+
children?: React.ReactNode;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export type EmptyStateHeaderProps = {
|
|
16
|
+
icon?: JSX.Element;
|
|
17
|
+
title: string;
|
|
18
|
+
description?: React.ReactNode;
|
|
19
|
+
/** Defaults to 3 when the root is small, 2 otherwise. */
|
|
20
|
+
heading?: 1 | 2 | 3 | 4 | 5 | 6;
|
|
21
|
+
/** Merged onto `newspack-empty-state__header`. */
|
|
22
|
+
className?: string;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export type EmptyStateActionsProps = {
|
|
26
|
+
/** `column` stacks the actions instead, for a button above a link or a note. */
|
|
27
|
+
orientation?: EmptyStateActionsOrientation;
|
|
28
|
+
/** Gap between actions, on the design-system scale. */
|
|
29
|
+
gap?: GapSize;
|
|
30
|
+
/** Merged onto the stack. */
|
|
31
|
+
className?: string;
|
|
32
|
+
children?: React.ReactNode;
|
|
33
|
+
};
|
package/src/index.js
CHANGED
|
@@ -1,9 +1,7 @@
|
|
|
1
|
-
export { default as Accordion, AccordionPanel } from './accordion';
|
|
2
1
|
export { default as ActionCard } from './action-card';
|
|
3
2
|
export { default as AutocompleteTokenField } from './autocomplete-tokenfield';
|
|
4
3
|
export { default as AutocompleteWithSuggestions } from './autocomplete-with-suggestions';
|
|
5
4
|
export { default as AutocompleteWithLatestPosts } from './autocomplete-with-latest-posts';
|
|
6
|
-
export { default as Badge } from './badge';
|
|
7
5
|
export { default as Breadcrumbs } from './breadcrumbs';
|
|
8
6
|
export { default as Button } from './button';
|
|
9
7
|
export { default as BoxContrast } from './box-contrast';
|
|
@@ -14,12 +12,14 @@ export { default as CardSettingsGroup } from './card-settings-group';
|
|
|
14
12
|
export { default as CardSortableList } from './card-sortable-list';
|
|
15
13
|
export { default as IntegrationIcon } from './integration-icon';
|
|
16
14
|
export { default as CategoryAutocomplete } from './category-autocomplete';
|
|
15
|
+
export { default as CollapsibleGroup } from './collapsible-group';
|
|
17
16
|
export { default as ColorPicker } from './color-picker';
|
|
18
17
|
export { default as ConfirmDialog } from './confirm-dialog';
|
|
19
18
|
export { default as CustomSelectControl } from './custom-select-control';
|
|
20
19
|
export { default as DataViews } from './dataviews';
|
|
21
20
|
export { default as Divider } from './divider';
|
|
22
21
|
export { default as Drawer } from './drawer';
|
|
22
|
+
export { default as EmptyState } from './empty-state';
|
|
23
23
|
export { default as FormTokenField } from './form-token-field';
|
|
24
24
|
export { default as Footer } from './footer';
|
|
25
25
|
export { default as Handoff } from './handoff';
|
|
@@ -42,6 +42,8 @@ export { default as RadioControl } from './radio-control';
|
|
|
42
42
|
export { default as SectionHeader } from './section-header';
|
|
43
43
|
export { default as SelectControl } from './select-control';
|
|
44
44
|
export { default as Settings } from './settings';
|
|
45
|
+
export { default as StatCard, STAT_CARD_NULL_GLYPH } from './stat-card';
|
|
46
|
+
export { default as StatusIndicator } from './status-indicator';
|
|
45
47
|
export { default as StepsList } from './steps-list';
|
|
46
48
|
export { default as StepsListItem } from './steps-list-item';
|
|
47
49
|
export { default as StyleCard } from './style-card';
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# InfoButton
|
|
2
|
+
|
|
3
|
+
An icon button that reveals supplementary context on hover, tap or activation.
|
|
4
|
+
It renders nothing but the button and its popup; most often it is placed next to
|
|
5
|
+
a label, explaining the setting beside it.
|
|
6
|
+
|
|
7
|
+
```jsx
|
|
8
|
+
import { InfoButton } from 'newspack-components';
|
|
9
|
+
|
|
10
|
+
<InfoButton description={ __( 'Number of articles read in the last 30 day period.', 'newspack-plugin' ) } />
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
The component imports its own stylesheet, so the barrel ships the CSS with it.
|
|
14
|
+
There is nothing separate to import.
|
|
15
|
+
|
|
16
|
+
It carries no inline margin, so the row it sits in owns the spacing beside it.
|
|
17
|
+
In a flex or grid row that is the row's own `gap`; anywhere else the caller sets
|
|
18
|
+
it.
|
|
19
|
+
|
|
20
|
+
Vertically it is a 24px control, `$button-size-small`, and it makes no assumption
|
|
21
|
+
about the line it lands on. Beside text on a shorter line it grows the row unless
|
|
22
|
+
the row pulls it back, which is the host's call rather than the component's: a row
|
|
23
|
+
that is happy at 24px needs nothing. `StatCard.Label` and `SettingsSection` both
|
|
24
|
+
trim it to the 20px line their text sits on, so a label carrying one stays level
|
|
25
|
+
with a label without.
|
|
26
|
+
|
|
27
|
+
## What belongs in it
|
|
28
|
+
|
|
29
|
+
**Supplementary context only.** Anything a reader needs in order to use the
|
|
30
|
+
control belongs in visible help text beside it, not behind an affordance they
|
|
31
|
+
have to find first. The design system stops short of this: it only rules out
|
|
32
|
+
hiding an important description behind a *tooltip*, and offers a popover as the
|
|
33
|
+
alternative when space is tight. The stricter line is ours, not the library's.
|
|
34
|
+
|
|
35
|
+
## Props
|
|
36
|
+
|
|
37
|
+
| Prop | Type | Default | Description |
|
|
38
|
+
|------|------|---------|-------------|
|
|
39
|
+
| `description` | `string` | — | The context to reveal. The whole component renders nothing without it. |
|
|
40
|
+
| `className` | `string` | — | Additional CSS class on the trigger. |
|
|
41
|
+
| `triggerLabel` | `string` | `'More information'` | Accessible name for the trigger. |
|
|
42
|
+
|
|
43
|
+
Unrecognised props pass through to the trigger, so `id`, `data-*` and event
|
|
44
|
+
handlers all land on the button, and a `ref` reaches the button itself. An
|
|
45
|
+
`aria-label` names the popup as well as the trigger, so the two never disagree.
|
|
46
|
+
|
|
47
|
+
## Name each one for its setting
|
|
48
|
+
|
|
49
|
+
**Pass `triggerLabel` whenever a screen holds more than one.** The default name
|
|
50
|
+
is the same for every instance, so a screen with a dozen of them gives a screen
|
|
51
|
+
reader user a dozen identical entries in its controls list with nothing to tell
|
|
52
|
+
them apart.
|
|
53
|
+
|
|
54
|
+
```jsx
|
|
55
|
+
<InfoButton
|
|
56
|
+
description={ criteria.description }
|
|
57
|
+
triggerLabel={ sprintf(
|
|
58
|
+
// translators: %s is the name of the setting being explained.
|
|
59
|
+
__( 'More information about %s', 'newspack-plugin' ),
|
|
60
|
+
criteria.name
|
|
61
|
+
) }
|
|
62
|
+
/>
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
**Pass it in the calling plugin's own text domain.** The samples here say
|
|
66
|
+
`newspack-plugin` because that catalogue picks the package's strings up through
|
|
67
|
+
the symlink at `plugins/newspack-plugin/packages/components`. Everywhere else
|
|
68
|
+
the package resolves through `node_modules`, which `make-pot` skips, so a
|
|
69
|
+
`newspack-plugin`-domained string passed in from another plugin ships
|
|
70
|
+
untranslated.
|
|
71
|
+
|
|
72
|
+
## Built on Popover, not Tooltip
|
|
73
|
+
|
|
74
|
+
It looks like a tooltip and is not one. `@wordpress/ui` documents its `Tooltip`
|
|
75
|
+
as visual-only, not exposed to assistive technology, and unavailable on touch
|
|
76
|
+
devices, so it "should not be used for infotips, descriptions, or dynamic status
|
|
77
|
+
messages". Both components gate hover on a mouse-like pointer, but only a
|
|
78
|
+
popover trigger also opens on press, which is what makes it reachable by tap.
|
|
79
|
+
|
|
80
|
+
The consequences, all of which a tooltip would lose:
|
|
81
|
+
|
|
82
|
+
- Hover opens it on a desktop after 200ms, and a 200ms close delay means
|
|
83
|
+
overshooting the 24px trigger does not dismiss it instantly.
|
|
84
|
+
- A tap opens it on a touch device.
|
|
85
|
+
- Escape closes it and returns focus to the trigger.
|
|
86
|
+
- The popup carries the context on its own `aria-describedby` rather than
|
|
87
|
+
folding it into the button's accessible name, so the trigger keeps a short
|
|
88
|
+
name of its own.
|
|
89
|
+
|
|
90
|
+
The library marks `Popover` "use with caution" next to `@wordpress/components`,
|
|
91
|
+
pending a review of overlay compatibility. The portal answers that by opting into
|
|
92
|
+
the shared overlay slot, a body-level container the library reserves above the
|
|
93
|
+
older z-index map, so the popup clears a core popover instead of tying with it.
|
|
94
|
+
Outside a WordPress screen the slot is absent and the stylesheet's own z-index
|
|
95
|
+
applies.
|
|
96
|
+
|
|
97
|
+
`Popover.Popup` is rendered with `variant="unstyled"`, which skips the design
|
|
98
|
+
system's own light card surface so the stylesheet can reproduce a tooltip's
|
|
99
|
+
appearance instead. That is also why the popup carries no enter animation: the
|
|
100
|
+
motion layer ships with the default surface.
|
|
101
|
+
|
|
102
|
+
## Accessibility
|
|
103
|
+
|
|
104
|
+
The trigger is a native `<button>` with no action of its own. Activating it
|
|
105
|
+
opens the popup and moves focus into it, so the description is the next thing a
|
|
106
|
+
keyboard user reaches.
|
|
107
|
+
|
|
108
|
+
The popup is a `role="dialog"` named by a visually hidden `Popover.Title`. The
|
|
109
|
+
title repeats the trigger's name, which is what the design system's own infotip
|
|
110
|
+
reference does, and the description carries the prose.
|
|
111
|
+
|
|
112
|
+
The description keeps the design system's body type rather than a tooltip's
|
|
113
|
+
smaller size, because `Popover.Description` renders a `Text` and does not expose
|
|
114
|
+
its variant. At the 320px width cap, a long description wraps to about four
|
|
115
|
+
lines.
|