@wordpress/block-editor 15.13.2-next.v.202602241322.0 → 15.13.2

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 (175) hide show
  1. package/build/components/block-bindings/attribute-control.cjs +5 -2
  2. package/build/components/block-bindings/attribute-control.cjs.map +2 -2
  3. package/build/components/block-inspector/edit-contents.cjs +5 -4
  4. package/build/components/block-inspector/edit-contents.cjs.map +2 -2
  5. package/build/components/block-list/block-html.cjs +6 -2
  6. package/build/components/block-list/block-html.cjs.map +2 -2
  7. package/build/components/block-list/block.cjs +9 -1
  8. package/build/components/block-list/block.cjs.map +2 -2
  9. package/build/components/block-list/use-block-props/index.cjs +10 -1
  10. package/build/components/block-list/use-block-props/index.cjs.map +2 -2
  11. package/build/components/block-settings-menu-controls/index.cjs +1 -0
  12. package/build/components/block-settings-menu-controls/index.cjs.map +2 -2
  13. package/build/components/block-styles/index.cjs +7 -1
  14. package/build/components/block-styles/index.cjs.map +3 -3
  15. package/build/components/block-switcher/block-transformations-menu.cjs +2 -0
  16. package/build/components/block-switcher/block-transformations-menu.cjs.map +2 -2
  17. package/build/components/block-switcher/block-variation-transformations.cjs +2 -0
  18. package/build/components/block-switcher/block-variation-transformations.cjs.map +2 -2
  19. package/build/components/block-toolbar/block-toolbar-icon.cjs +1 -1
  20. package/build/components/block-toolbar/block-toolbar-icon.cjs.map +2 -2
  21. package/build/components/block-toolbar/index.cjs +8 -4
  22. package/build/components/block-toolbar/index.cjs.map +2 -2
  23. package/build/components/block-tools/index.cjs +4 -3
  24. package/build/components/block-tools/index.cjs.map +2 -2
  25. package/build/components/block-variation-transforms/index.cjs +11 -5
  26. package/build/components/block-variation-transforms/index.cjs.map +2 -2
  27. package/build/components/block-visibility/modal.cjs +2 -2
  28. package/build/components/block-visibility/modal.cjs.map +1 -1
  29. package/build/components/block-visibility/use-block-visibility.cjs +4 -3
  30. package/build/components/block-visibility/use-block-visibility.cjs.map +2 -2
  31. package/build/components/block-visibility/viewport-visibility-info.cjs +6 -1
  32. package/build/components/block-visibility/viewport-visibility-info.cjs.map +2 -2
  33. package/build/components/dimensions-tool/scale-tool.cjs +2 -2
  34. package/build/components/dimensions-tool/scale-tool.cjs.map +2 -2
  35. package/build/components/iframe/index.cjs +1 -3
  36. package/build/components/iframe/index.cjs.map +2 -2
  37. package/build/components/iframe/use-scale-canvas.cjs +0 -1
  38. package/build/components/iframe/use-scale-canvas.cjs.map +2 -2
  39. package/build/components/inspector-controls-tabs/use-inspector-controls-tabs.cjs +8 -4
  40. package/build/components/inspector-controls-tabs/use-inspector-controls-tabs.cjs.map +2 -2
  41. package/build/components/link-picker/link-preview.cjs +2 -1
  42. package/build/components/link-picker/link-preview.cjs.map +3 -3
  43. package/build/components/provider/index.cjs +23 -7
  44. package/build/components/provider/index.cjs.map +2 -2
  45. package/build/components/provider/use-media-upload-settings.cjs +1 -0
  46. package/build/components/provider/use-media-upload-settings.cjs.map +2 -2
  47. package/build/components/writing-flow/use-arrow-nav.cjs +1 -1
  48. package/build/components/writing-flow/use-arrow-nav.cjs.map +2 -2
  49. package/build/hooks/auto-inspector-controls.cjs +26 -2
  50. package/build/hooks/auto-inspector-controls.cjs.map +3 -3
  51. package/build/hooks/cross-origin-isolation.cjs +9 -75
  52. package/build/hooks/cross-origin-isolation.cjs.map +2 -2
  53. package/build/hooks/custom-css.cjs +26 -0
  54. package/build/hooks/custom-css.cjs.map +3 -3
  55. package/build/private-apis.cjs +1 -0
  56. package/build/private-apis.cjs.map +2 -2
  57. package/build/store/actions.cjs +0 -8
  58. package/build/store/actions.cjs.map +2 -2
  59. package/build/store/private-keys.cjs +3 -0
  60. package/build/store/private-keys.cjs.map +2 -2
  61. package/build/store/private-selectors.cjs +4 -3
  62. package/build/store/private-selectors.cjs.map +2 -2
  63. package/build/store/reducer.cjs +78 -74
  64. package/build/store/reducer.cjs.map +2 -2
  65. package/build/store/selectors.cjs +16 -14
  66. package/build/store/selectors.cjs.map +2 -2
  67. package/build-module/components/block-bindings/attribute-control.mjs +5 -2
  68. package/build-module/components/block-bindings/attribute-control.mjs.map +2 -2
  69. package/build-module/components/block-inspector/edit-contents.mjs +5 -4
  70. package/build-module/components/block-inspector/edit-contents.mjs.map +2 -2
  71. package/build-module/components/block-list/block-html.mjs +7 -3
  72. package/build-module/components/block-list/block-html.mjs.map +2 -2
  73. package/build-module/components/block-list/block.mjs +10 -2
  74. package/build-module/components/block-list/block.mjs.map +2 -2
  75. package/build-module/components/block-list/use-block-props/index.mjs +11 -2
  76. package/build-module/components/block-list/use-block-props/index.mjs.map +2 -2
  77. package/build-module/components/block-settings-menu-controls/index.mjs +1 -0
  78. package/build-module/components/block-settings-menu-controls/index.mjs.map +2 -2
  79. package/build-module/components/block-styles/index.mjs +7 -1
  80. package/build-module/components/block-styles/index.mjs.map +2 -2
  81. package/build-module/components/block-switcher/block-transformations-menu.mjs +2 -0
  82. package/build-module/components/block-switcher/block-transformations-menu.mjs.map +2 -2
  83. package/build-module/components/block-switcher/block-variation-transformations.mjs +2 -0
  84. package/build-module/components/block-switcher/block-variation-transformations.mjs.map +2 -2
  85. package/build-module/components/block-toolbar/block-toolbar-icon.mjs +1 -1
  86. package/build-module/components/block-toolbar/block-toolbar-icon.mjs.map +2 -2
  87. package/build-module/components/block-toolbar/index.mjs +8 -4
  88. package/build-module/components/block-toolbar/index.mjs.map +2 -2
  89. package/build-module/components/block-tools/index.mjs +4 -3
  90. package/build-module/components/block-tools/index.mjs.map +2 -2
  91. package/build-module/components/block-variation-transforms/index.mjs +11 -5
  92. package/build-module/components/block-variation-transforms/index.mjs.map +2 -2
  93. package/build-module/components/block-visibility/modal.mjs +2 -2
  94. package/build-module/components/block-visibility/modal.mjs.map +1 -1
  95. package/build-module/components/block-visibility/use-block-visibility.mjs +4 -3
  96. package/build-module/components/block-visibility/use-block-visibility.mjs.map +2 -2
  97. package/build-module/components/block-visibility/viewport-visibility-info.mjs +6 -1
  98. package/build-module/components/block-visibility/viewport-visibility-info.mjs.map +2 -2
  99. package/build-module/components/dimensions-tool/scale-tool.mjs +2 -2
  100. package/build-module/components/dimensions-tool/scale-tool.mjs.map +2 -2
  101. package/build-module/components/iframe/index.mjs +2 -9
  102. package/build-module/components/iframe/index.mjs.map +2 -2
  103. package/build-module/components/iframe/use-scale-canvas.mjs +0 -1
  104. package/build-module/components/iframe/use-scale-canvas.mjs.map +2 -2
  105. package/build-module/components/inspector-controls-tabs/use-inspector-controls-tabs.mjs +8 -4
  106. package/build-module/components/inspector-controls-tabs/use-inspector-controls-tabs.mjs.map +2 -2
  107. package/build-module/components/link-picker/link-preview.mjs +2 -1
  108. package/build-module/components/link-picker/link-preview.mjs.map +2 -2
  109. package/build-module/components/provider/index.mjs +23 -7
  110. package/build-module/components/provider/index.mjs.map +2 -2
  111. package/build-module/components/provider/use-media-upload-settings.mjs +1 -0
  112. package/build-module/components/provider/use-media-upload-settings.mjs.map +2 -2
  113. package/build-module/components/writing-flow/use-arrow-nav.mjs +1 -1
  114. package/build-module/components/writing-flow/use-arrow-nav.mjs.map +2 -2
  115. package/build-module/hooks/auto-inspector-controls.mjs +28 -4
  116. package/build-module/hooks/auto-inspector-controls.mjs.map +2 -2
  117. package/build-module/hooks/cross-origin-isolation.mjs +9 -75
  118. package/build-module/hooks/cross-origin-isolation.mjs.map +2 -2
  119. package/build-module/hooks/custom-css.mjs +28 -2
  120. package/build-module/hooks/custom-css.mjs.map +2 -2
  121. package/build-module/private-apis.mjs +3 -1
  122. package/build-module/private-apis.mjs.map +2 -2
  123. package/build-module/store/actions.mjs +0 -8
  124. package/build-module/store/actions.mjs.map +2 -2
  125. package/build-module/store/private-keys.mjs +2 -0
  126. package/build-module/store/private-keys.mjs.map +2 -2
  127. package/build-module/store/private-selectors.mjs +4 -3
  128. package/build-module/store/private-selectors.mjs.map +2 -2
  129. package/build-module/store/reducer.mjs +78 -73
  130. package/build-module/store/reducer.mjs.map +2 -2
  131. package/build-module/store/selectors.mjs +16 -14
  132. package/build-module/store/selectors.mjs.map +2 -2
  133. package/build-style/content-rtl.css +25 -0
  134. package/build-style/content.css +25 -0
  135. package/build-style/style-rtl.css +3 -0
  136. package/build-style/style.css +3 -0
  137. package/package.json +39 -39
  138. package/src/components/block-bindings/attribute-control.js +8 -3
  139. package/src/components/block-inspector/edit-contents.js +5 -3
  140. package/src/components/block-list/block-html.js +13 -3
  141. package/src/components/block-list/block.js +10 -1
  142. package/src/components/block-list/content.scss +35 -0
  143. package/src/components/block-list/use-block-props/index.js +11 -1
  144. package/src/components/block-settings-menu-controls/index.js +1 -0
  145. package/src/components/block-styles/index.js +7 -1
  146. package/src/components/block-switcher/block-transformations-menu.js +2 -0
  147. package/src/components/block-switcher/block-variation-transformations.js +2 -0
  148. package/src/components/block-toolbar/block-toolbar-icon.js +6 -1
  149. package/src/components/block-toolbar/index.js +7 -2
  150. package/src/components/block-tools/index.js +11 -4
  151. package/src/components/block-tools/style.scss +5 -0
  152. package/src/components/block-variation-transforms/index.js +41 -36
  153. package/src/components/block-visibility/use-block-visibility.js +4 -2
  154. package/src/components/block-visibility/viewport-visibility-info.js +8 -1
  155. package/src/components/dimensions-tool/scale-tool.js +2 -2
  156. package/src/components/iframe/index.js +2 -12
  157. package/src/components/iframe/use-scale-canvas.js +0 -1
  158. package/src/components/inspector-controls-tabs/use-inspector-controls-tabs.js +9 -5
  159. package/src/components/link-picker/link-preview.js +2 -1
  160. package/src/components/provider/index.js +49 -6
  161. package/src/components/provider/use-media-upload-settings.js +1 -0
  162. package/src/components/writing-flow/use-arrow-nav.js +9 -2
  163. package/src/hooks/auto-inspector-controls.js +31 -4
  164. package/src/hooks/cross-origin-isolation.js +10 -109
  165. package/src/hooks/custom-css.js +36 -2
  166. package/src/hooks/test/cross-origin-isolation.js +15 -42
  167. package/src/private-apis.js +2 -0
  168. package/src/store/actions.js +0 -8
  169. package/src/store/private-keys.js +1 -0
  170. package/src/store/private-selectors.js +6 -3
  171. package/src/store/reducer.js +119 -97
  172. package/src/store/selectors.js +59 -22
  173. package/src/store/test/private-selectors.js +67 -34
  174. package/src/store/test/reducer.js +545 -57
  175. package/src/store/test/selectors.js +611 -53
