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,168 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* External dependencies
|
|
3
|
+
*/
|
|
4
|
+
import { fireEvent, render, screen } from '@testing-library/react';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Internal dependencies
|
|
8
|
+
*/
|
|
9
|
+
import CollapsibleGroup from './index';
|
|
10
|
+
|
|
11
|
+
const renderItems = ( count, props = {} ) =>
|
|
12
|
+
render(
|
|
13
|
+
<CollapsibleGroup { ...props }>
|
|
14
|
+
{ Array.from( { length: count }, ( _, i ) => (
|
|
15
|
+
<CollapsibleGroup.Item key={ i } title={ `Panel ${ i }` }>
|
|
16
|
+
content
|
|
17
|
+
</CollapsibleGroup.Item>
|
|
18
|
+
) ) }
|
|
19
|
+
</CollapsibleGroup>
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
const dividers = container => container.querySelectorAll( '.newspack-divider' );
|
|
23
|
+
const items = container => container.querySelectorAll( '.newspack-collapsible-group__item' );
|
|
24
|
+
|
|
25
|
+
describe( 'CollapsibleGroup dividers', () => {
|
|
26
|
+
it( 'renders no divider for a single item', () => {
|
|
27
|
+
const { container } = renderItems( 1 );
|
|
28
|
+
expect( items( container ) ).toHaveLength( 1 );
|
|
29
|
+
expect( dividers( container ) ).toHaveLength( 0 );
|
|
30
|
+
} );
|
|
31
|
+
|
|
32
|
+
it( 'renders a divider between items but not after the last', () => {
|
|
33
|
+
const { container } = renderItems( 3 );
|
|
34
|
+
expect( items( container ) ).toHaveLength( 3 );
|
|
35
|
+
expect( dividers( container ) ).toHaveLength( 2 );
|
|
36
|
+
expect( container.querySelector( '.newspack-collapsible-group' ).lastElementChild ).not.toHaveClass( 'newspack-divider' );
|
|
37
|
+
} );
|
|
38
|
+
|
|
39
|
+
it( 'counts only rendered items when placing dividers', () => {
|
|
40
|
+
const { container } = render(
|
|
41
|
+
<CollapsibleGroup>
|
|
42
|
+
<CollapsibleGroup.Item title="Panel 0">content</CollapsibleGroup.Item>
|
|
43
|
+
<CollapsibleGroup.Item title="Panel 1">content</CollapsibleGroup.Item>
|
|
44
|
+
{ false }
|
|
45
|
+
{ 'trailing text' }
|
|
46
|
+
</CollapsibleGroup>
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
expect( items( container ) ).toHaveLength( 2 );
|
|
50
|
+
expect( dividers( container ) ).toHaveLength( 1 );
|
|
51
|
+
expect( container.querySelector( '.newspack-collapsible-group' ).lastElementChild ).not.toHaveClass( 'newspack-divider' );
|
|
52
|
+
} );
|
|
53
|
+
|
|
54
|
+
it( 'renders tertiary dividers', () => {
|
|
55
|
+
const { container } = renderItems( 2 );
|
|
56
|
+
expect( dividers( container )[ 0 ] ).toHaveClass( 'newspack-divider--variant-tertiary' );
|
|
57
|
+
} );
|
|
58
|
+
} );
|
|
59
|
+
|
|
60
|
+
describe( 'CollapsibleGroup titleLevel', () => {
|
|
61
|
+
it( 'renders item titles as h2 by default', () => {
|
|
62
|
+
renderItems( 2 );
|
|
63
|
+
expect( screen.getAllByRole( 'heading', { level: 2 } ) ).toHaveLength( 2 );
|
|
64
|
+
} );
|
|
65
|
+
|
|
66
|
+
it( 'renders every item title at the level given', () => {
|
|
67
|
+
renderItems( 2, { titleLevel: 3 } );
|
|
68
|
+
expect( screen.getAllByRole( 'heading', { level: 3 } ) ).toHaveLength( 2 );
|
|
69
|
+
expect( screen.queryByRole( 'heading', { level: 2 } ) ).not.toBeInTheDocument();
|
|
70
|
+
} );
|
|
71
|
+
|
|
72
|
+
it( 'clamps a level above the heading range', () => {
|
|
73
|
+
renderItems( 2, { titleLevel: 7 } );
|
|
74
|
+
expect( screen.getAllByRole( 'heading', { level: 6 } ) ).toHaveLength( 2 );
|
|
75
|
+
} );
|
|
76
|
+
|
|
77
|
+
it( 'clamps a level below the heading range', () => {
|
|
78
|
+
renderItems( 1, { titleLevel: 0 } );
|
|
79
|
+
expect( screen.getByRole( 'heading', { level: 1 } ) ).toBeInTheDocument();
|
|
80
|
+
} );
|
|
81
|
+
|
|
82
|
+
it( 'inherits the level in a nested group', () => {
|
|
83
|
+
render(
|
|
84
|
+
<CollapsibleGroup titleLevel={ 4 }>
|
|
85
|
+
<CollapsibleGroup.Item title="Outer" defaultOpen>
|
|
86
|
+
<CollapsibleGroup>
|
|
87
|
+
<CollapsibleGroup.Item title="Inner">content</CollapsibleGroup.Item>
|
|
88
|
+
</CollapsibleGroup>
|
|
89
|
+
</CollapsibleGroup.Item>
|
|
90
|
+
</CollapsibleGroup>
|
|
91
|
+
);
|
|
92
|
+
expect( screen.getByRole( 'heading', { level: 4, name: 'Inner' } ) ).toBeInTheDocument();
|
|
93
|
+
} );
|
|
94
|
+
|
|
95
|
+
it( 'inherits the level in a nested group when the title is hidden', () => {
|
|
96
|
+
render(
|
|
97
|
+
<CollapsibleGroup titleLevel={ 4 } hideSingleTitle>
|
|
98
|
+
<CollapsibleGroup.Item title="Outer">
|
|
99
|
+
<CollapsibleGroup>
|
|
100
|
+
<CollapsibleGroup.Item title="Inner">content</CollapsibleGroup.Item>
|
|
101
|
+
</CollapsibleGroup>
|
|
102
|
+
</CollapsibleGroup.Item>
|
|
103
|
+
</CollapsibleGroup>
|
|
104
|
+
);
|
|
105
|
+
expect( screen.getByRole( 'heading', { level: 4, name: 'Inner' } ) ).toBeInTheDocument();
|
|
106
|
+
} );
|
|
107
|
+
} );
|
|
108
|
+
|
|
109
|
+
describe( 'CollapsibleGroup hideSingleTitle', () => {
|
|
110
|
+
it( 'keeps the title on a lone item by default', () => {
|
|
111
|
+
renderItems( 1 );
|
|
112
|
+
expect( screen.getByRole( 'button', { name: 'Panel 0' } ) ).toBeInTheDocument();
|
|
113
|
+
} );
|
|
114
|
+
|
|
115
|
+
it( 'drops the title and opens a lone item when set', () => {
|
|
116
|
+
renderItems( 1, { hideSingleTitle: true } );
|
|
117
|
+
expect( screen.queryByRole( 'button', { name: 'Panel 0' } ) ).not.toBeInTheDocument();
|
|
118
|
+
expect( screen.getByText( 'content' ) ).toBeVisible();
|
|
119
|
+
} );
|
|
120
|
+
|
|
121
|
+
it( 'leaves titles alone when there is more than one item', () => {
|
|
122
|
+
renderItems( 2, { hideSingleTitle: true } );
|
|
123
|
+
expect( screen.getByRole( 'button', { name: 'Panel 0' } ) ).toBeInTheDocument();
|
|
124
|
+
expect( screen.getByRole( 'button', { name: 'Panel 1' } ) ).toBeInTheDocument();
|
|
125
|
+
} );
|
|
126
|
+
} );
|
|
127
|
+
|
|
128
|
+
describe( 'CollapsibleGroup item state', () => {
|
|
129
|
+
it( 'expands only the item marked defaultOpen', () => {
|
|
130
|
+
render(
|
|
131
|
+
<CollapsibleGroup>
|
|
132
|
+
<CollapsibleGroup.Item title="Open" defaultOpen>
|
|
133
|
+
open content
|
|
134
|
+
</CollapsibleGroup.Item>
|
|
135
|
+
<CollapsibleGroup.Item title="Closed">closed content</CollapsibleGroup.Item>
|
|
136
|
+
</CollapsibleGroup>
|
|
137
|
+
);
|
|
138
|
+
expect( screen.getByRole( 'button', { name: 'Open' } ) ).toHaveAttribute( 'aria-expanded', 'true' );
|
|
139
|
+
expect( screen.getByRole( 'button', { name: 'Closed' } ) ).toHaveAttribute( 'aria-expanded', 'false' );
|
|
140
|
+
} );
|
|
141
|
+
|
|
142
|
+
it( 'leaves a collapsed panel reachable by find-in-page', () => {
|
|
143
|
+
const { container } = renderItems( 1 );
|
|
144
|
+
expect( container.querySelector( '.newspack-collapsible-group__panel' ) ).toHaveAttribute( 'hidden', 'until-found' );
|
|
145
|
+
expect( screen.getByText( 'content' ) ).toBeInTheDocument();
|
|
146
|
+
} );
|
|
147
|
+
|
|
148
|
+
it( 'opens an item when its trigger is clicked', () => {
|
|
149
|
+
const { container } = renderItems( 1 );
|
|
150
|
+
const trigger = screen.getByRole( 'button', { name: 'Panel 0' } );
|
|
151
|
+
|
|
152
|
+
fireEvent.click( trigger );
|
|
153
|
+
|
|
154
|
+
expect( trigger ).toHaveAttribute( 'aria-expanded', 'true' );
|
|
155
|
+
expect( container.querySelector( '.newspack-collapsible-group__panel' ) ).not.toHaveAttribute( 'hidden' );
|
|
156
|
+
} );
|
|
157
|
+
|
|
158
|
+
it( 'closes an open item when its trigger is clicked', () => {
|
|
159
|
+
const { container } = renderItems( 1 );
|
|
160
|
+
const trigger = screen.getByRole( 'button', { name: 'Panel 0' } );
|
|
161
|
+
|
|
162
|
+
fireEvent.click( trigger );
|
|
163
|
+
fireEvent.click( trigger );
|
|
164
|
+
|
|
165
|
+
expect( trigger ).toHaveAttribute( 'aria-expanded', 'false' );
|
|
166
|
+
expect( container.querySelector( '.newspack-collapsible-group__panel' ) ).toHaveAttribute( 'hidden', 'until-found' );
|
|
167
|
+
} );
|
|
168
|
+
} );
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CollapsibleGroup
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Internal dependencies
|
|
7
|
+
*/
|
|
8
|
+
import Item from './item';
|
|
9
|
+
import Root from './root';
|
|
10
|
+
|
|
11
|
+
// The group is itself renderable, so `Item` hangs off it rather than forming a namespace object like `Drawer`.
|
|
12
|
+
const CollapsibleGroup = Object.assign( Root, { Item } );
|
|
13
|
+
|
|
14
|
+
( CollapsibleGroup as { displayName?: string } ).displayName = 'CollapsibleGroup';
|
|
15
|
+
( Item as { displayName?: string } ).displayName = 'CollapsibleGroup.Item';
|
|
16
|
+
|
|
17
|
+
export default CollapsibleGroup;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WordPress dependencies
|
|
3
|
+
*/
|
|
4
|
+
import { Icon, chevronDown } from '@wordpress/icons';
|
|
5
|
+
import { Collapsible } from '@wordpress/ui';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* External dependencies
|
|
9
|
+
*/
|
|
10
|
+
import classNames from 'classnames';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Internal dependencies
|
|
14
|
+
*/
|
|
15
|
+
import { useTitleLevel } from './context';
|
|
16
|
+
import type { CollapsibleGroupItemProps } from './types';
|
|
17
|
+
|
|
18
|
+
const Item = ( { children, className, defaultOpen = false, title }: CollapsibleGroupItemProps ) => {
|
|
19
|
+
const titleLevel = useTitleLevel();
|
|
20
|
+
|
|
21
|
+
if ( ! title ) {
|
|
22
|
+
return <div className={ classNames( 'newspack-collapsible-group__item', className ) }>{ children }</div>;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const Heading = `h${ titleLevel }` as const;
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<Collapsible.Root className={ classNames( 'newspack-collapsible-group__item', className ) } defaultOpen={ defaultOpen }>
|
|
29
|
+
<Heading className="newspack-collapsible-group__heading">
|
|
30
|
+
<Collapsible.Trigger className="newspack-collapsible-group__trigger">
|
|
31
|
+
{ title }
|
|
32
|
+
<Icon icon={ chevronDown } size={ 24 } />
|
|
33
|
+
</Collapsible.Trigger>
|
|
34
|
+
</Heading>
|
|
35
|
+
<Collapsible.Panel className="newspack-collapsible-group__panel" hiddenUntilFound>
|
|
36
|
+
<div className="newspack-collapsible-group__panel-inner">{ children }</div>
|
|
37
|
+
</Collapsible.Panel>
|
|
38
|
+
</Collapsible.Root>
|
|
39
|
+
);
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export default Item;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WordPress dependencies
|
|
3
|
+
*/
|
|
4
|
+
import { Children, Fragment, cloneElement, isValidElement } 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 Divider from '../divider';
|
|
16
|
+
import { TitleLevelContext, useTitleLevel } from './context';
|
|
17
|
+
import type { CollapsibleGroupItemProps, CollapsibleGroupProps, HeadingLevel } from './types';
|
|
18
|
+
import './style.scss';
|
|
19
|
+
|
|
20
|
+
const clampTitleLevel = ( level: unknown ): HeadingLevel | undefined =>
|
|
21
|
+
Number.isFinite( level ) ? ( Math.min( 6, Math.max( 1, Math.round( level as number ) ) ) as HeadingLevel ) : undefined;
|
|
22
|
+
|
|
23
|
+
const Root = ( { children, className, gap = 'xl', hideSingleTitle = false, titleLevel }: CollapsibleGroupProps ) => {
|
|
24
|
+
const inheritedTitleLevel = useTitleLevel();
|
|
25
|
+
const items = Children.toArray( children ).filter( isValidElement ) as React.ReactElement< CollapsibleGroupItemProps >[];
|
|
26
|
+
const hideTitle = hideSingleTitle && items.length === 1;
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<TitleLevelContext.Provider value={ clampTitleLevel( titleLevel ) ?? inheritedTitleLevel }>
|
|
30
|
+
<Stack className={ classNames( 'newspack-collapsible-group', className ) } direction="column" gap={ gap }>
|
|
31
|
+
{ items.map( ( item, index ) => (
|
|
32
|
+
<Fragment key={ item.key }>
|
|
33
|
+
{ hideTitle ? cloneElement( item, { title: undefined } ) : item }
|
|
34
|
+
{ index < items.length - 1 && <Divider variant="tertiary" marginBottom={ 0 } marginTop={ 0 } /> }
|
|
35
|
+
</Fragment>
|
|
36
|
+
) ) }
|
|
37
|
+
</Stack>
|
|
38
|
+
</TitleLevelContext.Provider>
|
|
39
|
+
);
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export default Root;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
@use "../mixins" as mixins;
|
|
2
|
+
@use "~@wordpress/base-styles/mixins" as wp-mixins;
|
|
3
|
+
@use "~@wordpress/base-styles/variables" as wp-vars;
|
|
4
|
+
|
|
5
|
+
.newspack-collapsible-group {
|
|
6
|
+
&__heading {
|
|
7
|
+
margin: 0;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
&__trigger {
|
|
11
|
+
@include wp-mixins.heading-large();
|
|
12
|
+
align-items: center;
|
|
13
|
+
background: none;
|
|
14
|
+
border: 0;
|
|
15
|
+
color: inherit;
|
|
16
|
+
cursor: pointer;
|
|
17
|
+
display: flex;
|
|
18
|
+
gap: wp-vars.$grid-unit-10;
|
|
19
|
+
justify-content: space-between;
|
|
20
|
+
padding: 0;
|
|
21
|
+
text-align: left;
|
|
22
|
+
width: 100%;
|
|
23
|
+
|
|
24
|
+
&:focus-visible {
|
|
25
|
+
@include mixins.wpds-focus-ring;
|
|
26
|
+
outline-offset: wp-vars.$grid-unit-05;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
svg {
|
|
30
|
+
flex-shrink: 0;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
&[aria-expanded="true"] svg {
|
|
34
|
+
transform: rotate(180deg);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
@media (prefers-reduced-motion: no-preference) {
|
|
38
|
+
svg {
|
|
39
|
+
transition: transform 0.1s ease-out;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
&__panel-inner {
|
|
45
|
+
padding-top: wp-vars.$grid-unit-30;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { GapSize } from '@wordpress/theme';
|
|
2
|
+
|
|
3
|
+
export type HeadingLevel = 1 | 2 | 3 | 4 | 5 | 6;
|
|
4
|
+
|
|
5
|
+
export type CollapsibleGroupProps = {
|
|
6
|
+
className?: string;
|
|
7
|
+
/** `Stack` gap either side of each divider, so items sit twice this far apart. */
|
|
8
|
+
gap?: GapSize;
|
|
9
|
+
/** Renders a lone item open and untitled, since it has nothing to collapse against. */
|
|
10
|
+
hideSingleTitle?: boolean;
|
|
11
|
+
/** Heading level for every item title, so the group shares one place in the outline. Defaults to the level inherited from an enclosing group, or 2. */
|
|
12
|
+
titleLevel?: HeadingLevel;
|
|
13
|
+
children?: React.ReactNode;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export type CollapsibleGroupItemProps = {
|
|
17
|
+
className?: string;
|
|
18
|
+
defaultOpen?: boolean;
|
|
19
|
+
/** Without a title there is no trigger, so the content renders permanently open. */
|
|
20
|
+
title?: string;
|
|
21
|
+
children?: React.ReactNode;
|
|
22
|
+
};
|
package/src/drawer/README.md
CHANGED
|
@@ -10,8 +10,8 @@ the one part with no counterpart there.
|
|
|
10
10
|
|
|
11
11
|
The parts hang off one exported object rather than the flat named exports the
|
|
12
12
|
rest of this package uses. That is deliberate, to keep them recognisable against
|
|
13
|
-
`@wordpress/ui`'s own `Drawer
|
|
14
|
-
|
|
13
|
+
`@wordpress/ui`'s own `Drawer`. It is also the shape compound components in this
|
|
14
|
+
package follow, `EmptyState` included.
|
|
15
15
|
|
|
16
16
|
```jsx
|
|
17
17
|
import { Drawer } from 'newspack-components';
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
# EmptyState
|
|
2
|
+
|
|
3
|
+
The "nothing here yet" treatment for a list screen or an onboarding view: an
|
|
4
|
+
icon, a title, a short description, and usually one call to action.
|
|
5
|
+
|
|
6
|
+
The API is compound: an `EmptyState.Root` and one subcomponent per slot. The
|
|
7
|
+
parts hang off one exported object, as `Drawer`'s do.
|
|
8
|
+
|
|
9
|
+
Root brings the layout its consumers used to hand-write, including the
|
|
10
|
+
`data-start` and `data-end` attributes the `Grid` stylesheet matches on. The
|
|
11
|
+
parts are composed from `Stack`, so every gap belongs to a stack rather than to
|
|
12
|
+
a margin, and the component's own stylesheet covers only the icon disc and the
|
|
13
|
+
type of the title and description.
|
|
14
|
+
|
|
15
|
+
## Importing
|
|
16
|
+
|
|
17
|
+
The package barrel and the component's own entry point both work:
|
|
18
|
+
|
|
19
|
+
```jsx
|
|
20
|
+
// The barrel.
|
|
21
|
+
import { EmptyState } from 'newspack-components';
|
|
22
|
+
|
|
23
|
+
// The component on its own, from newspack-plugin.
|
|
24
|
+
import EmptyState from '../../packages/components/src/empty-state';
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
The by-path form above is newspack-plugin's, which compiles the monorepo source
|
|
28
|
+
directly. newspack-newsletters resolves `newspack-components` to the built
|
|
29
|
+
`dist/esm/index.js`, so its only per-component route is
|
|
30
|
+
`newspack-components/dist/esm/empty-state`, which reaches past the package's
|
|
31
|
+
public surface: `main` is the only declared entry and there is no `exports` map
|
|
32
|
+
to make that path supported. Importing per component holds for one of the two
|
|
33
|
+
consumers until the package declares one.
|
|
34
|
+
|
|
35
|
+
Take the barrel where the bundle already pulls the package in wholesale, as the
|
|
36
|
+
newsletters admin shell does. Import by path where a bundle should stay narrow:
|
|
37
|
+
the barrel reaches `Page`, whose stylesheet carries a `:root` block of
|
|
38
|
+
design-system token overrides, and that block then rides into every bundle that
|
|
39
|
+
touches the barrel.
|
|
40
|
+
|
|
41
|
+
One by-path import does not settle it, and today none of them do. The package
|
|
42
|
+
declares no `sideEffects`, so a bundler cannot drop anything the barrel
|
|
43
|
+
re-exports, and every newspack-plugin screen using `EmptyState` still imports
|
|
44
|
+
`Button` or `Card` from the barrel on a neighbouring line. Until the package
|
|
45
|
+
declares `sideEffects`, importing by path is the direction of travel rather than
|
|
46
|
+
a saving already banked.
|
|
47
|
+
|
|
48
|
+
## Usage
|
|
49
|
+
|
|
50
|
+
```jsx
|
|
51
|
+
import { __ } from '@wordpress/i18n';
|
|
52
|
+
import { Button } from '@wordpress/components';
|
|
53
|
+
import { envelope } from '@wordpress/icons';
|
|
54
|
+
import { EmptyState } from 'newspack-components';
|
|
55
|
+
|
|
56
|
+
<EmptyState.Root className={ EMPTY_STATE_CLASS }>
|
|
57
|
+
<EmptyState.Header
|
|
58
|
+
icon={ envelope }
|
|
59
|
+
title={ __( 'Get started with newsletters', 'newspack-newsletters' ) }
|
|
60
|
+
description={ __( 'Compose, schedule, and send newsletters to your subscribers.', 'newspack-newsletters' ) }
|
|
61
|
+
/>
|
|
62
|
+
<EmptyState.Actions>
|
|
63
|
+
<Button variant="primary" href={ addNewHref }>
|
|
64
|
+
{ __( 'Add Newsletter', 'newspack-newsletters' ) }
|
|
65
|
+
</Button>
|
|
66
|
+
</EmptyState.Actions>
|
|
67
|
+
</EmptyState.Root>
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Every slot except `Root` is optional, and anything else you pass to `Root`
|
|
71
|
+
becomes a sibling of the header at the same 8-unit gap. A screen that offers
|
|
72
|
+
choices rather than one action can drop a stack of cards in instead of
|
|
73
|
+
`EmptyState.Actions`. Pass elements: `Root`'s stack keeps a lone string but drops
|
|
74
|
+
one sitting beside an element, so wrap loose text in a `<p>`. The `Grid` margin
|
|
75
|
+
reset reaches direct children only, so a `<p>` inside a slot keeps the browser's
|
|
76
|
+
default block margin and you zero it where you use it. The component resets
|
|
77
|
+
margins on the two elements it renders itself and stops there: a blanket reset on
|
|
78
|
+
slot content would silently flatten a consumer's own stack of cards or prose, and
|
|
79
|
+
the gaps this component owns all come from its stacks anyway.
|
|
80
|
+
|
|
81
|
+
## Consumers own their wrappers
|
|
82
|
+
|
|
83
|
+
The component does not position itself on the page, so each screen decides
|
|
84
|
+
whether it needs a wrapper at all.
|
|
85
|
+
|
|
86
|
+
Pass a class to `Root` when the styling targets the empty state itself. The
|
|
87
|
+
newsletters screens do that, exporting the class from the shell rather than
|
|
88
|
+
repeating the literal: the shell keys `:has()` off it to hide its header and
|
|
89
|
+
hold the main region to 1006px, and both are rules about an empty state being
|
|
90
|
+
on screen.
|
|
91
|
+
|
|
92
|
+
Wrap `Root` in your own element when the wrapper is page layout that would
|
|
93
|
+
still be there without an empty state. `institutions/onboarding.tsx` does that:
|
|
94
|
+
`newspack-wizard__column` is the wizard's own column width, and the view
|
|
95
|
+
would want it whatever it rendered.
|
|
96
|
+
|
|
97
|
+
## Strict-empty only
|
|
98
|
+
|
|
99
|
+
**Render this when the *unfiltered* collection is empty.** A search or filter
|
|
100
|
+
that matches nothing keeps the DataViews "no results" treatment, which tells
|
|
101
|
+
the reader their query was too narrow rather than that they have nothing.
|
|
102
|
+
|
|
103
|
+
The component cannot enforce that: it never sees the collection. In the
|
|
104
|
+
newsletters admin shell the rule lives in `isStrictlyEmpty`.
|
|
105
|
+
|
|
106
|
+
## Actions take any button
|
|
107
|
+
|
|
108
|
+
`EmptyState.Actions` renders whatever you give it, so each consumer keeps its
|
|
109
|
+
own `Button`. newspack-newsletters passes the `@wordpress/components` one and
|
|
110
|
+
newspack-plugin passes this package's.
|
|
111
|
+
|
|
112
|
+
That is also why there is no CTA invariant. The component this replaced took a
|
|
113
|
+
`ctaHref` / `ctaOnClick` pair and required exactly one, throwing in development.
|
|
114
|
+
With a children slot there is no pair to check. A button that navigates takes
|
|
115
|
+
`href`; one that opens something in place takes `onClick`.
|
|
116
|
+
|
|
117
|
+
## `EmptyState.Root`
|
|
118
|
+
|
|
119
|
+
| Prop | Type | Default | Description |
|
|
120
|
+
|------|------|---------|-------------|
|
|
121
|
+
| `children` | `React.ReactNode` | — | The slots, plus any custom body. |
|
|
122
|
+
| `className` | `string` | — | Merged onto the grid. |
|
|
123
|
+
| `size` | `'default'` \| `'small'` | `'default'` | Read by `EmptyState.Header`. `small` suits an empty state standing in for a panel inside a card. |
|
|
124
|
+
|
|
125
|
+
The grid always carries `newspack-empty-state`, and `className` lands there
|
|
126
|
+
rather than on a wrapper, because consumers key off both. Inside it, the stack
|
|
127
|
+
carries `newspack-empty-state__stack`, sits in columns two to four, and is
|
|
128
|
+
capped at `--wpds-dimension-surface-width-lg` so it cannot sprawl in a wider
|
|
129
|
+
container.
|
|
130
|
+
|
|
131
|
+
## `EmptyState.Header`
|
|
132
|
+
|
|
133
|
+
| Prop | Type | Default | Description |
|
|
134
|
+
|------|------|---------|-------------|
|
|
135
|
+
| `className` | `string` | — | Merged onto `newspack-empty-state__header`. |
|
|
136
|
+
| `description` | `React.ReactNode` | — | One or two sentences on what would fill the screen. |
|
|
137
|
+
| `heading` | `1`–`6` | `3` when small, `2` otherwise | HTML heading level. |
|
|
138
|
+
| `icon` | `JSX.Element` | — | From `@wordpress/icons` or `newspack-icons`. |
|
|
139
|
+
| `title` | `string` | — | **Required.** |
|
|
140
|
+
|
|
141
|
+
`heading` follows `size` by default but stays yours to set. Heading level is a
|
|
142
|
+
document-outline concern rather than a visual one, so a headerless screen that
|
|
143
|
+
needs this to be its `h1` passes `heading={ 1 }`.
|
|
144
|
+
|
|
145
|
+
## `EmptyState.Actions`
|
|
146
|
+
|
|
147
|
+
| Prop | Type | Default | Description |
|
|
148
|
+
|------|------|---------|-------------|
|
|
149
|
+
| `children` | `React.ReactNode` | — | Usually one primary button. |
|
|
150
|
+
| `className` | `string` | — | Merged onto the stack. |
|
|
151
|
+
| `orientation` | `'row'` \| `'column'` | `'row'` | `column` stacks the actions, for a button above a link or an explanatory note. |
|
|
152
|
+
| `gap` | `GapSize` | `'sm'` | Gap between actions, on the design-system scale (`xs` to `3xl`). |
|
|
153
|
+
|
|
154
|
+
A centred stack, carrying `newspack-empty-state__actions`. A row wraps rather
|
|
155
|
+
than overflowing, since the empty state only gets half the grid on a wide screen.
|
|
156
|
+
With one action, prefer a single primary button: an empty state asking for two
|
|
157
|
+
decisions at once is usually a sign the screen needs an onboarding view instead.
|
|
158
|
+
|
|
159
|
+
## Outside the Root
|
|
160
|
+
|
|
161
|
+
`EmptyState.Header` and `EmptyState.Actions` both throw "EmptyState
|
|
162
|
+
subcomponents must be rendered inside EmptyState.Root." when rendered anywhere
|
|
163
|
+
else, in development only. In production `Header` falls back to the default
|
|
164
|
+
size and `Actions` renders as it would inside a Root, so a misplaced part never
|
|
165
|
+
blanks an admin screen.
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WordPress dependencies.
|
|
3
|
+
*/
|
|
4
|
+
import { Stack } from '@wordpress/ui';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* External dependencies.
|
|
8
|
+
*/
|
|
9
|
+
import classnames from 'classnames';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Internal dependencies.
|
|
13
|
+
*/
|
|
14
|
+
import { useEmptyStateInvariant } from './context';
|
|
15
|
+
import type { EmptyStateActionsProps } from './types';
|
|
16
|
+
|
|
17
|
+
const Actions = ( { orientation = 'row', gap = 'sm', className, children }: EmptyStateActionsProps ) => {
|
|
18
|
+
useEmptyStateInvariant();
|
|
19
|
+
|
|
20
|
+
const isColumn = orientation === 'column';
|
|
21
|
+
|
|
22
|
+
// Rows wrap: the empty state only gets half the grid above 1054px.
|
|
23
|
+
return (
|
|
24
|
+
<Stack
|
|
25
|
+
direction={ isColumn ? 'column' : 'row' }
|
|
26
|
+
align="center"
|
|
27
|
+
justify="center"
|
|
28
|
+
gap={ gap }
|
|
29
|
+
wrap={ isColumn ? undefined : 'wrap' }
|
|
30
|
+
className={ classnames( 'newspack-empty-state__actions', className ) }
|
|
31
|
+
>
|
|
32
|
+
{ children }
|
|
33
|
+
</Stack>
|
|
34
|
+
);
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export default Actions;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WordPress dependencies.
|
|
3
|
+
*/
|
|
4
|
+
import { createContext, useContext } from '@wordpress/element';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Internal dependencies.
|
|
8
|
+
*/
|
|
9
|
+
import type { EmptyStateSize } from './types';
|
|
10
|
+
|
|
11
|
+
type EmptyStateContextValue = {
|
|
12
|
+
size: EmptyStateSize;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export const EmptyStateContext = createContext< EmptyStateContextValue | null >( null );
|
|
16
|
+
|
|
17
|
+
const FALLBACK_CONTEXT: EmptyStateContextValue = { size: 'default' };
|
|
18
|
+
|
|
19
|
+
export const useEmptyStateContext = (): EmptyStateContextValue => {
|
|
20
|
+
const context = useContext( EmptyStateContext );
|
|
21
|
+
if ( ! context && process.env.NODE_ENV !== 'production' ) {
|
|
22
|
+
throw new Error( 'EmptyState subcomponents must be rendered inside EmptyState.Root.' );
|
|
23
|
+
}
|
|
24
|
+
return context ?? FALLBACK_CONTEXT;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Assert placement inside a `Root` without depending on the value.
|
|
29
|
+
*
|
|
30
|
+
* For subcomponents that read nothing from context. Both hooks throw in
|
|
31
|
+
* development so the mistake surfaces while it is cheap, and neither throws in
|
|
32
|
+
* production, because a stray subcomponent should not blank an admin screen
|
|
33
|
+
* over a layout hint.
|
|
34
|
+
*/
|
|
35
|
+
export const useEmptyStateInvariant = (): void => {
|
|
36
|
+
const context = useContext( EmptyStateContext );
|
|
37
|
+
if ( ! context && process.env.NODE_ENV !== 'production' ) {
|
|
38
|
+
throw new Error( 'EmptyState subcomponents must be rendered inside EmptyState.Root.' );
|
|
39
|
+
}
|
|
40
|
+
};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WordPress dependencies.
|
|
3
|
+
*/
|
|
4
|
+
import { Icon } from '@wordpress/icons';
|
|
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 { useEmptyStateContext } from './context';
|
|
16
|
+
import type { EmptyStateHeaderProps } from './types';
|
|
17
|
+
|
|
18
|
+
const Header = ( { icon, title, description, heading, className }: EmptyStateHeaderProps ) => {
|
|
19
|
+
const { size } = useEmptyStateContext();
|
|
20
|
+
const isSmall = size === 'small';
|
|
21
|
+
|
|
22
|
+
// Heading level is a document-outline concern, so the size only sets a default.
|
|
23
|
+
const level = heading ?? ( isSmall ? 3 : 2 );
|
|
24
|
+
const HeadingTag = `h${ level }` as keyof JSX.IntrinsicElements;
|
|
25
|
+
|
|
26
|
+
// Two stacks so each gap belongs to exactly one of them: the inner one spaces the
|
|
27
|
+
// icon from the title, the outer one the title from the description.
|
|
28
|
+
return (
|
|
29
|
+
<Stack
|
|
30
|
+
direction="column"
|
|
31
|
+
align="center"
|
|
32
|
+
gap="sm"
|
|
33
|
+
className={ classnames( 'newspack-empty-state__header', isSmall && 'newspack-empty-state__header--small', className ) }
|
|
34
|
+
>
|
|
35
|
+
<Stack direction="column" align="center" gap={ isSmall ? 'md' : 'lg' }>
|
|
36
|
+
{ icon && (
|
|
37
|
+
<div className="newspack-empty-state__icon">
|
|
38
|
+
<Icon icon={ icon } size={ isSmall ? 24 : 48 } />
|
|
39
|
+
</div>
|
|
40
|
+
) }
|
|
41
|
+
<HeadingTag className="newspack-empty-state__title">{ title }</HeadingTag>
|
|
42
|
+
</Stack>
|
|
43
|
+
{ description && <p className="newspack-empty-state__description">{ description }</p> }
|
|
44
|
+
</Stack>
|
|
45
|
+
);
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export default Header;
|