@wordpress/block-library 9.31.1-next.233ccab9b.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 (177) 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 -7
  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 -12
  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 +6 -8
  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 +5 -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-item/save.js.map +1 -0
  23. package/build/accordion-panel/index.js +3 -2
  24. package/build/accordion-panel/index.js.map +1 -1
  25. package/build/block/index.js +1 -0
  26. package/build/block/index.js.map +1 -1
  27. package/build/index.js +24 -4
  28. package/build/index.js.map +1 -1
  29. package/build/navigation-link/edit.js +3 -140
  30. package/build/navigation-link/edit.js.map +1 -1
  31. package/build/navigation-link/shared/controls.js +171 -0
  32. package/build/navigation-link/shared/controls.js.map +1 -0
  33. package/build/navigation-link/shared/index.js +13 -0
  34. package/build/navigation-link/shared/index.js.map +1 -0
  35. package/build/navigation-submenu/edit.js +5 -110
  36. package/build/navigation-submenu/edit.js.map +1 -1
  37. package/build/pattern/index.js +1 -0
  38. package/build/pattern/index.js.map +1 -1
  39. package/build/post-time-to-read/edit.js +16 -61
  40. package/build/post-time-to-read/edit.js.map +1 -1
  41. package/build/post-time-to-read/index.js +5 -7
  42. package/build/post-time-to-read/index.js.map +1 -1
  43. package/build/post-time-to-read/variations.js +41 -0
  44. package/build/post-time-to-read/variations.js.map +1 -0
  45. package/build/query-title/edit.js +1 -1
  46. package/build/query-title/edit.js.map +1 -1
  47. package/build/template-part/index.js +1 -0
  48. package/build/template-part/index.js.map +1 -1
  49. package/build/utils/get-transformed-metadata.js +7 -0
  50. package/build/utils/get-transformed-metadata.js.map +1 -1
  51. package/build-module/accordion/edit.js +66 -16
  52. package/build-module/accordion/edit.js.map +1 -1
  53. package/build-module/accordion/index.js +15 -7
  54. package/build-module/accordion/index.js.map +1 -1
  55. package/build-module/accordion/view.js +15 -15
  56. package/build-module/accordion/view.js.map +1 -1
  57. package/build-module/accordion-heading/edit.js +61 -0
  58. package/build-module/accordion-heading/edit.js.map +1 -0
  59. package/build-module/accordion-heading/icon.js.map +1 -0
  60. package/build-module/{accordion-header → accordion-heading}/index.js +9 -12
  61. package/build-module/accordion-heading/index.js.map +1 -0
  62. package/build-module/{accordion-content → accordion-heading}/init.js.map +1 -1
  63. package/build-module/{accordion-header → accordion-heading}/save.js +6 -8
  64. package/build-module/accordion-heading/save.js.map +1 -0
  65. package/build-module/{accordion-content → accordion-item}/edit.js +8 -2
  66. package/build-module/accordion-item/edit.js.map +1 -0
  67. package/build-module/accordion-item/icon.js.map +1 -0
  68. package/build-module/{accordion-content → accordion-item}/index.js +5 -5
  69. package/build-module/accordion-item/index.js.map +1 -0
  70. package/build-module/accordion-item/init.js.map +1 -0
  71. package/build-module/accordion-item/save.js.map +1 -0
  72. package/build-module/accordion-panel/index.js +3 -2
  73. package/build-module/accordion-panel/index.js.map +1 -1
  74. package/build-module/block/index.js +1 -0
  75. package/build-module/block/index.js.map +1 -1
  76. package/build-module/index.js +25 -5
  77. package/build-module/index.js.map +1 -1
  78. package/build-module/navigation-link/edit.js +4 -141
  79. package/build-module/navigation-link/edit.js.map +1 -1
  80. package/build-module/navigation-link/shared/controls.js +165 -0
  81. package/build-module/navigation-link/shared/controls.js.map +1 -0
  82. package/build-module/navigation-link/shared/index.js +9 -0
  83. package/build-module/navigation-link/shared/index.js.map +1 -0
  84. package/build-module/navigation-submenu/edit.js +6 -111
  85. package/build-module/navigation-submenu/edit.js.map +1 -1
  86. package/build-module/pattern/index.js +1 -0
  87. package/build-module/pattern/index.js.map +1 -1
  88. package/build-module/post-time-to-read/edit.js +17 -62
  89. package/build-module/post-time-to-read/edit.js.map +1 -1
  90. package/build-module/post-time-to-read/index.js +5 -7
  91. package/build-module/post-time-to-read/index.js.map +1 -1
  92. package/build-module/post-time-to-read/variations.js +33 -0
  93. package/build-module/post-time-to-read/variations.js.map +1 -0
  94. package/build-module/query-title/edit.js +1 -1
  95. package/build-module/query-title/edit.js.map +1 -1
  96. package/build-module/template-part/index.js +1 -0
  97. package/build-module/template-part/index.js.map +1 -1
  98. package/build-module/utils/get-transformed-metadata.js +7 -0
  99. package/build-module/utils/get-transformed-metadata.js.map +1 -1
  100. package/build-style/{accordion-header → accordion-heading}/style-rtl.css +8 -7
  101. package/build-style/{accordion-header → accordion-heading}/style.css +8 -7
  102. package/build-style/{accordion-content → accordion-item}/style-rtl.css +5 -5
  103. package/build-style/{accordion-content → accordion-item}/style.css +5 -5
  104. package/build-style/style-rtl.css +13 -12
  105. package/build-style/style.css +13 -12
  106. package/package.json +35 -35
  107. package/src/accordion/block.json +11 -3
  108. package/src/accordion/edit.js +70 -13
  109. package/src/accordion/index.js +4 -4
  110. package/src/accordion/index.php +1 -1
  111. package/src/accordion/view.js +15 -15
  112. package/src/{accordion-header → accordion-heading}/block.json +10 -12
  113. package/src/accordion-heading/edit.js +70 -0
  114. package/src/{accordion-header → accordion-heading}/save.js +6 -8
  115. package/src/{accordion-header → accordion-heading}/style.scss +11 -7
  116. package/src/{accordion-content → accordion-item}/block.json +5 -5
  117. package/src/{accordion-content → accordion-item}/edit.js +12 -2
  118. package/src/{accordion-content → accordion-item}/index.php +11 -11
  119. package/src/{accordion-content → accordion-item}/style.scss +3 -3
  120. package/src/accordion-panel/block.json +3 -2
  121. package/src/block/block.json +1 -0
  122. package/src/index.js +23 -4
  123. package/src/navigation-link/edit.js +3 -142
  124. package/src/navigation-link/shared/README.md +47 -0
  125. package/src/navigation-link/shared/controls.js +167 -0
  126. package/src/navigation-link/shared/index.js +8 -0
  127. package/src/navigation-link/shared/test/controls.js +210 -0
  128. package/src/navigation-submenu/edit.js +7 -125
  129. package/src/pattern/block.json +1 -0
  130. package/src/post-time-to-read/block.json +3 -7
  131. package/src/post-time-to-read/edit.js +36 -94
  132. package/src/post-time-to-read/index.js +2 -0
  133. package/src/post-time-to-read/index.php +12 -7
  134. package/src/post-time-to-read/variations.js +39 -0
  135. package/src/query-title/edit.js +2 -1
  136. package/src/query-title/index.php +3 -1
  137. package/src/style.scss +2 -2
  138. package/src/template-part/block.json +1 -0
  139. package/src/utils/get-transformed-metadata.js +8 -0
  140. package/build/accordion-content/edit.js.map +0 -1
  141. package/build/accordion-content/icon.js.map +0 -1
  142. package/build/accordion-content/index.js.map +0 -1
  143. package/build/accordion-content/save.js.map +0 -1
  144. package/build/accordion-header/edit.js +0 -84
  145. package/build/accordion-header/edit.js.map +0 -1
  146. package/build/accordion-header/icon.js.map +0 -1
  147. package/build/accordion-header/index.js.map +0 -1
  148. package/build/accordion-header/init.js.map +0 -1
  149. package/build/accordion-header/save.js.map +0 -1
  150. package/build-module/accordion-content/edit.js.map +0 -1
  151. package/build-module/accordion-content/icon.js.map +0 -1
  152. package/build-module/accordion-content/index.js.map +0 -1
  153. package/build-module/accordion-content/save.js.map +0 -1
  154. package/build-module/accordion-header/edit.js +0 -77
  155. package/build-module/accordion-header/edit.js.map +0 -1
  156. package/build-module/accordion-header/icon.js.map +0 -1
  157. package/build-module/accordion-header/index.js.map +0 -1
  158. package/build-module/accordion-header/init.js.map +0 -1
  159. package/build-module/accordion-header/save.js.map +0 -1
  160. package/src/accordion-header/edit.js +0 -87
  161. /package/build/{accordion-header → accordion-heading}/icon.js +0 -0
  162. /package/build/{accordion-content → accordion-heading}/init.js +0 -0
  163. /package/build/{accordion-content → accordion-item}/icon.js +0 -0
  164. /package/build/{accordion-header → accordion-item}/init.js +0 -0
  165. /package/build/{accordion-content → accordion-item}/save.js +0 -0
  166. /package/build-module/{accordion-header → accordion-heading}/icon.js +0 -0
  167. /package/build-module/{accordion-content → accordion-heading}/init.js +0 -0
  168. /package/build-module/{accordion-content → accordion-item}/icon.js +0 -0
  169. /package/build-module/{accordion-header → accordion-item}/init.js +0 -0
  170. /package/build-module/{accordion-content → accordion-item}/save.js +0 -0
  171. /package/src/{accordion-header → accordion-heading}/icon.js +0 -0
  172. /package/src/{accordion-content → accordion-heading}/index.js +0 -0
  173. /package/src/{accordion-content → accordion-heading}/init.js +0 -0
  174. /package/src/{accordion-content → accordion-item}/icon.js +0 -0
  175. /package/src/{accordion-header → accordion-item}/index.js +0 -0
  176. /package/src/{accordion-header → accordion-item}/init.js +0 -0
  177. /package/src/{accordion-content → accordion-item}/save.js +0 -0