@@ -152,60 +152,33 @@ describe( 'cross-origin-isolation', () => {
152
152
  expect( observeSpy ).not.toHaveBeenCalled();
153
153
  } );
154
154
 
155
- it( 'should handle iframe contentDocument errors gracefully', () => {
155
+ it( 'should not add crossorigin="anonymous" to images', async () => {
156
156
  Object.defineProperty( window, 'crossOriginIsolated', {
157
157
  value: true,
158
158
  writable: true,
159
159
  configurable: true,
160
160
  } );
161
161
 
162
- // Re-import the module
162
+ // Re-import the module to trigger the side effects
163
163
  jest.isolateModules( () => {
164
164
  require( '../cross-origin-isolation' );
165
165
  } );
166
166
 
167
- // Create an iframe that throws when accessing contentDocument
168
- const iframe = document.createElement( 'iframe' );
169
- Object.defineProperty( iframe, 'contentDocument', {
170
- get() {
171
- throw new Error( 'Cross-origin access denied' );
172
- },
173
- } );
174
-
175
- // This should not throw an error
176
- expect( () => {
177
- document.body.appendChild( iframe );
178
- iframe.dispatchEvent( new Event( 'load' ) );
179
- } ).not.toThrow();
180
- } );
181
-
182
- it( 'should register embed preview filter when cross-origin isolated', () => {
183
- Object.defineProperty( window, 'crossOriginIsolated', {
184
- value: true,
185
- writable: true,
186
- configurable: true,
187
- } );
188
-
189
- const hasFilter = jest.spyOn(
190
- require( '@wordpress/hooks' ),
191
- 'hasFilter'
192
- );
167
+ // Create an image and add it to the DOM
168
+ const img = document.createElement( 'img' );
169
+ img.setAttribute( 'src', 'https://example.com/image.jpg' );
170
+ document.body.appendChild( img );
193
171
 
194
- // Re-import the module to register filters
195
- jest.isolateModules( () => {
196
- require( '../cross-origin-isolation' );
197
- } );
172
+ // Wait for MutationObserver callback to fire (async microtask).
173
+ await new Promise( ( resolve ) => setTimeout( resolve, 0 ) );
198
174
 
199
- // The module should register a filter when cross-origin isolated
200
- // We can't easily test the filter itself without a full React environment,
201
- // but we can verify the module loads without errors
202
- expect( () => {
203
- require( '@wordpress/hooks' ).hasFilter(
204
- 'editor.BlockEdit',
205
- 'media-experiments/disable-embed-previews'
206
- );
207
- } ).not.toThrow();
175
+ // Images should NOT get the crossorigin attribute.
176
+ // Under Document-Isolation-Policy: isolate-and-credentialless,
177
+ // the credentialless mode handles image loading without CORS headers.
178
+ // Adding crossorigin="anonymous" would override this and break
179
+ // external images that don't serve CORS headers.
180
+ expect( img ).not.toHaveAttribute( 'crossorigin' );
208
181
 
209
- hasFilter.mockRestore();
182
+ document.body.removeChild( img );
210
183
  } );
211
184
  } );
