@wordpress/block-library 9.40.2-next.v.202602241322.0 → 9.41.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 (153) hide show
  1. package/CHANGELOG.md +2 -0
  2. package/build/accordion/view.cjs +0 -34
  3. package/build/accordion/view.cjs.map +2 -2
  4. package/build/button/block.json +11 -3
  5. package/build/button/deprecated.cjs +246 -13
  6. package/build/button/deprecated.cjs.map +2 -2
  7. package/build/button/edit.cjs +45 -58
  8. package/build/button/edit.cjs.map +3 -3
  9. package/build/button/save.cjs +3 -7
  10. package/build/button/save.cjs.map +2 -2
  11. package/build/button/utils.cjs +59 -0
  12. package/build/button/utils.cjs.map +7 -0
  13. package/build/image/image.cjs +1 -1
  14. package/build/image/image.cjs.map +2 -2
  15. package/build/navigation/edit/index.cjs +4 -2
  16. package/build/navigation/edit/index.cjs.map +3 -3
  17. package/build/navigation/edit/leaf-more-menu.cjs +68 -6
  18. package/build/navigation/edit/leaf-more-menu.cjs.map +3 -3
  19. package/build/navigation/edit/menu-inspector-controls.cjs +20 -91
  20. package/build/navigation/edit/menu-inspector-controls.cjs.map +3 -3
  21. package/build/navigation/edit/navigation-link-ui.cjs +97 -0
  22. package/build/navigation/edit/navigation-link-ui.cjs.map +7 -0
  23. package/build/navigation/edit/navigation-list-view-header.cjs +86 -0
  24. package/build/navigation/edit/navigation-list-view-header.cjs.map +7 -0
  25. package/build/navigation/edit/navigation-menu-selector.cjs +4 -2
  26. package/build/navigation/edit/navigation-menu-selector.cjs.map +3 -3
  27. package/build/navigation/edit/placeholder/index.cjs +2 -2
  28. package/build/navigation/edit/placeholder/index.cjs.map +3 -3
  29. package/build/navigation-link/shared/controls.cjs +29 -52
  30. package/build/navigation-link/shared/controls.cjs.map +3 -3
  31. package/build/navigation-link/shared/use-link-preview.cjs +8 -9
  32. package/build/navigation-link/shared/use-link-preview.cjs.map +2 -2
  33. package/build/page-list-item/edit.cjs +6 -3
  34. package/build/page-list-item/edit.cjs.map +2 -2
  35. package/build/playlist/edit.cjs +43 -136
  36. package/build/playlist/edit.cjs.map +3 -3
  37. package/build/playlist/view.cjs +56 -38
  38. package/build/playlist/view.cjs.map +2 -2
  39. package/build/playlist-track/edit.cjs +0 -1
  40. package/build/playlist-track/edit.cjs.map +2 -2
  41. package/build/post-title/block.json +3 -0
  42. package/build/post-title/edit.cjs +2 -2
  43. package/build/post-title/edit.cjs.map +2 -2
  44. package/build/utils/waveform-player.cjs +68 -0
  45. package/build/utils/waveform-player.cjs.map +7 -0
  46. package/build/utils/waveform-utils.cjs +171 -0
  47. package/build/utils/waveform-utils.cjs.map +7 -0
  48. package/build-module/accordion/view.mjs +1 -35
  49. package/build-module/accordion/view.mjs.map +2 -2
  50. package/build-module/button/block.json +11 -3
  51. package/build-module/button/deprecated.mjs +246 -13
  52. package/build-module/button/deprecated.mjs.map +2 -2
  53. package/build-module/button/edit.mjs +47 -63
  54. package/build-module/button/edit.mjs.map +2 -2
  55. package/build-module/button/save.mjs +3 -7
  56. package/build-module/button/save.mjs.map +2 -2
  57. package/build-module/button/utils.mjs +33 -0
  58. package/build-module/button/utils.mjs.map +7 -0
  59. package/build-module/image/image.mjs +1 -1
  60. package/build-module/image/image.mjs.map +2 -2
  61. package/build-module/navigation/edit/index.mjs +4 -2
  62. package/build-module/navigation/edit/index.mjs.map +2 -2
  63. package/build-module/navigation/edit/leaf-more-menu.mjs +73 -7
  64. package/build-module/navigation/edit/leaf-more-menu.mjs.map +2 -2
  65. package/build-module/navigation/edit/menu-inspector-controls.mjs +21 -101
  66. package/build-module/navigation/edit/menu-inspector-controls.mjs.map +2 -2
  67. package/build-module/navigation/edit/navigation-link-ui.mjs +76 -0
  68. package/build-module/navigation/edit/navigation-link-ui.mjs.map +7 -0
  69. package/build-module/navigation/edit/navigation-list-view-header.mjs +58 -0
  70. package/build-module/navigation/edit/navigation-list-view-header.mjs.map +7 -0
  71. package/build-module/navigation/edit/navigation-menu-selector.mjs +5 -3
  72. package/build-module/navigation/edit/navigation-menu-selector.mjs.map +2 -2
  73. package/build-module/navigation/edit/placeholder/index.mjs +2 -2
  74. package/build-module/navigation/edit/placeholder/index.mjs.map +2 -2
  75. package/build-module/navigation-link/shared/controls.mjs +29 -53
  76. package/build-module/navigation-link/shared/controls.mjs.map +2 -2
  77. package/build-module/navigation-link/shared/use-link-preview.mjs +8 -9
  78. package/build-module/navigation-link/shared/use-link-preview.mjs.map +2 -2
  79. package/build-module/page-list-item/edit.mjs +6 -3
  80. package/build-module/page-list-item/edit.mjs.map +2 -2
  81. package/build-module/playlist/edit.mjs +41 -139
  82. package/build-module/playlist/edit.mjs.map +2 -2
  83. package/build-module/playlist/view.mjs +56 -38
  84. package/build-module/playlist/view.mjs.map +2 -2
  85. package/build-module/playlist-track/edit.mjs +0 -1
  86. package/build-module/playlist-track/edit.mjs.map +2 -2
  87. package/build-module/post-title/block.json +3 -0
  88. package/build-module/post-title/edit.mjs +2 -2
  89. package/build-module/post-title/edit.mjs.map +2 -2
  90. package/build-module/utils/waveform-player.mjs +43 -0
  91. package/build-module/utils/waveform-player.mjs.map +7 -0
  92. package/build-module/utils/waveform-utils.mjs +131 -0
  93. package/build-module/utils/waveform-utils.mjs.map +7 -0
  94. package/build-style/button/style-rtl.css +6 -0
  95. package/build-style/button/style.css +6 -0
  96. package/build-style/editor-rtl.css +13 -3
  97. package/build-style/editor.css +13 -3
  98. package/build-style/navigation-link/editor-rtl.css +10 -0
  99. package/build-style/navigation-link/editor.css +10 -0
  100. package/build-style/playlist/editor-rtl.css +3 -3
  101. package/build-style/playlist/editor.css +3 -3
  102. package/build-style/playlist/style-rtl.css +351 -17
  103. package/build-style/playlist/style.css +351 -17
  104. package/build-style/style-rtl.css +357 -17
  105. package/build-style/style.css +357 -17
  106. package/package.json +39 -38
  107. package/src/accordion/view.js +1 -44
  108. package/src/accordion-item/index.php +0 -1
  109. package/src/button/block.json +11 -3
  110. package/src/button/deprecated.js +254 -16
  111. package/src/button/edit.js +50 -61
  112. package/src/button/index.php +68 -0
  113. package/src/button/save.js +2 -8
  114. package/src/button/style.scss +49 -7
  115. package/src/button/test/utils.js +84 -0
  116. package/src/button/utils.js +42 -0
  117. package/src/cover/index.php +4 -4
  118. package/src/image/image.js +14 -15
  119. package/src/image/index.php +3 -1
  120. package/src/navigation/edit/index.js +4 -2
  121. package/src/navigation/edit/leaf-more-menu.js +86 -11
  122. package/src/navigation/edit/menu-inspector-controls.js +23 -142
  123. package/src/navigation/edit/navigation-link-ui.js +115 -0
  124. package/src/navigation/edit/navigation-list-view-header.js +62 -0
  125. package/src/navigation/edit/navigation-menu-selector.js +5 -3
  126. package/src/navigation/edit/placeholder/index.js +3 -2
  127. package/src/navigation/edit/test/navigation-menu-selector.js +23 -20
  128. package/src/navigation-link/editor.scss +18 -0
  129. package/src/navigation-link/shared/controls.js +35 -62
  130. package/src/navigation-link/shared/test/controls.js +5 -5
  131. package/src/navigation-link/shared/test/use-link-preview.test.js +19 -1
  132. package/src/navigation-link/shared/use-link-preview.js +14 -15
  133. package/src/page-list/index.php +1 -1
  134. package/src/page-list-item/edit.js +8 -7
  135. package/src/playlist/edit.js +60 -154
  136. package/src/playlist/editor.scss +3 -3
  137. package/src/playlist/index.php +15 -40
  138. package/src/playlist/style.scss +34 -27
  139. package/src/playlist/test/edit.js +137 -0
  140. package/src/playlist/view.js +97 -40
  141. package/src/playlist-track/edit.js +0 -1
  142. package/src/post-title/block.json +3 -0
  143. package/src/post-title/edit.js +4 -2
  144. package/src/query-title/index.php +1 -1
  145. package/src/search/index.php +1 -1
  146. package/src/utils/test/waveform-utils.js +328 -0
  147. package/src/utils/waveform-player.js +77 -0
  148. package/src/utils/waveform-utils.js +232 -0
  149. package/build/navigation/use-navigation-entities.cjs +0 -67
  150. package/build/navigation/use-navigation-entities.cjs.map +0 -7
  151. package/build-module/navigation/use-navigation-entities.mjs +0 -46
  152. package/build-module/navigation/use-navigation-entities.mjs.map +0 -7
  153. package/src/navigation/use-navigation-entities.js +0 -72
