@automattic/launchpad 1.0.0 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (145) hide show
  1. package/.storybook/main.ts +16 -0
  2. package/.storybook/preview.ts +6 -0
  3. package/README.md +11 -2
  4. package/dist/cjs/action-components/index.js +6 -0
  5. package/dist/cjs/action-components/index.js.map +1 -0
  6. package/dist/cjs/action-components/share-site-modal/index.js +44 -0
  7. package/dist/cjs/action-components/share-site-modal/index.js.map +1 -0
  8. package/dist/cjs/action-components/share-site-modal/style.scss +159 -0
  9. package/dist/cjs/badge/index.js +2 -2
  10. package/dist/cjs/badge/index.js.map +1 -1
  11. package/dist/cjs/checklist/index.js +16 -8
  12. package/dist/cjs/checklist/index.js.map +1 -1
  13. package/dist/cjs/checklist/index.stories.js +29 -0
  14. package/dist/cjs/checklist/index.stories.js.map +1 -0
  15. package/dist/cjs/checklist-item/index.js +30 -13
  16. package/dist/cjs/checklist-item/index.js.map +1 -1
  17. package/dist/cjs/checklist-item/index.stories.js +79 -0
  18. package/dist/cjs/checklist-item/index.stories.js.map +1 -0
  19. package/dist/cjs/checklist-item/style.scss +142 -22
  20. package/dist/cjs/index.js +6 -1
  21. package/dist/cjs/index.js.map +1 -1
  22. package/dist/cjs/launchpad-container/index.js +11 -0
  23. package/dist/cjs/launchpad-container/index.js.map +1 -0
  24. package/dist/cjs/launchpad-container/style.scss +59 -0
  25. package/dist/cjs/launchpad-internal.js +40 -0
  26. package/dist/cjs/launchpad-internal.js.map +1 -0
  27. package/dist/cjs/launchpad-internal.stories.js +45 -0
  28. package/dist/cjs/launchpad-internal.stories.js.map +1 -0
  29. package/dist/cjs/launchpad.js +39 -10
  30. package/dist/cjs/launchpad.js.map +1 -1
  31. package/dist/cjs/msw/link-in-bio.js +68 -0
  32. package/dist/cjs/msw/link-in-bio.js.map +1 -0
  33. package/dist/cjs/msw/resolver.js +19 -0
  34. package/dist/cjs/msw/resolver.js.map +1 -0
  35. package/dist/cjs/msw/setup-free.js +88 -0
  36. package/dist/cjs/msw/setup-free.js.map +1 -0
  37. package/dist/cjs/setup-actions.js +51 -54
  38. package/dist/cjs/setup-actions.js.map +1 -1
  39. package/dist/cjs/use-tracking/index.js +72 -0
  40. package/dist/cjs/use-tracking/index.js.map +1 -0
  41. package/dist/esm/action-components/index.js +2 -0
  42. package/dist/esm/action-components/index.js.map +1 -0
  43. package/dist/esm/action-components/share-site-modal/index.js +42 -0
  44. package/dist/esm/action-components/share-site-modal/index.js.map +1 -0
  45. package/dist/esm/action-components/share-site-modal/style.scss +159 -0
  46. package/dist/esm/badge/index.js +2 -2
  47. package/dist/esm/badge/index.js.map +1 -1
  48. package/dist/esm/checklist/index.js +14 -8
  49. package/dist/esm/checklist/index.js.map +1 -1
  50. package/dist/esm/checklist/index.stories.js +25 -0
  51. package/dist/esm/checklist/index.stories.js.map +1 -0
  52. package/dist/esm/checklist-item/index.js +29 -14
  53. package/dist/esm/checklist-item/index.js.map +1 -1
  54. package/dist/esm/checklist-item/index.stories.js +75 -0
  55. package/dist/esm/checklist-item/index.stories.js.map +1 -0
  56. package/dist/esm/checklist-item/style.scss +142 -22
  57. package/dist/esm/index.js +3 -0
  58. package/dist/esm/index.js.map +1 -1
  59. package/dist/esm/launchpad-container/index.js +8 -0
  60. package/dist/esm/launchpad-container/index.js.map +1 -0
  61. package/dist/esm/launchpad-container/style.scss +59 -0
  62. package/dist/esm/launchpad-internal.js +37 -0
  63. package/dist/esm/launchpad-internal.js.map +1 -0
  64. package/dist/esm/launchpad-internal.stories.js +41 -0
  65. package/dist/esm/launchpad-internal.stories.js.map +1 -0
  66. package/dist/esm/launchpad.js +41 -13
  67. package/dist/esm/launchpad.js.map +1 -1
  68. package/dist/esm/msw/link-in-bio.js +66 -0
  69. package/dist/esm/msw/link-in-bio.js.map +1 -0
  70. package/dist/esm/msw/resolver.js +14 -0
  71. package/dist/esm/msw/resolver.js.map +1 -0
  72. package/dist/esm/msw/setup-free.js +86 -0
  73. package/dist/esm/msw/setup-free.js.map +1 -0
  74. package/dist/esm/setup-actions.js +51 -54
  75. package/dist/esm/setup-actions.js.map +1 -1
  76. package/dist/esm/use-tracking/index.js +68 -0
  77. package/dist/esm/use-tracking/index.js.map +1 -0
  78. package/dist/tsconfig-cjs.tsbuildinfo +1 -1
  79. package/dist/tsconfig.tsbuildinfo +1 -1
  80. package/dist/types/action-components/index.d.ts +2 -0
  81. package/dist/types/action-components/index.d.ts.map +1 -0
  82. package/dist/types/action-components/share-site-modal/index.d.ts +9 -0
  83. package/dist/types/action-components/share-site-modal/index.d.ts.map +1 -0
  84. package/dist/types/badge/index.d.ts.map +1 -1
  85. package/dist/types/checklist/index.d.ts +6 -7
  86. package/dist/types/checklist/index.d.ts.map +1 -1
  87. package/dist/types/checklist/index.stories.d.ts +8 -0
  88. package/dist/types/checklist/index.stories.d.ts.map +1 -0
  89. package/dist/types/checklist-item/index.d.ts +11 -8
  90. package/dist/types/checklist-item/index.d.ts.map +1 -1
  91. package/dist/types/checklist-item/index.stories.d.ts +14 -0
  92. package/dist/types/checklist-item/index.stories.d.ts.map +1 -0
  93. package/dist/types/index.d.ts +3 -0
  94. package/dist/types/index.d.ts.map +1 -1
  95. package/dist/types/launchpad-container/index.d.ts +15 -0
  96. package/dist/types/launchpad-container/index.d.ts.map +1 -0
  97. package/dist/types/launchpad-internal.d.ts +20 -0
  98. package/dist/types/launchpad-internal.d.ts.map +1 -0
  99. package/dist/types/launchpad-internal.stories.d.ts +8 -0
  100. package/dist/types/launchpad-internal.stories.d.ts.map +1 -0
  101. package/dist/types/launchpad.d.ts +10 -9
  102. package/dist/types/launchpad.d.ts.map +1 -1
  103. package/dist/types/msw/link-in-bio.d.ts +35 -0
  104. package/dist/types/msw/link-in-bio.d.ts.map +1 -0
  105. package/dist/types/msw/resolver.d.ts +3 -0
  106. package/dist/types/msw/resolver.d.ts.map +1 -0
  107. package/dist/types/msw/setup-free.d.ts +35 -0
  108. package/dist/types/msw/setup-free.d.ts.map +1 -0
  109. package/dist/types/setup-actions.d.ts +1 -1
  110. package/dist/types/setup-actions.d.ts.map +1 -1
  111. package/dist/types/types.d.ts +19 -9
  112. package/dist/types/types.d.ts.map +1 -1
  113. package/dist/types/use-tracking/index.d.ts +14 -0
  114. package/dist/types/use-tracking/index.d.ts.map +1 -0
  115. package/package.json +24 -16
  116. package/src/action-components/index.ts +1 -0
  117. package/src/action-components/share-site-modal/index.tsx +100 -0
  118. package/src/action-components/share-site-modal/style.scss +159 -0
  119. package/src/badge/index.tsx +2 -4
  120. package/src/checklist/index.stories.tsx +45 -0
  121. package/src/checklist/index.tsx +22 -21
  122. package/src/checklist/test/index.tsx +26 -0
  123. package/src/checklist-item/index.stories.tsx +92 -0
  124. package/src/checklist-item/index.tsx +62 -22
  125. package/src/checklist-item/style.scss +142 -22
  126. package/src/checklist-item/test/index.tsx +232 -0
  127. package/src/index.ts +3 -0
  128. package/src/launchpad-container/index.tsx +45 -0
  129. package/src/launchpad-container/style.scss +59 -0
  130. package/src/launchpad-container/test/index.tsx +72 -0
  131. package/src/launchpad-internal.stories.tsx +49 -0
  132. package/src/launchpad-internal.tsx +85 -0
  133. package/src/launchpad.tsx +80 -30
  134. package/src/msw/link-in-bio.ts +66 -0
  135. package/src/msw/resolver.ts +18 -0
  136. package/src/msw/setup-free.ts +85 -0
  137. package/src/setup-actions.ts +67 -54
  138. package/src/test/launchpad.tsx +51 -24
  139. package/src/test/lib/fixtures.ts +7 -2
  140. package/src/types.ts +24 -9
  141. package/src/use-tracking/index.tsx +87 -0
  142. package/src/use-tracking/test/index.tsx +116 -0
  143. package/static/mockServiceWorker.js +287 -0
  144. package/src/test/checklist-item.tsx +0 -96
  145. package/src/test/checklist.tsx +0 -25
