@wordpress/block-library 9.29.1-next.f34ab90e9.0 → 9.30.1-next.6870dfe5b.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 (168) hide show
  1. package/CHANGELOG.md +2 -0
  2. package/build/accordion/edit.js +18 -1
  3. package/build/accordion/edit.js.map +1 -1
  4. package/build/accordion/index.js +0 -3
  5. package/build/accordion/index.js.map +1 -1
  6. package/build/accordion-content/edit.js +8 -8
  7. package/build/accordion-content/edit.js.map +1 -1
  8. package/build/accordion-content/index.js +2 -5
  9. package/build/accordion-content/index.js.map +1 -1
  10. package/build/accordion-header/index.js +0 -4
  11. package/build/accordion-header/index.js.map +1 -1
  12. package/build/accordion-panel/edit.js +5 -30
  13. package/build/accordion-panel/edit.js.map +1 -1
  14. package/build/accordion-panel/index.js +2 -5
  15. package/build/accordion-panel/index.js.map +1 -1
  16. package/build/accordion-panel/save.js +3 -29
  17. package/build/accordion-panel/save.js.map +1 -1
  18. package/build/audio/edit.js +3 -1
  19. package/build/audio/edit.js.map +1 -1
  20. package/build/gallery/index.js +2 -1
  21. package/build/gallery/index.js.map +1 -1
  22. package/build/group/variations.js +0 -12
  23. package/build/group/variations.js.map +1 -1
  24. package/build/image/image.js +1 -1
  25. package/build/image/image.js.map +1 -1
  26. package/build/navigation/constants.js +5 -1
  27. package/build/navigation/constants.js.map +1 -1
  28. package/build/navigation/edit/index.js +45 -1
  29. package/build/navigation/edit/index.js.map +1 -1
  30. package/build/navigation/edit/leaf-more-menu.js +0 -1
  31. package/build/navigation/edit/leaf-more-menu.js.map +1 -1
  32. package/build/navigation/edit/menu-inspector-controls.js +40 -5
  33. package/build/navigation/edit/menu-inspector-controls.js.map +1 -1
  34. package/build/navigation-link/block-inserter.js +69 -0
  35. package/build/navigation-link/block-inserter.js.map +1 -0
  36. package/build/navigation-link/dialog-wrapper.js +80 -0
  37. package/build/navigation-link/dialog-wrapper.js.map +1 -0
  38. package/build/navigation-link/link-ui.js +80 -116
  39. package/build/navigation-link/link-ui.js.map +1 -1
  40. package/build/navigation-link/page-creator.js +137 -0
  41. package/build/navigation-link/page-creator.js.map +1 -0
  42. package/build/query/edit/index.js.map +1 -1
  43. package/build/query/edit/query-content.js +7 -6
  44. package/build/query/edit/query-content.js.map +1 -1
  45. package/build/query/edit/query-placeholder.js +30 -9
  46. package/build/query/edit/query-placeholder.js.map +1 -1
  47. package/build/query/edit/query-toolbar.js +4 -2
  48. package/build/query/edit/query-toolbar.js.map +1 -1
  49. package/build/search/edit.js +22 -14
  50. package/build/search/edit.js.map +1 -1
  51. package/build/template-part/edit/placeholder.js +2 -1
  52. package/build/template-part/edit/placeholder.js.map +1 -1
  53. package/build/video/edit.js +3 -1
  54. package/build/video/edit.js.map +1 -1
  55. package/build-module/accordion/edit.js +20 -3
  56. package/build-module/accordion/edit.js.map +1 -1
  57. package/build-module/accordion/index.js +0 -3
  58. package/build-module/accordion/index.js.map +1 -1
  59. package/build-module/accordion-content/edit.js +8 -8
  60. package/build-module/accordion-content/edit.js.map +1 -1
  61. package/build-module/accordion-content/index.js +2 -5
  62. package/build-module/accordion-content/index.js.map +1 -1
  63. package/build-module/accordion-header/index.js +0 -4
  64. package/build-module/accordion-header/index.js.map +1 -1
  65. package/build-module/accordion-panel/edit.js +6 -29
  66. package/build-module/accordion-panel/edit.js.map +1 -1
  67. package/build-module/accordion-panel/index.js +2 -5
  68. package/build-module/accordion-panel/index.js.map +1 -1
  69. package/build-module/accordion-panel/save.js +4 -28
  70. package/build-module/accordion-panel/save.js.map +1 -1
  71. package/build-module/audio/edit.js +4 -2
  72. package/build-module/audio/edit.js.map +1 -1
  73. package/build-module/gallery/index.js +2 -1
  74. package/build-module/gallery/index.js.map +1 -1
  75. package/build-module/group/variations.js +0 -12
  76. package/build-module/group/variations.js.map +1 -1
  77. package/build-module/image/image.js +1 -1
  78. package/build-module/image/image.js.map +1 -1
  79. package/build-module/navigation/constants.js +5 -1
  80. package/build-module/navigation/constants.js.map +1 -1
  81. package/build-module/navigation/edit/index.js +50 -4
  82. package/build-module/navigation/edit/index.js.map +1 -1
  83. package/build-module/navigation/edit/leaf-more-menu.js +0 -1
  84. package/build-module/navigation/edit/leaf-more-menu.js.map +1 -1
  85. package/build-module/navigation/edit/menu-inspector-controls.js +40 -5
  86. package/build-module/navigation/edit/menu-inspector-controls.js.map +1 -1
  87. package/build-module/navigation-link/block-inserter.js +61 -0
  88. package/build-module/navigation-link/block-inserter.js.map +1 -0
  89. package/build-module/navigation-link/dialog-wrapper.js +75 -0
  90. package/build-module/navigation-link/dialog-wrapper.js.map +1 -0
  91. package/build-module/navigation-link/link-ui.js +85 -121
  92. package/build-module/navigation-link/link-ui.js.map +1 -1
  93. package/build-module/navigation-link/page-creator.js +130 -0
  94. package/build-module/navigation-link/page-creator.js.map +1 -0
  95. package/build-module/query/edit/index.js.map +1 -1
  96. package/build-module/query/edit/query-content.js +8 -7
  97. package/build-module/query/edit/query-content.js.map +1 -1
  98. package/build-module/query/edit/query-placeholder.js +30 -10
  99. package/build-module/query/edit/query-placeholder.js.map +1 -1
  100. package/build-module/query/edit/query-toolbar.js +4 -2
  101. package/build-module/query/edit/query-toolbar.js.map +1 -1
  102. package/build-module/search/edit.js +22 -14
  103. package/build-module/search/edit.js.map +1 -1
  104. package/build-module/template-part/edit/placeholder.js +2 -1
  105. package/build-module/template-part/edit/placeholder.js.map +1 -1
  106. package/build-module/video/edit.js +4 -2
  107. package/build-module/video/edit.js.map +1 -1
  108. package/build-style/accordion/style-rtl.css +8 -18
  109. package/build-style/accordion/style.css +8 -18
  110. package/build-style/editor-rtl.css +18 -0
  111. package/build-style/editor.css +18 -0
  112. package/build-style/form-input/style-rtl.css +4 -3
  113. package/build-style/form-input/style.css +4 -3
  114. package/build-style/navigation-link/editor-rtl.css +14 -0
  115. package/build-style/navigation-link/editor.css +14 -0
  116. package/build-style/navigation-link/style-rtl.css +1 -1
  117. package/build-style/navigation-link/style.css +1 -1
  118. package/build-style/post-comments-form/style-rtl.css +8 -5
  119. package/build-style/post-comments-form/style.css +8 -5
  120. package/build-style/query/editor-rtl.css +4 -0
  121. package/build-style/query/editor.css +4 -0
  122. package/build-style/search/style-rtl.css +11 -12
  123. package/build-style/search/style.css +11 -12
  124. package/build-style/style-rtl.css +32 -40
  125. package/build-style/style.css +32 -40
  126. package/package.json +35 -35
  127. package/src/accordion/block.json +0 -3
  128. package/src/accordion/edit.js +20 -0
  129. package/src/accordion/style.scss +12 -21
  130. package/src/accordion-content/block.json +2 -4
  131. package/src/accordion-content/edit.js +21 -27
  132. package/src/accordion-content/index.js +0 -1
  133. package/src/accordion-header/block.json +0 -3
  134. package/src/accordion-header/index.js +0 -1
  135. package/src/accordion-panel/block.json +2 -4
  136. package/src/accordion-panel/edit.js +11 -51
  137. package/src/accordion-panel/index.js +0 -1
  138. package/src/accordion-panel/save.js +4 -45
  139. package/src/audio/edit.js +6 -1
  140. package/src/cover/test/edit.js +1 -5
  141. package/src/form-input/style.scss +3 -2
  142. package/src/gallery/block.json +2 -1
  143. package/src/gallery/index.php +1 -1
  144. package/src/gallery/test/helpers.native.js +3 -3
  145. package/src/group/variations.js +0 -12
  146. package/src/image/image.js +2 -1
  147. package/src/navigation/constants.js +4 -0
  148. package/src/navigation/edit/index.js +50 -1
  149. package/src/navigation/edit/leaf-more-menu.js +0 -1
  150. package/src/navigation/edit/menu-inspector-controls.js +40 -5
  151. package/src/navigation-link/block-inserter.js +65 -0
  152. package/src/navigation-link/dialog-wrapper.js +74 -0
  153. package/src/navigation-link/editor.scss +17 -0
  154. package/src/navigation-link/link-ui.js +108 -158
  155. package/src/navigation-link/page-creator.js +157 -0
  156. package/src/navigation-link/style.scss +1 -1
  157. package/src/post-comments-form/style.scss +11 -11
  158. package/src/post-date/index.php +2 -1
  159. package/src/query/edit/index.js +1 -0
  160. package/src/query/edit/query-content.js +8 -4
  161. package/src/query/edit/query-placeholder.js +47 -17
  162. package/src/query/edit/query-toolbar.js +10 -2
  163. package/src/query/editor.scss +6 -1
  164. package/src/search/edit.js +44 -13
  165. package/src/search/index.php +16 -2
  166. package/src/search/style.scss +15 -16
  167. package/src/template-part/edit/placeholder.js +2 -1
  168. package/src/video/edit.js +6 -1
