newspack-components 4.3.0 → 4.4.0-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (263) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/DEVELOPMENT.md +868 -0
  3. package/colors/colors.module.scss +52 -2
  4. package/copy-styles.js +3 -2
  5. package/dist/cjs/accordion/index.js +17 -12
  6. package/dist/cjs/action-card/index.js +176 -139
  7. package/dist/cjs/autocomplete-tokenfield/index.js +254 -234
  8. package/dist/cjs/autocomplete-with-latest-posts/index.js +172 -110
  9. package/dist/cjs/autocomplete-with-suggestions/index.js +229 -126
  10. package/dist/cjs/badge/index.js +7 -7
  11. package/dist/cjs/box-contrast/index.js +19 -15
  12. package/dist/cjs/button/index.js +41 -26
  13. package/dist/cjs/button-props/button-props.js +5 -5
  14. package/dist/cjs/button-props/index.js +2 -2
  15. package/dist/cjs/card/core-card.js +110 -52
  16. package/dist/cjs/card/index.js +74 -67
  17. package/dist/cjs/card/style-core.scss +93 -25
  18. package/dist/cjs/card/style.scss +2 -2
  19. package/dist/cjs/card-feature/index.js +141 -0
  20. package/dist/cjs/card-feature/style.scss +75 -0
  21. package/dist/cjs/card-form/index.js +144 -0
  22. package/dist/cjs/card-form/style.scss +20 -0
  23. package/dist/cjs/card-settings-group/index.js +36 -16
  24. package/dist/cjs/card-sortable-list/index.js +176 -140
  25. package/dist/cjs/category-autocomplete/index.js +175 -155
  26. package/dist/cjs/color-picker/index.js +41 -23
  27. package/dist/cjs/color-picker/index.test.js +82 -0
  28. package/dist/cjs/color-picker/style.scss +11 -13
  29. package/dist/cjs/confirm-dialog/index.js +86 -22
  30. package/dist/cjs/consts.js +1 -1
  31. package/dist/cjs/custom-select-control/index.js +39 -28
  32. package/dist/cjs/dataviews/index.js +38 -0
  33. package/dist/cjs/dataviews/style.scss +9 -0
  34. package/dist/cjs/divider/index.js +26 -21
  35. package/dist/cjs/footer/index.js +36 -30
  36. package/dist/cjs/form-token-field/index.js +51 -40
  37. package/dist/cjs/global-notices/index.js +5 -5
  38. package/dist/cjs/grid/index.js +25 -19
  39. package/dist/cjs/grid/style.scss +6 -1
  40. package/dist/cjs/handoff/index.js +204 -156
  41. package/dist/cjs/handoff-message/index.js +12 -7
  42. package/dist/cjs/hooks/index.js +10 -8
  43. package/dist/cjs/hooks/use-confirm-dialog.js +79 -0
  44. package/dist/cjs/hooks/use-unsaved-changes-dialog.js +144 -0
  45. package/dist/cjs/hooks/useObjectState.js +21 -12
  46. package/dist/cjs/hooks/useObjectState.test.js +55 -33
  47. package/dist/cjs/hooks/useOnClickOutside.js +6 -6
  48. package/dist/cjs/hooks/usePrompt.js +10 -13
  49. package/dist/cjs/image-upload/index.js +114 -123
  50. package/dist/cjs/image-upload/index.test.js +11 -13
  51. package/dist/cjs/image-upload/style.scss +12 -40
  52. package/dist/cjs/index.js +136 -104
  53. package/dist/cjs/info-button/index.js +43 -32
  54. package/dist/cjs/integration-icons/active-campaign.js +22 -0
  55. package/dist/cjs/integration-icons/constant-contact.js +28 -0
  56. package/dist/cjs/integration-icons/fundraise-up.js +25 -0
  57. package/dist/cjs/integration-icons/index.js +48 -0
  58. package/dist/cjs/integration-icons/mailchimp.js +19 -0
  59. package/dist/cjs/integration-icons/salesforce.js +17 -0
  60. package/dist/cjs/integration-icons/wisepops.js +20 -0
  61. package/dist/cjs/modal/index.js +22 -19
  62. package/dist/cjs/modal/style.scss +4 -0
  63. package/dist/cjs/newspack-icon/index.js +45 -38
  64. package/dist/cjs/notice/index.js +64 -55
  65. package/dist/cjs/plugin-installer/index.js +259 -253
  66. package/dist/cjs/plugin-settings/SettingsSection.js +66 -60
  67. package/dist/cjs/plugin-settings/index.js +217 -200
  68. package/dist/cjs/plugin-toggle/index.js +192 -199
  69. package/dist/cjs/popover/index.js +41 -28
  70. package/dist/cjs/position-control/index.js +24 -23
  71. package/dist/cjs/progress-bar/index.js +71 -59
  72. package/dist/cjs/progress-bar/index.test.js +62 -68
  73. package/dist/cjs/proxied-imports/router.js +3 -3
  74. package/dist/cjs/radio-control/index.js +40 -27
  75. package/dist/cjs/section-header/index.js +103 -58
  76. package/dist/cjs/section-header/style.scss +31 -11
  77. package/dist/cjs/select-control/ButtonGroupControl.js +26 -21
  78. package/dist/cjs/select-control/GroupedSelectControl.js +41 -35
  79. package/dist/cjs/select-control/index.js +52 -40
  80. package/dist/cjs/settings/MinMaxSetting.js +30 -20
  81. package/dist/cjs/settings/SettingsCard.js +20 -17
  82. package/dist/cjs/settings/SettingsSection.js +22 -21
  83. package/dist/cjs/settings/index.js +5 -5
  84. package/dist/cjs/sortable-newsletter-list-control/index.js +78 -57
  85. package/dist/cjs/steps-list/index.js +44 -34
  86. package/dist/cjs/steps-list-item/index.js +47 -39
  87. package/dist/cjs/style-card/index.js +70 -63
  88. package/dist/cjs/tabbed-navigation/index.js +38 -36
  89. package/dist/cjs/text-control/index.js +27 -25
  90. package/dist/cjs/utils/color.js +4 -4
  91. package/dist/cjs/utils/editor-toolbar-back-button.js +61 -0
  92. package/dist/cjs/utils/index.js +31 -19
  93. package/dist/cjs/utils/style.scss +8 -0
  94. package/dist/cjs/waiting/index.js +48 -37
  95. package/dist/cjs/web-preview/index.js +217 -211
  96. package/dist/cjs/with-wizard/index.js +362 -364
  97. package/dist/cjs/with-wizard/style.scss +4 -54
  98. package/dist/cjs/with-wizard-screen/index.js +54 -58
  99. package/dist/cjs/with-wizard-screen/style.scss +41 -3
  100. package/dist/cjs/wizard/components/WizardError.js +20 -18
  101. package/dist/cjs/wizard/components/WizardSnackbar.js +26 -23
  102. package/dist/cjs/wizard/index.js +176 -114
  103. package/dist/cjs/wizard/store/index.js +171 -123
  104. package/dist/cjs/wizard/store/utils.js +13 -8
  105. package/dist/esm/accordion/index.js +14 -9
  106. package/dist/esm/action-card/index.js +168 -123
  107. package/dist/esm/autocomplete-tokenfield/index.js +248 -222
  108. package/dist/esm/autocomplete-with-latest-posts/index.js +169 -103
  109. package/dist/esm/autocomplete-with-suggestions/index.js +226 -119
  110. package/dist/esm/badge/index.js +5 -5
  111. package/dist/esm/box-contrast/index.js +15 -11
  112. package/dist/esm/button/index.js +39 -21
  113. package/dist/esm/button-props/button-props.js +1 -1
  114. package/dist/esm/card/core-card.js +109 -47
  115. package/dist/esm/card/index.js +69 -54
  116. package/dist/esm/card/style-core.scss +93 -25
  117. package/dist/esm/card/style.scss +2 -2
  118. package/dist/esm/card-feature/index.js +131 -0
  119. package/dist/esm/card-feature/style.scss +75 -0
  120. package/dist/esm/card-form/index.js +134 -0
  121. package/dist/esm/card-form/style.scss +20 -0
  122. package/dist/esm/card-settings-group/index.js +33 -14
  123. package/dist/esm/card-sortable-list/index.js +170 -126
  124. package/dist/esm/category-autocomplete/index.js +168 -140
  125. package/dist/esm/color-picker/index.js +39 -21
  126. package/dist/esm/color-picker/index.test.js +78 -0
  127. package/dist/esm/color-picker/style.scss +11 -13
  128. package/dist/esm/confirm-dialog/index.js +85 -18
  129. package/dist/esm/consts.js +1 -1
  130. package/dist/esm/custom-select-control/index.js +34 -15
  131. package/dist/esm/dataviews/index.js +34 -0
  132. package/dist/esm/dataviews/style.scss +9 -0
  133. package/dist/esm/divider/index.js +24 -16
  134. package/dist/esm/footer/index.js +34 -28
  135. package/dist/esm/form-token-field/index.js +46 -27
  136. package/dist/esm/global-notices/index.js +3 -3
  137. package/dist/esm/grid/index.js +23 -14
  138. package/dist/esm/grid/style.scss +6 -1
  139. package/dist/esm/handoff/index.js +199 -143
  140. package/dist/esm/handoff-message/index.js +10 -6
  141. package/dist/esm/hooks/index.js +8 -6
  142. package/dist/esm/hooks/use-confirm-dialog.js +73 -0
  143. package/dist/esm/hooks/use-unsaved-changes-dialog.js +136 -0
  144. package/dist/esm/hooks/useObjectState.js +19 -9
  145. package/dist/esm/hooks/useObjectState.test.js +54 -32
  146. package/dist/esm/hooks/useOnClickOutside.js +4 -4
  147. package/dist/esm/hooks/usePrompt.js +9 -11
  148. package/dist/esm/image-upload/index.js +110 -111
  149. package/dist/esm/image-upload/index.test.js +11 -13
  150. package/dist/esm/image-upload/style.scss +12 -40
  151. package/dist/esm/index.js +7 -1
  152. package/dist/esm/info-button/index.js +38 -19
  153. package/dist/esm/integration-icons/active-campaign.js +16 -0
  154. package/dist/esm/integration-icons/constant-contact.js +22 -0
  155. package/dist/esm/integration-icons/fundraise-up.js +19 -0
  156. package/dist/esm/integration-icons/index.js +6 -0
  157. package/dist/esm/integration-icons/mailchimp.js +13 -0
  158. package/dist/esm/integration-icons/salesforce.js +11 -0
  159. package/dist/esm/integration-icons/wisepops.js +14 -0
  160. package/dist/esm/modal/index.js +20 -13
  161. package/dist/esm/modal/style.scss +4 -0
  162. package/dist/esm/newspack-icon/index.js +40 -25
  163. package/dist/esm/notice/index.js +59 -42
  164. package/dist/esm/plugin-installer/index.js +255 -243
  165. package/dist/esm/plugin-settings/SettingsSection.js +61 -50
  166. package/dist/esm/plugin-settings/index.js +212 -189
  167. package/dist/esm/plugin-toggle/index.js +187 -190
  168. package/dist/esm/popover/index.js +35 -16
  169. package/dist/esm/position-control/index.js +22 -18
  170. package/dist/esm/progress-bar/index.js +66 -49
  171. package/dist/esm/progress-bar/index.test.js +61 -67
  172. package/dist/esm/radio-control/index.js +34 -15
  173. package/dist/esm/section-header/index.js +102 -57
  174. package/dist/esm/section-header/style.scss +31 -11
  175. package/dist/esm/select-control/ButtonGroupControl.js +23 -18
  176. package/dist/esm/select-control/GroupedSelectControl.js +38 -30
  177. package/dist/esm/select-control/index.js +47 -27
  178. package/dist/esm/settings/MinMaxSetting.js +27 -16
  179. package/dist/esm/settings/SettingsCard.js +18 -13
  180. package/dist/esm/settings/SettingsSection.js +20 -19
  181. package/dist/esm/settings/index.js +3 -3
  182. package/dist/esm/sortable-newsletter-list-control/index.js +74 -53
  183. package/dist/esm/steps-list/index.js +39 -21
  184. package/dist/esm/steps-list-item/index.js +42 -26
  185. package/dist/esm/style-card/index.js +65 -50
  186. package/dist/esm/tabbed-navigation/index.js +35 -33
  187. package/dist/esm/text-control/index.js +25 -19
  188. package/dist/esm/utils/color.js +4 -4
  189. package/dist/esm/utils/editor-toolbar-back-button.js +53 -0
  190. package/dist/esm/utils/index.js +29 -16
  191. package/dist/esm/utils/style.scss +8 -0
  192. package/dist/esm/waiting/index.js +43 -24
  193. package/dist/esm/web-preview/index.js +213 -201
  194. package/dist/esm/with-wizard/index.js +360 -358
  195. package/dist/esm/with-wizard/style.scss +4 -54
  196. package/dist/esm/with-wizard-screen/index.js +48 -46
  197. package/dist/esm/with-wizard-screen/style.scss +41 -3
  198. package/dist/esm/wizard/components/WizardError.js +18 -16
  199. package/dist/esm/wizard/components/WizardSnackbar.js +23 -17
  200. package/dist/esm/wizard/index.js +171 -103
  201. package/dist/esm/wizard/store/index.js +169 -116
  202. package/dist/esm/wizard/store/utils.js +13 -6
  203. package/package.json +30 -16
  204. package/release.config.js +1 -0
  205. package/shared/hooks/use-coauthors.js +195 -27
  206. package/shared/hooks/use-coauthors.test.js +232 -6
  207. package/shared/scss/_mixins.scss +13 -5
  208. package/src/accordion/index.js +8 -7
  209. package/src/action-card/index.js +15 -1
  210. package/src/badge/index.tsx +3 -1
  211. package/src/card/core-card.js +95 -18
  212. package/src/card/index.js +1 -0
  213. package/src/card/style-core.scss +93 -25
  214. package/src/card/style.scss +2 -2
  215. package/src/card-feature/README.md +188 -0
  216. package/src/card-feature/index.tsx +186 -0
  217. package/src/card-feature/style.scss +75 -0
  218. package/src/card-form/README.md +113 -0
  219. package/src/card-form/index.tsx +128 -0
  220. package/src/card-form/style.scss +20 -0
  221. package/src/card-settings-group/index.tsx +28 -3
  222. package/src/card-sortable-list/index.tsx +27 -4
  223. package/src/color-picker/index.js +15 -8
  224. package/src/color-picker/index.test.js +52 -0
  225. package/src/color-picker/style.scss +11 -13
  226. package/src/confirm-dialog/README.md +128 -0
  227. package/src/confirm-dialog/index.tsx +74 -6
  228. package/src/dataviews/index.tsx +34 -0
  229. package/src/dataviews/style.scss +9 -0
  230. package/src/grid/style.scss +6 -1
  231. package/src/handoff/README.md +93 -0
  232. package/src/handoff/index.js +46 -8
  233. package/src/hooks/index.ts +2 -1
  234. package/src/hooks/use-confirm-dialog.tsx +80 -0
  235. package/src/hooks/use-unsaved-changes-dialog.tsx +140 -0
  236. package/src/image-upload/index.js +10 -15
  237. package/src/image-upload/style.scss +12 -40
  238. package/src/index.js +7 -1
  239. package/src/integration-icons/active-campaign.js +14 -0
  240. package/src/integration-icons/constant-contact.js +22 -0
  241. package/src/integration-icons/fundraise-up.js +18 -0
  242. package/src/integration-icons/index.js +6 -0
  243. package/src/integration-icons/mailchimp.js +10 -0
  244. package/src/integration-icons/salesforce.js +10 -0
  245. package/src/integration-icons/wisepops.js +14 -0
  246. package/src/modal/style.scss +4 -0
  247. package/src/plugin-installer/index.js +30 -19
  248. package/src/section-header/index.js +56 -24
  249. package/src/section-header/style.scss +31 -11
  250. package/src/select-control/index.js +4 -0
  251. package/src/utils/editor-toolbar-back-button.tsx +48 -0
  252. package/src/utils/index.js +6 -0
  253. package/src/utils/style.scss +8 -0
  254. package/src/with-wizard/style.scss +4 -54
  255. package/src/with-wizard-screen/style.scss +41 -3
  256. package/src/wizard/index.js +90 -53
  257. package/src/wizard/store/index.js +2 -2
  258. package/dist/cjs/button-card/index.js +0 -72
  259. package/dist/cjs/button-card/style.scss +0 -180
  260. package/dist/esm/button-card/index.js +0 -64
  261. package/dist/esm/button-card/style.scss +0 -180
  262. package/src/button-card/index.js +0 -52
  263. package/src/button-card/style.scss +0 -180
