@wordpress/block-library 9.31.1-next.f56bd8138.0 → 9.32.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (215) hide show
  1. package/CHANGELOG.md +2 -0
  2. package/build/accordion/edit.js +62 -12
  3. package/build/accordion/edit.js.map +1 -1
  4. package/build/accordion/index.js +15 -8
  5. package/build/accordion/index.js.map +1 -1
  6. package/build/accordion/view.js +15 -15
  7. package/build/accordion/view.js.map +1 -1
  8. package/build/accordion-heading/edit.js +68 -0
  9. package/build/accordion-heading/edit.js.map +1 -0
  10. package/build/accordion-heading/icon.js.map +1 -0
  11. package/build/{accordion-header → accordion-heading}/index.js +9 -17
  12. package/build/accordion-heading/index.js.map +1 -0
  13. package/build/{accordion-content → accordion-heading}/init.js.map +1 -1
  14. package/build/{accordion-header → accordion-heading}/save.js +7 -20
  15. package/build/accordion-heading/save.js.map +1 -0
  16. package/build/{accordion-content → accordion-item}/edit.js +8 -2
  17. package/build/accordion-item/edit.js.map +1 -0
  18. package/build/accordion-item/icon.js.map +1 -0
  19. package/build/{accordion-content → accordion-item}/index.js +6 -5
  20. package/build/accordion-item/index.js.map +1 -0
  21. package/build/accordion-item/init.js.map +1 -0
  22. package/build/{accordion-content → accordion-item}/save.js +5 -7
  23. package/build/accordion-item/save.js.map +1 -0
  24. package/build/accordion-panel/edit.js +2 -1
  25. package/build/accordion-panel/edit.js.map +1 -1
  26. package/build/accordion-panel/index.js +5 -3
  27. package/build/accordion-panel/index.js.map +1 -1
  28. package/build/accordion-panel/save.js +3 -1
  29. package/build/accordion-panel/save.js.map +1 -1
  30. package/build/block/index.js +1 -0
  31. package/build/block/index.js.map +1 -1
  32. package/build/group/variations.js +3 -4
  33. package/build/group/variations.js.map +1 -1
  34. package/build/index.js +24 -4
  35. package/build/index.js.map +1 -1
  36. package/build/navigation/edit/leaf-more-menu.js +6 -1
  37. package/build/navigation/edit/leaf-more-menu.js.map +1 -1
  38. package/build/navigation-link/edit.js +3 -140
  39. package/build/navigation-link/edit.js.map +1 -1
  40. package/build/navigation-link/shared/controls.js +171 -0
  41. package/build/navigation-link/shared/controls.js.map +1 -0
  42. package/build/navigation-link/shared/index.js +13 -0
  43. package/build/navigation-link/shared/index.js.map +1 -0
  44. package/build/navigation-submenu/edit.js +7 -114
  45. package/build/navigation-submenu/edit.js.map +1 -1
  46. package/build/pattern/index.js +1 -0
  47. package/build/pattern/index.js.map +1 -1
  48. package/build/post-date/edit.js +16 -3
  49. package/build/post-date/edit.js.map +1 -1
  50. package/build/post-time-to-read/edit.js +27 -15
  51. package/build/post-time-to-read/edit.js.map +1 -1
  52. package/build/post-time-to-read/index.js +7 -1
  53. package/build/post-time-to-read/index.js.map +1 -1
  54. package/build/post-time-to-read/variations.js +41 -0
  55. package/build/post-time-to-read/variations.js.map +1 -0
  56. package/build/query-title/edit.js +1 -1
  57. package/build/query-title/edit.js.map +1 -1
  58. package/build/table-of-contents/index.js +1 -0
  59. package/build/table-of-contents/index.js.map +1 -1
  60. package/build/template-part/index.js +1 -0
  61. package/build/template-part/index.js.map +1 -1
  62. package/build/utils/get-transformed-metadata.js +7 -0
  63. package/build/utils/get-transformed-metadata.js.map +1 -1
  64. package/build-module/accordion/edit.js +66 -16
  65. package/build-module/accordion/edit.js.map +1 -1
  66. package/build-module/accordion/index.js +15 -8
  67. package/build-module/accordion/index.js.map +1 -1
  68. package/build-module/accordion/view.js +15 -15
  69. package/build-module/accordion/view.js.map +1 -1
  70. package/build-module/accordion-heading/edit.js +61 -0
  71. package/build-module/accordion-heading/edit.js.map +1 -0
  72. package/build-module/accordion-heading/icon.js.map +1 -0
  73. package/build-module/{accordion-header → accordion-heading}/index.js +9 -17
  74. package/build-module/accordion-heading/index.js.map +1 -0
  75. package/build-module/{accordion-content → accordion-heading}/init.js.map +1 -1
  76. package/build-module/{accordion-header → accordion-heading}/save.js +7 -18
  77. package/build-module/accordion-heading/save.js.map +1 -0
  78. package/build-module/{accordion-content → accordion-item}/edit.js +8 -2
  79. package/build-module/accordion-item/edit.js.map +1 -0
  80. package/build-module/accordion-item/icon.js.map +1 -0
  81. package/build-module/{accordion-content → accordion-item}/index.js +6 -5
  82. package/build-module/accordion-item/index.js.map +1 -0
  83. package/build-module/accordion-item/init.js.map +1 -0
  84. package/build-module/{accordion-content → accordion-item}/save.js +5 -7
  85. package/build-module/accordion-item/save.js.map +1 -0
  86. package/build-module/accordion-panel/edit.js +2 -1
  87. package/build-module/accordion-panel/edit.js.map +1 -1
  88. package/build-module/accordion-panel/index.js +5 -3
  89. package/build-module/accordion-panel/index.js.map +1 -1
  90. package/build-module/accordion-panel/save.js +3 -1
  91. package/build-module/accordion-panel/save.js.map +1 -1
  92. package/build-module/block/index.js +1 -0
  93. package/build-module/block/index.js.map +1 -1
  94. package/build-module/group/variations.js +3 -4
  95. package/build-module/group/variations.js.map +1 -1
  96. package/build-module/index.js +25 -5
  97. package/build-module/index.js.map +1 -1
  98. package/build-module/navigation/edit/leaf-more-menu.js +6 -1
  99. package/build-module/navigation/edit/leaf-more-menu.js.map +1 -1
  100. package/build-module/navigation-link/edit.js +4 -141
  101. package/build-module/navigation-link/edit.js.map +1 -1
  102. package/build-module/navigation-link/shared/controls.js +165 -0
  103. package/build-module/navigation-link/shared/controls.js.map +1 -0
  104. package/build-module/navigation-link/shared/index.js +9 -0
  105. package/build-module/navigation-link/shared/index.js.map +1 -0
  106. package/build-module/navigation-submenu/edit.js +7 -114
  107. package/build-module/navigation-submenu/edit.js.map +1 -1
  108. package/build-module/pattern/index.js +1 -0
  109. package/build-module/pattern/index.js.map +1 -1
  110. package/build-module/post-date/edit.js +17 -4
  111. package/build-module/post-date/edit.js.map +1 -1
  112. package/build-module/post-time-to-read/edit.js +27 -15
  113. package/build-module/post-time-to-read/edit.js.map +1 -1
  114. package/build-module/post-time-to-read/index.js +7 -1
  115. package/build-module/post-time-to-read/index.js.map +1 -1
  116. package/build-module/post-time-to-read/variations.js +33 -0
  117. package/build-module/post-time-to-read/variations.js.map +1 -0
  118. package/build-module/query-title/edit.js +1 -1
  119. package/build-module/query-title/edit.js.map +1 -1
  120. package/build-module/table-of-contents/index.js +1 -0
  121. package/build-module/table-of-contents/index.js.map +1 -1
  122. package/build-module/template-part/index.js +1 -0
  123. package/build-module/template-part/index.js.map +1 -1
  124. package/build-module/utils/get-transformed-metadata.js +7 -0
  125. package/build-module/utils/get-transformed-metadata.js.map +1 -1
  126. package/build-style/{accordion → accordion-heading}/style-rtl.css +9 -54
  127. package/build-style/{accordion → accordion-heading}/style.css +9 -54
  128. package/build-style/accordion-item/style-rtl.css +155 -0
  129. package/build-style/accordion-item/style.css +155 -0
  130. package/build-style/accordion-panel/style-rtl.css +140 -0
  131. package/build-style/accordion-panel/style.css +140 -0
  132. package/build-style/style-rtl.css +23 -42
  133. package/build-style/style.css +23 -42
  134. package/package.json +35 -35
  135. package/src/accordion/block.json +11 -4
  136. package/src/accordion/edit.js +70 -13
  137. package/src/accordion/index.js +4 -4
  138. package/src/accordion/index.php +1 -1
  139. package/src/accordion/view.js +15 -15
  140. package/src/{accordion-header → accordion-heading}/block.json +10 -17
  141. package/src/accordion-heading/edit.js +70 -0
  142. package/src/{accordion-header → accordion-heading}/save.js +8 -18
  143. package/src/accordion-heading/style.scss +43 -0
  144. package/src/{accordion-content → accordion-item}/block.json +6 -5
  145. package/src/{accordion-content → accordion-item}/edit.js +12 -2
  146. package/src/{accordion-content → accordion-item}/index.php +11 -11
  147. package/src/{accordion-content → accordion-item}/save.js +4 -10
  148. package/src/accordion-item/style.scss +21 -0
  149. package/src/accordion-panel/block.json +5 -3
  150. package/src/accordion-panel/edit.js +1 -0
  151. package/src/accordion-panel/save.js +3 -1
  152. package/src/accordion-panel/style.scss +8 -0
  153. package/src/block/block.json +1 -0
  154. package/src/comments/index.php +2 -2
  155. package/src/group/variations.js +3 -14
  156. package/src/index.js +23 -4
  157. package/src/navigation/edit/leaf-more-menu.js +9 -1
  158. package/src/navigation/index.php +2 -2
  159. package/src/navigation-link/edit.js +3 -142
  160. package/src/navigation-link/shared/README.md +47 -0
  161. package/src/navigation-link/shared/controls.js +167 -0
  162. package/src/navigation-link/shared/index.js +8 -0
  163. package/src/navigation-link/shared/test/controls.js +210 -0
  164. package/src/navigation-submenu/edit.js +8 -129
  165. package/src/pattern/block.json +1 -0
  166. package/src/post-date/edit.js +16 -16
  167. package/src/post-time-to-read/block.json +5 -1
  168. package/src/post-time-to-read/edit.js +87 -59
  169. package/src/post-time-to-read/index.js +2 -0
  170. package/src/post-time-to-read/index.php +48 -23
  171. package/src/post-time-to-read/variations.js +39 -0
  172. package/src/query-title/edit.js +2 -1
  173. package/src/query-title/index.php +3 -1
  174. package/src/social-link/index.php +2 -2
  175. package/src/style.scss +3 -1
  176. package/src/table-of-contents/block.json +1 -0
  177. package/src/table-of-contents/index.php +44 -0
  178. package/src/template-part/block.json +1 -0
  179. package/src/utils/get-transformed-metadata.js +8 -0
  180. package/build/accordion-content/edit.js.map +0 -1
  181. package/build/accordion-content/icon.js.map +0 -1
  182. package/build/accordion-content/index.js.map +0 -1
  183. package/build/accordion-content/save.js.map +0 -1
  184. package/build/accordion-header/edit.js +0 -94
  185. package/build/accordion-header/edit.js.map +0 -1
  186. package/build/accordion-header/icon.js.map +0 -1
  187. package/build/accordion-header/index.js.map +0 -1
  188. package/build/accordion-header/init.js.map +0 -1
  189. package/build/accordion-header/save.js.map +0 -1
  190. package/build-module/accordion-content/edit.js.map +0 -1
  191. package/build-module/accordion-content/icon.js.map +0 -1
  192. package/build-module/accordion-content/index.js.map +0 -1
  193. package/build-module/accordion-content/save.js.map +0 -1
  194. package/build-module/accordion-header/edit.js +0 -85
  195. package/build-module/accordion-header/edit.js.map +0 -1
  196. package/build-module/accordion-header/icon.js.map +0 -1
  197. package/build-module/accordion-header/index.js.map +0 -1
  198. package/build-module/accordion-header/init.js.map +0 -1
  199. package/build-module/accordion-header/save.js.map +0 -1
  200. package/src/accordion/style.scss +0 -90
  201. package/src/accordion-header/edit.js +0 -94
  202. /package/build/{accordion-header → accordion-heading}/icon.js +0 -0
  203. /package/build/{accordion-content → accordion-heading}/init.js +0 -0
  204. /package/build/{accordion-content → accordion-item}/icon.js +0 -0
  205. /package/build/{accordion-header → accordion-item}/init.js +0 -0
  206. /package/build-module/{accordion-header → accordion-heading}/icon.js +0 -0
  207. /package/build-module/{accordion-content → accordion-heading}/init.js +0 -0
  208. /package/build-module/{accordion-content → accordion-item}/icon.js +0 -0
  209. /package/build-module/{accordion-header → accordion-item}/init.js +0 -0
  210. /package/src/{accordion-header → accordion-heading}/icon.js +0 -0
  211. /package/src/{accordion-content → accordion-heading}/index.js +0 -0
  212. /package/src/{accordion-content → accordion-heading}/init.js +0 -0
  213. /package/src/{accordion-content → accordion-item}/icon.js +0 -0
  214. /package/src/{accordion-header → accordion-item}/index.js +0 -0
  215. /package/src/{accordion-header → accordion-item}/init.js +0 -0
