@wordpress/block-editor 15.12.1-next.v.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 (124) 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-removal-warning-modal/index.cjs +30 -5
  5. package/build/components/block-removal-warning-modal/index.cjs.map +3 -3
  6. package/build/components/block-visibility/use-block-visibility.cjs +14 -29
  7. package/build/components/block-visibility/use-block-visibility.cjs.map +2 -2
  8. package/build/components/global-styles/hooks.cjs +7 -0
  9. package/build/components/global-styles/hooks.cjs.map +2 -2
  10. package/build/components/global-styles/typography-panel.cjs +71 -3
  11. package/build/components/global-styles/typography-panel.cjs.map +3 -3
  12. package/build/components/grid/grid-visualizer.cjs +49 -13
  13. package/build/components/grid/grid-visualizer.cjs.map +2 -2
  14. package/build/components/iframe/index.cjs +3 -1
  15. package/build/components/iframe/index.cjs.map +2 -2
  16. package/build/components/iframe/use-scale-canvas.cjs +1 -0
  17. package/build/components/iframe/use-scale-canvas.cjs.map +2 -2
  18. package/build/components/link-control/index.cjs +73 -2
  19. package/build/components/link-control/index.cjs.map +3 -3
  20. package/build/components/link-control/is-url-like.cjs +15 -3
  21. package/build/components/link-control/is-url-like.cjs.map +2 -2
  22. package/build/components/link-control/search-input.cjs +4 -1
  23. package/build/components/link-control/search-input.cjs.map +2 -2
  24. package/build/components/link-control/use-search-handler.cjs +1 -1
  25. package/build/components/link-control/use-search-handler.cjs.map +2 -2
  26. package/build/components/provider/use-block-sync.cjs +60 -8
  27. package/build/components/provider/use-block-sync.cjs.map +2 -2
  28. package/build/components/text-indent-control/index.cjs +121 -0
  29. package/build/components/text-indent-control/index.cjs.map +7 -0
  30. package/build/components/url-input/index.cjs +22 -2
  31. package/build/components/url-input/index.cjs.map +3 -3
  32. package/build/components/url-popover/image-url-input-ui.cjs +1 -1
  33. package/build/components/url-popover/image-url-input-ui.cjs.map +2 -2
  34. package/build/components/writing-flow/use-arrow-nav.cjs +0 -3
  35. package/build/components/writing-flow/use-arrow-nav.cjs.map +2 -2
  36. package/build/hooks/aria-label.cjs +2 -1
  37. package/build/hooks/aria-label.cjs.map +2 -2
  38. package/build/hooks/grid-visualizer.cjs +20 -4
  39. package/build/hooks/grid-visualizer.cjs.map +2 -2
  40. package/build/hooks/layout-child.cjs +8 -3
  41. package/build/hooks/layout-child.cjs.map +2 -2
  42. package/build/hooks/typography.cjs +2 -0
  43. package/build/hooks/typography.cjs.map +2 -2
  44. package/build/hooks/utils.cjs +4 -0
  45. package/build/hooks/utils.cjs.map +2 -2
  46. package/build/store/actions.cjs +2 -2
  47. package/build/store/actions.cjs.map +2 -2
  48. package/build-module/components/block-allowed-blocks/modal.mjs +2 -2
  49. package/build-module/components/block-allowed-blocks/modal.mjs.map +2 -2
  50. package/build-module/components/block-removal-warning-modal/index.mjs +34 -7
  51. package/build-module/components/block-removal-warning-modal/index.mjs.map +2 -2
  52. package/build-module/components/block-visibility/use-block-visibility.mjs +14 -29
  53. package/build-module/components/block-visibility/use-block-visibility.mjs.map +2 -2
  54. package/build-module/components/global-styles/hooks.mjs +7 -0
  55. package/build-module/components/global-styles/hooks.mjs.map +2 -2
  56. package/build-module/components/global-styles/typography-panel.mjs +73 -4
  57. package/build-module/components/global-styles/typography-panel.mjs.map +2 -2
  58. package/build-module/components/grid/grid-visualizer.mjs +50 -14
  59. package/build-module/components/grid/grid-visualizer.mjs.map +2 -2
  60. package/build-module/components/iframe/index.mjs +9 -2
  61. package/build-module/components/iframe/index.mjs.map +2 -2
  62. package/build-module/components/iframe/use-scale-canvas.mjs +1 -0
  63. package/build-module/components/iframe/use-scale-canvas.mjs.map +2 -2
  64. package/build-module/components/link-control/index.mjs +74 -3
  65. package/build-module/components/link-control/index.mjs.map +2 -2
  66. package/build-module/components/link-control/is-url-like.mjs +10 -3
  67. package/build-module/components/link-control/is-url-like.mjs.map +2 -2
  68. package/build-module/components/link-control/search-input.mjs +4 -1
  69. package/build-module/components/link-control/search-input.mjs.map +2 -2
  70. package/build-module/components/link-control/use-search-handler.mjs +2 -2
  71. package/build-module/components/link-control/use-search-handler.mjs.map +2 -2
  72. package/build-module/components/provider/use-block-sync.mjs +60 -8
  73. package/build-module/components/provider/use-block-sync.mjs.map +2 -2
  74. package/build-module/components/text-indent-control/index.mjs +110 -0
  75. package/build-module/components/text-indent-control/index.mjs.map +7 -0
  76. package/build-module/components/url-input/index.mjs +24 -4
  77. package/build-module/components/url-input/index.mjs.map +2 -2
  78. package/build-module/components/url-popover/image-url-input-ui.mjs +2 -2
  79. package/build-module/components/url-popover/image-url-input-ui.mjs.map +2 -2
  80. package/build-module/components/writing-flow/use-arrow-nav.mjs +0 -3
  81. package/build-module/components/writing-flow/use-arrow-nav.mjs.map +2 -2
  82. package/build-module/hooks/aria-label.mjs +2 -1
  83. package/build-module/hooks/aria-label.mjs.map +2 -2
  84. package/build-module/hooks/grid-visualizer.mjs +20 -4
  85. package/build-module/hooks/grid-visualizer.mjs.map +2 -2
  86. package/build-module/hooks/layout-child.mjs +8 -3
  87. package/build-module/hooks/layout-child.mjs.map +2 -2
  88. package/build-module/hooks/typography.mjs +2 -0
  89. package/build-module/hooks/typography.mjs.map +2 -2
  90. package/build-module/hooks/utils.mjs +4 -0
  91. package/build-module/hooks/utils.mjs.map +2 -2
  92. package/build-module/store/actions.mjs +2 -2
  93. package/build-module/store/actions.mjs.map +2 -2
  94. package/package.json +39 -39
  95. package/src/components/block-allowed-blocks/modal.js +2 -2
  96. package/src/components/block-removal-warning-modal/index.js +55 -19
  97. package/src/components/block-switcher/block-transformations-menu.native.js +1 -0
  98. package/src/components/block-toolbar/test/__snapshots__/block-toolbar-menu.native.js.snap +4 -6
  99. package/src/components/block-toolbar/test/block-toolbar-menu.native.js +2 -2
  100. package/src/components/block-visibility/use-block-visibility.js +17 -32
  101. package/src/components/global-styles/hooks.js +10 -0
  102. package/src/components/global-styles/typography-panel.js +78 -1
  103. package/src/components/grid/grid-visualizer.js +58 -12
  104. package/src/components/iframe/index.js +12 -2
  105. package/src/components/iframe/use-scale-canvas.js +1 -0
  106. package/src/components/inserter/menu.native.js +1 -0
  107. package/src/components/link-control/index.js +160 -3
  108. package/src/components/link-control/is-url-like.js +43 -8
  109. package/src/components/link-control/search-input.js +7 -0
  110. package/src/components/link-control/test/index.js +260 -0
  111. package/src/components/link-control/test/is-url-like.js +49 -1
  112. package/src/components/link-control/use-search-handler.js +2 -2
  113. package/src/components/provider/test/use-block-sync.js +105 -0
  114. package/src/components/provider/use-block-sync.js +118 -9
  115. package/src/components/text-indent-control/index.js +138 -0
  116. package/src/components/url-input/index.js +21 -2
  117. package/src/components/url-popover/image-url-input-ui.js +2 -2
  118. package/src/components/writing-flow/use-arrow-nav.js +0 -4
  119. package/src/hooks/aria-label.js +9 -1
  120. package/src/hooks/grid-visualizer.js +53 -33
  121. package/src/hooks/layout-child.js +6 -0
  122. package/src/hooks/typography.js +2 -0
  123. package/src/hooks/utils.js +4 -0
  124. package/src/store/actions.js +8 -6
