newspack-components 4.2.0 → 4.4.0-epic-editor-refactor.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 (315) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/DEVELOPMENT.md +868 -0
  3. package/copy-styles.js +3 -2
  4. package/dist/cjs/accordion/index.js +17 -12
  5. package/dist/cjs/action-card/index.js +186 -140
  6. package/dist/cjs/action-card/style.scss +25 -2
  7. package/dist/cjs/autocomplete-tokenfield/index.js +254 -234
  8. package/dist/cjs/autocomplete-with-latest-posts/index.js +351 -0
  9. package/dist/cjs/autocomplete-with-latest-posts/style.scss +150 -0
  10. package/dist/cjs/autocomplete-with-suggestions/index.js +229 -126
  11. package/dist/cjs/badge/index.js +32 -0
  12. package/dist/cjs/badge/style.scss +36 -0
  13. package/dist/cjs/box-contrast/index.js +19 -15
  14. package/dist/cjs/button/index.js +41 -24
  15. package/dist/cjs/button-props/button-props.js +5 -5
  16. package/dist/cjs/button-props/index.js +2 -2
  17. package/dist/cjs/card/core-card.js +199 -0
  18. package/dist/cjs/card/index.js +75 -35
  19. package/dist/cjs/card/style-core.scss +295 -0
  20. package/dist/cjs/card/style.scss +4 -4
  21. package/dist/cjs/card-feature/index.js +141 -0
  22. package/dist/cjs/card-feature/style.scss +75 -0
  23. package/dist/cjs/card-form/index.js +144 -0
  24. package/dist/cjs/card-form/style.scss +20 -0
  25. package/dist/cjs/card-settings-group/index.js +69 -0
  26. package/dist/cjs/card-settings-group/style.scss +33 -0
  27. package/dist/cjs/card-sortable-list/index.js +426 -0
  28. package/dist/cjs/card-sortable-list/style.scss +48 -0
  29. package/dist/cjs/category-autocomplete/index.js +175 -155
  30. package/dist/cjs/color-picker/index.js +41 -23
  31. package/dist/cjs/color-picker/index.test.js +82 -0
  32. package/dist/cjs/color-picker/style.scss +11 -13
  33. package/dist/cjs/confirm-dialog/index.js +123 -0
  34. package/dist/cjs/consts.js +1 -1
  35. package/dist/cjs/custom-select-control/index.js +39 -28
  36. package/dist/cjs/dataviews/index.js +38 -0
  37. package/dist/cjs/dataviews/style.scss +9 -0
  38. package/dist/cjs/divider/index.js +47 -0
  39. package/dist/cjs/divider/style.scss +51 -0
  40. package/dist/cjs/footer/index.js +36 -30
  41. package/dist/cjs/form-token-field/index.js +51 -40
  42. package/dist/cjs/form-token-field/style.scss +0 -5
  43. package/dist/cjs/global-notices/index.js +5 -5
  44. package/dist/cjs/grid/index.js +25 -18
  45. package/dist/cjs/grid/style.scss +67 -8
  46. package/dist/cjs/handoff/index.js +204 -156
  47. package/dist/cjs/handoff-message/index.js +12 -7
  48. package/dist/cjs/hooks/index.js +10 -8
  49. package/dist/cjs/hooks/use-confirm-dialog.js +79 -0
  50. package/dist/cjs/hooks/use-unsaved-changes-dialog.js +144 -0
  51. package/dist/cjs/hooks/useObjectState.js +21 -12
  52. package/dist/cjs/hooks/useObjectState.test.js +55 -33
  53. package/dist/cjs/hooks/useOnClickOutside.js +6 -6
  54. package/dist/cjs/hooks/usePrompt.js +10 -13
  55. package/dist/cjs/image-upload/index.js +114 -123
  56. package/dist/cjs/image-upload/index.test.js +11 -13
  57. package/dist/cjs/image-upload/style.scss +12 -40
  58. package/dist/cjs/index.js +166 -92
  59. package/dist/cjs/info-button/index.js +43 -32
  60. package/dist/cjs/integration-icons/active-campaign.js +22 -0
  61. package/dist/cjs/integration-icons/constant-contact.js +28 -0
  62. package/dist/cjs/integration-icons/fundraise-up.js +25 -0
  63. package/dist/cjs/integration-icons/index.js +48 -0
  64. package/dist/cjs/integration-icons/mailchimp.js +19 -0
  65. package/dist/cjs/integration-icons/salesforce.js +17 -0
  66. package/dist/cjs/integration-icons/wisepops.js +20 -0
  67. package/dist/cjs/modal/index.js +29 -19
  68. package/dist/cjs/modal/style.scss +47 -13
  69. package/dist/cjs/newspack-icon/index.js +45 -38
  70. package/dist/cjs/newspack-icon/style.scss +5 -3
  71. package/dist/cjs/notice/index.js +64 -55
  72. package/dist/cjs/plugin-installer/index.js +259 -253
  73. package/dist/cjs/plugin-settings/SettingsSection.js +66 -60
  74. package/dist/cjs/plugin-settings/index.js +217 -200
  75. package/dist/cjs/plugin-toggle/index.js +192 -199
  76. package/dist/cjs/popover/index.js +41 -28
  77. package/dist/cjs/position-control/index.js +24 -23
  78. package/dist/cjs/progress-bar/index.js +71 -59
  79. package/dist/cjs/progress-bar/index.test.js +62 -68
  80. package/dist/cjs/proxied-imports/router.js +3 -3
  81. package/dist/cjs/radio-control/index.js +40 -27
  82. package/dist/cjs/section-header/index.js +143 -39
  83. package/dist/cjs/section-header/style.scss +129 -4
  84. package/dist/cjs/select-control/ButtonGroupControl.js +26 -21
  85. package/dist/cjs/select-control/GroupedSelectControl.js +41 -35
  86. package/dist/cjs/select-control/index.js +52 -40
  87. package/dist/cjs/settings/MinMaxSetting.js +30 -20
  88. package/dist/cjs/settings/SettingsCard.js +20 -17
  89. package/dist/cjs/settings/SettingsSection.js +22 -21
  90. package/dist/cjs/settings/index.js +5 -5
  91. package/dist/cjs/sortable-newsletter-list-control/index.js +78 -57
  92. package/dist/cjs/steps-list/index.js +44 -34
  93. package/dist/cjs/steps-list-item/index.js +47 -39
  94. package/dist/cjs/style-card/index.js +70 -63
  95. package/dist/cjs/style.scss +2 -0
  96. package/dist/cjs/tabbed-navigation/index.js +38 -36
  97. package/dist/cjs/text-control/index.js +27 -25
  98. package/dist/cjs/utils/color.js +4 -4
  99. package/dist/cjs/utils/editor-toolbar-back-button.js +61 -0
  100. package/dist/cjs/utils/index.js +31 -19
  101. package/dist/cjs/utils/style.scss +8 -0
  102. package/dist/cjs/waiting/index.js +48 -35
  103. package/dist/cjs/waiting/style.scss +4 -0
  104. package/dist/cjs/web-preview/index.js +219 -154
  105. package/dist/cjs/web-preview/style.scss +20 -53
  106. package/dist/cjs/with-wizard/index.js +362 -364
  107. package/dist/cjs/with-wizard/style.scss +28 -60
  108. package/dist/cjs/with-wizard-screen/index.js +54 -58
  109. package/dist/cjs/with-wizard-screen/style.scss +77 -10
  110. package/dist/cjs/wizard/components/WizardError.js +20 -18
  111. package/dist/cjs/wizard/components/WizardSnackbar.js +73 -0
  112. package/dist/cjs/wizard/components/style.scss +48 -0
  113. package/dist/cjs/wizard/index.js +207 -64
  114. package/dist/cjs/wizard/store/index.js +191 -97
  115. package/dist/cjs/wizard/store/utils.js +13 -8
  116. package/dist/esm/accordion/index.js +14 -9
  117. package/dist/esm/action-card/index.js +178 -124
  118. package/dist/esm/action-card/style.scss +25 -2
  119. package/dist/esm/autocomplete-tokenfield/index.js +248 -222
  120. package/dist/esm/autocomplete-with-latest-posts/index.js +347 -0
  121. package/dist/esm/autocomplete-with-latest-posts/style.scss +150 -0
  122. package/dist/esm/autocomplete-with-suggestions/index.js +226 -119
  123. package/dist/esm/badge/index.js +24 -0
  124. package/dist/esm/badge/style.scss +36 -0
  125. package/dist/esm/box-contrast/index.js +15 -11
  126. package/dist/esm/button/index.js +39 -19
  127. package/dist/esm/button-props/button-props.js +1 -1
  128. package/dist/esm/card/core-card.js +195 -0
  129. package/dist/esm/card/index.js +70 -22
  130. package/dist/esm/card/style-core.scss +295 -0
  131. package/dist/esm/card/style.scss +4 -4
  132. package/dist/esm/card-feature/index.js +131 -0
  133. package/dist/esm/card-feature/style.scss +75 -0
  134. package/dist/esm/card-form/index.js +134 -0
  135. package/dist/esm/card-form/style.scss +20 -0
  136. package/dist/esm/card-settings-group/index.js +61 -0
  137. package/dist/esm/card-settings-group/style.scss +33 -0
  138. package/dist/esm/card-sortable-list/index.js +425 -0
  139. package/dist/esm/card-sortable-list/style.scss +48 -0
  140. package/dist/esm/category-autocomplete/index.js +168 -140
  141. package/dist/esm/color-picker/index.js +39 -21
  142. package/dist/esm/color-picker/index.test.js +78 -0
  143. package/dist/esm/color-picker/style.scss +11 -13
  144. package/dist/esm/confirm-dialog/index.js +116 -0
  145. package/dist/esm/consts.js +1 -1
  146. package/dist/esm/custom-select-control/index.js +34 -15
  147. package/dist/esm/dataviews/index.js +34 -0
  148. package/dist/esm/dataviews/style.scss +9 -0
  149. package/dist/esm/divider/index.js +42 -0
  150. package/dist/esm/divider/style.scss +51 -0
  151. package/dist/esm/footer/index.js +34 -28
  152. package/dist/esm/form-token-field/index.js +46 -27
  153. package/dist/esm/form-token-field/style.scss +0 -5
  154. package/dist/esm/global-notices/index.js +3 -3
  155. package/dist/esm/grid/index.js +23 -13
  156. package/dist/esm/grid/style.scss +67 -8
  157. package/dist/esm/handoff/index.js +199 -143
  158. package/dist/esm/handoff-message/index.js +10 -6
  159. package/dist/esm/hooks/index.js +8 -6
  160. package/dist/esm/hooks/use-confirm-dialog.js +73 -0
  161. package/dist/esm/hooks/use-unsaved-changes-dialog.js +136 -0
  162. package/dist/esm/hooks/useObjectState.js +19 -9
  163. package/dist/esm/hooks/useObjectState.test.js +54 -32
  164. package/dist/esm/hooks/useOnClickOutside.js +4 -4
  165. package/dist/esm/hooks/usePrompt.js +9 -11
  166. package/dist/esm/image-upload/index.js +110 -111
  167. package/dist/esm/image-upload/index.test.js +11 -13
  168. package/dist/esm/image-upload/style.scss +12 -40
  169. package/dist/esm/index.js +13 -1
  170. package/dist/esm/info-button/index.js +38 -19
  171. package/dist/esm/integration-icons/active-campaign.js +16 -0
  172. package/dist/esm/integration-icons/constant-contact.js +22 -0
  173. package/dist/esm/integration-icons/fundraise-up.js +19 -0
  174. package/dist/esm/integration-icons/index.js +6 -0
  175. package/dist/esm/integration-icons/mailchimp.js +13 -0
  176. package/dist/esm/integration-icons/salesforce.js +11 -0
  177. package/dist/esm/integration-icons/wisepops.js +14 -0
  178. package/dist/esm/modal/index.js +27 -13
  179. package/dist/esm/modal/style.scss +47 -13
  180. package/dist/esm/newspack-icon/index.js +40 -25
  181. package/dist/esm/newspack-icon/style.scss +5 -3
  182. package/dist/esm/notice/index.js +59 -42
  183. package/dist/esm/plugin-installer/index.js +255 -243
  184. package/dist/esm/plugin-settings/SettingsSection.js +61 -50
  185. package/dist/esm/plugin-settings/index.js +212 -189
  186. package/dist/esm/plugin-toggle/index.js +187 -190
  187. package/dist/esm/popover/index.js +35 -16
  188. package/dist/esm/position-control/index.js +22 -18
  189. package/dist/esm/progress-bar/index.js +66 -49
  190. package/dist/esm/progress-bar/index.test.js +61 -67
  191. package/dist/esm/radio-control/index.js +34 -15
  192. package/dist/esm/section-header/index.js +140 -38
  193. package/dist/esm/section-header/style.scss +129 -4
  194. package/dist/esm/select-control/ButtonGroupControl.js +23 -18
  195. package/dist/esm/select-control/GroupedSelectControl.js +38 -30
  196. package/dist/esm/select-control/index.js +47 -27
  197. package/dist/esm/settings/MinMaxSetting.js +27 -16
  198. package/dist/esm/settings/SettingsCard.js +18 -13
  199. package/dist/esm/settings/SettingsSection.js +20 -19
  200. package/dist/esm/settings/index.js +3 -3
  201. package/dist/esm/sortable-newsletter-list-control/index.js +74 -53
  202. package/dist/esm/steps-list/index.js +39 -21
  203. package/dist/esm/steps-list-item/index.js +42 -26
  204. package/dist/esm/style-card/index.js +65 -50
  205. package/dist/esm/style.scss +2 -0
  206. package/dist/esm/tabbed-navigation/index.js +35 -33
  207. package/dist/esm/text-control/index.js +25 -19
  208. package/dist/esm/utils/color.js +4 -4
  209. package/dist/esm/utils/editor-toolbar-back-button.js +53 -0
  210. package/dist/esm/utils/index.js +29 -16
  211. package/dist/esm/utils/style.scss +8 -0
  212. package/dist/esm/waiting/index.js +43 -22
  213. package/dist/esm/waiting/style.scss +4 -0
  214. package/dist/esm/web-preview/index.js +218 -147
  215. package/dist/esm/web-preview/style.scss +20 -53
  216. package/dist/esm/with-wizard/index.js +360 -358
  217. package/dist/esm/with-wizard/style.scss +28 -60
  218. package/dist/esm/with-wizard-screen/index.js +48 -46
  219. package/dist/esm/with-wizard-screen/style.scss +77 -10
  220. package/dist/esm/wizard/components/WizardError.js +18 -16
  221. package/dist/esm/wizard/components/WizardSnackbar.js +70 -0
  222. package/dist/esm/wizard/components/style.scss +48 -0
  223. package/dist/esm/wizard/index.js +207 -58
  224. package/dist/esm/wizard/store/index.js +189 -90
  225. package/dist/esm/wizard/store/utils.js +13 -6
  226. package/package.json +30 -17
  227. package/release.config.js +1 -0
  228. package/shared/author-context.js +26 -0
  229. package/shared/hooks/use-coauthors.js +388 -0
  230. package/shared/hooks/use-coauthors.test.js +850 -0
  231. package/shared/hooks/use-custom-byline.js +47 -0
  232. package/shared/hooks/use-custom-byline.test.js +133 -0
  233. package/shared/scss/_mixins.scss +54 -0
  234. package/src/accordion/index.js +8 -7
  235. package/src/action-card/action-card.d.ts +7 -3
  236. package/src/action-card/index.js +39 -12
  237. package/src/action-card/style.scss +25 -2
  238. package/src/autocomplete-tokenfield/index.js +2 -2
  239. package/src/autocomplete-with-latest-posts/index.js +290 -0
  240. package/src/autocomplete-with-latest-posts/style.scss +150 -0
  241. package/src/badge/index.tsx +26 -0
  242. package/src/badge/style.scss +36 -0
  243. package/src/button/index.tsx +3 -2
  244. package/src/card/core-card.js +224 -0
  245. package/src/card/index.js +40 -1
  246. package/src/card/style-core.scss +295 -0
  247. package/src/card/style.scss +4 -4
  248. package/src/card-feature/README.md +188 -0
  249. package/src/card-feature/index.tsx +186 -0
  250. package/src/card-feature/style.scss +75 -0
  251. package/src/card-form/README.md +113 -0
  252. package/src/card-form/index.tsx +128 -0
  253. package/src/card-form/style.scss +20 -0
  254. package/src/card-settings-group/README.md +43 -0
  255. package/src/card-settings-group/index.tsx +78 -0
  256. package/src/card-settings-group/style.scss +33 -0
  257. package/src/card-sortable-list/README.md +51 -0
  258. package/src/card-sortable-list/index.tsx +435 -0
  259. package/src/card-sortable-list/style.scss +48 -0
  260. package/src/color-picker/index.js +15 -8
  261. package/src/color-picker/index.test.js +52 -0
  262. package/src/color-picker/style.scss +11 -13
  263. package/src/confirm-dialog/README.md +128 -0
  264. package/src/confirm-dialog/index.tsx +132 -0
  265. package/src/dataviews/index.tsx +34 -0
  266. package/src/dataviews/style.scss +9 -0
  267. package/src/divider/README.md +25 -0
  268. package/src/divider/index.js +31 -0
  269. package/src/divider/style.scss +51 -0
  270. package/src/form-token-field/style.scss +0 -5
  271. package/src/grid/index.tsx +2 -1
  272. package/src/grid/style.scss +67 -8
  273. package/src/handoff/README.md +93 -0
  274. package/src/handoff/index.js +46 -8
  275. package/src/hooks/index.ts +2 -1
  276. package/src/hooks/use-confirm-dialog.tsx +80 -0
  277. package/src/hooks/use-unsaved-changes-dialog.tsx +140 -0
  278. package/src/image-upload/index.js +10 -15
  279. package/src/image-upload/style.scss +12 -40
  280. package/src/index.js +13 -1
  281. package/src/integration-icons/active-campaign.js +14 -0
  282. package/src/integration-icons/constant-contact.js +22 -0
  283. package/src/integration-icons/fundraise-up.js +18 -0
  284. package/src/integration-icons/index.js +6 -0
  285. package/src/integration-icons/mailchimp.js +10 -0
  286. package/src/integration-icons/salesforce.js +10 -0
  287. package/src/integration-icons/wisepops.js +14 -0
  288. package/src/modal/index.js +12 -3
  289. package/src/modal/style.scss +47 -13
  290. package/src/newspack-icon/style.scss +5 -3
  291. package/src/plugin-installer/index.js +30 -19
  292. package/src/section-header/index.js +105 -14
  293. package/src/section-header/style.scss +129 -4
  294. package/src/select-control/index.js +4 -0
  295. package/src/style.scss +2 -0
  296. package/src/utils/editor-toolbar-back-button.tsx +48 -0
  297. package/src/utils/index.js +6 -0
  298. package/src/utils/style.scss +8 -0
  299. package/src/waiting/index.js +2 -1
  300. package/src/waiting/style.scss +4 -0
  301. package/src/web-preview/index.js +90 -35
  302. package/src/web-preview/style.scss +20 -53
  303. package/src/with-wizard/index.js +1 -1
  304. package/src/with-wizard/style.scss +28 -60
  305. package/src/with-wizard-screen/style.scss +77 -10
  306. package/src/wizard/components/WizardSnackbar.js +58 -0
  307. package/src/wizard/components/style.scss +48 -0
  308. package/src/wizard/index.js +152 -21
  309. package/src/wizard/store/index.js +28 -2
  310. package/dist/cjs/button-card/index.js +0 -72
  311. package/dist/cjs/button-card/style.scss +0 -180
  312. package/dist/esm/button-card/index.js +0 -64
  313. package/dist/esm/button-card/style.scss +0 -180
  314. package/src/button-card/index.js +0 -52
  315. package/src/button-card/style.scss +0 -180
