@wordpress/block-editor 15.12.0 → 15.12.2-next.v.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 (149) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/build/components/block-allowed-blocks/modal.cjs +1 -1
  3. package/build/components/block-allowed-blocks/modal.cjs.map +2 -2
  4. package/build/components/block-inspector/index.cjs +9 -9
  5. package/build/components/block-inspector/index.cjs.map +3 -3
  6. package/build/components/block-removal-warning-modal/index.cjs +30 -5
  7. package/build/components/block-removal-warning-modal/index.cjs.map +3 -3
  8. package/build/components/block-visibility/use-block-visibility.cjs +14 -29
  9. package/build/components/block-visibility/use-block-visibility.cjs.map +2 -2
  10. package/build/components/global-styles/hooks.cjs +7 -0
  11. package/build/components/global-styles/hooks.cjs.map +2 -2
  12. package/build/components/global-styles/typography-panel.cjs +71 -3
  13. package/build/components/global-styles/typography-panel.cjs.map +3 -3
  14. package/build/components/grid/grid-visualizer.cjs +49 -13
  15. package/build/components/grid/grid-visualizer.cjs.map +2 -2
  16. package/build/components/iframe/index.cjs +3 -1
  17. package/build/components/iframe/index.cjs.map +2 -2
  18. package/build/components/iframe/use-scale-canvas.cjs +1 -0
  19. package/build/components/iframe/use-scale-canvas.cjs.map +2 -2
  20. package/build/components/inspector-controls/last-item.cjs +41 -0
  21. package/build/components/inspector-controls/last-item.cjs.map +7 -0
  22. package/build/components/inspector-controls-tabs/styles-tab.cjs +3 -3
  23. package/build/components/inspector-controls-tabs/styles-tab.cjs.map +2 -2
  24. package/build/components/link-control/index.cjs +73 -2
  25. package/build/components/link-control/index.cjs.map +3 -3
  26. package/build/components/link-control/is-url-like.cjs +15 -3
  27. package/build/components/link-control/is-url-like.cjs.map +2 -2
  28. package/build/components/link-control/search-input.cjs +4 -1
  29. package/build/components/link-control/search-input.cjs.map +2 -2
  30. package/build/components/link-control/use-search-handler.cjs +1 -1
  31. package/build/components/link-control/use-search-handler.cjs.map +2 -2
  32. package/build/components/provider/use-block-sync.cjs +60 -8
  33. package/build/components/provider/use-block-sync.cjs.map +2 -2
  34. package/build/components/text-indent-control/index.cjs +121 -0
  35. package/build/components/text-indent-control/index.cjs.map +7 -0
  36. package/build/components/url-input/index.cjs +22 -2
  37. package/build/components/url-input/index.cjs.map +3 -3
  38. package/build/components/url-popover/image-url-input-ui.cjs +1 -1
  39. package/build/components/url-popover/image-url-input-ui.cjs.map +2 -2
  40. package/build/components/writing-flow/use-arrow-nav.cjs +0 -3
  41. package/build/components/writing-flow/use-arrow-nav.cjs.map +2 -2
  42. package/build/hooks/anchor.cjs +1 -1
  43. package/build/hooks/anchor.cjs.map +1 -1
  44. package/build/hooks/aria-label.cjs +2 -1
  45. package/build/hooks/aria-label.cjs.map +2 -2
  46. package/build/hooks/grid-visualizer.cjs +59 -6
  47. package/build/hooks/grid-visualizer.cjs.map +3 -3
  48. package/build/hooks/layout-child.cjs +47 -6
  49. package/build/hooks/layout-child.cjs.map +3 -3
  50. package/build/hooks/typography.cjs +2 -0
  51. package/build/hooks/typography.cjs.map +2 -2
  52. package/build/hooks/utils.cjs +4 -0
  53. package/build/hooks/utils.cjs.map +2 -2
  54. package/build/private-apis.cjs +2 -0
  55. package/build/private-apis.cjs.map +3 -3
  56. package/build/store/actions.cjs +2 -2
  57. package/build/store/actions.cjs.map +2 -2
  58. package/build-module/components/block-allowed-blocks/modal.mjs +2 -2
  59. package/build-module/components/block-allowed-blocks/modal.mjs.map +2 -2
  60. package/build-module/components/block-inspector/index.mjs +10 -9
  61. package/build-module/components/block-inspector/index.mjs.map +2 -2
  62. package/build-module/components/block-removal-warning-modal/index.mjs +34 -7
  63. package/build-module/components/block-removal-warning-modal/index.mjs.map +2 -2
  64. package/build-module/components/block-visibility/use-block-visibility.mjs +14 -29
  65. package/build-module/components/block-visibility/use-block-visibility.mjs.map +2 -2
  66. package/build-module/components/global-styles/hooks.mjs +7 -0
  67. package/build-module/components/global-styles/hooks.mjs.map +2 -2
  68. package/build-module/components/global-styles/typography-panel.mjs +73 -4
  69. package/build-module/components/global-styles/typography-panel.mjs.map +2 -2
  70. package/build-module/components/grid/grid-visualizer.mjs +50 -14
  71. package/build-module/components/grid/grid-visualizer.mjs.map +2 -2
  72. package/build-module/components/iframe/index.mjs +9 -2
  73. package/build-module/components/iframe/index.mjs.map +2 -2
  74. package/build-module/components/iframe/use-scale-canvas.mjs +1 -0
  75. package/build-module/components/iframe/use-scale-canvas.mjs.map +2 -2
  76. package/build-module/components/inspector-controls/last-item.mjs +23 -0
  77. package/build-module/components/inspector-controls/last-item.mjs.map +7 -0
  78. package/build-module/components/inspector-controls-tabs/styles-tab.mjs +3 -3
  79. package/build-module/components/inspector-controls-tabs/styles-tab.mjs.map +2 -2
  80. package/build-module/components/link-control/index.mjs +74 -3
  81. package/build-module/components/link-control/index.mjs.map +2 -2
  82. package/build-module/components/link-control/is-url-like.mjs +10 -3
  83. package/build-module/components/link-control/is-url-like.mjs.map +2 -2
  84. package/build-module/components/link-control/search-input.mjs +4 -1
  85. package/build-module/components/link-control/search-input.mjs.map +2 -2
  86. package/build-module/components/link-control/use-search-handler.mjs +2 -2
  87. package/build-module/components/link-control/use-search-handler.mjs.map +2 -2
  88. package/build-module/components/provider/use-block-sync.mjs +60 -8
  89. package/build-module/components/provider/use-block-sync.mjs.map +2 -2
  90. package/build-module/components/text-indent-control/index.mjs +110 -0
  91. package/build-module/components/text-indent-control/index.mjs.map +7 -0
  92. package/build-module/components/url-input/index.mjs +24 -4
  93. package/build-module/components/url-input/index.mjs.map +2 -2
  94. package/build-module/components/url-popover/image-url-input-ui.mjs +2 -2
  95. package/build-module/components/url-popover/image-url-input-ui.mjs.map +2 -2
  96. package/build-module/components/writing-flow/use-arrow-nav.mjs +0 -3
  97. package/build-module/components/writing-flow/use-arrow-nav.mjs.map +2 -2
  98. package/build-module/hooks/anchor.mjs +1 -1
  99. package/build-module/hooks/anchor.mjs.map +1 -1
  100. package/build-module/hooks/aria-label.mjs +2 -1
  101. package/build-module/hooks/aria-label.mjs.map +2 -2
  102. package/build-module/hooks/grid-visualizer.mjs +37 -6
  103. package/build-module/hooks/grid-visualizer.mjs.map +2 -2
  104. package/build-module/hooks/layout-child.mjs +37 -6
  105. package/build-module/hooks/layout-child.mjs.map +2 -2
  106. package/build-module/hooks/typography.mjs +2 -0
  107. package/build-module/hooks/typography.mjs.map +2 -2
  108. package/build-module/hooks/utils.mjs +4 -0
  109. package/build-module/hooks/utils.mjs.map +2 -2
  110. package/build-module/private-apis.mjs +2 -0
  111. package/build-module/private-apis.mjs.map +2 -2
  112. package/build-module/store/actions.mjs +2 -2
  113. package/build-module/store/actions.mjs.map +2 -2
  114. package/package.json +39 -39
  115. package/src/components/block-allowed-blocks/modal.js +2 -2
  116. package/src/components/block-inspector/index.js +19 -17
  117. package/src/components/block-removal-warning-modal/index.js +55 -19
  118. package/src/components/block-switcher/block-transformations-menu.native.js +1 -0
  119. package/src/components/block-toolbar/test/__snapshots__/block-toolbar-menu.native.js.snap +4 -6
  120. package/src/components/block-toolbar/test/block-toolbar-menu.native.js +2 -2
  121. package/src/components/block-visibility/use-block-visibility.js +17 -32
  122. package/src/components/global-styles/hooks.js +10 -0
  123. package/src/components/global-styles/typography-panel.js +78 -1
  124. package/src/components/grid/grid-visualizer.js +58 -12
  125. package/src/components/iframe/index.js +12 -2
  126. package/src/components/iframe/use-scale-canvas.js +1 -0
  127. package/src/components/inserter/menu.native.js +1 -0
  128. package/src/components/inspector-controls/last-item.js +29 -0
  129. package/src/components/inspector-controls-tabs/styles-tab.js +3 -3
  130. package/src/components/link-control/index.js +160 -3
  131. package/src/components/link-control/is-url-like.js +43 -8
  132. package/src/components/link-control/search-input.js +7 -0
  133. package/src/components/link-control/test/index.js +260 -0
  134. package/src/components/link-control/test/is-url-like.js +49 -1
  135. package/src/components/link-control/use-search-handler.js +2 -2
  136. package/src/components/provider/test/use-block-sync.js +105 -0
  137. package/src/components/provider/use-block-sync.js +118 -9
  138. package/src/components/text-indent-control/index.js +138 -0
  139. package/src/components/url-input/index.js +21 -2
  140. package/src/components/url-popover/image-url-input-ui.js +2 -2
  141. package/src/components/writing-flow/use-arrow-nav.js +0 -4
  142. package/src/hooks/anchor.js +1 -1
  143. package/src/hooks/aria-label.js +9 -1
  144. package/src/hooks/grid-visualizer.js +63 -24
  145. package/src/hooks/layout-child.js +45 -3
  146. package/src/hooks/typography.js +2 -0
  147. package/src/hooks/utils.js +4 -0
  148. package/src/private-apis.js +2 -0
  149. package/src/store/actions.js +8 -6
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Internal dependencies
3
3
  */