@@ -0,0 +1,65 @@
1
+ /**
2
+ * WordPress dependencies
3
+ */
4
+ import { __ } from '@wordpress/i18n';
5
+ import { useSelect } from '@wordpress/data';
6
+ import {
7
+ store as blockEditorStore,
8
+ privateApis as blockEditorPrivateApis,
9
+ } from '@wordpress/block-editor';
10
+
11
+ /**
12
+ * Internal dependencies
13
+ */
14
+ import DialogWrapper from './dialog-wrapper';
15
+ import { unlock } from '../lock-unlock';
16
+
17
+ const { PrivateQuickInserter: QuickInserter } = unlock(
18
+ blockEditorPrivateApis
19
+ );
20
+
21
+ /**
22
+ * Component for inserting blocks within the Navigation Link UI.
23
+ *
24
+ * @param {Object} props Component props.
25
+ * @param {string} props.clientId Client ID of the navigation link block.
26
+ * @param {Function} props.onBack Callback when user wants to go back.
27
+ * @param {Function} props.onBlockInsert Callback when a block is inserted.
28
+ */
29
+ function LinkUIBlockInserter( { clientId, onBack, onBlockInsert } ) {
30
+ const { rootBlockClientId } = useSelect(
31
+ ( select ) => {
32
+ const { getBlockRootClientId } = select( blockEditorStore );
33
+
34
+ return {
35
+ rootBlockClientId: getBlockRootClientId( clientId ),
36
+ };
37
+ },
38
+ [ clientId ]
39
+ );
40
+
41
+ if ( ! clientId ) {
42
+ return null;
43
+ }
44
+
45
+ return (
46
+ <DialogWrapper
47
+ className="link-ui-block-inserter"
48
+ title={ __( 'Add block' ) }
49
+ description={ __( 'Choose a block to add to your Navigation.' ) }
50
+ onBack={ onBack }
51
+ >
52
+ <QuickInserter
53
+ rootClientId={ rootBlockClientId }
54
+ clientId={ clientId }
55
+ isAppender={ false }
56
+ prioritizePatterns={ false }
57
+ selectBlockOnInsert={ ! onBlockInsert }
58
+ onSelect={ onBlockInsert ? onBlockInsert : undefined }
59
+ hasSearch={ false }
60
+ />
61
+ </DialogWrapper>
62
+ );
63
+ }
64
+
65
+ export default LinkUIBlockInserter;
@@ -0,0 +1,74 @@
1
+ /**
2
+ * WordPress dependencies
3
+ */
4
+ import { Button, VisuallyHidden } from '@wordpress/components';
5
+ import { __, isRTL } from '@wordpress/i18n';
6
+ import { chevronLeftSmall, chevronRightSmall } from '@wordpress/icons';
7
+ import { useInstanceId, useFocusOnMount } from '@wordpress/compose';
8
+
9
+ /**
10
+ * Shared BackButton component for consistent navigation across LinkUI sub-components.
11
+ *
12
+ * @param {Object} props Component props.
13
+ * @param {string} props.className CSS class name for the button.
14
+ * @param {Function} props.onBack Callback when user wants to go back.
15
+ */
16
+ function BackButton( { className, onBack } ) {
17
+ return (
18
+ <Button
19
+ className={ className }
20
+ icon={ isRTL() ? chevronRightSmall : chevronLeftSmall }
21
+ onClick={ ( e ) => {
22
+ e.preventDefault();
23
+ onBack();
24
+ } }
25
+ size="small"
26
+ >
27
+ { __( 'Back' ) }
28
+ </Button>
29
+ );
30
+ }
31
+
32
+ /**
33
+ * Shared DialogWrapper component for consistent dialog structure across LinkUI sub-components.
34
+ *
35
+ * @param {Object} props Component props.
36
+ * @param {string} props.className CSS class name for the dialog container.
37
+ * @param {string} props.title Dialog title for accessibility.
38
+ * @param {string} props.description Dialog description for accessibility.
39
+ * @param {Function} props.onBack Callback when user wants to go back.
40
+ * @param {Object} props.children Child components to render inside the dialog.
41
+ */
42
+ function DialogWrapper( { className, title, description, onBack, children } ) {
43
+ const dialogTitleId = useInstanceId(
44
+ DialogWrapper,
45
+ 'link-ui-dialog-title'
46
+ );
47
+ const dialogDescriptionId = useInstanceId(
48
+ DialogWrapper,
49
+ 'link-ui-dialog-description'
50
+ );
51
+ const focusOnMountRef = useFocusOnMount( 'firstElement' );
52
+ const backButtonClassName = `${ className }__back`;
53
+
54
+ return (
55
+ <div
56
+ className={ className }
57
+ role="dialog"
58
+ aria-labelledby={ dialogTitleId }
59
+ aria-describedby={ dialogDescriptionId }
60
+ ref={ focusOnMountRef }
61
+ >
62
+ <VisuallyHidden>
63
+ <h2 id={ dialogTitleId }>{ title }</h2>
64
+ <p id={ dialogDescriptionId }>{ description }</p>
65
+ </VisuallyHidden>
66
+
67
+ <BackButton className={ backButtonClassName } onBack={ onBack } />
68
+
69
+ { children }
70
+ </div>
71
+ );
72
+ }
73
+
74
+ export default DialogWrapper;
@@ -122,3 +122,20 @@
122
122
  gap: $grid-unit-10;