@@ -13,7 +13,7 @@
13
13
 
14
14
  // No Margin
15
15
  &--no-margin {
16
- margin: 0;
16
+ margin: 0 !important;
17
17
  }
18
18
 
19
19
  // Gutter 0
@@ -41,6 +41,11 @@
41
41
  grid-gap: 32px;
42
42
  }
43
43
 
44
+ // Gutter 64
45
+ &__gutter-64 {
46
+ grid-gap: 64px;
47
+ }
48
+
44
49
  // Row Gap 0
45
50
  &__row-gap-0 {
46
51
  grid-row-gap: 0;
@@ -61,11 +66,6 @@
61
66
  grid-row-gap: 32px;
62
67
  }
63
68
 
64
- // Gutter 64
65
- &__gutter-64 {
66
- grid-gap: 64px;
67
- }
68
-
69
69
  // Columns 6
70
70
  &__columns-6 {
71
71
  grid-template-columns: repeat(2, 1fr);
@@ -77,6 +77,14 @@
77
77
  &__columns-3,
78
78
  &__columns-4 {
79
79
  grid-template-columns: repeat(2, 1fr);
80
+
81
+ > [start] {
82
+ grid-column-start: 1;
83
+ }
84
+
85
+ > [end] {
86
+ grid-column-end: -1;
87
+ }
80
88
  }
81
89
 
82
90
  &__columns-6 {
@@ -84,8 +92,8 @@
84
92
  }
85
93
  }