@@ -0,0 +1,115 @@
1
+ /**
2
+ * WordPress dependencies
3
+ */
4
+ import { store as blockEditorStore } from '@wordpress/block-editor';
5
+ import { useDispatch } from '@wordpress/data';
6
+
7
+ /**
8
+ * Internal dependencies
9
+ */
10
+ import {
11
+ LinkUI,
12
+ updateAttributes,
13
+ useEntityBinding,
14
+ } from '../../navigation-link/shared';
15
+
16
+ const BLOCKS_WITH_LINK_UI_SUPPORT = [
17
+ 'core/navigation-link',
18
+ 'core/navigation-submenu',
19
+ ];
20
+
21
+ export function NavigationLinkUI( { block, insertedBlock, setInsertedBlock } ) {
22
+ const { updateBlockAttributes, removeBlock } =
23
+ useDispatch( blockEditorStore );
24
+
25
+ const supportsLinkControls = BLOCKS_WITH_LINK_UI_SUPPORT?.includes(
26
+ insertedBlock?.name
27
+ );
28
+ const blockWasJustInserted = insertedBlock?.clientId === block.clientId;
29
+ const showLinkControls = supportsLinkControls && blockWasJustInserted;
30
+
31
+ // Get binding utilities for the inserted block
32
+ const { createBinding, clearBinding } = useEntityBinding( {
33
+ clientId: insertedBlock?.clientId,
34
+ attributes: insertedBlock?.attributes || {},
35
+ } );
36
+
37
+ if ( ! showLinkControls ) {
38
+ return null;
39
+ }
40
+
41
+ /**
42
+ * Cleanup function for auto-inserted Navigation Link blocks.
43
+ *
44
+ * Removes the block if it has no URL and clears the inserted block state.
45
+ * This ensures consistent cleanup behavior across different contexts.
46
+ */
47
+ const cleanupInsertedBlock = () => {
48
+ // Prevent automatic block selection when removing blocks in list view context
49
+ // This avoids focus stealing that would close the list view and switch to canvas
50
+ const shouldAutoSelectBlock = false;
51
+
52
+ // Follows the exact same pattern as Navigation Link block's onClose handler
53
+ // If there is no URL then remove the auto-inserted block to avoid empty blocks
54
+ if ( ! insertedBlock?.attributes?.url && insertedBlock?.clientId ) {
55
+ // Remove the block entirely to avoid poor UX
56
+ // This matches the Navigation Link block's behavior
57
+ removeBlock( insertedBlock.clientId, shouldAutoSelectBlock );
58
+ }
59
+ setInsertedBlock( null );
60
+ };
61
+
62
+ const setInsertedBlockAttributes =
63
+ ( _insertedBlockClientId ) => ( _updatedAttributes ) => {
64
+ if ( ! _insertedBlockClientId ) {
65
+ return;
66
+ }
67
+ updateBlockAttributes( _insertedBlockClientId, _updatedAttributes );
68
+ };
69
+
70
+ // Wrapper function to clean up original block when a new block is selected
71
+ const handleSetInsertedBlock = ( newBlock ) => {
72
+ // Prevent automatic block selection when removing blocks in list view context
73
+ // This avoids focus stealing that would close the list view and switch to canvas
74
+ const shouldAutoSelectBlock = false;
75
+
76
+ // If we have an existing inserted block and a new block is being set,
77
+ // remove the original block to avoid duplicates
78
+ if ( insertedBlock?.clientId && newBlock ) {
79
+ removeBlock( insertedBlock.clientId, shouldAutoSelectBlock );
80
+ }
81
+ setInsertedBlock( newBlock );
82
+ };
83
+
84
+ return (
85
+ <LinkUI
86
+ clientId={ insertedBlock?.clientId }
87
+ link={ insertedBlock?.attributes }
88
+ onBlockInsert={ handleSetInsertedBlock }
89
+ onClose={ () => {
90
+ // Use cleanup function
91
+ cleanupInsertedBlock();
92
+ } }
93
+ onChange={ ( updatedValue ) => {
94
+ // updateAttributes determines the final state and returns metadata
95
+ const { isEntityLink, attributes: updatedAttributes } =
96
+ updateAttributes(
97
+ updatedValue,
98
+ setInsertedBlockAttributes( insertedBlock?.clientId ),
99
+ insertedBlock?.attributes
100
+ );
101
+
102
+ // Handle URL binding based on the final computed state
103
+ // Only create bindings for entity links (posts, pages, taxonomies)
104
+ // Never create bindings for custom links (manual URLs)
105
+ if ( isEntityLink ) {
106
+ createBinding( updatedAttributes );
107
+ } else {
108
+ clearBinding();
109
+ }
110
+
111
+ setInsertedBlock( null );
112
+ } }
113
+ />
114
+ );
115
+ }
@@ -0,0 +1,62 @@
1
+ /**
2
+ * WordPress dependencies
3
+ */
4
+ import {
5
+ __experimentalHStack as HStack,
6
+ __experimentalHeading as Heading,
7
+ } from '@wordpress/components';
8
+ import { __ } from '@wordpress/i18n';
9
+ import { privateApis as blockEditorPrivateApis } from '@wordpress/block-editor';
10
+
11
+ /**
12
+ * Internal dependencies
13
+ */
14
+ import NavigationMenuSelector from './navigation-menu-selector';
15
+ import { unlock } from '../../lock-unlock';
16
+
17
+ const { useBlockDisplayTitle } = unlock( blockEditorPrivateApis );
18
+
19
+ const actionLabel =
20
+ /* translators: %s: The name of a menu. */ __( "Switch to '%s'" );
21
+
22
+ export default function NavigationListViewHeader( {
23
+ clientId,
24
+ blockEditingMode,
25
+ currentMenuId,
26
+ onSelectClassicMenu,
27
+ onSelectNavigationMenu,
28
+ onCreateNew,
29
+ createNavigationMenuIsSuccess,
30
+ createNavigationMenuIsError,
31
+ isManageMenusButtonDisabled,
32
+ } ) {
33
+ const blockTitle = useBlockDisplayTitle( {
34
+ clientId,
35
+ context: 'list-view',
36
+ } );
37
+
38
+ return (
39
+ <HStack className="wp-block-navigation-off-canvas-editor__header">
40
+ <Heading
41
+ className="wp-block-navigation-off-canvas-editor__title"
42
+ level={ 2 }
43
+ >
44
+ { blockTitle }
45
+ </Heading>
46
+ { blockEditingMode === 'default' && (
47
+ <NavigationMenuSelector
48
+ currentMenuId={ currentMenuId }
49
+ onSelectClassicMenu={ onSelectClassicMenu }
50
+ onSelectNavigationMenu={ onSelectNavigationMenu }
51
+ onCreateNew={ onCreateNew }
52
+ createNavigationMenuIsSuccess={
53
+ createNavigationMenuIsSuccess
54
+ }
55
+ createNavigationMenuIsError={ createNavigationMenuIsError }
56
+ actionLabel={ actionLabel }
57
+ isManageMenusButtonDisabled={ isManageMenusButtonDisabled }
58
+ />
59
+ ) }
60
+ </HStack>
61
+ );
62
+ }
@@ -11,13 +11,12 @@ import { moreVertical } from '@wordpress/icons';
11
11
  import { __, sprintf } from '@wordpress/i18n';