@@ -2,8 +2,8 @@
2
2
  @import "@wordpress/base-styles/breakpoints";
3
3
  @import "@automattic/typography/styles/variables";
4
4
 
5
- .checklist-item__task-content {
6
- align-items: flex-start;
5
+ .checklist-item__task-content.components-button {
6
+ align-items: center;
7
7
  background-color: transparent;
8
8
  border-bottom: 1px solid var(--studio-gray-5);
9
9
  border-left: none;
@@ -15,10 +15,51 @@
15
15
  width: 100%;
16
16
  margin: 0;
17
17
  color: var(--color-neutral-100);
18
+ height: auto;
19
+ font-family: $sans;
20
+ font-size: $font-body-small;
21
+ line-height: 22px;
22
+ outline: 0;
23
+ overflow: hidden;
24
+ text-decoration: none;
25
+ padding-left: 2px;
26
+
27
+ &:is(button, a) {
28
+ cursor: pointer;
29
+ }
30
+
31
+ &:is(div) {
32
+ cursor: default;
33
+ }
34
+
35
+ &:visited {
36
+ color: var(--color-neutral-100);
37
+ }
38
+
39
+ &:disabled {
40
+ opacity: 1;
41
+ color: var(--color-neutral-20);
42
+ }
18
43
 
19
44
  &.is-placeholder {
20
45
  padding: 16px 0 16px 0;
21
46
  }
47
+
48
+ &:focus-visible {
49
+ outline: var(--color-primary);
50
+ box-shadow: 0 0 0 2px var(--color-primary-light);
51
+ }
52
+
53
+ .badge {
54
+ margin-left: auto;
55
+ background-color: var(--studio-blue-5);
56
+ color: var(--studio-blue-80);
57
+ font-size: $font-body-extra-small;
58
+ }
59
+
60
+ .checklist-item__chevron {
61
+ width: auto;
62
+ }
22
63
  }
23
64
 
24
65
  .checklist-item__content {
@@ -38,27 +79,28 @@
38
79
  }
39
80
  }