4
- import isURLLike from '../is-url-like';
4
+ import isURLLike, { isHashLink, isRelativePath } from '../is-url-like';
5
5
 
6
6
  describe( 'isURLLike', () => {
7
7
  it.each( [ 'https://wordpress.org', 'http://wordpress.org' ] )(
@@ -41,6 +41,13 @@ describe( 'isURLLike', () => {
41
41
  expect( isURLLike( '#someinternallink' ) ).toBe( true );
42
42
  } );
43
43
 
44
+ it.each( [ '/handbook', '/path/to/page', './relative', '../parent' ] )(
45
+ 'returns true for relative paths (e.g. "%s")',
46
+ ( testString ) => {
47
+ expect( isURLLike( testString ) ).toBe( true );
48
+ }
49
+ );
50
+
44
51
  // use .each to test multiple cases
45
52
  it.each( [
46
53
  [ true, 'http://example.com' ],
@@ -64,3 +71,44 @@ describe( 'isURLLike', () => {
64
71
  }
65
72
  );
66
73
  } );
74
+
75
+ describe( 'isHashLink', () => {
76
+ it( 'returns true for valid hash links', () => {
77
+ expect( isHashLink( '#section' ) ).toBe( true );
78
+ expect( isHashLink( '#top' ) ).toBe( true );
79
+ } );
80
+
81
+ it( 'returns false for invalid links that start with #', () => {
82
+ expect( isHashLink( '# test with space' ) ).toBe( false );
83
+ expect( isHashLink( '#test#multiple' ) ).toBe( false );
84
+ } );
85
+
86
+ it( 'returns false for non-hash links', () => {
87
+ expect( isHashLink( '/page' ) ).toBe( false );
88
+ expect( isHashLink( 'https://example.com' ) ).toBe( false );
89
+ expect( isHashLink( 'not a link' ) ).toBe( false );
90
+ } );
91
+ } );
92
+
93
+ describe( 'isRelativePath', () => {
94
+ it( 'returns true for absolute paths starting with /', () => {
95
+ expect( isRelativePath( '/handbook' ) ).toBe( true );
96
+ expect( isRelativePath( '/path/to/page' ) ).toBe( true );
97
+ } );
98
+
99
+ it( 'returns true for relative paths starting with ./', () => {
100
+ expect( isRelativePath( './page' ) ).toBe( true );
101
+ expect( isRelativePath( './nested/page' ) ).toBe( true );
102
+ } );
103
+
104
+ it( 'returns true for parent relative paths starting with ../', () => {
105
+ expect( isRelativePath( '../page' ) ).toBe( true );
106
+ expect( isRelativePath( '../parent/page' ) ).toBe( true );
107
+ } );
108
+
109
+ it( 'returns false for non-relative paths', () => {
110
+ expect( isRelativePath( 'https://example.com' ) ).toBe( false );
111
+ expect( isRelativePath( '#section' ) ).toBe( false );
112
+ expect( isRelativePath( 'www.example.com' ) ).toBe( false );
113
+ } );
114
+ } );
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * WordPress dependencies
3
3
  */
4
- import { getProtocol, prependHTTP } from '@wordpress/url';
4
+ import { getProtocol, prependHTTPS } from '@wordpress/url';
5
5
  import { useCallback } from '@wordpress/element';
6
6
  import { useSelect } from '@wordpress/data';
7
7
 
@@ -41,7 +41,7 @@ export const handleDirectEntry = ( val ) => {
41
41
  {
42
42
  id: val,
43
43
  title: val,
44
- url: type === 'URL' ? prependHTTP( val ) : val,
44
+ url: type === 'URL' ? prependHTTPS( val ) : val,
45
45
  type,
46
46
  },
47
47
  ] );
@@ -566,4 +566,109 @@ describe( 'useBlockSync hook', () => {
566
566
  } )
567
567
  );
568
568
  } );