12
12
  import { decodeEntities } from '@wordpress/html-entities';
13
13
  import { useEffect, useMemo, useState } from '@wordpress/element';
14
- import { useEntityProp } from '@wordpress/core-data';
14
+ import { useEntityRecords, useEntityProp } from '@wordpress/core-data';
15
15
 
16
16
  /**
17
17
  * Internal dependencies
18
18
  */
19
19
  import useNavigationMenu from '../use-navigation-menu';
20
- import useNavigationEntities from '../use-navigation-entities';
21
20
 
22
21
  function buildMenuLabel( title, id, status ) {
23
22
  if ( ! title ) {
@@ -53,7 +52,10 @@ function NavigationMenuSelector( {
53
52
 
54
53
  actionLabel = actionLabel || createActionLabel;
55
54
 
56
- const { menus: classicMenus } = useNavigationEntities();
55
+ const { records: classicMenus } = useEntityRecords( 'root', 'menu', {
56
+ per_page: -1,
57
+ context: 'view',
58
+ } );
57
59
 
58
60
  const {
59
61
  navigationMenus,
@@ -6,11 +6,11 @@ import { __ } from '@wordpress/i18n';
6
6
  import { navigation, Icon } from '@wordpress/icons';
7
7
  import { speak } from '@wordpress/a11y';
8
8
  import { useEffect } from '@wordpress/element';
9
+ import { useEntityRecords } from '@wordpress/core-data';
9
10
 
10
11
  /**
11
12
  * Internal dependencies
12
13
  */
13
- import useNavigationEntities from '../../use-navigation-entities';
14
14
  import PlaceholderPreview from './placeholder-preview';
15
15
  import NavigationMenuSelector from '../navigation-menu-selector';
16
16
 
@@ -24,7 +24,8 @@ export default function NavigationPlaceholder( {
24
24
  onSelectClassicMenu,
25
25
  onCreateEmpty,
26
26
  } ) {
27
- const { isResolvingMenus, hasResolvedMenus } = useNavigationEntities();
27
+ const { isResolving: isResolvingMenus, hasResolved: hasResolvedMenus } =
28
+ useEntityRecords( 'root', 'menu', { per_page: -1, context: 'view' } );
28
29
 
29
30
  useEffect( () => {
30
31
  if ( ! isSelected ) {
@@ -4,12 +4,16 @@
4
4
  import { render, screen } from '@testing-library/react';
5
5
  import userEvent from '@testing-library/user-event';
6
6
 
7
+ /**
8
+ * WordPress dependencies
9
+ */
10
+ import { useEntityRecords } from '@wordpress/core-data';
11
+
7
12
  /**
8
13
  * Internal dependencies
9
14
  */
10
15
  import NavigationMenuSelector from '../navigation-menu-selector';
11
16
  import useNavigationMenu from '../../use-navigation-menu';
12
- import useNavigationEntities from '../../use-navigation-entities';
13
17
 
14
18
  jest.mock( '../../use-navigation-menu', () => {
15
19
  // This allows us to tweak the returned value on each test.
@@ -17,14 +21,13 @@ jest.mock( '../../use-navigation-menu', () => {
17
21
  return mock;
18
22
  } );
19
23
 
20
- jest.mock( '../../use-navigation-entities', () => {
21
- // This allows us to tweak the returned value on each test.
22
- const mock = jest.fn();
23
- return mock;
24
- } );
24
+ jest.mock( '@wordpress/core-data', () => ( {
25
+ ...jest.requireActual( '@wordpress/core-data' ),
26
+ useEntityRecords: jest.fn(),
27
+ } ) );
25
28
 
26
- useNavigationEntities.mockReturnValue( {
27
- menus: [],
29
+ useEntityRecords.mockReturnValue( {
30
+ records: [],
28
31
  } );
29
32
 
30
33
  const navigationMenu1 = {
@@ -485,8 +488,8 @@ describe( 'NavigationMenuSelector', () => {
485
488
  it( 'should not show classic menus if there are no classic menus', async () => {
486
489
  const user = userEvent.setup();
487
490
 
488
- useNavigationEntities.mockReturnValue( {
489
- menus: [],
491
+ useEntityRecords.mockReturnValue( {
492
+ records: [],
490
493
  } );
491
494
 
492
495
  render( <NavigationMenuSelector /> );
@@ -507,8 +510,8 @@ describe( 'NavigationMenuSelector', () => {
507
510
  canUserCreateNavigationMenus: false,
508
511
  } );
509
512
 
510
- useNavigationEntities.mockReturnValue( {
511
- menus: classicMenusFixture,
513
+ useEntityRecords.mockReturnValue( {
514
+ records: classicMenusFixture,
512
515
  } );
513
516
 
514
517
  render( <NavigationMenuSelector /> );
@@ -529,8 +532,8 @@ describe( 'NavigationMenuSelector', () => {
529
532
  canUserCreateNavigationMenus: true,
530
533
  } );
531
534
 
532
- useNavigationEntities.mockReturnValue( {
533
- menus: classicMenusFixture,
535
+ useEntityRecords.mockReturnValue( {
536
+ records: classicMenusFixture,
534
537
  } );
535
538
 
536
539
  render( <NavigationMenuSelector /> );
@@ -566,8 +569,8 @@ describe( 'NavigationMenuSelector', () => {
566
569
  canUserCreateNavigationMenus: true,
567
570
  } );
568
571
 
569
- useNavigationEntities.mockReturnValue( {
570
- menus: classicMenusFixture,
572
+ useEntityRecords.mockReturnValue( {
573
+ records: classicMenusFixture,
571
574
  } );
572
575
 
573
576
  const { rerender } = render(
@@ -598,8 +601,8 @@ describe( 'NavigationMenuSelector', () => {
598
601
  canUserCreateNavigationMenus: true,
599
602
  } );
600
603
 
601
- useNavigationEntities.mockReturnValue( {
602
- menus: classicMenusFixture,
604
+ useEntityRecords.mockReturnValue( {
605
+ records: classicMenusFixture,
603
606
  } );
604
607
 
605
608
  rerender(
@@ -631,8 +634,8 @@ describe( 'NavigationMenuSelector', () => {
631
634
  canUserCreateNavigationMenus: true,
632
635
  } );
633
636
 
634
- useNavigationEntities.mockReturnValue( {
635
- menus: classicMenusFixture,
637
+ useEntityRecords.mockReturnValue( {
638
+ records: classicMenusFixture,
636
639
  } );
637
640
 
638
641
  // Simulate the menu being created and component being re-rendered.
@@ -150,3 +150,21 @@
150
150
  .navigation-link-control__error-text {
151
151
  color: $alert-red;
152
152
  }
153
+
154
+ .navigation-link-to__action-button {
155
+ grid-column: auto;
156
+ // When this is the last button AND in an odd position among its class
157
+ // siblings, it's unpaired — span the full grid row.
158
+ &:nth-last-child(1 of #{&}):nth-child(odd of #{&}) {
159
+ grid-column: 1 / -1;
160
+ }
161
+
162
+ // Artificially increase specificity to override `justify-content` styles.
163
+ // Not ideal, but it shouldn't be necessary once switching to the new `Button`
164
+ // component from `@wordpress/ui`.
165
+ &#{&}#{&} {
166
+ justify-content: center;
167
+ }
168
+
169
+ }
170
+
@@ -5,7 +5,6 @@ import {
5
5
  Button,
6
6
  __experimentalToolsPanel as ToolsPanel,
7
7
  __experimentalToolsPanelItem as ToolsPanelItem,
8
- __experimentalHStack as HStack,
9
8
  CheckboxControl,
10
9
  TextControl,
11
10
  TextareaControl,
@@ -150,7 +149,7 @@ export function Controls( {
150
149
 
151
150
  // Check if URL is viewable (not hash link or other relative path like ./ or ../)
152
151
  const isViewableUrl =
153
- url &&
152
+ !! url &&
154
153
  ( ! isHashLink( url ) ||
155
154
  ( isRelativePath( url ) && ! url.startsWith( '/' ) ) );
156
155
 
@@ -158,11 +157,6 @@ export function Controls( {
158
157
  const viewUrl =
159
158
  isViewableUrl && url.startsWith( '/' ) && homeUrl ? homeUrl + url : url;
160
159
 
161
- const entityTypeName = getEntityTypeName(
162
- attributes.type,
163
- attributes.kind
164
- );
165
-
166
160
  return (
167
161
  <ToolsPanel
168
162
  label={ __( 'Settings' ) }
@@ -220,61 +214,6 @@ export function Controls( {
220
214
  help={ helpText ? helpText : undefined }
221
215
  />
222
216
  </ToolsPanelItem>
223
-
224
- { url && (
225
- <HStack
226
- className="navigation-link-to__actions"
227
- alignment="left"
228
- justify="left"
229
- style={ { gridColumn: '1 / -1' } }
230
- >
231
- { hasUrlBinding &&
232
- isBoundEntityAvailable &&
233
- entityRecord?.id &&
234
- attributes.kind === 'post-type' &&
235
- onNavigateToEntityRecord && (
236
- <Button
237
- size="compact"
238
- variant="secondary"
239
- onClick={ () => {
240
- onNavigateToEntityRecord( {
241
- postId: entityRecord.id,
242
- postType: attributes.type,
243
- } );
244
- } }
245
- __next40pxDefaultSize
246
- >
247
- { sprintf(
248
- /* translators: %s: entity type (e.g., "page", "post", "category") */
249
- __( 'Edit %s' ),
250
- entityTypeName
251
- ) }
252
- </Button>
253
- ) }
254
- { isViewableUrl && (
255
- <Button
256
- size="compact"
257
- variant="secondary"
258
- href={ viewUrl }
259
- target="_blank"
260
- icon={ external }
261
- iconPosition="right"
262
- __next40pxDefaultSize
263
- >
264
- { sprintf(
265
- /* translators: %s: entity type (e.g., "page", "post", "category") or "link" for external links */
266
- __( 'View %s' ),
267
- attributes.kind &&
268
- attributes.type &&
269
- attributes.kind !== 'custom'
270
- ? entityTypeName
271
- : __( 'link' )
272
- ) }
273
- </Button>
274
- ) }
275
- </HStack>
276
- ) }
277
-
278
217
  <ToolsPanelItem
279
218
  hasValue={ () => !! opensInNewTab }
280
219
  label={ __( 'Open in new tab' ) }
@@ -291,6 +230,40 @@ export function Controls( {
291
230
  }
292
231
  />
293
232
  </ToolsPanelItem>
233
+
234
+ { !! url &&
235
+ hasUrlBinding &&
236
+ isBoundEntityAvailable &&
237
+ entityRecord?.id &&
238
+ attributes.kind === 'post-type' &&
239
+ onNavigateToEntityRecord && (
240
+ <Button
241
+ variant="secondary"
242
+ onClick={ () => {
243
+ onNavigateToEntityRecord( {
244
+ postId: entityRecord.id,
245
+ postType: attributes.type,
246
+ } );
247
+ } }
248
+ __next40pxDefaultSize
249
+ className="navigation-link-to__action-button"
250
+ >
251
+ { __( 'Edit' ) }
252
+ </Button>
253
+ ) }
254
+ { isViewableUrl && (
255
+ <Button
256
+ variant="secondary"
257
+ href={ viewUrl }
258
+ target="_blank"
259
+ icon={ external }
260
+ iconPosition="right"
261
+ __next40pxDefaultSize
262
+ className="navigation-link-to__action-button"
263
+ >
264
+ { __( 'View' ) }
265
+ </Button>
266
+ ) }
294
267
  </>
295
268
  ) }
296
269
 
@@ -237,7 +237,7 @@ describe( 'Controls', () => {
237
237
 
238
238
  render( <Controls { ...propsWithExternalLink } /> );
239
239
 
240
- expect( screen.getByText( 'View link' ) ).toBeVisible();
240
+ expect( screen.getByText( 'View' ) ).toBeVisible();
241
241
  } );
242
242
 
243
243
  it( 'shows "View page" for page links', () => {
@@ -253,7 +253,7 @@ describe( 'Controls', () => {
253
253
 
254
254
  render( <Controls { ...propsWithPageLink } /> );
255
255
 
256
- expect( screen.getByText( 'View page' ) ).toBeVisible();
256
+ expect( screen.getByText( 'View' ) ).toBeVisible();
257
257
  } );
258
258
 
259
259
  it( 'shows "View post" for post links', () => {
@@ -269,7 +269,7 @@ describe( 'Controls', () => {
269
269
 
270
270
  render( <Controls { ...propsWithPostLink } /> );
271
271
 
272
- expect( screen.getByText( 'View post' ) ).toBeVisible();
272
+ expect( screen.getByText( 'View' ) ).toBeVisible();
273
273
  } );
274
274
 
275
275
  it( 'shows "View category" for category links', () => {
@@ -285,7 +285,7 @@ describe( 'Controls', () => {
285
285
 
286
286
  render( <Controls { ...propsWithCategoryLink } /> );
287
287
 
288
- expect( screen.getByText( 'View category' ) ).toBeVisible();
288
+ expect( screen.getByText( 'View' ) ).toBeVisible();
289
289
  } );
290
290
 
291
291
  it( 'shows "View link" for custom type links', () => {
@@ -301,7 +301,7 @@ describe( 'Controls', () => {
301
301
 
302
302
  render( <Controls { ...propsWithCustomLink } /> );
303
303
 
304
- expect( screen.getByText( 'View link' ) ).toBeVisible();
304
+ expect( screen.getByText( 'View' ) ).toBeVisible();
305
305
  } );
306
306
  } );
307
307
  } );
@@ -92,10 +92,28 @@ describe( 'computeDisplayUrl', () => {
92
92
  it( 'should treat same-origin URLs as internal', () => {
93
93
  const result = computeDisplayUrl( {
94
94
  linkUrl: 'https://example.com/my-page',
95
- siteUrl: 'https://example.com',
95
+ homeUrl: 'https://example.com',
96
96
  } );
97
97
  expect( result.isExternal ).toBe( false );
98
98
  } );
99
+
100
+ it( 'should treat same-origin URLs as internal when homeUrl includes a path', () => {
101
+ const result = computeDisplayUrl( {
102
+ linkUrl: 'https://example.com/my-page',
103
+ homeUrl: 'https://example.com/blog',
104
+ } );
105
+ expect( result.isExternal ).toBe( false );
106
+ expect( result.displayUrl ).toBe( '/my-page' );
107
+ } );
108
+
109
+ it( 'should treat http and https to same host as internal (compare by host, not origin)', () => {
110
+ const result = computeDisplayUrl( {
111
+ linkUrl: 'http://example.com/my-page',
112
+ homeUrl: 'https://example.com',
113
+ } );
114
+ expect( result.isExternal ).toBe( false );
115
+ expect( result.displayUrl ).toBe( '/my-page' );
116
+ } );
99
117
  } );
100
118
 
101
119
  describe( 'special protocols and edge cases', () => {
@@ -31,10 +31,10 @@ function capitalize( str ) {
31
31
  *
32
32
  * @param {Object} options - Parameters object
33
33
  * @param {string} options.linkUrl - The URL to process
34
- * @param {string} options.siteUrl - The WordPress site URL (falls back to window.location.origin)
34
+ * @param {string} options.homeUrl - The WordPress site URL (required for internal/external detection)
35
35
  * @return {Object} Object with displayUrl and isExternal flag
36
36
  */
37
- export function computeDisplayUrl( { linkUrl, siteUrl } = {} ) {
37
+ export function computeDisplayUrl( { linkUrl, homeUrl } = {} ) {
38
38
  if ( ! linkUrl ) {
39
39
  return { displayUrl: '', isExternal: false };
40
40
  }
@@ -51,9 +51,10 @@ export function computeDisplayUrl( { linkUrl, siteUrl } = {} ) {
51
51
  // This must happen before trusting the type attribute
52
52
  try {
53
53
  const parsedUrl = new URL( linkUrl );
54
- // Use provided siteUrl or fall back to window.location.origin
55
- const siteDomain = siteUrl || window.location.origin;
56
- if ( parsedUrl.origin === siteDomain ) {
54
+ // Compare by host (not origin) so http/https to same site both count as internal
55
+ const siteHost = new URL( homeUrl ).host;
56
+
57
+ if ( parsedUrl.host === siteHost ) {
57
58
  // Show only the pathname (and search/hash if present)
58
59
  let path = parsedUrl.pathname + parsedUrl.search + parsedUrl.hash;
59
60
  // Remove trailing slash
@@ -62,12 +63,11 @@ export function computeDisplayUrl( { linkUrl, siteUrl } = {} ) {
62
63
  }
63
64
  displayUrl = path;
64
65
  } else {
65
- // Different origin - this is an external link
66
+ // Different host - this is an external link
66
67
  isExternal = true;
67
68
  }
68
69
  } catch ( e ) {
69
- // URL parsing failed - this means it's likely a URL without a protocol (e.g., "www.example.com")
70
- // Since we already checked for relative paths and hash links above, treat as external
70
+ // URL parsing failed - treat as external (e.g. no homeUrl, or URL without protocol)
71
71
  isExternal = true;
72
72
  }
73
73
 
@@ -174,13 +174,12 @@ export function useLinkPreview( {
174
174
  hasBinding,
175
175
  isEntityAvailable,
176
176
  } ) {
177
- // Get the WordPress site URL from settings
178
- const siteUrl = useSelect( ( select ) => {
179
- const siteEntity = select( coreDataStore ).getEntityRecord(
177
+ // Get the WordPress homepage URL from settings
178
+ const homeUrl = useSelect( ( select ) => {
179
+ return select( coreDataStore ).getEntityRecord(
180
180
  'root',
181
- 'site'
182
- );
183
- return siteEntity?.url;
181
+ '__unstableBase'
182
+ )?.home;
184
183
  }, [] );
185
184
 
186
185
  const title =
@@ -194,7 +193,7 @@ export function useLinkPreview( {
194
193
  // Compute display URL and external flag
195
194
  const { displayUrl, isExternal } = computeDisplayUrl( {
196
195
  linkUrl: url,
197
- siteUrl,
196
+ homeUrl,
198
197
  } );
199
198
 
200
199
  const image = useSelect(
@@ -166,7 +166,7 @@ function block_core_page_list_build_css_font_sizes( $context ) {
166
166
  *
167
167
  * @since 5.8.0
168
168
  *
169
- * @param boolean $open_submenus_on_click Whether to open submenus on click instead of hover.
169
+ * @param string $submenu_visibility The submenu visibility mode: 'hover', 'click', or 'always'.
170
170
  * @param boolean $show_submenu_icons Whether to show submenu indicator icons.
171
171
  * @param boolean $is_navigation_child If block is a child of Navigation block.
172
172
  * @param array $nested_pages The array of nested pages.
@@ -8,7 +8,6 @@ import clsx from 'clsx';
8
8
  import { useBlockProps, useInnerBlocksProps } from '@wordpress/block-editor';
9
9
  import { useSelect } from '@wordpress/data';
10
10
  import { store as coreStore } from '@wordpress/core-data';
11
- import { RawHTML } from '@wordpress/element';
12
11
  import { safeHTML } from '@wordpress/dom';
13
12
 
14
13
  /**
@@ -78,9 +77,10 @@ export default function PageListItemEdit( { context, attributes } ) {
78
77
  type="button"
79
78
  className="wp-block-navigation-item__content wp-block-navigation-submenu__toggle"
80
79
  aria-expanded="false"
81
- >
82
- <RawHTML>{ safeHTML( label ) }</RawHTML>
83
- </button>
80
+ dangerouslySetInnerHTML={ {
81
+ __html: safeHTML( label ),
82
+ } }
83
+ />
84
84
  <span className="wp-block-page-list__submenu-icon wp-block-navigation__submenu-icon">
85
85
  <ItemSubmenuIcon />
86
86
  </span>
@@ -91,9 +91,10 @@ export default function PageListItemEdit( { context, attributes } ) {
91
91
  'wp-block-navigation-item__content': isNavigationChild,
92
92
  } ) }
93
93
  href={ link }
94
- >
95
- <RawHTML>{ safeHTML( title ) }</RawHTML>
96
- </a>
94
+ dangerouslySetInnerHTML={ {
95
+ __html: safeHTML( title ),
96
+ } }
97
+ />
97
98
  ) }
98
99
  { hasChildren && (
99
100
  <>