123
123
  height: auto;
124
124
  }
125
+
126
+ .link-ui-page-creator {
127
+ // Match LinkControl width constraints for consistent UI sizing
128
+ max-width: 350px;
129
+ min-width: auto;
130
+ width: 90vw;
131
+ padding-top: $grid-unit-10;
132
+
133
+ &__inner {
134
+ padding: $grid-unit-20;
135
+ }
136
+
137
+ &__back {
138
+ margin-left: $grid-unit-10;
139
+ text-transform: uppercase;
140
+ }
141
+ }
@@ -8,37 +8,24 @@ import {
8
8
  VisuallyHidden,
9
9
  __experimentalVStack as VStack,
10
10
  } from '@wordpress/components';
11
- import { __, sprintf, isRTL } from '@wordpress/i18n';
11
+ import { __ } from '@wordpress/i18n';
12
+ import { LinkControl, useBlockEditingMode } from '@wordpress/block-editor';
12
13
  import {
13
- LinkControl,
14
- store as blockEditorStore,
15
- privateApis as blockEditorPrivateApis,
16
- } from '@wordpress/block-editor';
17
- import {
18
- createInterpolateElement,
19
14
  useMemo,
20
15
  useState,
21
16
  useRef,
22
17
  useEffect,
23
18
  forwardRef,
24
19
  } from '@wordpress/element';