569
+
570
+ it( 'preserves external client IDs in onChange callback for inner block controllers', async () => {
571
+ const originalClientId = 'original-external-id';
572
+ const innerBlockClientId = 'inner-external-id';
573
+ const onChange = jest.fn();
574
+ const onInput = jest.fn();
575
+ const replaceInnerBlocks = jest.spyOn(
576
+ blockEditorActions,
577
+ 'replaceInnerBlocks'
578
+ );
579
+
580
+ // Blocks with specific external client IDs
581
+ const controlledBlocks = [
582
+ {
583
+ name: 'test/test-block',
584
+ clientId: originalClientId,
585
+ innerBlocks: [
586
+ {
587
+ name: 'test/test-block',
588
+ clientId: innerBlockClientId,
589
+ innerBlocks: [],
590
+ attributes: { foo: 10 },
591
+ },
592
+ ],
593
+ attributes: { foo: 1 },
594
+ },
595
+ ];
596
+
597
+ let registry;
598
+ const setRegistry = ( reg ) => {
599
+ registry = reg;
600
+ };
601
+
602
+ render(
603
+ <TestWrapper
604
+ setRegistry={ setRegistry }
605
+ value={ controlledBlocks }
606
+ onChange={ onChange }
607
+ onInput={ onInput }
608
+ />
609
+ );
610
+
611
+ // For the root case (no clientId), blocks are not cloned
612
+ // So the external IDs should be preserved as-is
613
+ expect( replaceInnerBlocks ).not.toHaveBeenCalled();
614
+
615
+ onChange.mockClear();
616
+ onInput.mockClear();
617
+
618
+ registry
619
+ .dispatch( blockEditorStore )
620
+ .updateBlockAttributes( originalClientId, { foo: 2 } );
621
+
622
+ // The onChange callback should receive blocks with the same external IDs
623
+ expect( onChange ).toHaveBeenCalledWith(
624
+ expect.arrayContaining( [
625
+ expect.objectContaining( {
626
+ clientId: originalClientId,
627
+ attributes: { foo: 2 },
628
+ innerBlocks: expect.arrayContaining( [
629
+ expect.objectContaining( {
630
+ clientId: innerBlockClientId,
631
+ } ),
632
+ ] ),
633
+ } ),
634
+ ] ),
635
+ expect.objectContaining( {
636
+ selection: expect.any( Object ),
637
+ } )
638
+ );
639
+ } );
640
+
641
+ it( 'clones blocks with new internal IDs for inner block controllers', async () => {
642
+ const originalClientId = 'original-external-id';
643
+ const replaceInnerBlocks = jest.spyOn(
644
+ blockEditorActions,
645
+ 'replaceInnerBlocks'
646
+ );
647
+
648
+ // Blocks with specific external client IDs
649
+ const controlledBlocks = [
650
+ {
651
+ name: 'test/test-block',
652
+ clientId: originalClientId,
653
+ innerBlocks: [],
654
+ attributes: { foo: 1 },
655
+ },
656
+ ];
657
+
658
+ render(
659
+ <TestWrapper
660
+ clientId="test-controller"
661
+ value={ controlledBlocks }
662
+ onChange={ jest.fn() }
663
+ onInput={ jest.fn() }
664
+ />
665
+ );
666
+
667
+ // replaceInnerBlocks should have been called with cloned blocks
668
+ expect( replaceInnerBlocks ).toHaveBeenCalled();
669
+ const replacedBlocks = replaceInnerBlocks.mock.calls[ 0 ][ 1 ];
670
+
671
+ // The internal IDs should be different from the external IDs (due to cloning)
672
+ expect( replacedBlocks[ 0 ].clientId ).not.toBe( originalClientId );
673
+ } );
569
674
  } );