@@ -48,6 +48,7 @@ import {
48
48
  deviceTypeKey,
49
49
  isIsolatedEditorKey,
50
50
  isNavigationOverlayContextKey,
51
+ mediaUploadOnSuccessKey,
51
52
  } from './store/private-keys';
52
53
  import { requiresWrapperOnCopy } from './components/writing-flow/utils';
53
54
  import { PrivateRichText } from './components/rich-text/';
@@ -129,6 +130,7 @@ lock( privateApis, {
129
130
  deviceTypeKey,
130
131
  isIsolatedEditorKey,
131
132
  isNavigationOverlayContextKey,
133
+ mediaUploadOnSuccessKey,
132
134
  useBlockElement,
133
135
  useBlockElementRef,
134
136
  LinkPicker,
@@ -1767,10 +1767,6 @@ export const insertBeforeBlock =
1767
1767
  return;
1768
1768
  }
1769
1769
  const rootClientId = select.getBlockRootClientId( clientId );
1770
- const isLocked = select.getTemplateLock( rootClientId );
1771
- if ( isLocked ) {
1772
- return;
1773
- }
1774
1770
 
1775
1771
  const blockIndex = select.getBlockIndex( clientId );
1776
1772
  const directInsertBlock = rootClientId
@@ -1810,10 +1806,6 @@ export const insertAfterBlock =
1810
1806
  return;
1811
1807
  }