@@ -1,7 +1,8 @@
1
1
  /**
2
2
  * WordPress dependencies.
3
3
  */
4
- import { ColorPicker as ColorPickerComponent } from '@wordpress/components';
4
+ import { BaseControl, ColorPicker as ColorPickerComponent } from '@wordpress/components';
5
+ import { useInstanceId } from '@wordpress/compose';
5
6
  import { useState, useRef } from '@wordpress/element';
6
7
 
7
8
  /**
@@ -26,35 +27,41 @@ const { InteractiveDiv } = utils;
26
27
  *
27
28
  * @param {Object} props - Component props.
28
29
  * @param {JSX.Element|string} props.label - Label for the color picker.
30
+ * @param {JSX.Element|string} props.help - Help text for the color picker.
29
31
  * @param {string} [props.color] - Default color.
30
32
  * @param {Function} props.onChange - Function to call when the color changes.
31
33
  * @param {string} [props.className] - Additional class name.
32
34
  * @return {JSX.Element} ColorPicker component.
33
35
  */
34
- const ColorPicker = ( { label, color = '#fff', onChange, className } ) => {
36
+ const ColorPicker = ( { label, help, color = '#ffffff', onChange, className } ) => {
35
37
  const [ isExpanded, setIsExpanded ] = useState( false );
36
38
  const ref = useRef();
39
+ const id = useInstanceId( ColorPicker, 'newspack-color-picker' );
40
+ const labelId = `${ id }-label`;
37
41
  const colordColor = colord( color );
38
42
  hooks.useOnClickOutside( ref, () => setIsExpanded( false ) );
39
43
  return (
40
- <div className={ classnames( 'newspack-color-picker', className ) }>
41
- <div className="newspack-color-picker__label">{ label }</div>
42
-
44
+ <BaseControl id={ id } className={ classnames( 'newspack-color-picker', className ) } help={ help } __nextHasNoMarginBottom>
45
+ <BaseControl.VisualLabel id={ labelId }>{ label }</BaseControl.VisualLabel>
43
46
  <InteractiveDiv
47
+ id={ id }
48
+ aria-labelledby={ labelId }
49
+ aria-describedby={ help ? `${ id }__help` : undefined }
50
+ aria-expanded={ isExpanded }
44
51
  className={ 'newspack-color-picker__expander' }
45
52
  onClick={ () => setIsExpanded( ! isExpanded ) }
46
53
  style={ {
47
54
  backgroundColor: color,
48
- color: colordColor.contrast() > colordColor.contrast( '#000' ) ? '#fff' : '#000',
55
+ color: colordColor.contrast() > colordColor.contrast( '#000000' ) ? '#ffffff' : '#000000',
49
56
  } }
50
57
  >
51
58
  { color }
52
59
  </InteractiveDiv>
53
60
 
54
61
  <div className="newspack-color-picker__main" ref={ ref }>
55
- { isExpanded && <ColorPickerComponent color={ color } onChange={ hex => onChange( hex ) } enableAlpha={ false } /> }
62
+ { isExpanded && <ColorPickerComponent color={ color } onChange={ onChange } enableAlpha={ false } /> }
56
63
  </div>
57
- </div>
64
+ </BaseControl>
58
65
  );
59
66
  };
60
67
 
@@ -0,0 +1,52 @@
1
+ /**
2
+ * External dependencies
3
+ */
4
+ import { render, fireEvent } from '@testing-library/react';
5
+
6
+ /**
7
+ * Internal dependencies
8
+ */
9
+ import ColorPicker from './';
10
+
11
+ describe( 'ColorPicker', () => {
12
+ it( 'should render the label', () => {
13
+ const { getByText } = render( <ColorPicker label="Background color" onChange={ () => {} } /> );
14
+ expect( getByText( 'Background color' ) ).toBeInTheDocument();
15
+ } );
16
+
17
+ it( 'should render help text', () => {
18
+ const { getByText } = render( <ColorPicker label="Background color" help="Choose a color" onChange={ () => {} } /> );
19
+ expect( getByText( 'Choose a color' ) ).toBeInTheDocument();
20
+ } );
21
+
22
+ it( 'should start collapsed', () => {
23
+ const { getByRole } = render( <ColorPicker label="Background color" onChange={ () => {} } /> );
24
+ expect( getByRole( 'button' ) ).toHaveAttribute( 'aria-expanded', 'false' );
25
+ } );
26
+
27
+ it( 'should expand when the expander is clicked', () => {
28
+ const { getByRole } = render( <ColorPicker label="Background color" onChange={ () => {} } /> );
29
+ const expander = getByRole( 'button' );
30
+ fireEvent.click( expander );
31
+ expect( expander ).toHaveAttribute( 'aria-expanded', 'true' );
32
+ } );
33
+
34
+ it( 'should associate label via aria-labelledby', () => {
35
+ const { getByRole, getByText } = render( <ColorPicker label="Background color" onChange={ () => {} } /> );
36
+ const expander = getByRole( 'button' );
37
+ const label = getByText( 'Background color' );
38
+ expect( expander.getAttribute( 'aria-labelledby' ) ).toBe( label.id );
39
+ } );
40
+
41
+ it( 'should associate help text via aria-describedby when help is provided', () => {
42
+ const { getByRole, getByText } = render( <ColorPicker label="Background color" help="Choose a color" onChange={ () => {} } /> );
43
+ const expander = getByRole( 'button' );
44
+ const help = getByText( 'Choose a color' );
45
+ expect( expander.getAttribute( 'aria-describedby' ) ).toBe( help.id );
46
+ } );
47
+
48
+ it( 'should not set aria-describedby when no help is provided', () => {
49
+ const { getByRole } = render( <ColorPicker label="Background color" onChange={ () => {} } /> );
50
+ expect( getByRole( 'button' ) ).not.toHaveAttribute( 'aria-describedby' );
51
+ } );
52
+ } );
@@ -2,31 +2,29 @@
2
2
  * Color Picker
3
3
  */
4
4
 
5
+ @use "~@wordpress/base-styles/colors" as wp-colors;
6
+ @use "~@wordpress/base-styles/variables" as wp-vars;
7
+
5
8
  .newspack-color-picker {
6
9
  position: relative;
7
-
8
- &__label {
9
- margin-bottom: 8px;
10
- }
11
-
12
10
  &__expander {
13
- box-shadow: inset 0 0 0 1px rgb(0 0 0 / 20%);
14
- border-radius: 2px;
15
- height: 64px;
16
- padding: 12px;
11
+ box-shadow: inset 0 0 0 1px rgba(wp-colors.$black, 0.54);
12
+ border-radius: wp-vars.$radius-small;
13
+ height: wp-vars.$grid-unit-80;
14
+ padding: wp-vars.$grid-unit-10;
17
15
  text-align: right;
18
16
  text-transform: uppercase;
19
17
 
20
18
  &:focus-visible {
21
19
  box-shadow: 0 0 0 var(--wp-admin-border-width-focus) var(--wp-admin-theme-color);
22
- border-color: white;
20
+ border-color: wp-colors.$white;
23
21
  }
24
22
  }
25
23
 
26
24
  &__main {
27
- background: white;
28
- box-shadow: 0 0 8px 4px rgba(black, 0.08);
29
- margin: 12px 0;
25
+ background: wp-colors.$white;
26
+ box-shadow: wp-vars.$elevation-small;
27
+ margin: wp-vars.$grid-unit-10 0 0;
30
28
  position: absolute;
31
29
  z-index: 2;
32
30
 
@@ -0,0 +1,128 @@
1
+ # ConfirmDialog
2
+
3
+ A modal confirmation dialog that intercepts client-side navigation when there are unsaved changes. Built on top of WordPress's `__experimentalConfirmDialog`. The dialog is invisible until a navigation attempt is blocked; when `when` becomes `true` and the user navigates away, the dialog appears automatically and resumes or cancels the navigation based on the user's choice.
4
+
5
+ For most imperative use cases (confirming a destructive action, guarding an action behind an unsaved-changes check) prefer the [`useConfirmDialog` hook](#useconfirmdialog-hook) over using `ConfirmDialog` directly.
6
+
7
+ ## Props
8
+
9
+ | Prop | Type | Default | Description |
10
+ |------|------|---------|-------------|
11
+ | `cancelButtonText` | `string` | — | Label for the cancel button. |
12
+ | `children` | `React.ReactNode` | — | Content rendered in the modal body. |
13
+ | `className` | `string` | — | Additional CSS class. |
14
+ | `confirmButtonText` | `string` | — | Label for the confirm button. |
15
+ | `hideTitle` | `boolean` | — | When `true`, hides the modal title and the × close button. |
16
+ | `isDestructive` | `boolean` | — | When `true`, applies destructive (e.g. red) styling to the confirm button. |
17
+ | `isOpen` | `boolean` | `false` | When `true`, shows the dialog immediately without blocking navigation. Use this for imperative confirmation (e.g. confirming a destructive action on a button click). |
18
+ | `size` | `'small'` \| `'medium'` \| `'large'` \| `'x-large'` \| `'full'` | `'small'` | Controls the width of the modal. |
19
+ | `title` | `string` | — | Title displayed in the modal header. |
20
+ | `when` | `boolean` | `false` | When `true`, blocks router navigation and shows the dialog on any attempted navigation. Set this to reflect whether the current form has unsaved changes. |
21
+
22
+ ## Usage
23
+
24
+ ```jsx
25
+ import { ConfirmDialog } from 'newspack-components';
26
+
27
+ // Guard navigation when a form has unsaved changes.
28
+ // The dialog appears automatically when the user tries to navigate away.
29
+ <ConfirmDialog
30
+ when={ hasUnsavedChanges }
31
+ title="Unsaved changes"
32
+ confirmButtonText="Leave anyway"
33
+ cancelButtonText="Stay"
34
+ isDestructive
35
+ >
36
+ You have unsaved changes. Are you sure you want to leave?
37
+ </ConfirmDialog>
38
+
39
+ // Larger modal with a custom size
40
+ <ConfirmDialog
41
+ when={ hasUnsavedChanges }
42
+ size="medium"
43
+ title="Discard changes?"
44
+ confirmButtonText="Discard"
45
+ cancelButtonText="Keep editing"
46
+ >
47
+ Your changes will be lost if you leave this page.
48
+ </ConfirmDialog>
49
+
50
+ // Guard both navigation and an imperative action with a single dialog instance.
51
+ // isOpen triggers the dialog immediately (e.g. on a button click) without
52
+ // blocking navigation. Both when and isOpen can be used together.
53
+ const [ pendingAction, setPendingAction ] = useState( null );
54
+ <ConfirmDialog
55
+ when={ hasUnsavedChanges }
56
+ isOpen={ !! pendingAction }
57
+ confirmButtonText="Discard changes"
58
+ isDestructive
59
+ hideTitle
60
+ onConfirm={ () => {
61
+ pendingAction?.();
62
+ setPendingAction( null );
63
+ } }
64
+ onCancel={ () => setPendingAction( null ) }
65
+ >
66
+ You have unsaved changes that will be lost. Discard changes?
67
+ </ConfirmDialog>
68
+ ```
69
+
70
+ ---
71
+
72
+ ## `useConfirmDialog` hook
73
+
74
+ A higher-level hook that wraps `ConfirmDialog` and manages the pending-action state for you. Prefer this over using `ConfirmDialog` directly when you need imperative confirmation.
75
+
76
+ ```jsx
77
+ import { useConfirmDialog } from 'newspack-components';
78
+ ```
79
+
80
+ ### Options
81
+
82
+ Accepts all `ConfirmDialog` props except `isOpen`, `onConfirm`, `onCancel`, and `children`, plus:
83
+
84
+ | Option | Type | Default | Description |
85
+ |--------|------|---------|-------------|
86
+ | `message` | `React.ReactNode` | — | Content rendered in the modal body. |
87
+ | `when` | `boolean` | — | When explicitly `false`, `requestConfirm` skips the dialog and invokes the callback immediately. Omit (or pass `true`) to always show the dialog. Also controls router navigation blocking, same as the `when` prop on `ConfirmDialog`. |
88
+
89
+ ### Returns
90
+
91
+ | Key | Type | Description |
92
+ |-----|------|-------------|
93
+ | `confirmDialog` | `React.ReactElement` | The dialog element — render this somewhere in your JSX. |
94
+ | `requestConfirm` | `( callback: () => void ) => void` | Call with a callback to request confirmation. Shows the dialog (unless `when` is `false`); invokes the callback only if the user confirms. |
95
+
96
+ ### Usage
97
+
98
+ ```jsx
99
+ // Always-confirm dialog (e.g. destructive delete action).
100
+ const { confirmDialog, requestConfirm } = useConfirmDialog( {
101
+ title: 'Are you sure?',
102
+ confirmButtonText: 'Delete',
103
+ isDestructive: true,
104
+ message: 'This will permanently delete the item and cannot be undone.',
105
+ } );
106
+
107
+ // Render the dialog element in JSX:
108
+ { confirmDialog }
109
+
110
+ // Call requestConfirm when the user initiates the action:
111
+ <Button onClick={ () => requestConfirm( handleDelete ) }>Delete</Button>
112
+
113
+ // Conditional dialog — skips confirmation when there are no unsaved changes.
114
+ // Combines navigation blocking with imperative use in a single instance.
115
+ const { confirmDialog, requestConfirm } = useConfirmDialog( {
116
+ when: isDirty,
117
+ message: 'You have unsaved changes that will be lost. Discard changes?',
118
+ confirmButtonText: 'Discard changes',
119
+ isDestructive: true,
120
+ hideTitle: true,
121
+ } );
122
+
123
+ { confirmDialog }
124
+
125
+ // In an action handler — shows the dialog only if isDirty, otherwise
126
+ // calls the callback immediately:
127
+ requestConfirm( () => handleToggleEnabled( newConfig ) );
128
+ ```
@@ -5,8 +5,14 @@
5
5
  /**
6
6
  * WordPress dependencies.
7
7
  */
8
- import { forwardRef } from '@wordpress/element';
9
8
  import { __experimentalConfirmDialog as BaseComponent } from '@wordpress/components'; // eslint-disable-line @wordpress/no-unsafe-wp-apis
9
+ import { forwardRef, useCallback, useEffect, useRef, useState } from '@wordpress/element';
10
+
11
+ /**
12
+ * Internal dependencies.
13
+ */
14
+ import Router from '../proxied-imports/router';
15
+ const { useHistory } = Router;
10
16
 
11
17
  /**
12
18
  * External dependencies.
@@ -23,11 +29,13 @@ type ConfirmDialogProps = {
23
29
  hideTitle?: boolean;
24
30
  title?: string;
25
31
  isDestructive?: boolean;
32
+ isOpen?: boolean;
33
+ onConfirm?: () => void;
34
+ onCancel?: () => void;
26
35
  cancelButtonText?: string;
27
36
  confirmButtonText?: string;
28
- onConfirm: () => void;
29
- onCancel: () => void;
30
37
  children?: React.ReactNode;
38
+ when?: boolean;
31
39
  };
32
40
 
33
41
  const sizeClassMap = {
@@ -38,10 +46,70 @@ const sizeClassMap = {
38
46
  full: 'newspack-modal--size-full',
39
47
  };
40
48
 
49
+ const noOp = () => {};
50
+
41
51
  function ConfirmDialog(
42
- { className, size = 'small', hideTitle, isDestructive, onConfirm, onCancel, ...otherProps }: ConfirmDialogProps,
52
+ {
53
+ className,
54
+ size = 'small',
55
+ hideTitle,
56
+ isDestructive,
57
+ onConfirm = noOp,
58
+ onCancel = noOp,
59
+ when = false,
60
+ isOpen = false,
61
+ ...otherProps
62
+ }: ConfirmDialogProps,
43
63
  ref: React.Ref< HTMLDivElement >
44
64
  ) {
65
+ const [ showDialog, setShowDialog ] = useState( isOpen );
66
+ const history = useHistory();
67
+ const pendingNavigation = useRef< ( () => void ) | null >( null );
68
+
69
+ const handleOnConfirm = useCallback( () => {
70
+ setShowDialog( false );
71
+ pendingNavigation.current?.();
72
+ pendingNavigation.current = null;
73
+ onConfirm();
74
+ }, [ onConfirm, pendingNavigation ] );
75
+
76
+ const handleOnCancel = useCallback( () => {
77
+ setShowDialog( false );
78
+ pendingNavigation.current = null;
79
+ onCancel();
80
+ }, [ onCancel, pendingNavigation ] );
81
+
82
+ // Block navigation when there are unsaved changes.
83
+ useEffect( () => {
84
+ if ( ! when ) {
85
+ return;
86
+ }
87
+ const unblock = history.block( ( location: string, action: string ) => {
88
+ pendingNavigation.current = () => {
89
+ unblock();
90
+ if ( action === 'REPLACE' ) {
91
+ history.replace( location );
92
+ } else {
93
+ history.push( location );
94
+ }
95
+ };
96
+ setShowDialog( true );
97
+ return false;
98
+ } );
99
+ return unblock;
100
+ }, [ when, history ] );
101
+
102
+ // Show the dialog imperatively without blocking navigation.
103
+ useEffect( () => {
104
+ if ( isOpen ) {
105
+ setShowDialog( true );
106
+ }
107
+ }, [ isOpen ] );
108
+
109
+ if ( ! showDialog ) {
110
+ return null;
111
+ }
112
+
45
113
  const classes = classnames(
46
114
  'newspack-modal',
47
115
  sizeClassMap[ size ],
@@ -55,8 +123,8 @@ function ConfirmDialog(
55
123
  className={ classes }
56
124
  { ...otherProps }
57
125
  ref={ ref }
58
- onConfirm={ onConfirm }
59
- onCancel={ onCancel }
126
+ onConfirm={ handleOnConfirm }
127
+ onCancel={ handleOnCancel }
60
128
  __experimentalHideHeader={ false }
61
129
  />
62
130
  );
@@ -0,0 +1,34 @@
1
+ /**
2
+ * DataViews
3
+ *
4
+ * Wrapper around @wordpress/dataviews with Newspack styling.
5
+ */
6
+
7
+ /**
8
+ * WordPress dependencies
9
+ */
10
+ import { DataViews as BaseDataViews } from '@wordpress/dataviews';
11
+
12
+ /**
13
+ * External dependencies
14
+ */
15
+ import classnames from 'classnames';
16
+
17
+ /**
18
+ * Internal dependencies
19
+ */
20
+ import './style.scss';
21
+
22
+ type DataViewsProps = React.ComponentProps< typeof BaseDataViews > & {
23
+ className?: string;
24
+ };
25
+
26
+ export default function DataViews( { className, ...props }: DataViewsProps ) {
27
+ return (
28
+ <div className={ classnames( 'newspack-dataviews', className ) }>
29
+ <BaseDataViews { ...props } />
30
+ </div>
31
+ );
32
+ }
33
+
34
+ export type { Action, Field, View } from '@wordpress/dataviews';
@@ -0,0 +1,9 @@
1
+ /* stylelint-disable no-invalid-position-at-import-rule */
2
+ @import "@wordpress/dataviews/build-style/style.css";
3
+
4
+ .newspack-dataviews {
5
+ /* Make sure full-width DataViews aligns with the 48px margin. */
6
+ .newspack-wizard__content--full-width & {
7
+ margin-top: -16px;
8
+ }
9
+ }
@@ -117,6 +117,11 @@
117
117
  grid-template-columns: repeat(6, 1fr);
118
118
  }
119
119
 
120
+ // Columns 12
121
+ &__columns-12 {
122
+ grid-template-columns: repeat(12, 1fr);
123
+ }
124
+
120
125
  // Gutter 48
121
126
  &__gutter-48 {
122
127
  grid-gap: 48px;
@@ -217,4 +222,4 @@
217
222
  &__tbody + &__tbody {
218
223
  margin-top: -32px;
219
224
  }
220
- }
225
+ }
@@ -0,0 +1,93 @@
1
+ # Handoff
2
+
3
+ Navigates the user to another admin page and displays a return banner at the top of the destination so they can find their way back.
4
+
5
+ ## How it works
6
+
7
+ 1. The user clicks the Handoff button.
8
+ 2. A POST request registers the current URL as the return destination and stores optional banner customisations.
9
+ 3. The user is redirected to the destination page.
10
+ 4. A banner appears at the top of that page with a "Back to Newspack" button (or custom text) that returns them to the origin.
11
+
12
+ ## Usage
13
+
14
+ ### With a URL
15
+
16
+ Use the `url` prop to hand off to any WordPress admin URL. No plugin needs to be installed.
17
+
18
+ ```jsx
19
+ <Handoff url="/wp-admin/admin.php?page=newspack-dashboard#/">
20
+ Go to Dashboard
21
+ </Handoff>
22
+ ```
23
+
24
+ ### With a managed plugin slug
25
+
26
+ Use the `plugin` prop to hand off to a Newspack-managed plugin. The button is automatically disabled if the plugin is not installed or inactive.
27
+
28
+ ```jsx
29
+ <Handoff plugin="jetpack">
30
+ Configure Jetpack
31
+ </Handoff>
32
+ ```
33
+
34
+ Use `editLink` to override the destination to a specific page within the plugin:
35
+
36
+ ```jsx
37
+ <Handoff plugin="wordpress-seo" editLink="/wp-admin/admin.php?page=wpseo_dashboard">
38
+ Configure Yoast SEO
39
+ </Handoff>
40
+ ```
41
+
42
+ ### Via ActionCard
43
+
44
+ `ActionCard` accepts `handoffUrl` (URL-based) or `handoff` (plugin slug) as convenience props that render a `Handoff` as the action button. `bannerText` and `bannerButtonText` are supported on both:
45
+
46
+ ```jsx
47
+ <ActionCard
48
+ title="My Feature"
49
+ actionText="Configure"
50
+ handoffUrl="/wp-admin/admin.php?page=my-settings"
51
+ bannerText="Return here once you're done."
52
+ bannerButtonText="Back to My Feature"
53
+ />
54
+
55
+ <ActionCard
56
+ title="Jetpack"
57
+ actionText="Configure"
58
+ handoff="jetpack"
59
+ editLink="/wp-admin/admin.php?page=jetpack#/settings"
60
+ bannerText="Return here once you're done."
61
+ bannerButtonText="Back to My Feature"
62
+ />
63
+ ```
64
+
65
+ ## Props
66
+
67
+ | Prop | Type | Description |
68
+ |---|---|---|
69
+ | `url` | `string` | Admin URL to hand off to. Use this or `plugin`, not both. |
70
+ | `plugin` | `string` | Slug of a Newspack-managed plugin to hand off to. |
71
+ | `editLink` | `string` | Overrides the destination URL when using `plugin`. |
72
+ | `bannerText` | `string` | Custom body text for the return banner. Defaults to "Return to Newspack after completing configuration". |
73
+ | `bannerButtonText` | `string` | Custom label for the return button. Defaults to "Back to Newspack". |
74
+ | `showOnBlockEditor` | `bool` | Show the return banner inside the block editor. Default `false`. |
75
+ | `useModal` | `bool` | Show a confirmation modal before navigating. |
76
+ | `modalTitle` | `string` | Title for the confirmation modal. |
77
+ | `modalBody` | `string` | Body text for the confirmation modal. |
78
+ | `onReady` | `func` | Called with plugin info once loaded (plugin mode only). |
79
+ | `children` | `node` | Button label. Falls back to "Manage {Plugin Name}" in plugin mode. |
80
+
81
+ All other props are passed through to the underlying `Button` component (`isPrimary`, `isLink`, `isTertiary`, `className`, etc.).
82
+
83
+ ## Banner customisation
84
+
85
+ ```jsx
86
+ <Handoff
87
+ url="/wp-admin/admin.php?page=my-settings"
88
+ bannerText="Finish your configuration, then come back here."
89
+ bannerButtonText="Back to My Feature"
90
+ >
91
+ Open Settings
92
+ </Handoff>
93
+ ```
@@ -31,8 +31,10 @@ class Handoff extends Component {
31
31
 
32
32
  componentDidMount = () => {
33
33
  this._isMounted = true;
34
- const { plugin } = this.props;
35
- this.retrievePluginInfo( plugin );
34
+ const { plugin, url } = this.props;
35
+ if ( plugin && ! url ) {
36
+ this.retrievePluginInfo( plugin );
37
+ }
36
38
  };
37
39
 
38
40
  componentWillUnmount = () => {
@@ -60,8 +62,25 @@ class Handoff extends Component {
60
62
  return assign( defaults, this.props );
61
63
  };
62
64
 
65
+ goToUrl = () => {
66
+ const { url, showOnBlockEditor, bannerText, bannerButtonText } = this.props;
67
+ apiFetch( {
68
+ path: '/newspack/v1/handoff',
69
+ method: 'POST',
70
+ data: {
71
+ destinationUrl: url,
72
+ handoffReturnUrl: window && window.location.href,
73
+ showOnBlockEditor: showOnBlockEditor ? true : false,
74
+ bannerText,
75
+ bannerButtonText,
76
+ },
77
+ } ).then( response => {
78
+ window.location.href = response.HandoffLink;
79
+ } );
80
+ };
81
+
63
82
  goToPlugin = plugin => {
64
- const { editLink, showOnBlockEditor } = this.props;
83
+ const { editLink, showOnBlockEditor, bannerText, bannerButtonText } = this.props;
65
84
  apiFetch( {
66
85
  path: '/newspack/v1/plugins/' + plugin + '/handoff',
67
86
  method: 'POST',
@@ -69,6 +88,8 @@ class Handoff extends Component {
69
88
  editLink,
70
89
  handoffReturnUrl: window && window.location.href,
71
90
  showOnBlockEditor: showOnBlockEditor ? true : false,
91
+ bannerText,
92
+ bannerButtonText,
72
93
  },
73
94
  } ).then( response => {
74
95
  window.location.href = response.HandoffLink;
@@ -92,30 +113,47 @@ class Handoff extends Component {
92
113
  onReady,
93
114
  // eslint-disable-next-line no-unused-vars,@typescript-eslint/no-unused-vars
94
115
  editLink,
116
+ // eslint-disable-next-line no-unused-vars,@typescript-eslint/no-unused-vars
117
+ bannerText,
118
+ // eslint-disable-next-line no-unused-vars,@typescript-eslint/no-unused-vars
119
+ bannerButtonText,
120
+ // eslint-disable-next-line no-unused-vars,@typescript-eslint/no-unused-vars
121
+ url,
95
122
  ...otherProps
96
123
  } = this.props;
97
124
  const { pluginInfo, showModal } = this.state;
98
125
  const { modalBody, modalTitle, primaryButton, primaryModalButton, dismissModalButton } = this.textForPlugin( pluginInfo );
99
126
  const { Configured, Name, Slug, Status } = pluginInfo;
100
127
  const classes = classnames( Configured && 'is-configured', className );
128
+ const goTo = () => ( url ? this.goToUrl() : this.goToPlugin( Slug ) );
101
129
  return (
102
130
  <Fragment>
103
- { Name && 'active' === Status && (
131
+ { url && (
132
+ <Button
133
+ className={ classes }
134
+ isSecondary={ ! otherProps.isPrimary && ! otherProps.isTertiary && ! otherProps.isLink }
135
+ { ...otherProps }
136
+ onClick={ () => ( useModal && children ? this.setState( { showModal: true } ) : goTo() ) }
137
+ >
138
+ { children ? children : primaryButton }
139
+ </Button>
140
+ ) }
141
+ { ! url && Name && 'active' === Status && (
104
142
  <Button
105
143
  className={ classes }
106
144
  isSecondary={ ! otherProps.isPrimary && ! otherProps.isTertiary && ! otherProps.isLink }
107
145
  { ...otherProps }
108
- onClick={ () => ( useModal ? this.setState( { showModal: true } ) : this.goToPlugin( Slug ) ) }
146
+ onClick={ () => ( useModal ? this.setState( { showModal: true } ) : goTo() ) }
109
147
  >
110
148
  { children ? children : primaryButton }
111
149
  </Button>
112
150
  ) }
113
- { Name && 'active' !== Status && (
151
+ { ! url && Name && 'active' !== Status && (
114
152
  <Button className={ classes } variant="secondary" disabled { ...otherProps }>
115
153
  { Name + __( ' not installed', 'newspack-plugin' ) }
116
154
  </Button>
117
155
  ) }
118
- { ! Name && (
156
+ { ! url && ! Name && (
119
157
  <Button
120
158
  className={ classes }
121
159
  isSecondary={ ! otherProps.isPrimary && ! otherProps.isTertiary && ! otherProps.isLink }
@@ -134,7 +172,7 @@ class Handoff extends Component {
134
172
  <Button variant="secondary" onClick={ () => this.setState( { showModal: false } ) }>
135
173
  { dismissModalButton }
136
174
  </Button>
137
- <Button variant="primary" onClick={ () => this.goToPlugin( Slug ) }>
175
+ <Button variant="primary" onClick={ goTo }>
138
176
  { primaryModalButton }
139
177
  </Button>
140
178
  </Card>
@@ -6,10 +6,11 @@ import { useRef } from '@wordpress/element';
6
6
  import usePrompt from './usePrompt';
7
7
  import useObjectState from './useObjectState';
8
8
  import useOnClickOutside from './useOnClickOutside';
9
+ import useConfirmDialog from './use-confirm-dialog';
9
10
 
10
11
  const useUniqueId = ( prefix: string ) => {
11
12
  const id = useRef( `${ prefix }-${ Math.round( Math.random() * 99999 ) }` );
12
13
  return id.current;
13
14
  };
14
15
 
15
- export default { usePrompt, useObjectState, useOnClickOutside, useUniqueId };
16
+ export default { usePrompt, useObjectState, useOnClickOutside, useUniqueId, useConfirmDialog };