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
@@ -0,0 +1,47 @@
1
+ /**
2
+ * WordPress dependencies
3
+ */
4
+ import { useSelect } from '@wordpress/data';
5
+ import { store as coreStore } from '@wordpress/core-data';
6
+
7
+ /**
8
+ * Hook to get custom byline data.
9
+ *
10
+ * @param {number} postId Post ID.
11
+ * @param {string} postType Post type.
12
+ * @return {Object} Custom byline data.
13
+ */
14
+ export function useCustomByline( postId, postType ) {
15
+ const { bylineActive, bylineContent } = useSelect(
16
+ select => {
17
+ const { getEditedEntityRecord } = select( coreStore );
18
+ const postRecord = getEditedEntityRecord( 'postType', postType, postId );
19
+ return {
20
+ bylineActive: postRecord?.meta?._newspack_byline_active || false,
21
+ bylineContent: postRecord?.meta?._newspack_byline || '',
22
+ };
23
+ },
24
+ [ postId, postType ]
25
+ );
26
+
27
+ return { bylineActive, bylineContent };
28
+ }
29
+
30
+ /**
31
+ * Extract author IDs from custom byline shortcode content.
32
+ *
33
+ * @param {string} bylineContent Byline content with [Author id=X]...[/Author] shortcodes.
34
+ * @return {Array} Array of author IDs (integers).
35
+ */
36
+ export function extractAuthorIdsFromByline( bylineContent ) {
37
+ if ( ! bylineContent ) {
38
+ return [];
39
+ }
40
+ const regex = /\[Author\s+id\s*=\s*(\d+)\]/gi;
41
+ const ids = [];
42
+ let match;
43
+ while ( ( match = regex.exec( bylineContent ) ) !== null ) {
44
+ ids.push( parseInt( match[ 1 ], 10 ) );
45
+ }
46
+ return ids;
47
+ }
@@ -0,0 +1,133 @@
1
+ /**
2
+ * External dependencies
3
+ */
4
+ import { renderHook } from '@testing-library/react';
5
+
6
+ /**
7
+ * WordPress dependencies
8
+ */
9
+ import { useSelect } from '@wordpress/data';
10
+
11
+ /**
12
+ * Internal dependencies
13
+ */
14
+ import { useCustomByline, extractAuthorIdsFromByline } from './use-custom-byline';
15
+
16
+ jest.mock( '@wordpress/data', () => ( {
17
+ useSelect: jest.fn(),
18
+ } ) );
19
+
20
+ jest.mock( '@wordpress/core-data', () => ( {
21
+ store: 'core',
22
+ } ) );
23
+
24
+ describe( 'useCustomByline', () => {
25
+ beforeEach( () => {
26
+ jest.clearAllMocks();
27
+ } );
28
+
29
+ it( 'should return inactive byline when meta is not set', () => {
30
+ useSelect.mockImplementation( callback =>
31
+ callback( () => ( {
32
+ getEditedEntityRecord: () => ( {} ),
33
+ } ) )
34
+ );
35
+
36
+ const { result } = renderHook( () => useCustomByline( 123, 'post' ) );
37
+
38
+ expect( result.current.bylineActive ).toBe( false );
39
+ expect( result.current.bylineContent ).toBe( '' );
40
+ } );
41
+
42
+ it( 'should return inactive byline when meta exists but byline is inactive', () => {
43
+ useSelect.mockImplementation( callback =>
44
+ callback( () => ( {
45
+ getEditedEntityRecord: () => ( {
46
+ meta: {
47
+ _newspack_byline_active: false,
48
+ _newspack_byline: 'Some content',
49
+ },
50
+ } ),
51
+ } ) )
52
+ );
53
+
54
+ const { result } = renderHook( () => useCustomByline( 123, 'post' ) );
55
+
56
+ expect( result.current.bylineActive ).toBe( false );
57
+ expect( result.current.bylineContent ).toBe( 'Some content' );
58
+ } );
59
+
60
+ it( 'should return active byline with content', () => {
61
+ const bylineContent = 'By [Author id=5]Jane Doe[/Author]';
62
+
63
+ useSelect.mockImplementation( callback =>
64
+ callback( () => ( {
65
+ getEditedEntityRecord: () => ( {
66
+ meta: {
67
+ _newspack_byline_active: true,
68
+ _newspack_byline: bylineContent,
69
+ },
70
+ } ),
71
+ } ) )
72
+ );
73
+
74
+ const { result } = renderHook( () => useCustomByline( 123, 'post' ) );
75
+
76
+ expect( result.current.bylineActive ).toBe( true );
77
+ expect( result.current.bylineContent ).toBe( bylineContent );
78
+ } );
79
+
80
+ it( 'should handle null post record gracefully', () => {
81
+ useSelect.mockImplementation( callback =>
82
+ callback( () => ( {
83
+ getEditedEntityRecord: () => null,
84
+ } ) )
85
+ );
86
+
87
+ const { result } = renderHook( () => useCustomByline( 123, 'post' ) );
88
+
89
+ expect( result.current.bylineActive ).toBe( false );
90
+ expect( result.current.bylineContent ).toBe( '' );
91
+ } );
92
+ } );
93
+
94
+ describe( 'extractAuthorIdsFromByline', () => {
95
+ it( 'should return empty array for empty content', () => {
96
+ expect( extractAuthorIdsFromByline( '' ) ).toEqual( [] );
97
+ expect( extractAuthorIdsFromByline( null ) ).toEqual( [] );
98
+ expect( extractAuthorIdsFromByline( undefined ) ).toEqual( [] );
99
+ } );
100
+
101
+ it( 'should extract single author ID', () => {
102
+ const byline = 'By [Author id=5]Jane Doe[/Author]';
103
+ expect( extractAuthorIdsFromByline( byline ) ).toEqual( [ 5 ] );
104
+ } );
105
+
106
+ it( 'should extract multiple author IDs', () => {
107
+ const byline = 'By [Author id=5]Jane Doe[/Author] and [Author id=12]John Smith[/Author]';
108
+ expect( extractAuthorIdsFromByline( byline ) ).toEqual( [ 5, 12 ] );
109
+ } );
110
+
111
+ it( 'should handle content with no author shortcodes', () => {
112
+ const byline = 'By Some Random Text';
113
+ expect( extractAuthorIdsFromByline( byline ) ).toEqual( [] );
114
+ } );
115
+
116
+ it( 'should handle byline with HTML entities', () => {
117
+ const byline = 'By [Author id=17]Darth Vader[/Author] with love';
118
+ expect( extractAuthorIdsFromByline( byline ) ).toEqual( [ 17 ] );
119
+ } );
120
+
121
+ it( 'should return integers, not strings', () => {
122
+ const byline = '[Author id=123]Test[/Author]';
123
+ const result = extractAuthorIdsFromByline( byline );
124
+ expect( result ).toEqual( [ 123 ] );
125
+ expect( typeof result[ 0 ] ).toBe( 'number' );
126
+ } );
127
+
128
+ it( 'should tolerate whitespace and case variations in shortcode', () => {
129
+ expect( extractAuthorIdsFromByline( '[author id=7]Name[/Author]' ) ).toEqual( [ 7 ] );
130
+ expect( extractAuthorIdsFromByline( '[Author id = 5]Name[/Author]' ) ).toEqual( [ 5 ] );
131
+ expect( extractAuthorIdsFromByline( '[AUTHOR ID=3]Name[/Author]' ) ).toEqual( [ 3 ] );
132
+ } );
133
+ } );
@@ -10,3 +10,57 @@
10
10
  position: absolute !important;