25
- import {
26
- store as coreStore,
27
- useResourcePermissions,
28
- } from '@wordpress/core-data';
29
- import { decodeEntities } from '@wordpress/html-entities';
30
- import { useSelect, useDispatch } from '@wordpress/data';
31
- import { chevronLeftSmall, chevronRightSmall, plus } from '@wordpress/icons';
32
- import { useInstanceId, useFocusOnMount } from '@wordpress/compose';
20
+ import { useResourcePermissions } from '@wordpress/core-data';
21
+ import { plus } from '@wordpress/icons';
22
+ import { useInstanceId } from '@wordpress/compose';
33
23
 
34
24
  /**
35
25
  * Internal dependencies
36
26
  */
37
- import { unlock } from '../lock-unlock';
38
-
39
- const { PrivateQuickInserter: QuickInserter } = unlock(
40
- blockEditorPrivateApis
41
- );
27
+ import { LinkUIPageCreator } from './page-creator';
28
+ import LinkUIBlockInserter from './block-inserter';
42
29
 
43
30
  /**
44
31
  * Given the Link block's type attribute, return the query params to give to
@@ -78,110 +65,19 @@ export function getSuggestionsQuery( type, kind ) {
78
65
  }
79
66
  }
80
67
 
81
- function LinkUIBlockInserter( { clientId, onBack } ) {
82
- const { rootBlockClientId } = useSelect(
83
- ( select ) => {
84
- const { getBlockRootClientId } = select( blockEditorStore );
85
-
86
- return {
87
- rootBlockClientId: getBlockRootClientId( clientId ),
88
- };
89
- },
90
- [ clientId ]
91
- );
92
-
93
- const focusOnMountRef = useFocusOnMount( 'firstElement' );
94
-
95
- const dialogTitleId = useInstanceId(
96
- LinkControl,
97
- `link-ui-block-inserter__title`
98
- );
99
- const dialogDescriptionId = useInstanceId(
100
- LinkControl,
101
- `link-ui-block-inserter__description`
102
- );
103
-
104
- if ( ! clientId ) {
105
- return null;
106
- }
107
-
108
- return (
109
- <div
110
- className="link-ui-block-inserter"
111
- role="dialog"
112
- aria-labelledby={ dialogTitleId }
113
- aria-describedby={ dialogDescriptionId }
114
- ref={ focusOnMountRef }
115
- >
116
- <VisuallyHidden>
117
- <h2 id={ dialogTitleId }>{ __( 'Add block' ) }</h2>
118
-
119
- <p id={ dialogDescriptionId }>
120
- { __( 'Choose a block to add to your Navigation.' ) }
121
- </p>
122
- </VisuallyHidden>
123
-
124
- <Button
125
- className="link-ui-block-inserter__back"
126
- icon={ isRTL() ? chevronRightSmall : chevronLeftSmall }
127
- onClick={ ( e ) => {
128
- e.preventDefault();
129
- onBack();
130
- } }
131
- size="small"
132
- >
133
- { __( 'Back' ) }
134
- </Button>
135
-
136
- <QuickInserter
137
- rootClientId={ rootBlockClientId }
138
- clientId={ clientId }
139
- isAppender={ false }
140
- prioritizePatterns={ false }
141
- selectBlockOnInsert
142
- hasSearch={ false }
143
- />
144
- </div>
145
- );
146
- }
147
-
148
68
  function UnforwardedLinkUI( props, ref ) {
149
69
  const { label, url, opensInNewTab, type, kind } = props.link;
150
70
  const postType = type || 'page';
151
71
 
152
72
  const [ addingBlock, setAddingBlock ] = useState( false );
73
+ const [ addingPage, setAddingPage ] = useState( false );
153
74
  const [ focusAddBlockButton, setFocusAddBlockButton ] = useState( false );
154
- const { saveEntityRecord } = useDispatch( coreStore );
75
+ const [ focusAddPageButton, setFocusAddPageButton ] = useState( false );
155
76
  const permissions = useResourcePermissions( {
156
77
  kind: 'postType',
157
78
  name: postType,
158
79
  } );
159
80
 
160
- async function handleCreate( pageTitle ) {
161
- const page = await saveEntityRecord( 'postType', postType, {
162
- title: pageTitle,
163
- status: 'draft',
164
- } );
165
-
166
- return {
167
- id: page.id,
168
- type: postType,
169
- // Make `title` property consistent with that in `fetchLinkSuggestions` where the `rendered` title (containing HTML entities)
170
- // is also being decoded. By being consistent in both locations we avoid having to branch in the rendering output code.
171
- // Ideally in the future we will update both APIs to utilise the "raw" form of the title which is better suited to edit contexts.
172
- // e.g.
173
- // - title.raw = "Yes & No"
174
- // - title.rendered = "Yes &#038; No"
175
- // - decodeEntities( title.rendered ) = "Yes & No"
176
- // See:
177
- // - https://github.com/WordPress/gutenberg/pull/41063
178
- // - https://github.com/WordPress/gutenberg/blob/a1e1fdc0e6278457e9f4fc0b31ac6d2095f5450b/packages/core-data/src/fetch/__experimental-fetch-link-suggestions.js#L212-L218
179
- title: decodeEntities( page.title.rendered ),
180
- url: page.link,
181
- kind: 'post-type',
182
- };
183
- }
184
-
185
81
  // Memoize link value to avoid overriding the LinkControl's internal state.
186
82
  // This is a temporary fix. See https://github.com/WordPress/gutenberg/issues/50976#issuecomment-1568226407.
187
83
  const link = useMemo(
@@ -193,15 +89,24 @@ function UnforwardedLinkUI( props, ref ) {
193
89
  [ label, opensInNewTab, url ]
194
90
  );
195
91
 
92
+ const handlePageCreated = ( pageLink ) => {
93
+ // Set the new page as the current link
94
+ props.onChange( pageLink );
95
+ // Return to main Link UI
96
+ setAddingPage( false );
97
+ };
98
+
196
99
  const dialogTitleId = useInstanceId(
197
100
  LinkUI,
198
- `link-ui-link-control__title`
101
+ 'link-ui-link-control__title'
199
102
  );
200
103
  const dialogDescriptionId = useInstanceId(
201
104
  LinkUI,
202
- `link-ui-link-control__description`
105
+ 'link-ui-link-control__description'
203
106
  );
204
107
 
108
+ const blockEditingMode = useBlockEditingMode();
109
+
205
110
  return (
206
111
  <Popover
207
112
  ref={ ref }
@@ -210,7 +115,7 @@ function UnforwardedLinkUI( props, ref ) {
210
115
  anchor={ props.anchor }
211
116
  shift
212
117
  >
213
- { ! addingBlock && (
118
+ { ! addingBlock && ! addingPage && (
214
119
  <div
215
120
  role="dialog"
216
121
  aria-labelledby={ dialogTitleId }
@@ -230,47 +135,41 @@ function UnforwardedLinkUI( props, ref ) {
230
135
  hasRichPreviews
231
136
  value={ link }
232
137
  showInitialSuggestions
233
- withCreateSuggestion={ permissions.canCreate }
234
- createSuggestion={ handleCreate }
235
- createSuggestionButtonText={ ( searchTerm ) => {
236
- let format;
237
-
238
- if ( type === 'post' ) {
239
- /* translators: %s: search term. */
240
- format = __(
241
- 'Create draft post: <mark>%s</mark>'
242
- );
243
- } else {
244
- /* translators: %s: search term. */
245
- format = __(
246
- 'Create draft page: <mark>%s</mark>'
247
- );
248
- }
249
-
250
- return createInterpolateElement(
251
- sprintf( format, searchTerm ),
252
- {
253
- mark: <mark />,
254
- }
255
- );
256
- } }
138
+ withCreateSuggestion={ false }
257
139
  noDirectEntry={ !! type }
