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.
Files changed (251) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/DEVELOPMENT.md +42 -21
  3. package/dist/cjs/_mixins.scss +13 -0
  4. package/dist/cjs/_variables.scss +12 -0
  5. package/dist/cjs/action-card/index.js +18 -10
  6. package/dist/cjs/action-card/index.test.js +91 -0
  7. package/dist/cjs/action-card/style.scss +2 -40
  8. package/dist/cjs/button/index.js +8 -3
  9. package/dist/cjs/button/index.test.js +61 -5
  10. package/dist/cjs/card/core-card.js +0 -2
  11. package/dist/cjs/card/style-core.scss +7 -5
  12. package/dist/cjs/card-feature/index.js +75 -61
  13. package/dist/cjs/card-feature/index.test.js +396 -54
  14. package/dist/cjs/card-feature/style.scss +20 -31
  15. package/dist/cjs/card-form/index.js +5 -5
  16. package/dist/cjs/card-sortable-list/index.js +5 -3
  17. package/dist/cjs/card-sortable-list/index.test.js +50 -0
  18. package/dist/cjs/collapsible-group/context.js +19 -0
  19. package/dist/cjs/collapsible-group/index.js +24 -0
  20. package/dist/cjs/collapsible-group/index.test.js +215 -0
  21. package/dist/cjs/collapsible-group/item.js +61 -0
  22. package/dist/cjs/collapsible-group/root.js +62 -0
  23. package/dist/cjs/collapsible-group/style.scss +47 -0
  24. package/dist/cjs/collapsible-group/types.js +5 -0
  25. package/dist/cjs/empty-state/actions.js +45 -0
  26. package/dist/cjs/empty-state/context.js +41 -0
  27. package/dist/cjs/empty-state/header.js +66 -0
  28. package/dist/cjs/empty-state/index.js +31 -0
  29. package/dist/cjs/empty-state/index.test.js +303 -0
  30. package/dist/cjs/empty-state/root.js +56 -0
  31. package/dist/cjs/empty-state/style.scss +51 -0
  32. package/dist/cjs/empty-state/types.js +5 -0
  33. package/dist/cjs/index.js +34 -20
  34. package/dist/cjs/info-button/index.js +59 -43
  35. package/dist/cjs/info-button/index.test.js +199 -0
  36. package/dist/cjs/info-button/style.scss +41 -9
  37. package/dist/cjs/modal/style.scss +12 -12
  38. package/dist/cjs/newspack-icon/style.scss +4 -4
  39. package/dist/cjs/notice/style.scss +3 -3
  40. package/dist/cjs/page/style.scss +16 -16
  41. package/dist/cjs/plugin-installer/index.js +5 -9
  42. package/dist/cjs/plugin-installer/style.scss +3 -14
  43. package/dist/cjs/section-header/index.js +31 -18
  44. package/dist/cjs/section-header/index.test.js +40 -0
  45. package/dist/cjs/section-header/style.scss +22 -6
  46. package/dist/cjs/settings/DateRangeSetting.js +309 -0
  47. package/dist/cjs/settings/SettingsSection.js +19 -5
  48. package/dist/cjs/settings/SettingsSection.test.js +49 -0
  49. package/dist/cjs/settings/index.js +3 -1
  50. package/dist/cjs/settings/style.scss +40 -7
  51. package/dist/cjs/stat-card/body.js +41 -0
  52. package/dist/cjs/stat-card/constants.js +11 -0
  53. package/dist/cjs/stat-card/context.js +54 -0
  54. package/dist/cjs/stat-card/delta.js +91 -0
  55. package/dist/cjs/stat-card/footer.js +72 -0
  56. package/dist/cjs/stat-card/index.js +47 -0
  57. package/dist/cjs/stat-card/index.test.js +938 -0
  58. package/dist/cjs/stat-card/label.js +66 -0
  59. package/dist/cjs/stat-card/root.js +69 -0
  60. package/dist/cjs/stat-card/secondary.js +38 -0
  61. package/dist/cjs/stat-card/style.scss +89 -0
  62. package/dist/cjs/stat-card/types.js +5 -0
  63. package/dist/cjs/stat-card/value.js +78 -0
  64. package/dist/cjs/status-indicator/index.js +66 -0
  65. package/dist/cjs/status-indicator/index.test.js +101 -0
  66. package/dist/cjs/status-indicator/statuses.js +39 -0
  67. package/dist/cjs/status-indicator/style.scss +9 -0
  68. package/dist/cjs/status-indicator/types.js +5 -0
  69. package/dist/cjs/style.scss +9 -9
  70. package/dist/cjs/tabbed-navigation/style.scss +6 -4
  71. package/dist/cjs/types.js +5 -0
  72. package/dist/cjs/web-preview/style.scss +11 -11
  73. package/dist/cjs/with-wizard/style.scss +10 -10
  74. package/dist/cjs/with-wizard-screen/style.scss +9 -9
  75. package/dist/cjs/wizard/index.js +33 -4
  76. package/dist/cjs/wizard/store/index.js +23 -1
  77. package/dist/esm/_mixins.scss +13 -0
  78. package/dist/esm/_variables.scss +12 -0
  79. package/dist/esm/action-card/index.js +18 -10
  80. package/dist/esm/action-card/index.test.js +89 -0
  81. package/dist/esm/action-card/style.scss +2 -40
  82. package/dist/esm/button/index.js +9 -4
  83. package/dist/esm/button/index.test.js +63 -5
  84. package/dist/esm/card/core-card.js +0 -2
  85. package/dist/esm/card/style-core.scss +7 -5
  86. package/dist/esm/card-feature/index.js +78 -62
  87. package/dist/esm/card-feature/index.test.js +397 -55
  88. package/dist/esm/card-feature/style.scss +20 -31
  89. package/dist/esm/card-form/index.js +5 -5
  90. package/dist/esm/card-sortable-list/index.js +6 -4
  91. package/dist/esm/card-sortable-list/index.test.js +46 -0
  92. package/dist/esm/collapsible-group/context.js +13 -0
  93. package/dist/esm/collapsible-group/index.js +17 -0
  94. package/dist/esm/collapsible-group/index.test.js +215 -0
  95. package/dist/esm/collapsible-group/item.js +53 -0
  96. package/dist/esm/collapsible-group/root.js +54 -0
  97. package/dist/esm/collapsible-group/style.scss +47 -0
  98. package/dist/esm/collapsible-group/types.js +1 -0
  99. package/dist/esm/empty-state/actions.js +37 -0
  100. package/dist/esm/empty-state/context.js +35 -0
  101. package/dist/esm/empty-state/header.js +58 -0
  102. package/dist/esm/empty-state/index.js +23 -0
  103. package/dist/esm/empty-state/index.test.js +299 -0
  104. package/dist/esm/empty-state/root.js +56 -0
  105. package/dist/esm/empty-state/style.scss +51 -0
  106. package/dist/esm/empty-state/types.js +1 -0
  107. package/dist/esm/index.js +4 -2
  108. package/dist/esm/info-button/index.js +54 -43
  109. package/dist/esm/info-button/index.test.js +195 -0
  110. package/dist/esm/info-button/style.scss +41 -9
  111. package/dist/esm/modal/style.scss +12 -12
  112. package/dist/esm/newspack-icon/style.scss +4 -4
  113. package/dist/esm/notice/style.scss +3 -3
  114. package/dist/esm/page/style.scss +16 -16
  115. package/dist/esm/plugin-installer/index.js +5 -9
  116. package/dist/esm/plugin-installer/style.scss +3 -14
  117. package/dist/esm/section-header/index.js +27 -14
  118. package/dist/esm/section-header/index.test.js +36 -0
  119. package/dist/esm/section-header/style.scss +22 -6
  120. package/dist/esm/settings/DateRangeSetting.js +304 -0
  121. package/dist/esm/settings/SettingsSection.js +16 -3
  122. package/dist/esm/settings/SettingsSection.test.js +45 -0
  123. package/dist/esm/settings/index.js +3 -1
  124. package/dist/esm/settings/style.scss +40 -7
  125. package/dist/esm/stat-card/body.js +36 -0
  126. package/dist/esm/stat-card/constants.js +5 -0
  127. package/dist/esm/stat-card/context.js +48 -0
  128. package/dist/esm/stat-card/delta.js +86 -0
  129. package/dist/esm/stat-card/footer.js +67 -0
  130. package/dist/esm/stat-card/index.js +33 -0
  131. package/dist/esm/stat-card/index.test.js +939 -0
  132. package/dist/esm/stat-card/label.js +61 -0
  133. package/dist/esm/stat-card/root.js +64 -0
  134. package/dist/esm/stat-card/secondary.js +33 -0
  135. package/dist/esm/stat-card/style.scss +89 -0
  136. package/dist/esm/stat-card/types.js +1 -0
  137. package/dist/esm/stat-card/value.js +73 -0
  138. package/dist/esm/status-indicator/index.js +52 -0
  139. package/dist/esm/status-indicator/index.test.js +96 -0
  140. package/dist/esm/status-indicator/statuses.js +33 -0
  141. package/dist/esm/status-indicator/style.scss +9 -0
  142. package/dist/esm/status-indicator/types.js +1 -0
  143. package/dist/esm/style.scss +9 -9
  144. package/dist/esm/tabbed-navigation/style.scss +6 -4
  145. package/dist/esm/types.js +1 -0
  146. package/dist/esm/web-preview/style.scss +11 -11
  147. package/dist/esm/with-wizard/style.scss +10 -10
  148. package/dist/esm/with-wizard-screen/style.scss +9 -9
  149. package/dist/esm/wizard/index.js +38 -7
  150. package/dist/esm/wizard/store/index.js +23 -1
  151. package/package.json +1 -1
  152. package/src/_mixins.scss +13 -0
  153. package/src/_variables.scss +12 -0
  154. package/src/action-card/action-card.d.ts +3 -2
  155. package/src/action-card/index.js +12 -12
  156. package/src/action-card/index.test.js +56 -0
  157. package/src/action-card/style.scss +2 -40
  158. package/src/button/index.test.js +52 -5
  159. package/src/button/index.tsx +9 -4
  160. package/src/card/core-card.js +0 -2
  161. package/src/card/style-core.scss +7 -5
  162. package/src/card-feature/README.md +68 -27
  163. package/src/card-feature/index.test.js +242 -27
  164. package/src/card-feature/index.tsx +77 -67
  165. package/src/card-feature/style.scss +20 -31
  166. package/src/card-form/README.md +10 -10
  167. package/src/card-form/index.tsx +4 -8
  168. package/src/card-sortable-list/README.md +4 -5
  169. package/src/card-sortable-list/index.test.js +29 -0
  170. package/src/card-sortable-list/index.tsx +7 -4
  171. package/src/collapsible-group/README.md +50 -0
  172. package/src/collapsible-group/context.ts +13 -0
  173. package/src/collapsible-group/index.test.js +168 -0
  174. package/src/collapsible-group/index.tsx +17 -0
  175. package/src/collapsible-group/item.tsx +42 -0
  176. package/src/collapsible-group/root.tsx +42 -0
  177. package/src/collapsible-group/style.scss +47 -0
  178. package/src/collapsible-group/types.ts +22 -0
  179. package/src/drawer/README.md +2 -2
  180. package/src/empty-state/README.md +165 -0
  181. package/src/empty-state/actions.tsx +37 -0
  182. package/src/empty-state/context.ts +40 -0
  183. package/src/empty-state/header.tsx +48 -0
  184. package/src/empty-state/index.test.js +247 -0
  185. package/src/empty-state/index.tsx +22 -0
  186. package/src/empty-state/root.tsx +39 -0
  187. package/src/empty-state/style.scss +51 -0
  188. package/src/empty-state/types.ts +33 -0
  189. package/src/index.js +4 -2
  190. package/src/info-button/README.md +115 -0
  191. package/src/info-button/index.test.js +163 -0
  192. package/src/info-button/index.tsx +64 -0
  193. package/src/info-button/style.scss +41 -9
  194. package/src/modal/style.scss +12 -12
  195. package/src/newspack-icon/style.scss +4 -4
  196. package/src/notice/style.scss +3 -3
  197. package/src/page/style.scss +16 -16
  198. package/src/plugin-installer/index.js +3 -9
  199. package/src/plugin-installer/style.scss +3 -14
  200. package/src/section-header/index.js +27 -14
  201. package/src/section-header/index.test.js +28 -0
  202. package/src/section-header/style.scss +22 -6
  203. package/src/settings/DateRangeSetting.js +266 -0
  204. package/src/settings/SettingsSection.js +24 -4
  205. package/src/settings/SettingsSection.test.js +45 -0
  206. package/src/settings/index.js +2 -0
  207. package/src/settings/style.scss +40 -7
  208. package/src/stat-card/README.md +434 -0
  209. package/src/stat-card/body.tsx +28 -0
  210. package/src/stat-card/constants.ts +5 -0
  211. package/src/stat-card/context.ts +53 -0
  212. package/src/stat-card/delta.tsx +88 -0
  213. package/src/stat-card/footer.tsx +68 -0
  214. package/src/stat-card/index.test.js +818 -0
  215. package/src/stat-card/index.tsx +48 -0
  216. package/src/stat-card/label.tsx +61 -0
  217. package/src/stat-card/root.tsx +40 -0
  218. package/src/stat-card/secondary.tsx +27 -0
  219. package/src/stat-card/style.scss +89 -0
  220. package/src/stat-card/types.ts +84 -0
  221. package/src/stat-card/value.tsx +72 -0
  222. package/src/status-indicator/README.md +123 -0
  223. package/src/status-indicator/index.test.js +70 -0
  224. package/src/status-indicator/index.tsx +39 -0
  225. package/src/status-indicator/statuses.ts +35 -0
  226. package/src/status-indicator/style.scss +9 -0
  227. package/src/status-indicator/types.ts +28 -0
  228. package/src/style.scss +9 -9
  229. package/src/tabbed-navigation/style.scss +6 -4
  230. package/src/types.ts +25 -0
  231. package/src/web-preview/style.scss +11 -11
  232. package/src/with-wizard/style.scss +10 -10
  233. package/src/with-wizard-screen/style.scss +9 -9
  234. package/src/wizard/index.js +49 -7
  235. package/src/wizard/store/index.js +26 -1
  236. package/dist/cjs/accordion/index.js +0 -72
  237. package/dist/cjs/accordion/index.test.js +0 -80
  238. package/dist/cjs/accordion/style.scss +0 -35
  239. package/dist/cjs/badge/index.js +0 -32
  240. package/dist/cjs/badge/style.scss +0 -36
  241. package/dist/esm/accordion/index.js +0 -63
  242. package/dist/esm/accordion/index.test.js +0 -79
  243. package/dist/esm/accordion/style.scss +0 -35
  244. package/dist/esm/badge/index.js +0 -24
  245. package/dist/esm/badge/style.scss +0 -36
  246. package/src/accordion/index.js +0 -46
  247. package/src/accordion/index.test.js +0 -62
  248. package/src/accordion/style.scss +0 -35
  249. package/src/badge/index.tsx +0 -26
  250. package/src/badge/style.scss +0 -36
  251. 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.