@@ -0,0 +1,210 @@
1
+ /**
2
+ * @jest-environment jsdom
3
+ */
4
+
5
+ /**
6
+ * External dependencies
7
+ */
8
+ import { render, screen, fireEvent } from '@testing-library/react';
9
+
10
+ /**
11
+ * Internal dependencies
12
+ */
13
+ import { Controls } from '../controls';
14
+
15
+ // Mock the updateAttributes function
16
+ let mockUpdateAttributes;
17
+ jest.mock( '../../update-attributes', () => ( {
18
+ updateAttributes: ( ...args ) => mockUpdateAttributes( ...args ),
19
+ } ) );
20
+
21
+ // Mock the useToolsPanelDropdownMenuProps hook
22
+ jest.mock( '../../../utils/hooks', () => ( {
23
+ useToolsPanelDropdownMenuProps: () => ( {} ),
24
+ } ) );
25
+
26
+ describe( 'Controls', () => {
27
+ // Initialize the mock function
28
+ beforeAll( () => {
29
+ mockUpdateAttributes = jest.fn();
30
+ } );
31
+
32
+ const defaultProps = {
33
+ attributes: {
34
+ label: 'Test Link',
35
+ url: 'https://example.com',
36
+ description: 'Test description',
37
+ rel: 'nofollow',
38
+ opensInNewTab: false,
39
+ },
40
+ setAttributes: jest.fn(),
41
+ setIsEditingControl: jest.fn(),
42
+ };
43
+
44
+ beforeEach( () => {
45
+ jest.clearAllMocks();
46
+ mockUpdateAttributes.mockClear();
47
+ } );
48
+
49
+ it( 'renders all form controls', () => {
50
+ render( <Controls { ...defaultProps } /> );
51
+
52
+ expect( screen.getByLabelText( 'Text' ) ).toBeInTheDocument();
53
+ expect( screen.getByLabelText( 'Link' ) ).toBeInTheDocument();
54
+ expect(
55
+ screen.getByLabelText( 'Open in new tab' )
56
+ ).toBeInTheDocument();
57
+ expect( screen.getByLabelText( 'Description' ) ).toBeInTheDocument();
58
+ expect( screen.getByLabelText( 'Rel attribute' ) ).toBeInTheDocument();
59
+ } );
60
+
61
+ it( 'strips HTML from label values', () => {
62
+ const propsWithHtml = {
63
+ ...defaultProps,
64
+ attributes: {
65
+ ...defaultProps.attributes,
66
+ label: '<strong>Bold Text</strong>',
67
+ },
68
+ };
69
+ render( <Controls { ...propsWithHtml } /> );
70
+
71
+ const textInput = screen.getByLabelText( 'Text' );
72
+ expect( textInput.value ).toBe( 'Bold Text' );
73
+ } );
74
+
75
+ it( 'decodes URL values for display', () => {
76
+ const propsWithEncodedUrl = {
77
+ ...defaultProps,
78
+ attributes: {
79
+ ...defaultProps.attributes,
80
+ url: 'https://example.com/test%20page',
81
+ },
82
+ };
83
+ render( <Controls { ...propsWithEncodedUrl } /> );
84
+
85
+ const urlInput = screen.getByLabelText( 'Link' );
86
+ expect( urlInput.value ).toBe( 'https://example.com/test page' );
87
+ } );
88
+
89
+ it( 'encodes URL values when changed', () => {
90
+ render( <Controls { ...defaultProps } /> );
91
+
92
+ const urlInput = screen.getByLabelText( 'Link' );
93
+
94
+ fireEvent.change( urlInput, {
95
+ target: { value: 'https://example.com/test page' },
96
+ } );
97
+
98
+ expect( defaultProps.setAttributes ).toHaveBeenCalledWith( {
99
+ url: 'https://example.com/test%20page',
100
+ } );
101
+ } );
102
+
103
+ it( 'calls updateAttributes on URL blur', () => {
104
+ render( <Controls { ...defaultProps } /> );
105
+
106
+ const urlInput = screen.getByLabelText( 'Link' );
107
+
108
+ fireEvent.focus( urlInput );
109
+ fireEvent.blur( urlInput );
110
+
111
+ expect( mockUpdateAttributes ).toHaveBeenCalledWith(
112
+ { url: 'https://example.com' },
113
+ defaultProps.setAttributes,
114
+ { ...defaultProps.attributes, url: 'https://example.com' }
115
+ );
116
+ } );
117
+
118
+ it( 'stores last URL value on focus and uses it in updateAttributes', () => {
119
+ const propsWithDifferentUrl = {
120
+ ...defaultProps,
121
+ attributes: {
122
+ ...defaultProps.attributes,
123
+ url: 'https://different.com',
124
+ },
125
+ };
126
+ render( <Controls { ...propsWithDifferentUrl } /> );
127
+
128
+ const urlInput = screen.getByLabelText( 'Link' );
129
+
130
+ fireEvent.focus( urlInput );
131
+
132
+ // Change the URL
133
+ fireEvent.change( urlInput, {
134
+ target: { value: 'https://new.com' },
135
+ } );
136
+
137
+ // Blur should call updateAttributes with the current URL (since url exists)
138
+ fireEvent.blur( urlInput );
139
+
140
+ expect( mockUpdateAttributes ).toHaveBeenCalledWith(
141
+ { url: 'https://different.com' }, // Current URL from attributes (not input value)
142
+ defaultProps.setAttributes,
143
+ {
144
+ ...propsWithDifferentUrl.attributes,
145
+ url: 'https://different.com',
146
+ } // lastURLRef.current
147
+ );
148
+ } );
149
+
150
+ it( 'calls setIsEditingControl on focus and blur for all inputs', () => {
151
+ render( <Controls { ...defaultProps } /> );
152
+
153
+ const textInput = screen.getByLabelText( 'Text' );
154
+ const urlInput = screen.getByLabelText( 'Link' );
155
+
156
+ // Test text input
157
+ fireEvent.focus( textInput );
158
+ expect( defaultProps.setIsEditingControl ).toHaveBeenCalledWith( true );
159
+
160
+ fireEvent.blur( textInput );
161
+ expect( defaultProps.setIsEditingControl ).toHaveBeenCalledWith(
162
+ false
163
+ );
164
+
165
+ // Test URL input
166
+ fireEvent.focus( urlInput );
167
+ expect( defaultProps.setIsEditingControl ).toHaveBeenCalledWith( true );
168
+
169
+ fireEvent.blur( urlInput );
170
+ expect( defaultProps.setIsEditingControl ).toHaveBeenCalledWith(
171
+ false
172
+ );
173
+ } );
174
+
175
+ it( 'handles all form field changes correctly', () => {
176
+ render( <Controls { ...defaultProps } /> );
177
+
178
+ // Test text change
179
+ const textInput = screen.getByLabelText( 'Text' );
180
+ fireEvent.change( textInput, { target: { value: 'New Label' } } );
181
+ expect( defaultProps.setAttributes ).toHaveBeenCalledWith( {
182
+ label: 'New Label',
183
+ } );
184
+
185
+ // Test description change
186
+ const descriptionInput = screen.getByLabelText( 'Description' );
187
+ fireEvent.change( descriptionInput, {
188
+ target: { value: 'New Description' },
189
+ } );
190
+ expect( defaultProps.setAttributes ).toHaveBeenCalledWith( {
191
+ description: 'New Description',
192
+ } );
193
+
194
+ // Test rel change
195
+ const relInput = screen.getByLabelText( 'Rel attribute' );
196
+ fireEvent.change( relInput, {
197
+ target: { value: 'nofollow noopener' },
198
+ } );
199
+ expect( defaultProps.setAttributes ).toHaveBeenCalledWith( {
200
+ rel: 'nofollow noopener',
201
+ } );
202
+
203
+ // Test checkbox change
204
+ const checkbox = screen.getByLabelText( 'Open in new tab' );
205
+ fireEvent.click( checkbox );
206
+ expect( defaultProps.setAttributes ).toHaveBeenCalledWith( {
207
+ opensInNewTab: true,
208
+ } );
209
+ } );
210
+ } );
@@ -7,15 +7,7 @@ import clsx from 'clsx';
7
7
  * WordPress dependencies