258
140
  noURLSuggestion={ !! type }
259
141
  suggestionsQuery={ getSuggestionsQuery( type, kind ) }
260
142
  onChange={ props.onChange }
261
143
  onRemove={ props.onRemove }
262
144
  onCancel={ props.onCancel }
263
- renderControlBottom={ () =>
264
- ! link?.url?.length && (
145
+ renderControlBottom={ () => {
146
+ // Don't show the tools when there is submitted link (preview state).
147
+ if ( link?.url?.length ) {
148
+ return null;
149
+ }
150
+
151
+ return (
265
152
  <LinkUITools
266
153
  focusAddBlockButton={ focusAddBlockButton }
154
+ focusAddPageButton={ focusAddPageButton }
267
155
  setAddingBlock={ () => {
268
156
  setAddingBlock( true );
269
157
  setFocusAddBlockButton( false );
270
158
  } }
159
+ setAddingPage={ () => {
160
+ setAddingPage( true );
161
+ setFocusAddPageButton( false );
162
+ } }
163
+ canAddPage={
164
+ permissions?.canCreate &&
165
+ type === 'page'
166
+ }
167
+ canAddBlock={
168
+ blockEditingMode === 'default'
169
+ }
271
170
  />
272
- )
273
- }
171
+ );
172
+ } }
274
173
  />