@@ -12,6 +12,87 @@ import { store as blockEditorStore } from '../../store';
12
12
 
13
13
  const noop = () => {};
14
14
 
15
+ /**
16
+ * Clones a block and its inner blocks, building a bidirectional mapping
17
+ * between external (original) and internal (cloned) client IDs.
18
+ *
19
+ * This allows the block editor to use unique internal IDs while preserving
20
+ * stable external IDs for features like real-time collaboration.
21
+ *
22
+ * @param {Object} block The block to clone.
23
+ * @param {Object} mapping The mapping object with externalToInternal and internalToExternal Maps.
24
+ * @return {Object} The cloned block with a new clientId.
25
+ */
26
+ function cloneBlockWithMapping( block, mapping ) {
27
+ const clonedBlock = cloneBlock( block );
28
+
29
+ // Build bidirectional mapping
30
+ mapping.externalToInternal.set( block.clientId, clonedBlock.clientId );
31
+ mapping.internalToExternal.set( clonedBlock.clientId, block.clientId );
32
+
33
+ // Recursively map inner blocks
34
+ if ( block.innerBlocks?.length ) {
35
+ clonedBlock.innerBlocks = block.innerBlocks.map( ( innerBlock ) => {
36
+ const clonedInner = cloneBlockWithMapping( innerBlock, mapping );
37
+ // The clonedBlock already has cloned inner blocks from cloneBlock(),
38
+ // but we need to use our mapped versions to maintain the mapping.
39
+ return clonedInner;
40
+ } );
41
+ }
42
+
43
+ return clonedBlock;
44
+ }
45
+
46
+ /**
47
+ * Restores external (original) client IDs on blocks before passing them
48
+ * to onChange/onInput callbacks.
49
+ *
50
+ * @param {Object[]} blocks The blocks with internal client IDs.
51
+ * @param {Object} mapping The mapping object with internalToExternal Map.
52
+ * @return {Object[]} Blocks with external client IDs restored.
53
+ */
54
+ function restoreExternalIds( blocks, mapping ) {
55
+ return blocks.map( ( block ) => {
56
+ const externalId = mapping.internalToExternal.get( block.clientId );
57
+ return {
58
+ ...block,
59
+ // Use external ID if available, otherwise keep internal ID (for new blocks)
60
+ clientId: externalId ?? block.clientId,
61
+ innerBlocks: restoreExternalIds( block.innerBlocks, mapping ),
62
+ };
63
+ } );
64
+ }
65
+
66
+ /**
67
+ * Restores external client IDs in selection state.
68
+ *
69
+ * @param {Object} selection The selection state with internal client IDs.
70
+ * @param {Object} mapping The mapping object with internalToExternal Map.
71
+ * @return {Object} Selection state with external client IDs.
72
+ */
73
+ function restoreSelectionIds( selection, mapping ) {
74
+ const { selectionStart, selectionEnd, initialPosition } = selection;
75
+
76
+ const restoreClientId = ( selectionState ) => {
77
+ if ( ! selectionState?.clientId ) {
78
+ return selectionState;
79
+ }
80
+ const externalId = mapping.internalToExternal.get(
81
+ selectionState.clientId
82
+ );
83
+ return {
84
+ ...selectionState,
85
+ clientId: externalId ?? selectionState.clientId,
86
+ };
87
+ };
88
+
89
+ return {
90
+ selectionStart: restoreClientId( selectionStart ),
91
+ selectionEnd: restoreClientId( selectionEnd ),
92
+ initialPosition,
93
+ };
94
+ }
95
+
15
96
  /**
16
97
  * A function to call when the block value has been updated in the block-editor
17
98
  * store.
@@ -94,6 +175,13 @@ export default function useBlockSync( {
94
175
  const pendingChangesRef = useRef( { incoming: null, outgoing: [] } );
95
176
  const subscribedRef = useRef( false );
96
177
 
178
+ // Mapping between external (original) and internal (cloned) client IDs.
179
+ // This allows stable external IDs while using unique internal IDs.
180
+ const idMappingRef = useRef( {
181
+ externalToInternal: new Map(),
182
+ internalToExternal: new Map(),
183
+ } );
184
+
97
185
  const setControlledBlocks = () => {
98
186
  if ( ! controlledBlocks ) {
99
187
  return;
@@ -110,8 +198,14 @@ export default function useBlockSync( {
110
198
  // before the actual blocks get set properly in state.
111
199
  registry.batch( () => {
112
200
  setHasControlledInnerBlocks( clientId, true );
201
+
202
+ // Clear previous mappings and build new ones during cloning.
203
+ // This ensures the mapping stays in sync with the current blocks.
204
+ idMappingRef.current.externalToInternal.clear();
205
+ idMappingRef.current.internalToExternal.clear();
206
+
113
207
  const storeBlocks = controlledBlocks.map( ( block ) =>
114
- cloneBlock( block )
208
+ cloneBlockWithMapping( block, idMappingRef.current )
115
209
  );
116
210
  if ( subscribedRef.current ) {
117
211
  pendingChangesRef.current.incoming = storeBlocks;
@@ -262,24 +356,39 @@ export default function useBlockSync( {
262
356
 
263
357
  if ( areBlocksDifferent || didPersistenceChange ) {
264
358
  isPersistent = newIsPersistent;
359
+
360
+ // For inner block controllers (clientId is set), restore external IDs
361
+ // before passing blocks to the parent. This maintains stable external
362
+ // IDs for features like real-time collaboration while using unique
363
+ // internal IDs in the block-editor store.
364
+ const blocksForParent = clientId
365
+ ? restoreExternalIds( blocks, idMappingRef.current )
366
+ : blocks;
367
+
368
+ const selection = {
369
+ selectionStart: getSelectionStart(),
370
+ selectionEnd: getSelectionEnd(),
371
+ initialPosition: getSelectedBlocksInitialCaretPosition(),
372
+ };
373
+
374
+ // Also restore external IDs in selection state for inner block controllers.
375
+ const selectionForParent = clientId
376
+ ? restoreSelectionIds( selection, idMappingRef.current )
377
+ : selection;
378
+
265
379
  // We know that onChange/onInput will update controlledBlocks.
266
380
  // We need to be aware that it was caused by an outgoing change
267
381
  // so that we do not treat it as an incoming change later on,
268
382
  // which would cause a block reset.
269
- pendingChangesRef.current.outgoing.push( blocks );
383
+ pendingChangesRef.current.outgoing.push( blocksForParent );
270
384
 
271
385
  // Inform the controlling entity that changes have been made to
272
386
  // the block-editor store they should be aware about.
273
387
  const updateParent = isPersistent
274
388
  ? onChangeRef.current
275
389
  : onInputRef.current;
276
- updateParent( blocks, {
277
- selection: {
278
- selectionStart: getSelectionStart(),
279
- selectionEnd: getSelectionEnd(),
280
- initialPosition:
281
- getSelectedBlocksInitialCaretPosition(),
282
- },
390
+ updateParent( blocksForParent, {
391
+ selection: selectionForParent,
283
392
  } );
284
393
  }
285
394
  previousAreBlocksDifferent = areBlocksDifferent;
@@ -0,0 +1,138 @@
1
+ /**
2
+ * WordPress dependencies
3
+ */
4
+ import {
5
+ __experimentalUnitControl as UnitControl,
6
+ __experimentalUseCustomUnits as useCustomUnits,
7
+ __experimentalParseQuantityAndUnitFromRawValue as parseQuantityAndUnitFromRawValue,
8
+ __experimentalView as View,
9
+ RangeControl,
10
+ __experimentalSpacer as Spacer,
11
+ Flex,
12
+ FlexItem,
13
+ BaseControl,
14
+ } from '@wordpress/components';
15
+ import { __ } from '@wordpress/i18n';
16
+
17
+ /**
18
+ * Internal dependencies
19
+ */
20
+ import { useSettings } from '../../components/use-settings';
21
+
22
+ /**
23
+ * Control for line text indent.
24
+ *
25
+ * @param {Object} props Component props.
26
+ * @param {boolean} props.__next40pxDefaultSize Start opting into the larger default height that will become the default size in a future version.
27
+ * @param {string} props.value Currently selected text indent.
28
+ * @param {Function} props.onChange Handles change in text indent selection.
29
+ * @param {string|number|undefined} props.__unstableInputWidth Input width to pass through to inner UnitControl. Should be a valid CSS value.
30
+ * @param {boolean} props.withSlider Whether to show the slider control.
31
+ * @param {boolean} props.hasBottomMargin Whether to add bottom margin below the control.
32
+ * @param {string} props.help Help text to display below the control.
33
+ *
34
+ * @return {Element} Text indent control.
35
+ */
36
+ export default function TextIndentControl( {
37
+ __next40pxDefaultSize = false,
38
+ value,
39
+ onChange,
40
+ __unstableInputWidth = '60px',
41
+ withSlider = false,
42
+ hasBottomMargin = false,
43
+ help,
44
+ ...otherProps
45
+ } ) {
46
+ const [ availableUnits ] = useSettings( 'spacing.units' );
47
+ const units = useCustomUnits( {
48
+ availableUnits: availableUnits || [
49
+ 'px',
50
+ 'em',
51
+ 'rem',
52
+ 'ch',
53
+ '%',
54
+ 'vw',
55
+ 'vh',
56
+ ],
57
+ defaultValues: { px: 16, em: 2, rem: 2, ch: 2 },
58
+ } );
59
+
60
+ const [ valueQuantity, valueUnit ] = parseQuantityAndUnitFromRawValue(
61
+ value,
62
+ units
63
+ );
64
+ const isValueUnitRelative =
65
+ !! valueUnit &&
66
+ [ 'em', 'rem', '%', 'ch', 'vw', 'vh' ].includes( valueUnit );
67
+
68
+ if ( ! withSlider ) {
69
+ return (
70
+ <UnitControl
71
+ __next40pxDefaultSize={ __next40pxDefaultSize }
72
+ __shouldNotWarnDeprecated36pxSize
73
+ { ...otherProps }
74
+ label={ __( 'Line indent' ) }
75
+ value={ value }
76
+ __unstableInputWidth={ __unstableInputWidth }
77
+ units={ units }
78
+ onChange={ onChange }
79
+ help={ help }
80
+ />
81
+ );
82
+ }
83
+
84
+ return (
85
+ <View style={ hasBottomMargin ? { marginBottom: 12 } : undefined }>
86
+ <BaseControl.VisualLabel>
87
+ { __( 'Line indent' ) }
88
+ </BaseControl.VisualLabel>
89
+ <Flex>
90
+ <FlexItem isBlock>
91
+ <UnitControl
92
+ __next40pxDefaultSize={ __next40pxDefaultSize }
93
+ __shouldNotWarnDeprecated36pxSize
94
+ label={ __( 'Line indent' ) }
95
+ labelPosition="top"
96
+ hideLabelFromVision
97
+ value={ value }
98
+ onChange={ onChange }
99
+ size={ otherProps.size }
100
+ units={ units }
101
+ __unstableInputWidth={ __unstableInputWidth }
102
+ min={ 0 }
103
+ />
104
+ </FlexItem>
105
+ { withSlider && (
106
+ <FlexItem isBlock>
107
+ <Spacer marginX={ 2 } marginBottom={ 0 }>
108
+ <RangeControl
109
+ __next40pxDefaultSize={ __next40pxDefaultSize }
110
+ __shouldNotWarnDeprecated36pxSize
111
+ label={ __( 'Line indent' ) }
112
+ hideLabelFromVision
113
+ value={ valueQuantity }
114
+ withInputField={ false }
115
+ onChange={ ( newValue ) => {
116
+ if ( newValue === undefined ) {
117
+ onChange?.( undefined );
118
+ } else {
119
+ onChange?.(
120
+ newValue + ( valueUnit ?? 'px' )
121
+ );
122
+ }
123
+ } }
124
+ min={ 0 }
125
+ max={ isValueUnitRelative ? 10 : 100 }
126
+ step={ isValueUnitRelative ? 0.1 : 1 }
127
+ initialPosition={ 0 }
128
+ />
129
+ </Spacer>
130
+ </FlexItem>
131
+ ) }
132
+ </Flex>
133
+ { help && (
134
+ <p className="components-base-control__help">{ help }</p>
135
+ ) }
136
+ </View>
137
+ );
138
+ }
@@ -12,10 +12,10 @@ import { UP, DOWN, ENTER, TAB } from '@wordpress/keycodes';
12
12
  import {
13
13
  BaseControl,
14
14
  Button,
15
- __experimentalInputControl as InputControl,
16
15
  Spinner,
17
16
  withSpokenMessages,
18
17
  Popover,
18
+ privateApis as componentsPrivateApis,
19
19
  } from '@wordpress/components';