40
81
 
41
- .button.checklist-item__task-content:hover:not([disabled]),
42
- .button.checklist-item__task-content:focus:not([disabled]) {
43
- fill: var(--studio-blue-50);
44
- color: var(--studio-blue-50);
45
- border-bottom: 1px solid var(--studio-gray-5);
82
+ .checklist-item__task-content {
83
+ &.is-placeholder {
84
+ padding: 5px 0 5px 0;
85
+ }
46
86
  }
47
87
 
48
- .checklist-item__task-content .badge {
49
- margin-left: auto;
50
- background-color: var(--studio-blue-5);
51
- color: var(--studio-blue-80);
52
- font-size: $font-body-extra-small;
88
+ .checklist-item__task-content.components-button:hover:not([disabled]),
89
+ .checklist-item__task-content.components-button:focus:not([disabled]) {
90
+ &:is(button, a) {
91
+ fill: var(--studio-blue-50);
92
+ color: var(--studio-blue-50);
93
+ }
94
+ border-bottom: 1px solid var(--studio-gray-5);
53
95
  }
54
96
 
55
- .checklist-item__task-content[disabled],
56
- .checklist-item__task-content[disabled]:focus {
97
+ .checklist-item__task-content.components-button[disabled],
98
+ .checklist-item__task-content.components-button[disabled]:focus {
57
99
  background-color: transparent;
58
100
  pointer-events: none;
59
101
  }
60
102
 
61
- .checklist-item__task-content[data-task="verify_email"] {
103
+ .checklist-item__task-content.components-button[data-task="verify_email"] {
62
104
  color: var(--color-text);
63
105
  }
64
106
 
@@ -68,11 +110,13 @@
68
110
  }
69
111
 
70
112
  .checklist-item__checkmark-container {
71
- margin-right: 8px;
113
+ margin-right: 7px;
72
114
  width: 20px;
115
+ display: flex;
116
+ margin-left: 1px;
117
+ margin-bottom: -2px;
73
118
 
74
119
  .checklist-item__checkmark {
75
- top: 2px;
76
120
  vertical-align: text-bottom;
77
121
  }
78
122
  }
@@ -103,7 +147,7 @@
103
147
  }
104
148
  }
105
149
 
106
- .checklist-item__task-content:hover .checklist-item__subtext {
150
+ .checklist-item__task-content.components-button:hover .checklist-item__subtext {
107
151
  color: var(--color-neutral-100);
108
152
  }
109
153
 
@@ -125,6 +169,18 @@
125
169
  }
126
170
  }