1812
1808
  const rootClientId = select.getBlockRootClientId( clientId );
1813
- const isLocked = select.getTemplateLock( rootClientId );
1814
- if ( isLocked ) {
1815
- return;
1816
- }
1817
1809
 
1818
1810
  const blockIndex = select.getBlockIndex( clientId );
1819
1811
  const directInsertBlock = rootClientId
@@ -10,3 +10,4 @@ export const deviceTypeKey = Symbol( 'deviceTypeKey' );
10
10
  export const isNavigationOverlayContextKey = Symbol(
11
11
  'isNavigationOverlayContext'
12
12
  );
13
+ export const mediaUploadOnSuccessKey = Symbol( 'mediaUploadOnSuccess' );
@@ -147,7 +147,7 @@ export const getEnabledClientIdsTree = createRegistrySelector( () =>
147
147
  createSelector( getEnabledClientIdsTreeUnmemoized, ( state ) => [
148
148
  state.blocks.order,
149
149
  state.derivedBlockEditingModes,
150
- state.blockEditingModes,
150
+ state.blocks.blockEditingModes,
151
151
  ] )
152
152
  );
153
153
 
@@ -170,7 +170,7 @@ export const getEnabledBlockParents = createSelector(
170
170
  },
171
171
  ( state ) => [
172
172
  state.blocks.parents,
173
- state.blockEditingModes,
173
+ state.blocks.blockEditingModes,
174
174
  state.settings.templateLock,
175
175
  state.blockListSettings,
176
176
  ]