275
174
  </div>
276
175
  ) }
@@ -281,7 +180,22 @@ function UnforwardedLinkUI( props, ref ) {
281
180
  onBack={ () => {
282
181
  setAddingBlock( false );
283
182
  setFocusAddBlockButton( true );
183
+ setFocusAddPageButton( false );
184
+ } }
185
+ onBlockInsert={ props?.onBlockInsert }
186
+ />
187
+ ) }
188
+
189
+ { addingPage && (
190
+ <LinkUIPageCreator
191
+ postType={ postType }
192
+ onBack={ () => {
193
+ setAddingPage( false );
194
+ setFocusAddPageButton( true );
195
+ setFocusAddBlockButton( false );
284
196
  } }
197
+ onPageCreated={ handlePageCreated }
198
+ initialTitle={ link?.url || '' }
285
199
  />
286
200
  ) }
287
201
  </Popover>
@@ -290,9 +204,17 @@ function UnforwardedLinkUI( props, ref ) {
290
204
 
291
205
  export const LinkUI = forwardRef( UnforwardedLinkUI );
292
206
 
293
- const LinkUITools = ( { setAddingBlock, focusAddBlockButton } ) => {
207
+ const LinkUITools = ( {
208
+ setAddingBlock,
209
+ setAddingPage,
210
+ focusAddBlockButton,
211
+ focusAddPageButton,
212
+ canAddPage,
213
+ canAddBlock,
214
+ } ) => {
294
215
  const blockInserterAriaRole = 'listbox';
295
216
  const addBlockButtonRef = useRef();
217
+ const addPageButtonRef = useRef();
296
218
 
297
219
  // Focus the add block button when the popover is opened.
298
220
  useEffect( () => {
@@ -301,20 +223,48 @@ const LinkUITools = ( { setAddingBlock, focusAddBlockButton } ) => {
301
223
  }
302
224
  }, [ focusAddBlockButton ] );
303
225
 
226
+ // Focus the add page button when the popover is opened.
227
+ useEffect( () => {
228
+ if ( focusAddPageButton ) {
229
+ addPageButtonRef.current?.focus();
230
+ }
231
+ }, [ focusAddPageButton ] );
232
+
233
+ // Don't render anything if neither button should be shown
234
+ if ( ! canAddPage && ! canAddBlock ) {
235
+ return null;
236
+ }
237
+
304
238
  return (
305
- <VStack className="link-ui-tools">
306
- <Button
307
- __next40pxDefaultSize
308
- ref={ addBlockButtonRef }
309
- icon={ plus }
310
- onClick={ ( e ) => {
311
- e.preventDefault();
312
- setAddingBlock( true );
313
- } }
314
- aria-haspopup={ blockInserterAriaRole }
315
- >
316
- { __( 'Add block' ) }
317
- </Button>
239
+ <VStack spacing={ 0 } className="link-ui-tools">
240
+ { canAddPage && (
241
+ <Button
242
+ __next40pxDefaultSize
243
+ ref={ addPageButtonRef }
244
+ icon={ plus }
245
+ onClick={ ( e ) => {
246
+ e.preventDefault();
247
+ setAddingPage( true );
248
+ } }
249
+ aria-haspopup={ blockInserterAriaRole }
250
+ >
251
+ { __( 'Create page' ) }
252
+ </Button>
253
+ ) }
254
+ { canAddBlock && (
255
+ <Button
256
+ __next40pxDefaultSize
257
+ ref={ addBlockButtonRef }
258
+ icon={ plus }
259
+ onClick={ ( e ) => {
260
+ e.preventDefault();
261
+ setAddingBlock( true );
262
+ } }
263
+ aria-haspopup={ blockInserterAriaRole }
264
+ >
265
+ { __( 'Add block' ) }
266
+ </Button>
267
+ ) }
318
268
  </VStack>
319
269
  );
320
270
  };
@@ -0,0 +1,157 @@
1
+ /**
2
+ * WordPress dependencies
3
+ */
4
+ import {
5
+ Button,
6
+ TextControl,
7
+ Notice,
8
+ CheckboxControl,
9
+ __experimentalVStack as VStack,
10
+ __experimentalHStack as HStack,
11
+ } from '@wordpress/components';
12
+ import { __ } from '@wordpress/i18n';
13
+ import { useSelect, useDispatch } from '@wordpress/data';
14
+ import { store as coreStore } from '@wordpress/core-data';
15
+ import { decodeEntities } from '@wordpress/html-entities';
16
+ import { useState } from '@wordpress/element';
17
+
18
+ /**
19
+ * Internal dependencies
20
+ */
21
+ import DialogWrapper from './dialog-wrapper';
22
+
23
+ /**
24
+ * Component for creating new pages within the Navigation Link UI.
25
+ *
26
+ * @param {Object} props Component props.
27
+ * @param {string} props.postType The post type to create.
28
+ * @param {Function} props.onBack Callback when user wants to go back.
29
+ * @param {Function} props.onPageCreated Callback when page is successfully created.
30
+ * @param {string} [props.initialTitle] Initial title to pre-fill the form.
31
+ */
32
+ export function LinkUIPageCreator( {
33
+ postType,
34
+ onBack,
35
+ onPageCreated,
36
+ initialTitle = '',
37
+ } ) {
38
+ const [ title, setTitle ] = useState( initialTitle );
39
+ const [ shouldPublish, setShouldPublish ] = useState( false );
40
+
41
+ // Check if the title is valid for submission
42
+ const isTitleValid = title.trim().length > 0;
43
+
44
+ // Get the last created entity record (without ID) to track creation state
45
+ const { lastError, isSaving } = useSelect(
46
+ ( select ) => ( {
47
+ lastError: select( coreStore ).getLastEntitySaveError(
48
+ 'postType',
49
+ postType
50
+ ),
51
+ isSaving: select( coreStore ).isSavingEntityRecord(
52
+ 'postType',
53
+ postType
54
+ ),
55
+ } ),
56
+ [ postType ]
57
+ );
58
+
59
+ const { saveEntityRecord } = useDispatch( coreStore );
60
+
61
+ async function createPage( event ) {
62
+ event.preventDefault();
63
+ if ( isSaving || ! isTitleValid ) {
64
+ return;
65
+ }
66
+
67
+ try {
68
+ const savedRecord = await saveEntityRecord(
69
+ 'postType',
70
+ postType,
71
+ {
72
+ title,
73
+ status: shouldPublish ? 'publish' : 'draft',
74
+ },
75
+ { throwOnError: true }
76
+ );
77
+
78
+ if ( savedRecord ) {
79
+ // Create the page link object from the saved record
80
+ const pageLink = {
81
+ id: savedRecord.id,
82
+ type: postType,
83
+ title: decodeEntities( savedRecord.title.rendered ),
84
+ url: savedRecord.link,
85
+ kind: 'post-type',
86
+ };
87
+
88
+ onPageCreated( pageLink );
89
+ }
90
+ } catch ( error ) {
91
+ // Error handling is done via the data store selectors
92
+ }
93
+ }
94
+
95
+ const isSubmitDisabled = isSaving || ! isTitleValid;
96
+
97
+ return (
98
+ <DialogWrapper
99
+ className="link-ui-page-creator"
100
+ title={ __( 'Create page' ) }
101
+ description={ __( 'Create a new page to add to your Navigation.' ) }
102
+ onBack={ onBack }
103
+ >
104
+ <VStack className="link-ui-page-creator__inner" spacing={ 4 }>
105
+ <form onSubmit={ createPage }>
106
+ <VStack spacing={ 4 }>
107
+ <TextControl
108
+ __next40pxDefaultSize
109
+ __nextHasNoMarginBottom
110
+ label={ __( 'Title' ) }
111
+ onChange={ setTitle }
112
+ placeholder={ __( 'No title' ) }
113
+ value={ title }
114
+ />
115
+
116
+ <CheckboxControl
117
+ __nextHasNoMarginBottom
118
+ label={ __( 'Publish immediately' ) }
119
+ help={ __(
120
+ 'If unchecked, the page will be created as a draft.'
121
+ ) }
122
+ checked={ shouldPublish }
123
+ onChange={ setShouldPublish }
124
+ />
125
+
126
+ { lastError && (
127
+ <Notice status="error" isDismissible={ false }>
128
+ { lastError.message }
129
+ </Notice>
130
+ ) }
131
+
132
+ <HStack spacing={ 2 } justify="flex-end">
133
+ <Button
134
+ __next40pxDefaultSize
135
+ variant="tertiary"
136
+ onClick={ onBack }
137
+ disabled={ isSaving }
138
+ accessibleWhenDisabled
139
+ >
140
+ { __( 'Cancel' ) }
141
+ </Button>
142
+ <Button
143
+ __next40pxDefaultSize
144
+ variant="primary"
145
+ type="submit"
146
+ isBusy={ isSaving }
147
+ aria-disabled={ isSubmitDisabled }
148
+ >
149
+ { __( 'Create page' ) }
150
+ </Button>
151
+ </HStack>
152
+ </VStack>
153
+ </form>
154
+ </VStack>
155
+ </DialogWrapper>
156
+ );
157
+ }
@@ -16,7 +16,7 @@
16
16
  }
17
17
 
18
18
  .link-ui-tools {
19
- border-top: 1px solid $gray-100;
19
+ outline: 1px solid $gray-100;
20
20
  padding: $grid-unit-10;
21
21
  }
22
22