@@ -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 (
@@ -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
  }
@@ -10,6 +10,7 @@ import { useSelect } from '@wordpress/data';
10
10
  */
11
11
  import { GridVisualizer, useGridLayoutSync } from '../components/grid';
12
12
  import { store as blockEditorStore } from '../store';
13
+ import { unlock } from '../lock-unlock';
13
14
  import useBlockVisibility from '../components/block-visibility/use-block-visibility';
14
15
  import { deviceTypeKey } from '../store/private-keys';
15
16
  import { BLOCK_VISIBILITY_VIEWPORTS } from '../components/block-visibility/constants';
@@ -19,40 +20,54 @@ function GridLayoutSync( props ) {
19
20
  }
20
21
 
21
22
  function GridTools( { clientId, layout } ) {
22
- const { isVisible, blockVisibility, deviceType } = useSelect(
23
- ( select ) => {
24
- const {
25
- isBlockSelected,
26
- isDraggingBlocks,
27
- getTemplateLock,
28
- getBlockEditingMode,
29
- getBlockAttributes,
30
- getSettings,
31
- } = select( blockEditorStore );
23
+ const { isVisible, blockVisibility, deviceType, isAnyAncestorHidden } =
24
+ useSelect(
25
+ ( select ) => {
26
+ const {
27
+ isBlockSelected,
28
+ hasSelectedInnerBlock,
29
+ isDraggingBlocks,
30
+ getTemplateLock,
31
+ getBlockEditingMode,
32
+ getBlockAttributes,
33
+ getSettings,
34
+ } = select( blockEditorStore );
32
35
 
33
- // These calls are purposely ordered from least expensive to most expensive.
34
- // Hides the visualizer in cases where the user is not or cannot interact with it.
35
- if (
36
- ( ! isDraggingBlocks() && ! isBlockSelected( clientId ) ) ||
37
- getTemplateLock( clientId ) ||
38
- getBlockEditingMode( clientId ) !== 'default'
39
- ) {
40
- return { isVisible: false };
41
- }
36
+ // These calls are purposely ordered from least expensive to most expensive.
37
+ // Hides the visualizer in cases where the user is not or cannot interact with it.
38
+ // Also hide if a child block is selected, because layout-child.js will render
39
+ // the visualizer in that case (with proper childGridClientId handling).
40
+ if (
41
+ ( ! isDraggingBlocks() && ! isBlockSelected( clientId ) ) ||
42
+ getTemplateLock( clientId ) ||
43
+ getBlockEditingMode( clientId ) !== 'default' ||
44
+ hasSelectedInnerBlock( clientId )
45
+ ) {
46
+ return { isVisible: false };
47
+ }
42
48
 
43
- const attributes = getBlockAttributes( clientId );
44
- const settings = getSettings();
49
+ const { isBlockParentHiddenAtViewport } = unlock(
50
+ select( blockEditorStore )
51
+ );
45
52
 
46
- return {
47
- isVisible: true,
48
- blockVisibility: attributes?.metadata?.blockVisibility,
49
- deviceType:
53
+ const attributes = getBlockAttributes( clientId );
54
+ const settings = getSettings();
55
+ const currentDeviceType =
50
56
  settings?.[ deviceTypeKey ]?.toLowerCase() ||
51
- BLOCK_VISIBILITY_VIEWPORTS.desktop.value,
52
- };
53
- },
54
- [ clientId ]
55
- );
57
+ BLOCK_VISIBILITY_VIEWPORTS.desktop.value;
58
+
59
+ return {
60
+ isVisible: true,
61
+ blockVisibility: attributes?.metadata?.blockVisibility,
62
+ deviceType: currentDeviceType,
63
+ isAnyAncestorHidden: isBlockParentHiddenAtViewport(
64
+ clientId,
65
+ currentDeviceType
66
+ ),
67
+ };
68
+ },
69
+ [ clientId ]
70
+ );
56
71
 
57
72
  const { isBlockCurrentlyHidden } = useBlockVisibility( {
58
73
  blockVisibility,
@@ -62,9 +77,14 @@ function GridTools( { clientId, layout } ) {
62
77
  return (
63
78
  <>
64
79
  <GridLayoutSync clientId={ clientId } />
65
- { isVisible && ! isBlockCurrentlyHidden && (
66
- <GridVisualizer clientId={ clientId } parentLayout={ layout } />
67
- ) }
80
+ { isVisible &&
81
+ ! isBlockCurrentlyHidden &&
82
+ ! isAnyAncestorHidden && (
83
+ <GridVisualizer
84
+ clientId={ clientId }
85
+ parentLayout={ layout }
86
+ />
87
+ ) }
68
88
  </>
69
89
  );
70
90
  }
@@ -208,6 +208,7 @@ function GridTools( {
208
208
  parentBlockVisibility,
209
209
  blockBlockVisibility,
210
210
  deviceType,
211
+ isChildBlockAGrid,
211
212
  } = useSelect(
212
213
  ( select ) => {
213
214
  const {
@@ -244,6 +245,8 @@ function GridTools( {
244
245
  deviceType:
245
246
  settings?.[ deviceTypeKey ]?.toLowerCase() ||
246
247
  BLOCK_VISIBILITY_VIEWPORTS.desktop.value,
248
+ // Check if the selected child block is itself a grid.
249
+ isChildBlockAGrid: blockAttributes?.layout?.type === 'grid',
247
250
  };
248
251
  },
249
252
  [ clientId ]
@@ -264,6 +267,8 @@ function GridTools( {
264
267
  // Use useState() instead of useRef() so that GridItemResizer updates when ref is set.
265
268
  const [ resizerBounds, setResizerBounds ] = useState();
266
269
 
270
+ const childGridClientId = isChildBlockAGrid ? clientId : undefined;
271
+
267
272
  if ( ! isVisible || isParentBlockCurrentlyHidden ) {
268
273
  return null;
269
274
  }
@@ -288,6 +293,7 @@ function GridTools( {
288
293
  clientId={ rootClientId }
289
294
  contentRef={ setResizerBounds }
290
295
  parentLayout={ parentLayout }
296
+ childGridClientId={ childGridClientId }
291
297
  />
292
298
  { showResizer && (
293
299
  <GridItemResizer
@@ -31,6 +31,7 @@ function omit( object, keys ) {
31
31
  const LETTER_SPACING_SUPPORT_KEY = 'typography.__experimentalLetterSpacing';
32
32
  const TEXT_TRANSFORM_SUPPORT_KEY = 'typography.__experimentalTextTransform';
33
33
  const TEXT_DECORATION_SUPPORT_KEY = 'typography.__experimentalTextDecoration';
34
+ const TEXT_INDENT_SUPPORT_KEY = 'typography.textIndent';
34
35
  const TEXT_COLUMNS_SUPPORT_KEY = 'typography.textColumns';
35
36
  const FONT_STYLE_SUPPORT_KEY = 'typography.__experimentalFontStyle';
36
37
  const FONT_WEIGHT_SUPPORT_KEY = 'typography.__experimentalFontWeight';
@@ -45,6 +46,7 @@ export const TYPOGRAPHY_SUPPORT_KEYS = [
45
46
  TEXT_ALIGN_SUPPORT_KEY,
46
47
  TEXT_COLUMNS_SUPPORT_KEY,
47
48
  TEXT_DECORATION_SUPPORT_KEY,
49
+ TEXT_INDENT_SUPPORT_KEY,
48
50
  WRITING_MODE_SUPPORT_KEY,
49
51
  TEXT_TRANSFORM_SUPPORT_KEY,
50
52
  LETTER_SPACING_SUPPORT_KEY,