@@ -0,0 +1,47 @@
1
+ # Navigation Blocks Shared Components
2
+
3
+ This directory contains shared components and utilities used by both the Navigation Link and Navigation Submenu blocks to reduce code duplication and ensure consistent behavior.
4
+
5
+ ## Purpose
6
+
7
+ The Navigation Link and Navigation Submenu blocks share significant functionality, particularly in their inspector controls (ToolsPanel). This shared directory was created to:
8
+
9
+ - **Reduce code duplication** - Eliminate identical code between the two blocks
10
+ - **Ensure consistency** - Both blocks now use the same components, preventing behavioral differences
11
+ - **Reduce maintenance burden** - Changes to shared functionality only need to be made in one place
12
+ - **Minimize bugs** - Less duplicated code means fewer places for bugs to hide
13
+ - **Improve testability** - Shared components can be tested once and reused
14
+
15
+ ## Current Shared Components
16
+
17
+ - **`Controls`** - Inspector controls component providing the ToolsPanel interface for both blocks
18
+
19
+ ## Future Direction
20
+
21
+ While this shared directory provides immediate benefits for reducing duplication, the long-term vision is to refactor towards a **unified Navigation Item block** that can behave differently based on context (link vs submenu). This would:
22
+
23
+ - Eliminate the need for separate Navigation Link and Navigation Submenu blocks
24
+ - Provide a single, more maintainable codebase
25
+ - Allow for more flexible navigation item types
26
+ - Simplify the user experience
27
+
28
+ However, this refactoring is beyond the current scope and would require significant architectural changes. For now, this shared directory provides a practical solution that:
29
+
30
+ - Maintains backward compatibility
31
+ - Reduces immediate technical debt
32
+ - Prepares the foundation for future unification
33
+ - Supports the integration of new features like Dynamic URL functionality
34
+
35
+ ## Testing
36
+
37
+ All shared components include comprehensive tests in the `test/` directory. The tests use pure mocking strategies to ensure isolated, reliable testing of component behavior.
38
+
39
+ ## Contributing
40
+
41
+ When adding new shared functionality:
42
+
43
+ 1. Place shared components in this directory
44
+ 2. Export them from `index.js`
45
+ 3. Add comprehensive tests
46
+ 4. Update both Navigation Link and Navigation Submenu blocks to use the shared component
47
+ 5. Remove any duplicated code from the individual blocks
@@ -0,0 +1,167 @@
1
+ /**
2
+ * WordPress dependencies
3
+ */
4
+ import {
5
+ __experimentalToolsPanel as ToolsPanel,
6
+ __experimentalToolsPanelItem as ToolsPanelItem,
7
+ CheckboxControl,
8
+ TextControl,
9
+ TextareaControl,
10
+ } from '@wordpress/components';
11
+ import { __ } from '@wordpress/i18n';
12
+ import { useRef } from '@wordpress/element';
13
+ import { safeDecodeURI } from '@wordpress/url';
14
+ import { __unstableStripHTML as stripHTML } from '@wordpress/dom';
15
+
16
+ /**
17
+ * Internal dependencies
18
+ */
19
+ import { useToolsPanelDropdownMenuProps } from '../../utils/hooks';
20
+ import { updateAttributes } from '../update-attributes';
21
+
22
+ /**
23
+ * Shared Controls component for Navigation Link and Navigation Submenu blocks.
24
+ *
25
+ * This component provides the inspector controls (ToolsPanel) that are identical
26
+ * between both navigation blocks.
27
+ *
28
+ * @param {Object} props - Component props
29
+ * @param {Object} props.attributes - Block attributes
30
+ * @param {Function} props.setAttributes - Function to update block attributes
31
+ * @param {Function} props.setIsEditingControl - Function to set editing state (optional)
32
+ */
33
+ export function Controls( {
34
+ attributes,
35
+ setAttributes,
36
+ setIsEditingControl = () => {},
37
+ } ) {
38
+ const { label, url, description, rel, opensInNewTab } = attributes;
39
+ const lastURLRef = useRef( url );
40
+ const dropdownMenuProps = useToolsPanelDropdownMenuProps();
41
+
42
+ return (
43
+ <ToolsPanel
44
+ label={ __( 'Settings' ) }
45
+ resetAll={ () => {
46
+ setAttributes( {
47
+ label: '',
48
+ url: '',
49
+ description: '',
50
+ rel: '',
51
+ opensInNewTab: false,
52
+ } );
53
+ } }
54
+ dropdownMenuProps={ dropdownMenuProps }
55
+ >
56
+ <ToolsPanelItem
57
+ hasValue={ () => !! label }
58
+ label={ __( 'Text' ) }
59
+ onDeselect={ () => setAttributes( { label: '' } ) }
60
+ isShownByDefault
61
+ >
62
+ <TextControl
63
+ __nextHasNoMarginBottom
64
+ __next40pxDefaultSize
65
+ label={ __( 'Text' ) }
66
+ value={ label ? stripHTML( label ) : '' }
67
+ onChange={ ( labelValue ) => {
68
+ setAttributes( { label: labelValue } );
69
+ } }
70
+ autoComplete="off"
71
+ onFocus={ () => setIsEditingControl( true ) }
72
+ onBlur={ () => setIsEditingControl( false ) }
73
+ />
74
+ </ToolsPanelItem>
75
+
76
+ <ToolsPanelItem
77
+ hasValue={ () => !! url }
78
+ label={ __( 'Link' ) }
79
+ onDeselect={ () => setAttributes( { url: '' } ) }
80
+ isShownByDefault
81
+ >
82
+ <TextControl
83
+ __nextHasNoMarginBottom
84
+ __next40pxDefaultSize
85
+ label={ __( 'Link' ) }
86
+ value={ url ? safeDecodeURI( url ) : '' }
87
+ onChange={ ( urlValue ) => {
88
+ setAttributes( {
89
+ url: encodeURI( safeDecodeURI( urlValue ) ),
90
+ } );
91
+ } }
92
+ autoComplete="off"
93
+ type="url"
94
+ onFocus={ () => {
95
+ lastURLRef.current = url;
96
+ setIsEditingControl( true );
97
+ } }
98
+ onBlur={ () => {
99
+ // Defer the updateAttributes call to ensure entity connection isn't severed by accident.
100
+ updateAttributes(
101
+ { url: ! url ? lastURLRef.current : url },
102
+ setAttributes,
103
+ { ...attributes, url: lastURLRef.current }
104
+ );
105
+ setIsEditingControl( false );
106
+ } }
107
+ />
108
+ </ToolsPanelItem>
109
+
110
+ <ToolsPanelItem
111
+ hasValue={ () => !! opensInNewTab }
112
+ label={ __( 'Open in new tab' ) }
113
+ onDeselect={ () => setAttributes( { opensInNewTab: false } ) }
114
+ isShownByDefault
115
+ >
116
+ <CheckboxControl
117
+ __nextHasNoMarginBottom
118
+ label={ __( 'Open in new tab' ) }
119
+ checked={ opensInNewTab }
120
+ onChange={ ( value ) =>
121
+ setAttributes( { opensInNewTab: value } )
122
+ }
123
+ />
124
+ </ToolsPanelItem>
125
+
126
+ <ToolsPanelItem
127
+ hasValue={ () => !! description }
128
+ label={ __( 'Description' ) }
129
+ onDeselect={ () => setAttributes( { description: '' } ) }
130
+ isShownByDefault
131
+ >
132
+ <TextareaControl
133
+ __nextHasNoMarginBottom
134
+ label={ __( 'Description' ) }
135
+ value={ description || '' }
136
+ onChange={ ( descriptionValue ) => {
137
+ setAttributes( { description: descriptionValue } );
138
+ } }
139
+ help={ __(
140
+ 'The description will be displayed in the menu if the current theme supports it.'
141
+ ) }
142
+ />
143
+ </ToolsPanelItem>
144
+
145
+ <ToolsPanelItem
146
+ hasValue={ () => !! rel }
147
+ label={ __( 'Rel attribute' ) }
148
+ onDeselect={ () => setAttributes( { rel: '' } ) }
149
+ isShownByDefault
150
+ >
151
+ <TextControl
152
+ __nextHasNoMarginBottom
153
+ __next40pxDefaultSize
154
+ label={ __( 'Rel attribute' ) }
155
+ value={ rel || '' }
156
+ onChange={ ( relValue ) => {
157
+ setAttributes( { rel: relValue } );
158
+ } }
159
+ autoComplete="off"
160
+ help={ __(
161
+ 'The relationship of the linked URL as space-separated link types.'
162
+ ) }
163
+ />
164
+ </ToolsPanelItem>
165
+ </ToolsPanel>
166
+ );
167
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Shared components for Navigation Link and Navigation Submenu blocks.
3
+ *
4
+ * This module provides common functionality that can be used by both blocks
5
+ * to reduce code duplication and ensure consistent behavior.
6
+ */
7
+
8
+ export { Controls } from './controls';
@@ -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,11 @@ 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';
49
41
  import { DEFAULT_BLOCK } from '../navigation/constants';
50
42
 
51
43
  const ALLOWED_BLOCKS = [
@@ -132,7 +124,7 @@ export default function NavigationSubmenuEdit( {
132
124
  context,
133
125
  clientId,
134
126
  } ) {
135
- const { label, url, description, rel, opensInNewTab } = attributes;
127
+ const { label, url, description } = attributes;
136
128
 
137
129
  const { showSubmenuIcon, maxNestingLevel, openSubmenusOnClick } = context;
138
130
 
@@ -151,7 +143,6 @@ export default function NavigationSubmenuEdit( {
151
143
  const isDraggingWithin = useIsDraggingWithin( listItemRef );
152
144
  const itemLabelPlaceholder = __( 'Add text…' );
153
145
  const ref = useRef();
154
- const dropdownMenuProps = useToolsPanelDropdownMenuProps();
155
146
 
156
147
  const {
157
148
  parentCount,
@@ -380,120 +371,11 @@ export default function NavigationSubmenuEdit( {
380
371
  />
381
372
  </ToolbarGroup>
382
373
  </BlockControls>
383
- { /* Warning, this duplicated in packages/block-library/src/navigation-link/edit.js */ }
384
374
  <InspectorControls>
385
- <ToolsPanel
386
- label={ __( 'Settings' ) }
387
- resetAll={ () => {
388
- setAttributes( {
389
- label: '',
390
- url: '',
391
- description: '',
392
- rel: '',
393
- opensInNewTab: false,
394
- } );
395
- } }
396
- dropdownMenuProps={ dropdownMenuProps }
397
- >
398
- <ToolsPanelItem
399
- label={ __( 'Text' ) }
400
- isShownByDefault
401
- hasValue={ () => !! label }
402
- onDeselect={ () => setAttributes( { label: '' } ) }
403
- >
404
- <TextControl
405
- __nextHasNoMarginBottom
406
- __next40pxDefaultSize
407
- value={ label || '' }
408
- onChange={ ( labelValue ) => {
409
- setAttributes( { label: labelValue } );
410
- } }
411
- label={ __( 'Text' ) }
412
- autoComplete="off"
413
- />
414
- </ToolsPanelItem>
415
-
416
- <ToolsPanelItem
417
- label={ __( 'Link' ) }
418
- isShownByDefault
419
- hasValue={ () => !! url }
420
- onDeselect={ () => setAttributes( { url: '' } ) }
421
- >
422
- <TextControl
423
- __nextHasNoMarginBottom
424
- __next40pxDefaultSize
425
- value={ url || '' }
426
- onChange={ ( urlValue ) => {
427
- setAttributes( { url: urlValue } );
428
- } }
429
- label={ __( 'Link' ) }
430
- autoComplete="off"
431
- type="url"
432
- />
433
- </ToolsPanelItem>
434
-
435
- <ToolsPanelItem
436
- hasValue={ () => !! opensInNewTab }
437
- label={ __( 'Open in new tab' ) }
438
- onDeselect={ () =>
439
- setAttributes( { opensInNewTab: false } )
440
- }
441
- isShownByDefault
442
- >
443
- <CheckboxControl
444
- __nextHasNoMarginBottom
445
- label={ __( 'Open in new tab' ) }
446
- checked={ opensInNewTab }
447
- onChange={ ( value ) =>
448
- setAttributes( { opensInNewTab: value } )
449
- }
450
- />
451
- </ToolsPanelItem>
452
-
453
- <ToolsPanelItem
454
- label={ __( 'Description' ) }
455
- isShownByDefault
456
- hasValue={ () => !! description }
457
- onDeselect={ () =>
458
- setAttributes( { description: '' } )
459
- }
460
- >
461
- <TextareaControl
462
- __nextHasNoMarginBottom
463
- value={ description || '' }
464
- onChange={ ( descriptionValue ) => {
465
- setAttributes( {
466
- description: descriptionValue,
467
- } );
468
- } }
469
- label={ __( 'Description' ) }
470
- help={ __(
471
- 'The description will be displayed in the menu if the current theme supports it.'
472
- ) }
473
- />
474
- </ToolsPanelItem>
475
-
476
- <ToolsPanelItem
477
- label={ __( 'Rel attribute' ) }
478
- isShownByDefault
479
- hasValue={ () => !! rel }
480
- onDeselect={ () => setAttributes( { rel: '' } ) }
481
- >
482
- <TextControl
483
- __nextHasNoMarginBottom
484
- __next40pxDefaultSize
485
- value={ rel || '' }
486
- onChange={ ( relValue ) => {
487
- setAttributes( { rel: relValue } );
488
- } }
489
- label={ __( 'Rel attribute' ) }
490
- autoComplete="off"
491
- help={ __(
492
- 'The relationship of the linked URL as space-separated link types.'
493
- ) }
494
- />
495
- </ToolsPanelItem>
496
- </ToolsPanel>
375
+ <Controls
376
+ attributes={ attributes }
377
+ setAttributes={ setAttributes }
378
+ />
497
379
  </InspectorControls>
498
380
  <div { ...blockProps }>
499
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
  }
@@ -16,13 +16,9 @@
16
16
  "type": "boolean",
17
17
  "default": true
18
18
  },
19
- "showTimeToRead": {
20
- "type": "boolean",
21
- "default": true
22
- },
23
- "showWordCount": {
24
- "type": "boolean",
25
- "default": false
19
+ "displayMode": {
20
+ "type": "string",
21
+ "default": "time"
26
22
  },
27
23
  "averageReadingSpeed": {
28
24
  "type": "number",