11
11
  word-wrap: normal !important; /* Many screen reader and browser combinations announce broken words as they would appear visually. */
12
12
  }
13
+
14
+ // Circular social button style
15
+ // Used by: author-profile-social, author-social-link blocks
16
+ // Hex fallbacks from newspack-block-theme theme.json defaults.
17
+ @mixin socialButton {
18
+ border-radius: 50%; // Circular
19
+ display: grid;
20
+ place-items: center;
21
+ text-decoration: none;
22
+
23
+ // Use :not(.wp-element-button) to beat wp-elements parent link colour rules,
24
+ // which use :where() and therefore have lower specificity.
25
+ &:not(.wp-element-button) {
26
+ background: var(--icon-background, var(--wp--preset--color--base-2, #f0f0f0));
27
+ color: var(--icon-color, var(--wp--preset--color--contrast, #1e1e1e));
28
+ }
29
+
30
+ @media not (prefers-reduced-motion) {
31
+ transition: transform 100ms ease;
32
+ }
33
+ // Sizing - makes circular by using equal width/height
34
+ // --icon-size controls the circle diameter (e.g., 36px)
35
+ // Icon inside is 2/3 of circle size (e.g., 24px icon in 36px circle)
36
+ aspect-ratio: 1;
37
+ width: calc(var(--icon-size, 24px) * 1.5);
38
+ height: calc(var(--icon-size, 24px) * 1.5);
39
+ padding: 0;
40
+
41
+ &:hover {
42
+ background: var(--icon-background, var(--wp--preset--color--base-2, #f0f0f0));
43
+ color: var(--icon-color, var(--wp--preset--color--contrast, #1e1e1e));
44
+ text-decoration: none;
45
+ transform: scale(1.1);
46
+ }
47
+
48
+ &:focus {
49
+ outline: none;
50
+ }
51
+
52
+ &:focus-visible {
53
+ outline: 2px solid;
54
+ outline-offset: 1px;
55
+ }
56
+
57
+ // SVG icon sizing - icon is 2/3 of button size
58
+ // If button is 36px, icon is 24px (24/36 = 2/3)
59
+ svg,
60
+ span {
61
+ display: block;
62
+ fill: currentcolor;
63
+ width: var(--icon-size, 24px);
64
+ height: var(--icon-size, 24px);
65
+ }
66
+ }
@@ -1,6 +1,3 @@
1
- /* eslint-disable jsx-a11y/click-events-have-key-events */
2
- /* eslint-disable jsx-a11y/no-static-element-interactions */
3
-
4
1
  /**
5
2
  * WordPress dependencies
6
3
  */
@@ -17,11 +14,15 @@ import classNames from 'classnames';
17
14
  */
18
15
  import './style.scss';
19
16
 
20
- const Accordion = ( { children, title } ) => {
21
- const [ isOpen, setIsOpen ] = useState( false );
17
+ const Accordion = ( { children, title, defaultOpen = false } ) => {
18
+ const [ isOpen, setIsOpen ] = useState( defaultOpen );
22
19
  return (
23
- <details className={ classNames( 'newspack-accordion', { 'newspack-accordion--is-open': isOpen } ) }>
24
- <summary onClick={ () => setIsOpen( ! isOpen ) }>
20
+ <details
21
+ className={ classNames( 'newspack-accordion', { 'newspack-accordion--is-open': isOpen } ) }
22
+ open={ isOpen }
23
+ onToggle={ e => setIsOpen( e.currentTarget.open ) }
24
+ >
25
+ <summary>
25
26
  { title }
26
27
  <Icon className="newspack-accordion__icon" icon={ chevronRight } size={ 24 } />
27
28
  </summary>
@@ -6,6 +6,7 @@ export interface ActionCardProps {
6
6
  description?: string | React.ReactNode;
7
7
  actionText?: React.ReactNode | string | null;
8
8
  badge?: string;
9
+ badgeLevel?: 'success' | 'info' | 'warning' | 'error';
9
10
  className?: string;
10
11
  indent?: string;
11
12
  notification?: string | Error | null;
@@ -13,8 +14,10 @@ export interface ActionCardProps {
13
14
  isMedium?: boolean;
14
15
  disabled?: boolean | string;
15
16
  hasGreyHeader?: boolean;
17
+ heading?: number;
16
18
  toggleChecked?: boolean;
17
- toggleOnChange?: ( a: boolean ) => void;
19
+ toggleOnChange?: (a: boolean) => void;
20
+ togglePosition?: 'leading' | 'trailing';
18
21
  actionContent?: boolean | React.ReactNode | null;
19
22
  error?: Error | string | null;
20
23
  handoff?: string | null;
@@ -33,8 +36,9 @@ export interface ActionCardProps {
33
36
  noMargin?: boolean;
34
37
  collapse?: boolean;
35
38
  expandable?: boolean;
39
+ isExpanded?: boolean;
36
40
  draggable?: boolean;
37
41
  dragIndex?: number;
38
- onDragCallback?: ( index: number ) => void;
39
- dragWrapperRef?: React.MutableRefObject< HTMLDivElement | null >;
42
+ onDragCallback?: (index: number) => void;
43
+ dragWrapperRef?: React.MutableRefObject<HTMLDivElement | null>;
40
44
  }
@@ -29,15 +29,19 @@ import classnames from 'classnames';
29
29
  */
30
30
  const ActionCard = ( {
31
31
  badge,
32
- badgeLevel = 'info',
32
+ badgeLevel,
33
33
  className,
34
34
  checkbox,
35
35
  children,
36
36
  collapse,
37
37
  disabled,
38
38
  title,
39
+ heading = 2,
39
40
  description,
40
41
  handoff,
42
+ handoffUrl,
43
+ bannerText,
44
+ bannerButtonText,
41
45
  editLink,
42
46
  href,
43
47
  notification,
@@ -60,11 +64,13 @@ const ActionCard = ( {
60
64
  titleLink,
61
65
  toggleChecked = false,
62
66
  toggleOnChange,
67
+ togglePosition = 'leading',
63
68
  hasGreyHeader,
64
69
  hasWhiteHeader,
65
70
  noBorder,
66
71
  isPending,
67
72
  expandable = false,
73
+ isExpanded,
68
74
  isButtonEnabled = false,
69
75
  // Draggable props. All are required to enable drag sorting.
70
76
  draggable = false,
@@ -72,11 +78,17 @@ const ActionCard = ( {
72
78
  dragWrapperRef,
73
79
  onDragCallback,
74
80
  } ) => {
75
- const [ expanded, setExpanded ] = useState( false );
81
+ const [ expanded, setExpanded ] = useState( Boolean( isExpanded ) );
76
82
  const [ dragging, setDragging ] = useState( false );
77
83
  const [ targetIndex, setTargetIndex ] = useState( null );
78
84
  const [ dragRef, setDragRef ] = useState( null );
79
85
 
86
+ useEffect( () => {
87
+ if ( typeof isExpanded === 'boolean' ) {
88
+ setExpanded( isExpanded );
89
+ }
90
+ }, [ isExpanded ] );
91
+
80
92
  useEffect( () => {
81
93
  if ( dragWrapperRef && ! dragRef ) {
82
94
  setDragRef( dragWrapperRef );
@@ -109,14 +121,18 @@ const ActionCard = ( {
109
121
  return url ? { backgroundImage: `url(${ url })` } : {};
110
122
  };
111
123
  const titleProps = toggleOnChange && ! titleLink && ! disabled ? { onClick: () => toggleOnChange( ! toggleChecked ), tabIndex: '0' } : {};
124
+ const togglePositionClass = togglePosition === 'trailing' ? 'is-toggle-trailing' : 'is-toggle-leading';
112
125
  const hasInternalLink = href && href.indexOf( 'http' ) !== 0;
113
126
  const isDisplayingSecondaryAction = secondaryActionText && onSecondaryActionClick;
114
127
  const badges = ! Array.isArray( badge ) && badge ? [ badge ] : badge;
128
+ const HeadingTag = `h${ heading }`;
115
129
 
116
- const Component = () => (
130
+ const cardContent = (
117
131
  <>
118
132
  <div className="newspack-action-card__region newspack-action-card__region-top">
119
- { toggleOnChange && <ToggleControl checked={ toggleChecked } onChange={ toggleOnChange } disabled={ disabled } /> }
133
+ { toggleOnChange && (
134
+ <ToggleControl checked={ toggleChecked } onChange={ toggleOnChange } disabled={ disabled } className={ togglePositionClass } />
135
+ ) }
120
136
  { image && ! toggleOnChange && (
121
137
  <div className="newspack-action-card__region newspack-action-card__region-left">
122
138
  <a href={ imageLink }>
@@ -140,7 +156,7 @@ const ActionCard = ( {
140
156
  ) }
141
157
  <div className="newspack-action-card__region newspack-action-card__region-center">
142
158
  <Grid columns={ 1 } gutter={ 8 } noMargin>
143
- <h2>
159
+ <HeadingTag>
144
160
  <span className="newspack-action-card__title" { ...titleProps }>
145
161
  { titleLink && <a href={ titleLink }>{ title }</a> }
146
162
  { ! titleLink && expandable && (
@@ -159,7 +175,7 @@ const ActionCard = ( {
159
175
  { badgeText }
160
176
  </span>
161
177
  ) ) }
162
- </h2>
178
+ </HeadingTag>
163
179
  { description && (
164
180
  <p>
165
181
  { typeof description === 'string' && description }
@@ -174,7 +190,18 @@ const ActionCard = ( {
174
190
  { actionContent && actionContent }
175
191
  { actionText &&
176
192
  ( handoff ? (
177
- <Handoff plugin={ handoff } editLink={ editLink } compact isLink>
193
+ <Handoff
194
+ plugin={ handoff }
195
+ editLink={ editLink }
196
+ bannerText={ bannerText }
197
+ bannerButtonText={ bannerButtonText }
198
+ compact
199
+ isLink
200
+ >
201
+ { actionText }
202
+ </Handoff>
203
+ ) : handoffUrl ? (
204
+ <Handoff url={ handoffUrl } bannerText={ bannerText } bannerButtonText={ bannerButtonText } compact isLink>
178
205
  { actionText }
179
206
  </Handoff>
180
207
  ) : onClick || hasInternalLink ? (
@@ -224,9 +251,9 @@ const ActionCard = ( {
224
251
  { 'warning' === notificationLevel && <Notice noticeText={ notification } isWarning rawHTML={ notificationHTML } /> }
225
252
  </div>
226
253
  ) }
227
- { children && ( ( expandable && expanded ) || ! expandable ) && (
254
+ { children && ( ( expandable && expanded ) || ! expandable ) ? (
228
255
  <div className="newspack-action-card__region-children">{ children }</div>
229
- ) }
256
+ ) : null }
230
257
  </>
231
258
  );
232
259
 
@@ -289,7 +316,7 @@ const ActionCard = ( {
289
316
  <Card className={ classes } onClick={ simple && onClick } id={ id ?? null } noBorder={ noBorder }>
290
317
  <div className="newspack-action-card__draggable-controls">
291
318
  <div className="drag-handle" draggable onDragStart={ onDraggableStart } onDragEnd={ onDraggableEnd }>
292
- <Icon icon={ dragHandle } height={ 18 } width={ 18 } />
319
+ <Icon icon={ dragHandle } />
293
320
  </div>
294
321
  <div className="movers">
295
322
  <Button
@@ -306,7 +333,7 @@ const ActionCard = ( {
306
333
  />
307
334
  </div>
308
335
  </div>
309
- <Component />
336
+ { cardContent }
310
337
  </Card>
311
338
  ) }
312
339
  </Draggable>
@@ -316,7 +343,7 @@ const ActionCard = ( {
316
343
 
317
344
  return (
318
345
  <Card className={ classes } onClick={ simple && onClick } id={ id ?? null } noBorder={ noBorder }>
319
- <Component />
346
+ { cardContent }
320
347
  </Card>
321
348
  );
322
349
  };
@@ -260,6 +260,16 @@
260
260
  @media screen and (min-width: 744px) {
261
261
  margin-bottom: 0;
262
262
  margin-right: 24px;
263
+
264
+ &.is-toggle-trailing {
265
+ margin-left: 24px;
266
+ margin-right: 0;
267
+ order: 999;
268
+ }
269
+ }
270
+
271
+ label {
272
+ display: none;
263
273
  }
264
274
 
265
275
  .components-base-control__field {
@@ -433,13 +443,21 @@
433
443
  text-decoration: inherit;
434
444
  }
435
445
 
436
- .newspack-action-card__region-children {
437
- border-top: 1px solid wp-colors.$gray-100;
446
+ > .newspack-action-card__region-children {
447
+ border-top: 1px solid wp-colors.$gray-300;
438
448
  padding-top: 24px;
439
449
 
440
450
  + .newspack-action-card__region-children {
441
451
  border-top: none;
442
452
  }
453
+
454
+ > * + * {
455
+ margin-top: 24px !important;
456
+ }
457
+
458
+ .newspack-action-card__region-children:has(> div:not(:empty)) {
459
+ margin-top: 16px;
460
+ }
443
461
  }
444
462
 
445
463
  &.is-medium .newspack-action-card__region-children {
@@ -469,6 +487,11 @@
469
487
  margin-left: 8px;
470
488
  }
471
489
  }
490
+
491
+ hr:not(.newspack-divider) {
492
+ background: wp-colors.$gray-100;
493
+ margin: 0;
494
+ }
472
495
  }
473
496
 
474
497
  .newspack-card {
@@ -120,7 +120,7 @@ class AutocompleteTokenField extends Component {
120
120
  Object.keys( validValues ).forEach( key => {
121
121
  if ( validValues[ key ] === label ) {
122
122
  // Preserve numeric or string type of values. Object.keys will convert numbers to strings.
123
- const value = isNaN( parseInt( key ) ) ? key.toString() : parseInt( key );
123
+ const value = /^\d+$/.test( key.toString() ) ? parseInt( key ) : key.toString();
124
124
  acc.push( { value, label } );
125
125
  }
126
126
  } );
@@ -131,7 +131,7 @@ class AutocompleteTokenField extends Component {
131
131
 
132
132
  return labels.map( label =>
133
133
  Object.keys( validValues )
134
- .map( key => ( isNaN( parseInt( key ) ) ? key.toString() : parseInt( key ) ) )
134
+ .map( key => ( /^\d+$/.test( key.toString() ) ? parseInt( key ) : key.toString() ) )
135
135
  .find( key => validValues[ key ] === label )
136
136
  );
137
137
  }