@@ -501,10 +501,13 @@ function isSectionBlockCandidate( state, clientId ) {
501
501
  const disableContentOnlyForUnsyncedPatterns =
502
502
  state.settings?.disableContentOnlyForUnsyncedPatterns;
503
503
 
504
+ const disableContentOnlyForTemplateParts =
505
+ state.settings?.disableContentOnlyForTemplateParts;
506
+
504
507
  if (
505
508
  ( ( ! disableContentOnlyForUnsyncedPatterns &&
506
509
  attributes?.metadata?.patternName ) ||
507
- isTemplatePart ) &&
510
+ ( isTemplatePart && ! disableContentOnlyForTemplateParts ) ) &&
508
511
  ! isIsolatedEditor
509
512
  ) {
510
513
  return true;
@@ -601,26 +601,23 @@ const withBlockReset = ( reducer ) => ( state, action ) => {
601
601
  if ( action.type === 'RESET_BLOCKS' ) {
602
602
  /**
603
603
  * Preserve controlled inner block flags across RESET_BLOCKS.
604
- * Previously this was cleared to `{}`, which caused nested
605
- * controllers (e.g. post-content, patterns) to lose their
606
- * controlled status and unnecessarily re-clone blocks. Stale
607
- * flags are cleaned up naturally by unsetControlledBlocks()
608
- * when useBlockSync unmounts.
604
+ * If there are old and new blocks that:
605
+ * - have the same `clientId`
606
+ * - have the `controlledInnerBlocks` flag
607
+ * - don't have any own, uncontrolled children
608
+ * then we preserve the `controlledInnerBlocks` flag and the controlled sub-trees.
609
+ * Nested controllers (e.g., `post-content`, patterns) don't lose their
610
+ * controlled status and don't unnecessarily re-clone blocks.
609
611
  */
612
+ const newState = reducer( undefined, {
613
+ type: 'INSERT_BLOCKS',
614
+ rootClientId: '',
615
+ blocks: action.blocks,
616
+ } );
617
+
610
618
  const preservedControlledInnerBlocks =
611
619
  state?.controlledInnerBlocks ?? {};
612
620
 
613
- const newState = {
614
- ...state,
615
- byClientId: new Map(
616
- getFlattenedBlocksWithoutAttributes( action.blocks )
617
- ),
618
- attributes: new Map( getFlattenedBlockAttributes( action.blocks ) ),
619
- order: mapBlockOrder( action.blocks ),
620
- parents: new Map( mapBlockParents( action.blocks ) ),
621
- controlledInnerBlocks: preservedControlledInnerBlocks,
622
- };
623
-
624
621
  // Preserve controlled inner blocks data from the old state.
625
622
  // The maps above are rebuilt solely from action.blocks, but
626
623
  // controlled inner blocks live under cloned IDs that aren't
@@ -637,23 +634,24 @@ const withBlockReset = ( reducer ) => ( state, action ) => {
637
634
  if ( ! newState.byClientId.has( clientId ) ) {
638
635
  continue;
639
636
  }
637
+ newState.controlledInnerBlocks[ clientId ] = true;
640
638
  const oldOrder = state.order.get( clientId );
641
639
  if ( ! oldOrder?.length ) {
642
640
  continue;
643
641
  }
644
642
  newState.order.set( clientId, oldOrder );
645
643
  const preserveBlock = ( blockId, parentId ) => {
646
- const blockData = state.byClientId?.get( blockId );
644
+ const blockData = state.byClientId.get( blockId );
647
645
  if ( ! blockData ) {
648
646
  return;
649
647
  }
650
648
  newState.byClientId.set( blockId, blockData );
651
649
  newState.attributes.set(
652
650
  blockId,
653
- state.attributes?.get( blockId )
651
+ state.attributes.get( blockId )
654
652
  );
655
653
  newState.parents.set( blockId, parentId );
656
- const childOrder = state.order?.get( blockId ) || [];
654
+ const childOrder = state.order.get( blockId ) || [];
657
655
  newState.order.set( blockId, childOrder );
658
656
  childOrder.forEach( ( childId ) =>
659
657
  preserveBlock( childId, blockId )
@@ -663,42 +661,47 @@ const withBlockReset = ( reducer ) => ( state, action ) => {
663
661
  }
664
662
  }
665
663
 
666
- newState.tree = new Map( state?.tree );
667
- updateBlockTreeForBlocks( newState, action.blocks );
668
-
669
664
  // Fix tree entries for controlled blocks. updateBlockTreeForBlocks
670
665
  // built tree entries using action.blocks' inner block structure
671
666
  // (entity-level IDs), but we need them to reference the preserved
672
667
  // cloned inner blocks instead. Mutating the existing object
673
668
  // preserves references held by ancestor tree entries.
674
669
  for ( const clientId of Object.keys(
675
- preservedControlledInnerBlocks
670
+ newState.controlledInnerBlocks
676
671
  ) ) {
677
- if ( ! preservedControlledInnerBlocks[ clientId ] ) {
678
- continue;
679
- }
680
- if ( ! newState.byClientId.has( clientId ) ) {
681
- continue;
682
- }
683
672
  const controlledOrder = newState.order.get( clientId );
684
673
  if ( ! controlledOrder?.length ) {
685
674
  continue;
686
675
  }
687
676
  const innerBlocks = controlledOrder.map( ( id ) =>
688
- newState.tree.get( id )
677
+ state.tree.get( id )
689
678
  );
690
679
  const existingEntry = newState.tree.get( clientId );
691
680
  if ( existingEntry ) {
692
681
  existingEntry.innerBlocks = innerBlocks;
693
682
  }
694
683
  newState.tree.set( 'controlled||' + clientId, { innerBlocks } );
684
+ const preserveTreeEntry = ( blockId ) => {
685
+ const treeEntry = state.tree.get( blockId );
686
+ if ( ! treeEntry ) {
687
+ return;
688
+ }
689
+ newState.tree.set( blockId, treeEntry );
690
+ const childOrder = newState.order.get( blockId ) || [];
691
+ childOrder.forEach( preserveTreeEntry );
692
+ };
693
+ controlledOrder.forEach( preserveTreeEntry );
695
694
  }
696
695
 
697
- newState.tree.set( '', {
698
- innerBlocks: action.blocks.map( ( subBlock ) =>
699
- newState.tree.get( subBlock.clientId )
700
- ),
701
- } );
696
+ // Preserve block editing modes for blocks that are not removed.
697
+ const preservedBlockEditingModes =
698
+ state?.blockEditingModes ?? new Map();
699
+ for ( const [ clientId, mode ] of preservedBlockEditingModes ) {
700
+ if ( ! newState.tree.has( clientId ) ) {
701
+ continue;
702
+ }
703
+ newState.blockEditingModes.set( clientId, mode );
704
+ }
702
705
 
703
706
  return newState;
704
707
  }
@@ -1083,9 +1086,6 @@ export const blocks = pipe(
1083
1086
  if ( fromRootClientId === toRootClientId ) {
1084
1087
  const subState = state.get( toRootClientId );
1085
1088
  const fromIndex = subState.indexOf( clientIds[ 0 ] );
1086
- if ( fromIndex === -1 ) {
1087
- return state;
1088
- }
1089
1089
  const newState = new Map( state );
1090
1090
  newState.set(
1091
1091
  toRootClientId,
@@ -1290,6 +1290,25 @@ export const blocks = pipe(
1290
1290
  }
1291
1291
  return state;
1292
1292
  },
1293
+
1294
+ blockEditingModes( state = new Map(), action ) {
1295
+ switch ( action.type ) {
1296
+ case 'SET_BLOCK_EDITING_MODE':
1297
+ if ( state.get( action.clientId ) === action.mode ) {
1298
+ return state;
1299
+ }
1300
+ return new Map( state ).set( action.clientId, action.mode );
1301
+ case 'UNSET_BLOCK_EDITING_MODE': {
1302
+ if ( ! state.has( action.clientId ) ) {
1303
+ return state;
1304
+ }
1305
+ const newState = new Map( state );
1306
+ newState.delete( action.clientId );
1307
+ return newState;
1308
+ }
1309
+ }
1310
+ return state;
1311
+ },
1293
1312
  } );
1294
1313
 
1295
1314
  /**
@@ -2104,38 +2123,33 @@ export function editedContentOnlySection( state, action ) {
2104
2123
  if ( action.type === 'EDIT_CONTENT_ONLY_SECTION' ) {
2105
2124
  return action.clientId;
2106
2125
  }
2107
- return state;
2108
- }
2109
2126
 
2110
- /**
2111
- * Reducer returning a map of block client IDs to block editing modes.
2112
- *
2113
- * @param {Map} state Current state.
2114
- * @param {Object} action Dispatched action.
2115
- *
2116
- * @return {Map} Updated state.
2117
- */
2118
- export function blockEditingModes( state = new Map(), action ) {
2127
+ // Early return if there's no section being edited.
2128
+ if ( ! state ) {
2129
+ return state;
2130
+ }
2131
+
2119
2132
  switch ( action.type ) {
2120
- case 'SET_BLOCK_EDITING_MODE':
2121
- if ( state.get( action.clientId ) === action.mode ) {
2122
- return state;
2123
- }
2124
- return new Map( state ).set( action.clientId, action.mode );
2125
- case 'UNSET_BLOCK_EDITING_MODE': {
2126
- if ( ! state.has( action.clientId ) ) {
2127
- return state;
2133
+ case 'REMOVE_BLOCKS':
2134
+ case 'REPLACE_BLOCKS':
2135
+ // Clear if the edited section is directly among the removed/replaced blocks.
2136
+ // Note: this doesn't catch the case where a parent of the edited section
2137
+ // is removed, since action.clientIds only contains the top-level IDs.
2138
+ // That edge case is handled by the StopEditingContentOnlySectionOnOutsideSelect
2139
+ // component in block-list/index.js.
2140
+ if ( action.clientIds.includes( state ) ) {
2141
+ return undefined;
2142
+ }
2143
+ break;
2144
+ case 'RESET_BLOCKS':
2145
+ // When all blocks are reset (e.g. navigating to a different post),
2146
+ // check whether the edited section still exists in the new block tree.
2147
+ if ( ! getFlattenedClientIds( action.blocks )[ state ] ) {
2148
+ return undefined;
2128
2149
  }
2129
- const newState = new Map( state );
2130
- newState.delete( action.clientId );
2131
- return newState;
2132
- }
2133
- case 'RESET_BLOCKS': {
2134
- return state.has( '' )
2135
- ? new Map().set( '', state.get( '' ) )
2136
- : state;
2137
- }
2150
+ break;
2138
2151
  }
2152
+
2139
2153
  return state;
2140
2154
  }
2141
2155
 
@@ -2279,9 +2293,6 @@ function openedListViewPanels(
2279
2293
  } );
2280
2294
  return hasChanges ? { ...state, panels: newPanels } : state;
2281
2295
  }
2282
- case 'RESET_BLOCKS':
2283
- // Clear all panel state when blocks are reset
2284
- return { allOpen: false, panels: {} };
2285
2296
  }
2286
2297
  return state;
2287
2298
  }
@@ -2378,7 +2389,6 @@ const combinedReducers = combineReducers( {
2378
2389
  editedContentOnlySection,
2379
2390
  blockVisibility,
2380
2391
  viewportModalClientIds,
2381
- blockEditingModes,
2382
2392
  styleOverrides,
2383
2393
  removalPromptData,
2384
2394
  blockRemovalRules,
@@ -2509,7 +2519,7 @@ function getDerivedBlockEditingModesForTree( state, treeClientId = '' ) {
2509
2519
  // so the default block editing mode is set to disabled.
2510
2520
  const sectionRootClientId = state.settings?.[ sectionRootClientIdKey ];
2511
2521
  const sectionClientIds = state.blocks.order.get( sectionRootClientId );
2512
- const hasDisabledBlocks = Array.from( state.blockEditingModes ).some(
2522
+ const hasDisabledBlocks = Array.from( state.blocks.blockEditingModes ).some(
2513
2523
  ( [ , mode ] ) => mode === 'disabled'
2514
2524
  );
2515
2525
  const templatePartClientIds = [];
@@ -2550,18 +2560,42 @@ function getDerivedBlockEditingModesForTree( state, treeClientId = '' ) {
2550
2560
  state.blocks.attributes.get( clientId )?.metadata
2551
2561
  ?.patternName
2552
2562
  );
2563
+ const disableContentOnlyForTemplateParts =
2564
+ state.settings?.disableContentOnlyForTemplateParts;
2565
+
2553
2566
  const contentOnlyParents = [
2554
2567
  ...contentOnlyTemplateLockedClientIds,
2555
2568
  ...unsyncedPatternClientIds,
2556
- ...( isIsolatedEditor ? [] : templatePartClientIds ),
2569
+ ...( isIsolatedEditor || disableContentOnlyForTemplateParts
2570
+ ? []
2571
+ : templatePartClientIds ),
2557
2572
  ];
2558
2573
 
2559
2574
  traverseBlockTree( state, treeClientId, ( block ) => {
2560
2575
  const { clientId, name: blockName } = block;
2561
2576
 
2577
+ const hasEditedContentOnlySection = !! state.editedContentOnlySection;
2578
+ let isWithinEditedContentOnlySection = false;
2579
+ if ( hasEditedContentOnlySection ) {
2580
+ isWithinEditedContentOnlySection =
2581
+ clientId === state.editedContentOnlySection ||
2582
+ !! findParentInClientIdsList( state, clientId, [
2583
+ state.editedContentOnlySection,
2584
+ ] );
2585
+
2586
+ // When a contentOnly section is being edited, all blocks outside
2587
+ // the section are disabled. This should never be overridable by any
2588
+ // other block editing modes, it helps to constrain keyboard navigation
2589
+ // to within the edited section.
2590
+ if ( ! isWithinEditedContentOnlySection ) {
2591
+ derivedBlockEditingModes.set( clientId, 'disabled' );
2592
+ return;
2593
+ }
2594
+ }
2595
+
2562
2596
  // If the block already has an explicit block editing mode set,
2563
2597
  // don't override it.
2564
- if ( state.blockEditingModes.has( clientId ) ) {
2598
+ if ( state.blocks.blockEditingModes.has( clientId ) ) {
2565
2599
  return;
2566
2600
  }
2567
2601
 
@@ -2572,12 +2606,12 @@ function getDerivedBlockEditingModesForTree( state, treeClientId = '' ) {
2572
2606
  let ancestorBlockEditingMode;
2573
2607
  let parent = state.blocks.parents.get( clientId );
2574
2608
  while ( parent !== undefined ) {
2575
- if ( state.blockEditingModes.has( parent ) ) {
2609
+ if ( state.blocks.blockEditingModes.has( parent ) ) {
2576
2610
  // Checking the explicit block editing mode will be slower,
2577
2611
  // as the block editing mode is more likely to be set on a
2578
2612
  // distant ancestor.
2579
2613
  ancestorBlockEditingMode =
2580
- state.blockEditingModes.get( parent );
2614
+ state.blocks.blockEditingModes.get( parent );
2581
2615
  }
2582
2616
  if ( ancestorBlockEditingMode ) {
2583
2617
  break;
@@ -2670,26 +2704,10 @@ function getDerivedBlockEditingModesForTree( state, treeClientId = '' ) {
2670
2704
  }
2671
2705
 
2672
2706
  // Set the edited section and all blocks within it to 'default', so that all changes can be made.
2673
- if ( state.editedContentOnlySection ) {
2674
- // If this is the edited section, use the default mode.
2675
- if ( state.editedContentOnlySection === clientId ) {
2676
- derivedBlockEditingModes.set( clientId, 'default' );
2677
- return;
2678
- }
2679
-
2680
- // If the block is within the edited section also use the default mode.
2681
- const parentTempEditedClientId = findParentInClientIdsList(
2682
- state,
2683
- clientId,
2684
- [ state.editedContentOnlySection ]
2685
- );
2686
- if ( parentTempEditedClientId ) {
2687
- derivedBlockEditingModes.set( clientId, 'default' );
2688
- return;
2689
- }
2690
-
2691
- // Disable blocks that are outside of the edited section.
2692
- derivedBlockEditingModes.set( clientId, 'disabled' );
2707
+ if ( hasEditedContentOnlySection && isWithinEditedContentOnlySection ) {
2708
+ derivedBlockEditingModes.set( clientId, 'default' );
2709
+ // When there's an editedContentOnlySection, it overrides any modes that are usually
2710
+ // set for `contentOnlyParents`, return early to prevent continuing to code below.
2693
2711
  return;
2694
2712
  }
2695
2713
 
@@ -3069,7 +3087,8 @@ export function withDerivedBlockEditingModes( reducer ) {
3069
3087
  case 'UPDATE_SETTINGS': {
3070
3088
  // Recompute the entire tree if the section root,
3071
3089
  // the effective disableContentOnlyForUnsyncedPatterns value,
3072
- // or the isIsolatedEditor value changes.
3090
+ // the isIsolatedEditor value, or the
3091
+ // disableContentOnlyForTemplateParts value changes.
3073
3092
  // These are all values that affect the computation.
3074
3093
  if (
3075
3094
  state?.settings?.[ sectionRootClientIdKey ] !==
@@ -3079,7 +3098,10 @@ export function withDerivedBlockEditingModes( reducer ) {
3079
3098
  !! nextState?.settings
3080
3099
  ?.disableContentOnlyForUnsyncedPatterns ||
3081
3100
  !! state?.settings?.[ isIsolatedEditorKey ] !==
3082
- !! nextState?.settings?.[ isIsolatedEditorKey ]
3101
+ !! nextState?.settings?.[ isIsolatedEditorKey ] ||
3102
+ !! state?.settings?.disableContentOnlyForTemplateParts !==
3103
+ !! nextState?.settings
3104
+ ?.disableContentOnlyForTemplateParts
3083
3105
  ) {
3084
3106
  return {
3085
3107
  ...nextState,
@@ -1705,9 +1705,21 @@ const canInsertBlockTypeUnmemoized = (
1705
1705
  }
1706
1706
 
1707
1707
  const blockEditingMode = getBlockEditingMode( state, rootClientId ?? '' );
1708
+
1709
+ // Compute section context early so the disabled check below can use it.
1710
+ const isParentSectionBlock = !! isSectionBlock( state, rootClientId );
1711
+ const sectionClientId = isParentSectionBlock
1712
+ ? rootClientId
1713
+ : getParentSectionBlock( state, rootClientId );
1714
+ const isWithinSection = !! sectionClientId;
1715
+
1716
+ // Disabled containers reject all blocks, with one exception: within a
1717
+ // section, the default block (paragraph) is allowed through so it can
1718
+ // reach the content-insertion logic further down (lines 1748-1772)
1719
+ // which conditionally permits it where a sibling paragraph exists.
1708
1720
  if (
1709
1721
  blockEditingMode === 'disabled' &&
1710
- blockName !== getDefaultBlockName()
1722
+ ( ! isWithinSection || blockName !== getDefaultBlockName() )
1711
1723
  ) {
1712
1724
  return false;
1713
1725
  }
@@ -1723,11 +1735,6 @@ const canInsertBlockTypeUnmemoized = (
1723
1735
  // It shouldn't be possible to insert inside a section block unless in
1724
1736
  // some cases when the block is a content block.
1725
1737
  const isContentRoleBlock = isContentBlock( blockName );
1726
- const isParentSectionBlock = !! isSectionBlock( state, rootClientId );
1727
- const sectionClientId = isParentSectionBlock
1728
- ? rootClientId
1729
- : getParentSectionBlock( state, rootClientId );
1730
- const isWithinSection = !! sectionClientId;
1731
1738
  if ( isWithinSection && ! isContentRoleBlock ) {
1732
1739
  return false;
1733
1740
  }
@@ -1740,22 +1747,40 @@ const canInsertBlockTypeUnmemoized = (
1740
1747
  return false;
1741
1748
  }
1742
1749
 
1743
- // In content only mode, check if this container allows insertion.
1750
+ /*
1751
+ * In content only mode, check if this container allows insertion.
1752
+ * We need the `isParentSectionBlock` check because section blocks
1753
+ * (synced patterns, contentOnly groups) have a `getBlockEditingMode`
1754
+ * of 'default', not 'contentOnly' — the 'contentOnly' mode is only
1755
+ * set on their *children*.
1756
+ *
1757
+ * Also include `disabled` alongside `contentOnly`: structural inner blocks
1758
+ * (e.g. Column) inside a content-only section use `disabled` mode, and they
1759
+ * need the same default-block sibling rules so insertion stays aligned with
1760
+ * `canRemoveBlock`.
1761
+ */
1744
1762
  if (
1745
- ( isParentSectionBlock || blockEditingMode === 'contentOnly' ) &&
1763
+ isWithinSection &&
1764
+ ( isParentSectionBlock ||
1765
+ blockEditingMode === 'contentOnly' ||
1766
+ blockEditingMode === 'disabled' ) &&
1746
1767
  ! isContainerInsertableToInContentOnlyMode(
1747
1768
  state,
1748
1769
  blockName,
1749
1770
  rootClientId
1750
1771
  )
1751
1772
  ) {
1752
- // Allow inserting the default block anywhere that another default block already exists
1753
- // when in contentOnly mode.
1754
- if ( blockName === getDefaultBlockName() ) {
1773
+ const defaultBlockName = getDefaultBlockName();
1774
+ /*
1775
+ * Allow inserting the default block anywhere that another default block already exists
1776
+ * when in contentOnly mode. The same sibling rule applies when the parent is `disabled`
1777
+ * within a content-only section (see the condition above).
1778
+ */
1779
+ if ( blockName === defaultBlockName ) {
1755
1780
  const existingBlocks = getBlockOrder( state, rootClientId );
1756
1781
  const hasDefaultBlock = existingBlocks.some(
1757
1782
  ( clientId ) =>
1758
- getBlockName( state, clientId ) === getDefaultBlockName()
1783
+ getBlockName( state, clientId ) === defaultBlockName
1759
1784
  );
1760
1785
  if ( ! hasDefaultBlock ) {
1761
1786
  return false;
@@ -1938,11 +1963,18 @@ export function canRemoveBlock( state, clientId ) {
1938
1963
 
1939
1964
  const rootBlockEditingMode = getBlockEditingMode( state, rootClientId );
1940
1965
  const blockName = getBlockName( state, clientId );
1941
- // Check if the parent container allows insertion/removal in contentOnly mode.
1966
+ const defaultBlockName = getDefaultBlockName();
1967
+
1968
+ // Check if the parent container allows insertion/removal in contentOnly
1969
+ // mode. We need the `isParentSectionBlock` check because section blocks
1970
+ // (synced patterns, contentOnly groups) have a `getBlockEditingMode` of
1971
+ // 'default', not 'contentOnly' — the 'contentOnly' mode is only set on
1972
+ // their *children*.
1942
1973
  if (
1974
+ isWithinSection &&
1943
1975
  ( isParentSectionBlock ||
1944
- rootBlockEditingMode === 'contentOnly' ||
1945
- blockName === getDefaultBlockName() ) &&
1976
+ blockName === defaultBlockName ||
1977
+ rootBlockEditingMode === 'contentOnly' ) &&
1946
1978
  ! isContainerInsertableToInContentOnlyMode(
1947
1979
  state,
1948
1980
  getBlockName( state, clientId ),
@@ -1951,18 +1983,18 @@ export function canRemoveBlock( state, clientId ) {
1951
1983
  ) {
1952
1984
  // Allow removing the default block when other default blocks exist
1953
1985
  // in contentOnly mode.
1954
- if ( blockName === getDefaultBlockName() ) {
1986
+ if ( blockName === defaultBlockName ) {
1955
1987
  const existingBlocks = getBlockOrder( state, rootClientId );
1956
1988
  const defaultBlocks = existingBlocks.filter(
1957
- ( id ) => getBlockName( state, id ) === getDefaultBlockName()
1989
+ ( id ) => getBlockName( state, id ) === defaultBlockName
1958
1990
  );
1959
1991
  // Allow removal if there are other default blocks besides this one
1960
1992
  if ( defaultBlocks.length > 1 ) {
1961
1993
  return true;
1962
1994
  }
1963
- } else {
1964
1995
  return false;
1965
1996
  }
1997
+ return false;
1966
1998
  }
1967
1999
 
1968
2000
  return rootBlockEditingMode !== 'disabled';
@@ -2016,11 +2048,16 @@ export function canMoveBlock( state, clientId ) {
2016
2048
  return false;
2017
2049
  }
2018
2050
 
2019
- // If the parent is a section or is `contentOnly`, then check is the inner block
2020
- // should be allowed to move.
2051
+ // If the block is within a section and the parent is either a section
2052
+ // block itself or has contentOnly editing mode, check whether the inner
2053
+ // block should be allowed to move. We need the `isParentSectionBlock`
2054
+ // check because section blocks (synced patterns, contentOnly groups)
2055
+ // have a `getBlockEditingMode` of 'default', not 'contentOnly' — the
2056
+ // 'contentOnly' mode is only set on their *children*.
2021
2057
  const isParentSectionBlock = !! isSectionBlock( state, rootClientId );
2022
2058
  const rootBlockEditingMode = getBlockEditingMode( state, rootClientId );
2023
2059
  if (
2060
+ isBlockWithinSection &&
2024
2061
  ( isParentSectionBlock || rootBlockEditingMode === 'contentOnly' ) &&
2025
2062
  ! isContainerInsertableToInContentOnlyMode(
2026
2063
  state,
@@ -3217,8 +3254,8 @@ export function getBlockEditingMode( state, clientId = '' ) {
3217
3254
  }
3218
3255
 
3219
3256
  // In normal mode, consider that an explicitly set editing mode takes over.
3220
- if ( state.blockEditingModes.has( clientId ) ) {
3221
- return state.blockEditingModes.get( clientId );
3257
+ if ( state.blocks.blockEditingModes.has( clientId ) ) {
3258
+ return state.blocks.blockEditingModes.get( clientId );
3222
3259
  }
3223
3260
 
3224
3261
  return 'default';