20
20
  import {
21
21
  compose,
@@ -30,6 +30,9 @@ import { isURL } from '@wordpress/url';
30
30
  * Internal dependencies
31
31
  */
32
32
  import { store as blockEditorStore } from '../../store';
33
+ import { unlock } from '../../lock-unlock';
34
+
35
+ const { ValidatedInputControl } = unlock( componentsPrivateApis );
33
36
 
34
37
  /**
35
38
  * Whether the argument is a function.
@@ -426,6 +429,8 @@ class URLInput extends Component {
426
429
  hideLabelFromVision = false,
427
430
  help = null,
428
431
  disabled = false,
432
+ customValidity,
433
+ markWhenOptional,
429
434
  } = this.props;
430
435
 
431
436
  const {
@@ -473,13 +478,27 @@ class URLInput extends Component {
473
478
  help,
474
479
  };
475
480
 
481
+ const validationProps = {
482
+ customValidity,
483
+ // Suppress the "(Required)" indicator in the label.
484
+ // The field is still required for validation, but the indicator
485
+ // can be hidden when markWhenOptional is set to true.
486
+ ...( markWhenOptional !== undefined && {
487
+ markWhenOptional,
488
+ } ),
489
+ };
490
+
476
491
  if ( renderControl ) {
477
492
  return renderControl( controlProps, inputProps, loading );
478
493
  }
479
494
 
480
495
  return (
481
496
  <BaseControl { ...controlProps }>
482
- <InputControl { ...inputProps } __next40pxDefaultSize />
497
+ <ValidatedInputControl
498
+ { ...inputProps }
499
+ { ...validationProps }
500
+ __next40pxDefaultSize
501
+ />
483
502
  { loading && <Spinner /> }
484
503
  </BaseControl>
485
504
  );
@@ -27,7 +27,7 @@ import {
27
27
  fullscreen,
28
28
  linkOff,
29
29
  } from '@wordpress/icons';
30
- import { prependHTTP } from '@wordpress/url';
30
+ import { prependHTTPS } from '@wordpress/url';
31
31
 
32
32
  /**
33
33
  * Internal dependencies
@@ -156,7 +156,7 @@ const ImageURLInputUI = ( {
156
156
  )?.linkDestination || LINK_DESTINATION_CUSTOM;
157
157
 
158
158
  onChangeUrl( {
159
- href: prependHTTP( urlInput ),
159
+ href: prependHTTPS( urlInput ),
160
160
  linkDestination: selectedDestination,
161
161
  lightbox: { enabled: false },
162
162
  } );
@@ -114,10 +114,6 @@ export function getClosestTabbable(
114
114
  }
115
115
 
116
116
  function isTabCandidate( node ) {
117
- if ( node.closest( '[inert]' ) ) {
118
- return;
119
- }
120
-
121
117
  // Skip if there's only one child that is content editable (and thus a
122
118
  // better candidate).
123
119
  if (
@@ -64,7 +64,7 @@ function BlockEditAnchorControlPure( { anchor, setAttributes } ) {
64
64
  help={
65
65
  <>
66
66
  { __(
67
- 'Enter a word or two without spaces to make a unique web address just for this block, called an “anchor”. Then, you’ll be able to link directly to this section of your page.'
67
+ 'Enter a word or two—without spaces—to make a unique web address just for this block, called an “anchor”. Then, you’ll be able to link directly to this section of your page.'
68
68
  ) }
69
69
  { isWeb && (
70
70
  <>
@@ -4,6 +4,11 @@
4
4
  import { addFilter } from '@wordpress/hooks';
5
5
  import { hasBlockSupport } from '@wordpress/blocks';
6
6
 
7
+ /**
8
+ * Internal dependencies
9
+ */
10
+ import { shouldSkipSerialization } from './utils';
11
+
7
12
  /**
8
13
  * Filters registered block settings, extending attributes with ariaLabel using aria-label
9
14
  * of the first node.
@@ -42,7 +47,10 @@ export function addAttribute( settings ) {
42
47
  * @return {Object} Filtered props applied to save element.
43
48
  */
44
49
  export function addSaveProps( extraProps, blockType, attributes ) {
45
- if ( hasBlockSupport( blockType, 'ariaLabel' ) ) {
50
+ if (
51
+ hasBlockSupport( blockType, 'ariaLabel' ) &&
52
+ ! shouldSkipSerialization( blockType, 'ariaLabel', 'ariaLabel' )
53
+ ) {
46
54
  extraProps[ 'aria-label' ] =
47
55
  attributes.ariaLabel === '' ? null : attributes.ariaLabel;
48
56
  }