127
171
 
172
+ .checklist-item__task.expanded {
173
+ .checklist-item__task-content,
174
+ .checklist-item__task-content:hover,
175
+ .checklist-item__task-content:focus {
176
+ border-bottom: none;
177
+ }
178
+
179
+ .checklist-item__task-content {
180
+ padding-bottom: 0;
181
+ }
182
+ }
183
+
128
184
  // completed tasks (both enabled and disabled)
129
185
  .checklist-item__task.completed.disabled,
130
186
  .checklist-item__task.completed.enabled {
@@ -145,8 +201,10 @@
145
201
  }
146
202
 
147
203
  // pending tasks and completed enabled
148
- .checklist-item__task.completed.enabled:hover,
149
- .checklist-item__task.pending:hover,
204
+ .checklist-item__task.completed.enabled > button:hover,
205
+ .checklist-item__task.pending > button:hover,
206
+ .checklist-item__task.completed.enabled > a:hover,
207
+ .checklist-item__task.pending > a:hover,
150
208
  .checklist-item__task.completed.enabled > a:focus,
151
209
  .checklist-item__task.pending > a:focus {
152
210
  .checklist-item__text {
@@ -171,7 +229,7 @@
171
229
  // pending disabled tasks
172
230
  .checklist-item__task.pending.disabled {
173
231
  .checklist-item__text {
174
- color: inherit;
232
+ color: var(--color-neutral-20);
175
233
  }
176
234
  }
177
235
 
@@ -191,14 +249,20 @@
191
249
  font-weight: 600;
192
250
  }
193
251
 
194
- .checklist-item__checklist-primary-button {
252
+ .checklist-item__checklist-expanded-action-button.components-button,
253
+ .checklist-item__checklist-primary-button.components-button {
195
254
  width: 100%;
196
255
  font-weight: 500;
197
256
  padding: 11px;
198
257
  margin-top: 12px;
199
258
  background-color: var(--color-primary);
200
259
  border-color: var(--color-primary);
260
+ border-radius: 4px;
201
261
  color: var(--color-text-inverted);
262
+ justify-content: center;
263
+ font-size: $font-body-small;
264
+ height: auto;
265
+ line-height: 22px;
202
266
 
203
267
  &:hover:not([disabled]) {
204
268
  background-color: var(--color-primary-60);
@@ -211,4 +275,60 @@
211
275
  border-color: var(--color-primary-10);
212
276
  color: var(--color-text-inverted);
213
277
  }
278
+
279
+ &:disabled {
280
+ opacity: 1;
281
+ &:hover {
282
+ color: var(--color-text-inverted);
283
+ }
284
+ }
285
+ }
286
+
287
+ .checklist-item__checklist-expanded-action-button.components-button {
288
+ display: block;
289
+ margin: 16px 0;
290
+ }
291
+
292
+ .checklist-item__expanded-content {
293
+ padding-left: 2px;
294
+ font-family: $sans;
295
+ font-size: $font-body-small;
296
+
297
+ p {
298
+ margin: 12px 0;
299
+ }
300
+ }
301
+
302
+ .checklist-item__checklist-secondary-button.components-button {
303
+ width: 100%;
304
+ font-weight: 500;
305
+ padding: 11px;
306
+ margin-top: 12px;
307
+ background-color: var(--studio-white);
308
+ border: 1px solid var(--studio-gray-100);
309
+ border-radius: 4px;
310
+ color: var(--color-text);
311
+ justify-content: center;
312
+ font-size: $font-body-small;
313
+ height: auto;
314
+ line-height: 22px;
315
+
316
+ &:hover:not([disabled]) {
317
+ background-color: var(--studio-gray-0);
318
+ border-color: var(--color-neutral-40);
319
+ color: var(--color-text);
320
+ }
321
+
322
+ &[disabled] {
323
+ background-color: var(--studio-gray-0);
324
+ border: none;
325
+ color: var(--color-neutral-40);
326
+ }
327
+
328
+ &:disabled {
329
+ opacity: 1;
330
+ &:hover {
331
+ color: var(--color-neutral-40);
332
+ }
333
+ }
214
334
  }
@@ -0,0 +1,232 @@
1
+ import { render, screen } from '@testing-library/react';
2
+ import userEvent from '@testing-library/user-event';
3
+ import React, { ComponentProps } from 'react';
4
+ import ChecklistItem from '..';
5
+ import { buildTask } from '../../test/lib/fixtures';
6
+ import '@testing-library/jest-dom';
7
+
8
+ describe( 'ChecklistItem', () => {
9
+ const defaultProps = {
10
+ task: buildTask( {
11
+ completed: false,
12
+ disabled: false,
13
+ actionDispatch: jest.fn(),
14
+ } ),
15
+ };
16
+
17
+ const renderComponent = ( props: Partial< ComponentProps< typeof ChecklistItem > > ) =>
18
+ render( <ChecklistItem { ...defaultProps } { ...props } /> );
19
+
20
+ it( 'displays a badge', () => {
21
+ const badge_text = 'Badge Text';
22
+ renderComponent( { task: buildTask( { badge_text } ) } );
23
+
24
+ expect( screen.getByText( badge_text ) ).toBeVisible();
25
+ } );
26
+
27
+ it( 'hides the task complete icon when the task is not completed', () => {
28
+ renderComponent( { task: buildTask( { completed: false } ) } );
29
+ const taskCompleteIcon = screen.queryByLabelText( 'Task complete' );
30
+
31
+ expect( taskCompleteIcon ).not.toBeInTheDocument();
32
+ } );
33
+
34
+ it( 'renders a button when it has the onClick event', () => {
35
+ const onClick = jest.fn();
36
+ renderComponent( { onClick } );
37
+
38
+ const taskButton = screen.getByRole( 'button' );
39
+
40
+ expect( taskButton ).toBeInTheDocument();
41
+ } );
42
+
43
+ it( 'calls onClick when the task is clicked', async () => {
44
+ const onClick = jest.fn();
45
+ renderComponent( { onClick } );
46
+
47
+ const taskButton = screen.getByRole( 'button' );
48
+ await userEvent.click( taskButton );
49
+
50
+ expect( onClick ).toHaveBeenCalledTimes( 1 );
51
+ } );
52
+
53
+ it( 'uses the task action dispatch when the task is clicked and no custom onClick prop is used', async () => {
54
+ const actionDispatch = jest.fn();
55
+ renderComponent( {
56
+ task: buildTask( { actionDispatch, completed: false, disabled: false } ),
57
+ } );
58
+
59
+ const taskButton = screen.getByRole( 'button' );
60
+ await userEvent.click( taskButton );
61
+
62
+ expect( actionDispatch ).toHaveBeenCalledTimes( 1 );
63
+ } );
64
+
65
+ it( 'calls the onclick when calypso path is available', async () => {
66
+ const onClick = jest.fn();
67
+ renderComponent( {
68
+ onClick,
69
+ isPrimaryAction: true,
70
+ task: buildTask( {
71
+ useCalypsoPath: true,
72
+ disabled: false,
73
+ calypso_path: 'https://wordpress.com',
74
+ } ),
75
+ } );
76
+
77
+ const taskButton = screen.getByRole( 'link' );
78
+ await userEvent.click( taskButton );
79
+
80
+ expect( onClick ).toHaveBeenCalledTimes( 1 );
81
+ } );
82
+
83
+ it( 'calls the task action dispatch when calypso path is available', async () => {
84
+ const actionDispatch = jest.fn();
85
+ renderComponent( {
86
+ isPrimaryAction: true,
87
+ task: buildTask( {
88
+ useCalypsoPath: true,
89
+ disabled: false,
90
+ calypso_path: 'https://wordpress.com',
91
+ actionDispatch,
92
+ } ),
93
+ } );
94
+
95
+ const taskButton = screen.getByRole( 'link' );
96
+ await userEvent.click( taskButton );
97
+
98
+ expect( actionDispatch ).toHaveBeenCalledTimes( 1 );
99
+ } );
100
+
101
+ it( 'renders a span when it does not have the click or a link', () => {
102
+ const title = 'Task';
103
+ const { container } = renderComponent( { task: buildTask( { title } ) } );
104
+
105
+ const taskButton = container.querySelector( '.components-button' )!;
106
+
107
+ expect( taskButton.tagName ).toEqual( 'DIV' );
108
+ } );
109
+
110
+ describe( 'when the task is disabled', () => {
111
+ it( 'disables the button', () => {
112
+ renderComponent( { task: buildTask( { disabled: true } ), onClick: () => {} } );
113
+ const taskButton = screen.queryByRole( 'button' );
114
+
115
+ expect( taskButton ).toBeDisabled();
116
+ } );
117
+
118
+ it( 'hides the task enabled icon', () => {
119
+ renderComponent( { task: buildTask( { disabled: true } ) } );
120
+ const taskEnabledIcon = screen.queryByLabelText( 'Task enabled' );
121
+
122
+ expect( taskEnabledIcon ).not.toBeInTheDocument();
123
+ } );
124
+ } );
125
+
126
+ describe( 'when the task is completed', () => {
127
+ it( 'shows the task completed icon', () => {
128
+ renderComponent( { task: buildTask( { completed: true } ) } );
129
+ const taskCompleteIcon = screen.queryByLabelText( 'Task complete' );
130
+
131
+ expect( taskCompleteIcon ).toBeVisible();
132
+ } );
133
+
134
+ it( 'hides the task enabled icon', () => {
135
+ renderComponent( { task: buildTask( { completed: true } ) } );
136
+ const taskEnabledIcon = screen.queryByLabelText( 'Task enabled' );
137
+ expect( taskEnabledIcon ).not.toBeInTheDocument();
138
+ } );
139
+
140
+ it( 'disables the task', () => {
141
+ renderComponent( { task: buildTask( { completed: true } ), onClick: () => {} } );
142
+ const taskButton = screen.queryByRole( 'button' );
143
+
144
+ expect( taskButton ).toBeDisabled();
145
+ } );
146
+
147
+ describe( 'and the task is kept enabled', () => {
148
+ it( 'hides the task enabled icon', () => {
149
+ renderComponent( { task: buildTask( { completed: true, disabled: false } ) } );
150
+ const taskEnabledIcon = screen.queryByLabelText( 'Task enabled' );
151
+
152
+ expect( taskEnabledIcon ).not.toBeInTheDocument();
153
+ } );
154
+
155
+ it( 'enables the task', () => {
156
+ renderComponent( {
157
+ task: buildTask( { completed: true, disabled: false } ),
158
+ onClick: () => {},
159
+ } );
160
+ const taskButton = screen.queryByRole( 'button' );
161
+
162
+ expect( taskButton ).toBeEnabled();
163
+ } );
164
+ } );
165
+ } );
166
+
167
+ describe( 'when a task is a primary action', () => {
168
+ it( 'displays a primary button', () => {
169
+ renderComponent( { isPrimaryAction: true } );
170
+ const taskButton = screen.queryByRole( 'button' );
171
+
172
+ expect( taskButton?.className ).toContain( 'checklist-item__checklist-primary-button' );
173
+ } );
174
+ } );
175
+
176
+ describe( 'when it has expandable content', () => {
177
+ describe( 'and when it is open', () => {
178
+ it( 'displays the content', () => {
179
+ renderComponent( { expandable: { isOpen: true, content: <div>Expanded</div> } } );
180
+
181
+ expect( screen.getByText( 'Expanded' ) ).toBeVisible();
182
+ } );
183
+
184
+ it( 'calls the action click', async () => {
185
+ const onClick = jest.fn();
186
+ renderComponent( {
187
+ expandable: {
188
+ isOpen: true,
189
+ content: <div>Expanded</div>,
190
+ action: {
191
+ label: 'Action',
192
+ onClick,
193
+ },
194
+ },
195
+ } );
196
+
197
+ const actionButton = screen.queryByRole( 'button', { name: 'Action' } )!;
198
+ await userEvent.click( actionButton );
199
+
200
+ expect( onClick ).toHaveBeenCalled();
201
+ } );
202
+
203
+ it( 'has the expanded class', () => {
204
+ const { container } = renderComponent( {
205
+ expandable: { isOpen: true, content: <div>Expanded</div> },
206
+ } );
207
+
208
+ expect(
209
+ container.querySelector( '.checklist-item__task' )?.classList.contains( 'expanded' )
210
+ ).toBeTruthy();
211
+ } );
212
+ } );
213
+
214
+ describe( 'and when it is not open', () => {
215
+ it( 'does not display the content', () => {
216
+ renderComponent( { expandable: { isOpen: false, content: <div>Expanded</div> } } );
217
+
218
+ expect( screen.queryByText( 'Expanded' ) ).not.toBeInTheDocument();
219
+ } );
220
+
221
+ it( 'does not have the expanded class', () => {
222
+ const { container } = renderComponent( {
223
+ expandable: { isOpen: false, content: <div>Expanded</div> },
224
+ } );
225
+
226
+ expect(
227
+ container.querySelector( '.checklist-item__task' )?.classList.contains( 'expanded' )
228
+ ).toBeFalsy();
229
+ } );
230
+ } );
231
+ } );
232
+ } );
package/src/index.ts CHANGED
@@ -1,5 +1,8 @@
1
1
  export { default as Checklist } from './checklist';
2
2
  export { default as ChecklistItem } from './checklist-item';
3
+ export { default as LaunchpadInternal } from './launchpad-internal';
4
+ export { default as LaunchpadContainer } from './launchpad-container';
3
5
  export { default as Launchpad } from './launchpad';
4
6
  export { setUpActionsForTasks } from './setup-actions';
7
+ export * from './action-components';
5
8
  export * from './types';
@@ -0,0 +1,45 @@
1
+ import clsx from 'clsx';
2
+ import React from 'react';
3
+
4
+ import './style.scss';
5
+
6
+ export interface ContainerProps {
7
+ className?: string;
8
+ headerClassName?: string;
9
+ contentClassName?: string;
10
+ sidebarClassName?: string;
11
+ mainContentClassName?: string;
12
+ header?: React.ReactNode;
13
+ sidebar: React.ReactNode;
14
+ children: React.ReactNode;
15
+ }
16
+
17
+ const LaunchpadContainer: React.FunctionComponent< ContainerProps > = ( {
18
+ className,
19
+ headerClassName,
20
+ contentClassName,
21
+ sidebarClassName,
22
+ mainContentClassName,
23
+ header,
24
+ sidebar,
25
+ children,
26
+ } ) => {
27
+ return (
28
+ <main className={ clsx( 'launchpad-container', className ) }>
29
+ { header && (
30
+ <div className={ clsx( 'launchpad-container__header', headerClassName ) }>{ header }</div>
31
+ ) }
32
+
33
+ <div className={ clsx( 'launchpad-container__content', contentClassName ) }>
34
+ <div className={ clsx( 'launchpad-container__sidebar', sidebarClassName ) }>
35
+ { sidebar }
36
+ </div>
37
+ <div className={ clsx( 'launchpad-container__main-content', mainContentClassName ) }>
38
+ { children }
39
+ </div>
40
+ </div>
41
+ </main>
42
+ );
43
+ };
44
+
45
+ export default LaunchpadContainer;
@@ -0,0 +1,59 @@
1
+ @import "@wordpress/base-styles/mixins";
2
+ @import "@wordpress/base-styles/breakpoints";
3
+
4
+ .launchpad-container {
5
+ height: 100%;
6
+ display: flex;
7
+ flex-direction: column;
8
+ }
9
+
10
+ // Launchpad Content.
11
+ .launchpad-container__content {
12
+ display: flex;
13
+ flex-direction: column;
14
+
15
+ @include break-large {
16
+ height: 100%;
17
+ min-height: calc(100vh - 60px); // Discount margin-top from `.step-route`.
18
+ flex-direction: row;
19
+ }
20
+ }
21
+
22
+ // Launchpad Sidebar.
23
+ .launchpad-container__sidebar {
24
+ display: flex;
25
+ flex-direction: column;
26
+ max-width: 100%;
27
+
28
+ @include break-large {
29
+ max-width: 360px;
30
+ }
31
+
32
+ // TODO: Migrate to the checklist components. The checklist could receive a special className as prop that would be responsible for this.
33
+ .launchpad-container .checklist__tasks .checklist-item__task-content.is-placeholder .checklist-item__content {
34
+ /* Override default placeholder colours to align with other elements on the page. */
35
+ background-color: var(--color-neutral-5);
36
+ }
37
+ }
38
+
39
+ // Main content.
40
+ .launchpad-container__main-content {
41
+ flex-grow: 1;
42
+ display: flex;
43
+ flex-direction: column;
44
+ }
45
+
46
+ // Container spacings.
47
+ .launchpad-container__sidebar,
48
+ .launchpad-container__main-content {
49
+ padding: 16px;
50
+
51
+ @include break-large {
52
+ padding: 40px;
53
+
54
+ // Exception for the sidebar.
55
+ &.launchpad-container__sidebar {
56
+ padding-right: 32px;
57
+ }
58
+ }
59
+ }
@@ -0,0 +1,72 @@
1
+ import { render } from '@testing-library/react';
2
+ import React from 'react';
3
+ import LaunchpadContainer from '../';
4
+
5
+ describe( 'LaunchpadContainer', () => {
6
+ it( 'renders the container with sidebar and content', () => {
7
+ const { getByText } = render(
8
+ <LaunchpadContainer sidebar={ <div>Sidebar</div> }>My Content</LaunchpadContainer>
9
+ );
10
+
11
+ expect( getByText( 'Sidebar' ) ).toBeInTheDocument();
12
+ expect( getByText( 'My Content' ) ).toBeInTheDocument();
13
+ } );
14
+
15
+ it( 'renders the container with header', () => {
16
+ const { getByText } = render(
17
+ <LaunchpadContainer sidebar={ <div>Sidebar</div> } header={ <div>Header</div> }>
18
+ My Content
19
+ </LaunchpadContainer>
20
+ );
21
+
22
+ expect( getByText( 'Header' ) ).toBeInTheDocument();
23
+ } );
24
+
25
+ it( 'renders the container without header', () => {
26
+ const { container } = render(
27
+ <LaunchpadContainer sidebar={ <div>Sidebar</div> }>My Content</LaunchpadContainer>
28
+ );
29
+
30
+ expect( container.querySelector( '.launchpad-container__header' ) ).not.toBeInTheDocument();
31
+ } );
32
+
33
+ it( 'renders the container with custom classes', () => {
34
+ const { container } = render(
35
+ <LaunchpadContainer
36
+ className="classname"
37
+ headerClassName="header-classname"
38
+ contentClassName="content-classname"
39
+ sidebarClassName="sidebar-classname"
40
+ mainContentClassName="main-content-classname"
41
+ sidebar={ <div>Sidebar</div> }
42
+ header={ <div>Header</div> }
43
+ >
44
+ My Content
45
+ </LaunchpadContainer>
46
+ );
47
+
48
+ expect(
49
+ container.querySelector( '.launchpad-container' )?.classList.contains( 'classname' )
50
+ ).toBeTruthy();
51
+ expect(
52
+ container
53
+ .querySelector( '.launchpad-container__header' )
54
+ ?.classList.contains( 'header-classname' )
55
+ ).toBeTruthy();
56
+ expect(
57
+ container
58
+ .querySelector( '.launchpad-container__content' )
59
+ ?.classList.contains( 'content-classname' )
60
+ ).toBeTruthy();
61
+ expect(
62
+ container
63
+ .querySelector( '.launchpad-container__sidebar' )
64
+ ?.classList.contains( 'sidebar-classname' )
65
+ ).toBeTruthy();
66
+ expect(
67
+ container
68
+ .querySelector( '.launchpad-container__main-content' )
69
+ ?.classList.contains( 'main-content-classname' )
70
+ ).toBeTruthy();
71
+ } );
72
+ } );