8
8
  */
9
9
  import { useSelect, useDispatch } from '@wordpress/data';
10
- import {
11
- CheckboxControl,
12
- TextControl,
13
- TextareaControl,
14
- ToolbarButton,
15
- ToolbarGroup,
16
- __experimentalToolsPanel as ToolsPanel,
17
- __experimentalToolsPanelItem as ToolsPanelItem,
18
- } from '@wordpress/components';
10
+ import { ToolbarButton, ToolbarGroup } from '@wordpress/components';
19
11
  import { displayShortcut, isKeyboardEvent } from '@wordpress/keycodes';
20
12
  import { __ } from '@wordpress/i18n';
21
13
  import {
@@ -41,11 +33,12 @@ import { useMergeRefs, usePrevious } from '@wordpress/compose';
41
33
  import { ItemSubmenuIcon } from './icons';
42
34
  import { LinkUI } from '../navigation-link/link-ui';
43
35
  import { updateAttributes } from '../navigation-link/update-attributes';
36
+ import { Controls } from '../navigation-link/shared';
44
37
  import {
45
38
  getColors,
46
39
  getNavigationChildBlockProps,
47
40
  } from '../navigation/edit/utils';
48
- import { useToolsPanelDropdownMenuProps } from '../utils/hooks';
41
+ import { DEFAULT_BLOCK } from '../navigation/constants';
49
42
 
50
43
  const ALLOWED_BLOCKS = [
51
44
  'core/navigation-link',
@@ -53,10 +46,6 @@ const ALLOWED_BLOCKS = [
53
46
  'core/page-list',
54
47
  ];
55
48
 
56
- const DEFAULT_BLOCK = {
57
- name: 'core/navigation-link',
58
- };
59
-
60
49
  /**
61
50
  * A React hook to determine if it's dragging within the target element.
62
51
  *
@@ -135,7 +124,7 @@ export default function NavigationSubmenuEdit( {
135
124
  context,
136
125
  clientId,
137
126
  } ) {
138
- const { label, url, description, rel, opensInNewTab } = attributes;
127
+ const { label, url, description } = attributes;
139
128
 
140
129
  const { showSubmenuIcon, maxNestingLevel, openSubmenusOnClick } = context;
141
130
 
@@ -154,7 +143,6 @@ export default function NavigationSubmenuEdit( {
154
143
  const isDraggingWithin = useIsDraggingWithin( listItemRef );
155
144
  const itemLabelPlaceholder = __( 'Add text…' );
156
145
  const ref = useRef();
157
- const dropdownMenuProps = useToolsPanelDropdownMenuProps();
158
146
 
159
147
  const {
160
148
  parentCount,
@@ -383,120 +371,11 @@ export default function NavigationSubmenuEdit( {
383
371
  />
384
372
  </ToolbarGroup>
385
373
  </BlockControls>
386
- { /* Warning, this duplicated in packages/block-library/src/navigation-link/edit.js */ }
387
374
  <InspectorControls>
388
- <ToolsPanel
389
- label={ __( 'Settings' ) }
390
- resetAll={ () => {
391
- setAttributes( {
392
- label: '',
393
- url: '',
394
- description: '',
395
- rel: '',
396
- opensInNewTab: false,
397
- } );
398
- } }
399
- dropdownMenuProps={ dropdownMenuProps }
400
- >
401
- <ToolsPanelItem
402
- label={ __( 'Text' ) }
403
- isShownByDefault
404
- hasValue={ () => !! label }
405
- onDeselect={ () => setAttributes( { label: '' } ) }
406
- >
407
- <TextControl
408
- __nextHasNoMarginBottom
409
- __next40pxDefaultSize
410
- value={ label || '' }
411
- onChange={ ( labelValue ) => {
412
- setAttributes( { label: labelValue } );
413
- } }
414
- label={ __( 'Text' ) }
415
- autoComplete="off"
416
- />
417
- </ToolsPanelItem>
418
-
419
- <ToolsPanelItem
420
- label={ __( 'Link' ) }
421
- isShownByDefault
422
- hasValue={ () => !! url }
423
- onDeselect={ () => setAttributes( { url: '' } ) }
424
- >
425
- <TextControl
426
- __nextHasNoMarginBottom
427
- __next40pxDefaultSize
428
- value={ url || '' }
429
- onChange={ ( urlValue ) => {
430
- setAttributes( { url: urlValue } );
431
- } }
432
- label={ __( 'Link' ) }
433
- autoComplete="off"
434
- type="url"
435
- />
436
- </ToolsPanelItem>
437
-
438
- <ToolsPanelItem
439
- hasValue={ () => !! opensInNewTab }
440
- label={ __( 'Open in new tab' ) }
441
- onDeselect={ () =>
442
- setAttributes( { opensInNewTab: false } )
443
- }
444
- isShownByDefault
445
- >
446
- <CheckboxControl
447
- __nextHasNoMarginBottom
448
- label={ __( 'Open in new tab' ) }
449
- checked={ opensInNewTab }
450
- onChange={ ( value ) =>
451
- setAttributes( { opensInNewTab: value } )
452
- }
453
- />
454
- </ToolsPanelItem>
455
-
456
- <ToolsPanelItem
457
- label={ __( 'Description' ) }
458
- isShownByDefault
459
- hasValue={ () => !! description }
460
- onDeselect={ () =>
461
- setAttributes( { description: '' } )
462
- }
463
- >
464
- <TextareaControl
465
- __nextHasNoMarginBottom
466
- value={ description || '' }
467
- onChange={ ( descriptionValue ) => {
468
- setAttributes( {
469
- description: descriptionValue,
470
- } );
471
- } }
472
- label={ __( 'Description' ) }
473
- help={ __(
474
- 'The description will be displayed in the menu if the current theme supports it.'
475
- ) }
476
- />
477
- </ToolsPanelItem>
478
-
479
- <ToolsPanelItem
480
- label={ __( 'Rel attribute' ) }
481
- isShownByDefault
482
- hasValue={ () => !! rel }
483
- onDeselect={ () => setAttributes( { rel: '' } ) }
484
- >
485
- <TextControl
486
- __nextHasNoMarginBottom
487
- __next40pxDefaultSize
488
- value={ rel || '' }
489
- onChange={ ( relValue ) => {
490
- setAttributes( { rel: relValue } );
491
- } }
492
- label={ __( 'Rel attribute' ) }
493
- autoComplete="off"
494
- help={ __(
495
- 'The relationship of the linked URL as space-separated link types.'
496
- ) }
497
- />
498
- </ToolsPanelItem>
499
- </ToolsPanel>
375
+ <Controls
376
+ attributes={ attributes }
377
+ setAttributes={ setAttributes }
378
+ />
500
379
  </InspectorControls>
501
380
  <div { ...blockProps }>
502
381
  { /* eslint-disable jsx-a11y/anchor-is-valid */ }
@@ -9,6 +9,7 @@
9
9
  "html": false,
10
10
  "inserter": false,
11
11
  "renaming": false,
12
+ "blockVisibility": false,
12
13
  "interactivity": {
13
14
  "clientNavigation": true
14
15
  }
@@ -6,7 +6,7 @@ import clsx from 'clsx';
6
6
  /**
7
7
  * WordPress dependencies
8
8
  */
9
- import { useEntityProp, store as coreStore } from '@wordpress/core-data';
9
+ import { store as coreStore } from '@wordpress/core-data';
10
10
  import { useEffect, useMemo, useState } from '@wordpress/element';
11
11
  import {
12
12
  dateI18n,
@@ -81,22 +81,22 @@ export default function PostDateEdit( {
81
81
 
82
82
  const isDescendentOfQueryLoop = Number.isFinite( queryId );
83
83
  const dateSettings = getDateSettings();
84
- const [ siteFormat = dateSettings.formats.date ] = useEntityProp(
85
- 'root',
86
- 'site',
87
- 'date_format'
88
- );
89
- const [ siteTimeFormat = dateSettings.formats.time ] = useEntityProp(
90
- 'root',
91
- 'site',
92
- 'time_format'
93
- );
94
84
 
95
- const postType = useSelect(
96
- ( select ) =>
97
- postTypeSlug
98
- ? select( coreStore ).getPostType( postTypeSlug )
99
- : null,
85
+ const {
86
+ postType,
87
+ siteFormat = dateSettings.formats.date,
88
+ siteTimeFormat = dateSettings.formats.time,
89
+ } = useSelect(
90
+ ( select ) => {
91
+ const { getPostType, getEntityRecord } = select( coreStore );
92
+ const siteSettings = getEntityRecord( 'root', 'site' );
93
+
94
+ return {
95
+ siteFormat: siteSettings?.date_format,
96
+ siteTimeFormat: siteSettings?.time_format,
97
+ postType: postTypeSlug ? getPostType( postTypeSlug ) : null,
98
+ };
99
+ },
100
100
  [ postTypeSlug ]
101
101
  );
102
102
 
@@ -5,7 +5,7 @@
5
5
  "name": "core/post-time-to-read",
6
6
  "title": "Time to Read",
7
7
  "category": "theme",
8
- "description": "Show minutes required to finish reading the post.",
8
+ "description": "Show minutes required to finish reading the post. Can also show a word count.",
9
9
  "textdomain": "default",
10
10
  "usesContext": [ "postId", "postType" ],
11
11
  "attributes": {
@@ -16,6 +16,10 @@
16
16
  "type": "boolean",
17
17
  "default": true
18
18
  },
19
+ "displayMode": {
20
+ "type": "string",
21
+ "default": "time"
22
+ },
19
23
  "averageReadingSpeed": {
20
24
  "type": "number",
21
25
  "default": 189
@@ -29,7 +29,9 @@ import { count as wordCount } from '@wordpress/wordcount';
29
29
  import { useToolsPanelDropdownMenuProps } from '../utils/hooks';
30
30
 
31
31
  function PostTimeToReadEdit( { attributes, setAttributes, context } ) {
32
- const { textAlign, displayAsRange, averageReadingSpeed } = attributes;
32
+ const { textAlign, displayAsRange, displayMode, averageReadingSpeed } =
33
+ attributes;
34
+
33
35
  const { postId, postType } = context;
34
36
  const dropdownMenuProps = useToolsPanelDropdownMenuProps();
35
37
 
@@ -44,7 +46,7 @@ function PostTimeToReadEdit( { attributes, setAttributes, context } ) {
44
46
  id: postId,
45
47
  } );
46
48
 
47
- const minutesToReadString = useMemo( () => {
49
+ const displayString = useMemo( () => {
48
50
  // Replicates the logic found in getEditedPostContent().
49
51
  let content;
50
52
  if ( contentStructure instanceof Function ) {
@@ -69,38 +71,62 @@ function PostTimeToReadEdit( { attributes, setAttributes, context } ) {
69
71
  );
70
72
 
71
73
  const totalWords = wordCount( content || '', wordCountType );
72
- if ( displayAsRange ) {
73
- let maxMinutes = Math.max(
74
- 1,
75
- Math.round( ( totalWords / averageReadingSpeed ) * 1.2 )
76
- );
77
- const minMinutes = Math.max(
74
+
75
+ // Add "time to read" part, if enabled.
76
+ if ( displayMode === 'time' ) {
77
+ if ( displayAsRange ) {
78
+ let maxMinutes = Math.max(
79
+ 1,
80
+ Math.round( ( totalWords / averageReadingSpeed ) * 1.2 )
81
+ );
82
+ const minMinutes = Math.max(
83
+ 1,
84
+ Math.round( ( totalWords / averageReadingSpeed ) * 0.8 )
85
+ );
86
+
87
+ if ( minMinutes === maxMinutes ) {
88
+ maxMinutes = maxMinutes + 1;
89
+ }
90
+ // translators: %1$s: minimum minutes, %2$s: maximum minutes to read the post.
91
+ const rangeLabel = _x(
92
+ '%1$s–%2$s minutes',
93
+ 'Range of minutes to read'
94
+ );
95
+ return sprintf( rangeLabel, minMinutes, maxMinutes );
96
+ }
97
+ const minutesToRead = Math.max(
78
98
  1,
79
- Math.round( ( totalWords / averageReadingSpeed ) * 0.8 )
99
+ Math.round( totalWords / averageReadingSpeed )
80
100
  );
81
101
 
82
- if ( minMinutes === maxMinutes ) {
83
- maxMinutes = maxMinutes + 1;
84
- }
85
- // translators: %1$s: minimum minutes, %2$s: maximum minutes to read the post.
86
- const rangeLabel = _x(
87
- '%1$s–%2$s minutes',
88
- 'Range of minutes to read'
102
+ return sprintf(
103
+ /* translators: %s: the number of minutes to read the post. */
104
+ _n( '%s minute', '%s minutes', minutesToRead ),
105
+ minutesToRead
89
106
  );
90
- return sprintf( rangeLabel, minMinutes, maxMinutes );
91
107
  }
92
108
 
93
- const minutesToRead = Math.max(
94
- 1,
95
- Math.round( totalWords / averageReadingSpeed )
96
- );
97
-
98
- return sprintf(
99
- /* translators: %s: the number of minutes to read the post. */
100
- _n( '%s minute', '%s minutes', minutesToRead ),
101
- minutesToRead
102
- );
103
- }, [ contentStructure, blocks, displayAsRange, averageReadingSpeed ] );
109
+ // Add "word count" part, if enabled.
110
+ if ( displayMode === 'words' ) {
111
+ return wordCountType === 'words'
112
+ ? sprintf(
113
+ /* translators: %s: the number of words in the post. */
114
+ _n( '%s word', '%s words', totalWords ),
115
+ totalWords.toLocaleString()
116
+ )
117
+ : sprintf(
118
+ /* translators: %s: the number of characters in the post. */
119
+ _n( '%s character', '%s characters', totalWords ),
120
+ totalWords.toLocaleString()
121
+ );
122
+ }
123
+ }, [
124
+ contentStructure,
125
+ blocks,
126
+ displayAsRange,
127
+ displayMode,
128
+ averageReadingSpeed,
129
+ ] );
104
130
 
105
131
  const blockProps = useBlockProps( {
106
132
  className: clsx( {
@@ -118,43 +144,45 @@ function PostTimeToReadEdit( { attributes, setAttributes, context } ) {
118
144
  } }
119
145
  />
120
146
  </BlockControls>
121
- <InspectorControls>
122
- <ToolsPanel
123
- label={ __( 'Settings' ) }
124
- resetAll={ () => {
125
- setAttributes( {
126
- displayAsRange: true,
127
- } );
128
- } }
129
- dropdownMenuProps={ dropdownMenuProps }
130
- >
131
- <ToolsPanelItem
132
- isShownByDefault
133
- label={ _x(
134
- 'Display as range',
135
- 'Turns reading time range display on or off'
136
- ) }
137
- hasValue={ () => ! displayAsRange }
138
- onDeselect={ () => {
147
+ { displayMode === 'time' && (
148
+ <InspectorControls>
149
+ <ToolsPanel
150
+ label={ __( 'Settings' ) }
151
+ resetAll={ () => {
139
152
  setAttributes( {
140
153
  displayAsRange: true,
141
154
  } );
142
155
  } }
156
+ dropdownMenuProps={ dropdownMenuProps }
143
157
  >
144
- <ToggleControl
145
- __nextHasNoMarginBottom
146
- label={ __( 'Display as range' ) }
147
- checked={ !! displayAsRange }
148
- onChange={ () =>
158
+ <ToolsPanelItem
159
+ isShownByDefault
160
+ label={ _x(
161
+ 'Display as range',
162
+ 'Turns reading time range display on or off'
163
+ ) }
164
+ hasValue={ () => ! displayAsRange }
165
+ onDeselect={ () => {
149
166
  setAttributes( {
150
- displayAsRange: ! displayAsRange,
151
- } )
152
- }
153
- />
154
- </ToolsPanelItem>
155
- </ToolsPanel>
156
- </InspectorControls>
157
- <div { ...blockProps }>{ minutesToReadString }</div>
167
+ displayAsRange: true,
168
+ } );
169
+ } }
170
+ >
171
+ <ToggleControl
172
+ __nextHasNoMarginBottom
173
+ label={ __( 'Display as range' ) }
174
+ checked={ !! displayAsRange }
175
+ onChange={ () =>
176
+ setAttributes( {
177
+ displayAsRange: ! displayAsRange,
178
+ } )
179
+ }
180
+ />
181
+ </ToolsPanelItem>
182
+ </ToolsPanel>
183
+ </InspectorControls>
184
+ ) }
185
+ <div { ...blockProps }>{ displayString }</div>
158
186
  </>
159
187
  );
160
188
  }
@@ -5,6 +5,7 @@ import initBlock from '../utils/init-block';
5
5
  import metadata from './block.json';
6
6
  import edit from './edit';
7
7
  import icon from './icon';
8
+ import variations from './variations';
8
9
 
9
10
  const { name } = metadata;
10
11
  export { metadata, name };
@@ -12,6 +13,7 @@ export { metadata, name };
12
13
  export const settings = {
13
14
  icon,
14
15
  edit,
16
+ variations,
15
17
  example: {},
16
18
  };
17
19