86
94
 
87
- // Media Queries - 1128px
88
- @media screen and ( min-width: 1128px ) {
95
+ // Media Queries - 1054px
96
+ @media screen and ( min-width: 1054px ) {
89
97
  // Columns 3
90
98
  &__columns-3 {
91
99
  grid-template-columns: repeat(3, 1fr);
@@ -94,6 +102,14 @@
94
102
  // Columns 4
95
103
  &__columns-4 {
96
104
  grid-template-columns: repeat(4, 1fr);
105
+
106
+ > [start="2"] {
107
+ grid-column-start: 2;
108
+ }
109
+
110
+ > [end="4"] {
111
+ grid-column-end: 4;
112
+ }
97
113
  }
98
114
 
99
115
  // Columns 6
@@ -101,6 +117,11 @@
101
117
  grid-template-columns: repeat(6, 1fr);
102
118
  }
103
119
 
120
+ // Columns 12
121
+ &__columns-12 {
122
+ grid-template-columns: repeat(12, 1fr);
123
+ }
124
+
104
125
  // Gutter 48
105
126
  &__gutter-48 {
106
127
  grid-gap: 48px;
@@ -116,6 +137,44 @@
116
137
  > * {
117
138
  margin-bottom: 0 !important;
118
139
  margin-top: 0 !important;
140
+ > h1:first-child,
141
+ > h2:first-child,
142
+ > h3:first-child,
143
+ > h4:first-child,
144
+ > h5:first-child,
145
+ > h6:first-child {
146
+ margin-top: 0;
147
+ }
148
+ }
149
+
150
+ // Borders
151
+ &__borders {
152
+ grid-gap: 0 !important;
153
+ > * {
154
+ border-bottom: 1px solid wp-colors.$gray-300;
155
+ &:last-child {
156
+ border-bottom: none;
157
+ }
158
+ @media screen and ( min-width: 744px ) {
159
+ border-bottom: none;
160
+ border-right: 1px solid wp-colors.$gray-300;
161
+ &:last-child {
162
+ border-right: none;
163
+ }
164
+ }
165
+ }
166
+ &.newspack-grid__gutter-8 > * {
167
+ padding: 8px;
168
+ }
169
+ &.newspack-grid__gutter-16 > * {
170
+ padding: 16px;
171
+ }
172
+ &.newspack-grid__gutter-32 > * {
173
+ padding: 32px;
174
+ }
175
+ &.newspack-grid__gutter-64 > * {
176
+ padding: 64px;
177
+ }
119
178
  }
120
179
 
121
180
  .components-base-control {
@@ -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 };
@@ -0,0 +1,80 @@
1
+ /**
2
+ * WordPress dependencies.
3
+ */
4
+ import { useCallback, useRef, useState } from '@wordpress/element';
5
+
6
+ /**
7
+ * Internal dependencies.
8
+ */
9
+ import ConfirmDialog from '../confirm-dialog';
10
+
11
+ type UseConfirmDialogOptions = {
12
+ when?: boolean;
13
+ message: React.ReactNode;
14
+ title?: string;
15
+ confirmButtonText?: string;
16
+ cancelButtonText?: string;
17
+ isDestructive?: boolean;
18
+ hideTitle?: boolean;
19
+ size?: 'small' | 'medium' | 'large' | 'x-large' | 'full';
20
+ className?: string;
21
+ };
22
+
23
+ type UseConfirmDialogResult = {
24
+ confirmDialog: React.ReactElement;
25
+ requestConfirm: ( callback: () => void ) => void;
26
+ };
27
+
28
+ /**
29
+ * A hook that encapsulates the ConfirmDialog component and provides a
30
+ * `requestConfirm` function for imperative use.
31
+ *
32
+ * Calling `requestConfirm( callback )` will show a confirmation dialog.
33
+ * If the user confirms, `callback` is invoked. If the user cancels, it is not.
34
+ *
35
+ * When `when` is explicitly `false`, `requestConfirm( callback )` calls
36
+ * `callback` immediately without showing the dialog. This is useful for
37
+ * guarding actions that are only destructive when there are unsaved changes
38
+ * (e.g. `when={ isDirty }`). When `when` is omitted or `true`, the dialog
39
+ * is always shown.
40
+ *
41
+ * The `confirmDialog` element must be rendered somewhere in the component's
42
+ * JSX for the dialog to appear.
43
+ */
44
+ function useConfirmDialog( options: UseConfirmDialogOptions ): UseConfirmDialogResult {
45
+ const { message, when, ...dialogProps } = options;
46
+ const [ pendingAction, setPendingAction ] = useState< ( () => void ) | null >( null );
47
+
48
+ // Keep a ref so `requestConfirm` can always read the latest `when` value
49
+ // without needing to be recreated on every render.
50
+ const whenRef = useRef( when );
51
+ whenRef.current = when;
52
+
53
+ const requestConfirm = useCallback( ( callback: () => void ) => {
54
+ if ( whenRef.current !== false ) {
55
+ // Store as a thunk to opt out of React's functional-update interpretation.
56
+ setPendingAction( () => callback );
57
+ } else {
58
+ callback();
59
+ }
60
+ }, [] );
61
+
62
+ const confirmDialog = (
63
+ <ConfirmDialog
64
+ { ...dialogProps }
65
+ when={ when }
66
+ isOpen={ !! pendingAction }
67
+ onConfirm={ () => {
68
+ pendingAction?.();
69
+ setPendingAction( null );
70
+ } }
71
+ onCancel={ () => setPendingAction( null ) }
72
+ >
73
+ { message }
74
+ </ConfirmDialog>
75
+ );
76
+
77
+ return { confirmDialog, requestConfirm };
78
+ }
79
+
80
+ export default useConfirmDialog;
@@ -0,0 +1,140 @@
1
+ /**
2
+ * WordPress dependencies.
3
+ */
4
+ import { useEffect, useRef } from '@wordpress/element';
5
+ import { __ } from '@wordpress/i18n';
6
+
7
+ /**
8
+ * Internal dependencies.
9
+ */
10
+ import useConfirmDialog from './use-confirm-dialog';
11
+
12
+ type UseUnsavedChangesDialogOptions = {
13
+ when: boolean;
14
+ };
15
+
16
+ // Stack of mounted guards; the last entry owns the document-level handler so
17
+ // only the most-recently-mounted guard prompts when several are active. Using a
18
+ // stack keeps ownership correct under out-of-order unmounts.
19
+ const activeGuards: symbol[] = [];
20
+
21
+ /**
22
+ * Returns true when `href` resolves to the same origin as the current page —
23
+ * i.e. it is an internal navigation that would unload the wizard. External
24
+ * `https://...` links, `mailto:`, `tel:`, and other schemes navigate to a new
25
+ * context (new tab, mail client, dialer) without unloading the wizard, and
26
+ * must not trigger the discard-changes prompt.
27
+ */
28
+ function isSameOriginNavigation( link: HTMLAnchorElement ): boolean {
29
+ try {
30
+ const url = new URL( link.href, window.location.href );
31
+ return url.origin === window.location.origin && ( url.protocol === 'http:' || url.protocol === 'https:' );
32
+ } catch ( e ) {
33
+ return false;
34
+ }
35
+ }
36
+
37
+ /**
38
+ * Shared unsaved-changes guard. Wraps `useConfirmDialog` with standardized
39
+ * messaging, intercepts same-origin link clicks so the dialog fires instead
40
+ * of a silent navigation, and adds a `beforeunload` listener as the last-resort
41
+ * guard for refresh / tab-close (browser-native, cannot be styled). The
42
+ * returned `confirmDialog` element must be rendered in JSX.
43
+ *
44
+ * Single-consumer constraint: the click handler is attached at the document
45
+ * level in capture phase. Two simultaneously-active instances will both fire
46
+ * a dialog. A development-only warning surfaces this.
47
+ */
48
+ function useUnsavedChangesDialog( { when }: UseUnsavedChangesDialogOptions ) {
49
+ const { confirmDialog, requestConfirm } = useConfirmDialog( {
50
+ when,
51
+ message: __( 'You have unsaved changes that will be lost. Discard changes?', 'newspack-plugin' ),
52
+ confirmButtonText: __( 'Discard changes', 'newspack-plugin' ),
53
+ hideTitle: true,
54
+ } );
55
+
56
+ // Tracks navigation the user has already approved via our custom dialog so
57
+ // the beforeunload guard doesn't fire a second native prompt on top of it.
58
+ const isNavigatingRef = useRef( false );
59
+
60
+ useEffect( () => {
61
+ if ( ! when ) {
62
+ return;
63
+ }
64
+ const ownerId = Symbol( 'unsaved-changes-guard' );
65
+ activeGuards.push( ownerId );
66
+ if ( process.env.NODE_ENV !== 'production' && activeGuards.length > 1 ) {
67
+ // eslint-disable-next-line no-console
68
+ console.warn(
69
+ 'useUnsavedChangesDialog: more than one active instance detected. ' +
70
+ 'Document-level click capture will fire a dialog per instance — ensure only one guard is active at a time.'
71
+ );
72
+ }
73
+ const handler = ( e: MouseEvent ) => {
74
+ // Only the top-of-stack guard prompts, so concurrent guards can't stack dialogs.
75
+ if ( activeGuards[ activeGuards.length - 1 ] !== ownerId ) {
76
+ return;
77
+ }
78
+ if ( e.defaultPrevented || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey || e.button !== 0 ) {
79
+ return;
80
+ }
81
+ const target = e.target as HTMLElement | null;
82
+ const link = target?.closest( 'a[href]' ) as HTMLAnchorElement | null;
83
+ if ( ! link ) {
84
+ return;
85
+ }
86
+ const href = link.getAttribute( 'href' );
87
+ if ( ! href || href.startsWith( '#' ) || href.startsWith( 'javascript:' ) ) {
88
+ // All `#`-prefixed links are skipped here: plain anchors
89
+ // (`#section`) are scroll targets, and HashRouter paths
90
+ // (`#/route`) are intercepted by `ConfirmDialog`'s built-in
91
+ // `history.block` listener instead. Routing them through
92
+ // `window.location.href` here would trigger a hashchange
93
+ // that the still-active block re-catches, double-prompting.
94
+ return;
95
+ }
96
+ if ( link.target && link.target !== '_self' ) {
97
+ return;
98
+ }
99
+ // Skip mailto:, tel:, external origins, and any non-http(s) scheme —
100
+ // they don't unload the wizard, so a discard prompt would be wrong.
101
+ if ( ! isSameOriginNavigation( link ) ) {
102
+ return;
103
+ }
104
+ e.preventDefault();
105
+ e.stopPropagation();
106
+ const destination = link.href;
107
+ requestConfirm( () => {
108
+ isNavigatingRef.current = true;
109
+ window.location.href = destination;
110
+ } );
111
+ };
112
+ document.addEventListener( 'click', handler, true );
113
+ return () => {
114
+ document.removeEventListener( 'click', handler, true );
115
+ const idx = activeGuards.indexOf( ownerId );
116
+ if ( idx !== -1 ) {
117
+ activeGuards.splice( idx, 1 );
118
+ }
119
+ };
120
+ }, [ when, requestConfirm ] );
121
+
122
+ useEffect( () => {
123
+ if ( ! when ) {
124
+ return;
125
+ }
126
+ const handler = ( e: BeforeUnloadEvent ) => {
127
+ if ( isNavigatingRef.current ) {
128
+ return;
129
+ }
130
+ e.preventDefault();
131
+ e.returnValue = '';
132
+ };
133
+ window.addEventListener( 'beforeunload', handler );
134
+ return () => window.removeEventListener( 'beforeunload', handler );
135
+ }, [ when ] );
136
+
137
+ return { confirmDialog, requestConfirm };
138
+ }
139
+
140
+ export default useUnsavedChangesDialog;
@@ -7,11 +7,12 @@
7
7
  */
8
8
  import { Component } from '@wordpress/element';
9
9
  import { __ } from '@wordpress/i18n';
10
+ import { BaseControl } from '@wordpress/components';
10
11
 
11
12
  /**
12
13
  * Internal dependencies.
13
14
  */
14
- import { Button, InfoButton } from '../';
15
+ import { Button } from '../';
15
16
  import './style.scss';
16
17
 
17
18
  /**
@@ -72,41 +73,35 @@ class ImageUpload extends Component {
72
73
  * Render.
73
74
  */
74
75
  render = () => {
75
- const { buttonLabel, className, disabled, help, image, info, isCovering, label, onChange, style = {} } = this.props;
76
+ const { buttonLabel, className, disabled, help, image, isCovering, label, onChange, style = {} } = this.props;
76
77
  const classes = classnames(
77
78
  'newspack-image-upload__image',
78
79
  { 'newspack-image-upload__image--has-image': image },
79
80
  { 'newspack-image-upload__image--covering': isCovering }
80
81
  );
81
82
  return (
82
- <div className={ classnames( 'newspack-image-upload', className ) }>
83
- <div className="newspack-image-upload__header">
84
- { /* eslint-disable-next-line jsx-a11y/label-has-for, jsx-a11y/label-has-associated-control */ }
85
- { label && <label className="newspack-image-upload__label">{ label }</label> }
86
- { info && <InfoButton text={ info } /> }
87
- </div>
88
- <div className={ classes } style={ { ...style } }>
83
+ <BaseControl __nextHasNoMarginBottom className={ classnames( 'newspack-image-upload', className ) } help={ help }>
84
+ { label && <BaseControl.VisualLabel>{ label }</BaseControl.VisualLabel> }
85
+ <div className={ classes } style={ style }>
89
86
  { image?.url ? (
90
87
  <>
91
88
  <img data-testid="image-upload" src={ image.url } alt={ __( 'Image preview', 'newspack-plugin' ) } />
92
89
  <div className="newspack-image-upload__controls">
93
- <Button disabled={ disabled } onClick={ this.openModal } isLink>
90
+ <Button disabled={ disabled } onClick={ this.openModal } variant="tertiary">
94
91
  { __( 'Replace', 'newspack-plugin' ) }
95
92
  </Button>
96
- <span className="sep" />
97
- <Button disabled={ disabled } onClick={ () => onChange( null ) } isLink isDestructive>
93
+ <Button disabled={ disabled } onClick={ () => onChange( null ) } variant="tertiary" isDestructive>
98
94
  { __( 'Remove', 'newspack-plugin' ) }
99
95
  </Button>
100
96
  </div>
101
97
  </>
102
98
  ) : (
103
- <Button disabled={ disabled } onClick={ this.openModal } isLink>
99
+ <Button disabled={ disabled } onClick={ this.openModal } variant="tertiary">
104
100
  { buttonLabel ? buttonLabel : __( 'Upload', 'newspack-plugin' ) }
105
101
  </Button>
106
102
  ) }
107
103
  </div>
108
- { help && <p className="newspack-image-upload__help">{ help }</p> }
109
- </div>
104
+ </BaseControl>
110
105
  );
111
106
  };
112
107
  }