@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
@@ -5,6 +5,7 @@ import { __ } from '@wordpress/i18n';
5
5
  import {
6
6
  getBlockType,
7
7
  getUnregisteredTypeHandlerName,
8
+ hasBlockSupport,
8
9
  store as blocksStore,
9
10
  } from '@wordpress/blocks';
10
11
  import { __unstableMotion as motion } from '@wordpress/components';
@@ -24,6 +25,7 @@ import BlockStyles from '../block-styles';
24
25
  import { default as InspectorControls } from '../inspector-controls';
25
26
  import { default as InspectorControlsTabs } from '../inspector-controls-tabs';
26
27
  import useInspectorControlsTabs from '../inspector-controls-tabs/use-inspector-controls-tabs';
28
+ import InspectorControlsLastItem from '../inspector-controls/last-item';
27
29
  import AdvancedControls from '../inspector-controls-tabs/advanced-controls-panel';
28
30
  import PositionControls from '../inspector-controls-tabs/position-controls-panel';
29
31
  import useBlockInspectorAnimationSettings from './useBlockInspectorAnimationSettings';
@@ -145,30 +147,29 @@ function BlockInspector() {
145
147
  renderedBlockClientId
146
148
  );
147
149
 
148
- // Temporary workaround for issue #71991
149
- // Exclude Navigation block children from Content sidebar until proper
150
- // drill-down experience is implemented (see #65699)
151
- // This prevents a poor UX where all Nav block sub-items are shown
152
- // when the parent block is in contentOnly mode.
153
- // Build a Set of all navigation block descendants for efficient lookup
154
- const navigationDescendants = new Set();
150
+ // Exclude items from the content tab that are already present in the
151
+ // List View tab.
152
+ const listViewDescendants = new Set();
155
153
  descendants.forEach( ( clientId ) => {
156
- if ( getBlockName( clientId ) === 'core/navigation' ) {
157
- const navChildren = getClientIdsOfDescendants( clientId );
158
- navChildren.forEach( ( childId ) =>
159
- navigationDescendants.add( childId )
154
+ const blockName = getBlockName( clientId );
155
+ // Navigation block doesn't have List View block support, but
156
+ // it does have a custom implementation that is shown within
157
+ // patterns, so it's included in this condition.
158
+ if (
159
+ blockName === 'core/navigation' ||
160
+ hasBlockSupport( blockName, 'listView' )
161
+ ) {
162
+ const listViewChildren =
163
+ getClientIdsOfDescendants( clientId );
164
+ listViewChildren.forEach( ( childId ) =>
165
+ listViewDescendants.add( childId )
160
166
  );
161
167
  }
162
168
  } );
163
169
 
164
170
  return descendants.filter( ( current ) => {
165
- // Exclude navigation block children
166
- if ( navigationDescendants.has( current ) ) {
167
- return false;
168
- }
169
-
170
171
  return (
171
- getBlockName( current ) !== 'core/list-item' &&
172
+ ! listViewDescendants.has( current ) &&
172
173
  getBlockEditingMode( current ) === 'contentOnly'
173
174
  );
174
175
  } );
@@ -369,6 +370,7 @@ const BlockInspectorSingleBlock = ( {
369
370
  ) }
370
371
  </>
371
372
  ) }
373
+ <InspectorControlsLastItem.Slot />
372
374
  <SkipToSelectedBlock key="back" />
373
375
  </div>
374
376
  );
@@ -1,12 +1,14 @@
1
1
  /**
2
2
  * WordPress dependencies
3
3
  */
4
- import { useEffect } from '@wordpress/element';
4
+ import { useEffect, useState } from '@wordpress/element';
5
5
  import { useDispatch, useSelect } from '@wordpress/data';
6
6
  import {
7
7
  Modal,
8
8
  Button,
9
+ CheckboxControl,
9
10
  __experimentalHStack as HStack,
11
+ __experimentalVStack as VStack,
10
12
  } from '@wordpress/components';
11
13
  import { __ } from '@wordpress/i18n';
12
14
 
@@ -17,6 +19,7 @@ import { store as blockEditorStore } from '../../store';
17
19
  import { unlock } from '../../lock-unlock';
18
20
 
19
21
  export function BlockRemovalWarningModal( { rules } ) {
22
+ const [ confirmed, setConfirmed ] = useState( false );
20
23
  const { clientIds, selectPrevious, message } = useSelect( ( select ) =>
21
24
  unlock( select( blockEditorStore ) ).getRemovalPromptData()
22
25
  );
@@ -36,10 +39,20 @@ export function BlockRemovalWarningModal( { rules } ) {
36
39
  };
37
40
  }, [ rules, setBlockRemovalRules ] );
38
41
 
42
+ // Reset confirmed state when modal opens with new content.
43
+ useEffect( () => {
44
+ setConfirmed( false );
45
+ }, [ clientIds ] );
46
+
39
47
  if ( ! message ) {
40
48
  return;
41
49
  }
42
50
 
51
+ const isStructured = typeof message === 'object' && message !== null;
52
+ const description = isStructured ? message.description : message;
53
+ const requireConfirmation = isStructured && message.requireConfirmation;
54
+ const isRemoveDisabled = requireConfirmation && ! confirmed;
55
+
43
56
  const onConfirmRemoval = () => {
44
57
  privateRemoveBlocks( clientIds, selectPrevious, /* force */ true );
45
58
  clearBlockRemovalPrompt();
@@ -47,27 +60,50 @@ export function BlockRemovalWarningModal( { rules } ) {
47
60
 
48
61
  return (
49
62
  <Modal
50
- title={ __( 'Be careful!' ) }
63
+ title={ __( 'Confirm deletion' ) }
51
64
  onRequestClose={ clearBlockRemovalPrompt }
52
65
  size="medium"
53
66
  >
54
- <p>{ message }</p>
55
- <HStack justify="right">
56
- <Button
57
- variant="tertiary"
58
- onClick={ clearBlockRemovalPrompt }
59
- __next40pxDefaultSize
60
- >
61
- { __( 'Cancel' ) }
62
- </Button>
63
- <Button
64
- variant="primary"
65
- onClick={ onConfirmRemoval }
66
- __next40pxDefaultSize
67
- >
68
- { __( 'Delete' ) }
69
- </Button>
70
- </HStack>
67
+ <VStack spacing={ 4 }>
68
+ <div>
69
+ <p>{ description }</p>
70
+ { isStructured &&
71
+ ( message.warning || message.subtext ) && (
72
+ <p>
73
+ { message.warning && (
74
+ <strong>{ message.warning }</strong>
75
+ ) }
76
+ { message.warning && message.subtext && ' ' }
77
+ { message.subtext }
78
+ </p>
79
+ ) }
80
+ </div>
81
+ { requireConfirmation && (
82
+ <CheckboxControl
83
+ label={ __( 'I understand the consequences' ) }
84
+ checked={ confirmed }
85
+ onChange={ setConfirmed }
86
+ />
87
+ ) }
88
+ <HStack justify="right">
89
+ <Button
90
+ variant="tertiary"
91
+ onClick={ clearBlockRemovalPrompt }
92
+ __next40pxDefaultSize
93
+ >
94
+ { __( 'Cancel' ) }
95
+ </Button>
96
+ <Button
97
+ variant="primary"
98
+ onClick={ onConfirmRemoval }
99
+ disabled={ isRemoveDisabled }
100
+ accessibleWhenDisabled
101
+ __next40pxDefaultSize
102
+ >
103
+ { __( 'Delete' ) }
104
+ </Button>
105
+ </HStack>
106
+ </VStack>
71
107
  </Modal>
72
108
  );
73
109
  }
@@ -33,6 +33,7 @@ const BlockTransformationsMenu = ( {
33
33
  const blocksThatSplitWhenTransformed = {
34
34
  'core/list': [ 'core/paragraph', 'core/heading' ],
35
35
  'core/quote': [ 'core/paragraph' ],
36
+ 'core/pullquote': [ 'core/paragraph' ],
36
37
  };
37
38
 
38
39
  return possibleTransformations.map( ( item ) => {
@@ -68,12 +68,10 @@ exports[`Block Actions Menu block options duplicates a block 1`] = `
68
68
  <!-- /wp:heading -->"
69
69
  `;
70
70
 
71
- exports[`Block Actions Menu block options transforms a Paragraph block into a Quote block 1`] = `
72
- "<!-- wp:quote -->
73
- <blockquote class="wp-block-quote"><!-- wp:paragraph -->
74
- <p>Hello!</p>
75
- <!-- /wp:paragraph --></blockquote>
76
- <!-- /wp:quote -->
71
+ exports[`Block Actions Menu block options transforms a Paragraph block into a Pullquote block 1`] = `
72
+ "<!-- wp:pullquote -->
73
+ <figure class="wp-block-pullquote"><blockquote><p>Hello!</p></blockquote></figure>
74
+ <!-- /wp:pullquote -->
77
75
 
78
76
  <!-- wp:spacer -->
79
77
  <div style="height:100px" aria-hidden="true" class="wp-block-spacer"></div>
@@ -357,7 +357,7 @@ describe( 'Block Actions Menu', () => {
357
357
  expect( getEditorHtml() ).toMatchSnapshot();
358
358
  } );
359
359
 
360
- it( 'transforms a Paragraph block into a Quote block', async () => {
360
+ it( 'transforms a Paragraph block into a Pullquote block', async () => {
361
361
  const screen = await initializeEditor();
362
362
  const { getByLabelText, getByRole } = screen;
363
363
 
@@ -397,7 +397,7 @@ describe( 'Block Actions Menu', () => {
397
397
  expect( headerTitle ).toBeVisible();
398
398
 
399
399
  // Tap on the Transform block button
400
- fireEvent.press( getByLabelText( /Quote/ ) );
400
+ fireEvent.press( getByLabelText( /Pullquote/ ) );
401
401
 
402
402
  expect( getEditorHtml() ).toMatchSnapshot();
403
403
  } );
@@ -2,7 +2,6 @@
2
2
  * WordPress dependencies
3
3
  */
4
4
  import { useViewportMatch } from '@wordpress/compose';
5
- import { useMemo } from '@wordpress/element';
6
5
 
7
6
  /**
8
7
  * Internal dependencies
@@ -15,7 +14,7 @@ import { BLOCK_VISIBILITY_VIEWPORTS } from './constants';
15
14
  * @param {Object} options Parameters to avoid extra store subscriptions.
16
15
  * @param {Object|boolean} options.blockVisibility Block visibility metadata.
17
16
  * @param {string} options.deviceType Current device type ('desktop', 'tablet', 'mobile').
18
- * @return {Object} Object with `isBlockCurrentlyHidden` and `currentViewport` boolean properties.
17
+ * @return {Object} Object with `isBlockCurrentlyHidden` (boolean) and `currentViewport` (string) properties.
19
18
  */
20
19
  export default function useBlockVisibility( options = {} ) {
21
20
  const {
@@ -31,37 +30,23 @@ export default function useBlockVisibility( options = {} ) {
31
30
  * 1. Device type override (Mobile/Tablet) - uses device type to determine viewport
32
31
  * 2. Actual window size (Desktop mode) - uses viewport detection
33
32
  */
34
- const currentViewport = useMemo( () => {
35
- if ( deviceType === BLOCK_VISIBILITY_VIEWPORTS.mobile.key ) {
36
- return BLOCK_VISIBILITY_VIEWPORTS.mobile.key;
37
- }
38
- if ( deviceType === BLOCK_VISIBILITY_VIEWPORTS.tablet.key ) {
39
- return BLOCK_VISIBILITY_VIEWPORTS.tablet.key;
40
- }
41
- if ( ! isLargerThanMobile ) {
42
- return BLOCK_VISIBILITY_VIEWPORTS.mobile.key;
43
- }
44
- if ( isLargerThanMobile && ! isLargerThanTablet ) {
45
- return BLOCK_VISIBILITY_VIEWPORTS.tablet.key;
46
- }
47
- return BLOCK_VISIBILITY_VIEWPORTS.desktop.key;
48
- }, [ deviceType, isLargerThanMobile, isLargerThanTablet ] );
33
+ let currentViewport;
34
+ if ( deviceType === BLOCK_VISIBILITY_VIEWPORTS.mobile.key ) {
35
+ currentViewport = BLOCK_VISIBILITY_VIEWPORTS.mobile.key;
36
+ } else if ( deviceType === BLOCK_VISIBILITY_VIEWPORTS.tablet.key ) {
37
+ currentViewport = BLOCK_VISIBILITY_VIEWPORTS.tablet.key;
38
+ } else if ( ! isLargerThanMobile ) {
39
+ currentViewport = BLOCK_VISIBILITY_VIEWPORTS.mobile.key;
40
+ } else if ( isLargerThanMobile && ! isLargerThanTablet ) {
41
+ currentViewport = BLOCK_VISIBILITY_VIEWPORTS.tablet.key;
42
+ } else {
43
+ currentViewport = BLOCK_VISIBILITY_VIEWPORTS.desktop.key;
44
+ }
49
45
 
50
46
  // Determine if block is currently hidden.
51
- const isBlockCurrentlyHidden = useMemo( () => {
52
- if ( blockVisibility === false ) {
53
- return true;
54
- }
47
+ const isBlockCurrentlyHidden =
48
+ blockVisibility === false ||
49
+ blockVisibility?.viewport?.[ currentViewport ] === false;
55
50
 
56
- if ( blockVisibility?.viewport?.[ currentViewport ] === false ) {
57
- return true;
58
- }
59
-
60
- return false;
61
- }, [ blockVisibility, currentViewport ] );
62
-
63
- return {
64
- isBlockCurrentlyHidden,
65
- currentViewport,
66
- };
51
+ return { isBlockCurrentlyHidden, currentViewport };
67
52
  }
@@ -100,6 +100,7 @@ export function useSettingsForBlockElement(
100
100
  'textAlign',
101
101
  'textTransform',
102
102
  'textDecoration',
103
+ 'textIndent',
103
104
  'writingMode',
104
105
  ].forEach( ( key ) => {
105
106
  if ( ! supportedStyles.includes( key ) ) {
@@ -110,6 +111,15 @@ export function useSettingsForBlockElement(
110
111
  }
111
112
  } );
112
113
 
114
+ // Text indent needs explicit handling since it may not be in parent settings.
115
+ if ( supportedStyles.includes( 'textIndent' ) ) {
116
+ updatedSettings.typography = {
117
+ ...updatedSettings.typography,
118
+ textIndent:
119
+ updatedSettings.typography?.textIndent ?? 'subsequent',
120
+ };
121
+ }
122
+
113
123
  // The column-count style is named text column to reduce confusion with
114
124
  // the columns block and manage expectations from the support.
115
125
  // See: https://github.com/WordPress/gutenberg/pull/33587
@@ -7,6 +7,7 @@ import {
7
7
  __experimentalToolsPanel as ToolsPanel,
8
8
  __experimentalToolsPanelItem as ToolsPanelItem,
9
9
  Notice,
10
+ ToggleControl,
10
11
  } from '@wordpress/components';
11
12
  import { __ } from '@wordpress/i18n';
12
13
  import { useCallback, useMemo } from '@wordpress/element';
@@ -22,6 +23,7 @@ import LetterSpacingControl from '../letter-spacing-control';
22
23
  import TextAlignmentControl from '../text-alignment-control';
23
24
  import TextTransformControl from '../text-transform-control';
24
25
  import TextDecorationControl from '../text-decoration-control';
26
+ import TextIndentControl from '../text-indent-control';
25
27
  import WritingModeControl from '../writing-mode-control';
26
28
  import { useToolsPanelDropdownMenuProps } from './utils';
27
29
  import { setImmutably } from '../../utils/object';
@@ -42,6 +44,7 @@ export function useHasTypographyPanel( settings ) {
42
44
  const hasTextAlign = useHasTextAlignmentControl( settings );
43
45
  const hasTextTransform = useHasTextTransformControl( settings );
44
46
  const hasTextDecoration = useHasTextDecorationControl( settings );
47
+ const hasTextIndent = useHasTextIndentControl( settings );
45
48
  const hasWritingMode = useHasWritingModeControl( settings );
46
49
  const hasTextColumns = useHasTextColumnsControl( settings );
47
50
  const hasFontSize = useHasFontSizeControl( settings );
@@ -55,6 +58,7 @@ export function useHasTypographyPanel( settings ) {
55
58
  hasTextTransform ||
56
59
  hasFontSize ||
57
60
  hasTextDecoration ||
61
+ hasTextIndent ||
58
62
  hasWritingMode ||
59
63
  hasTextColumns
60
64
  );
@@ -118,6 +122,10 @@ function useHasTextColumnsControl( settings ) {
118
122
  return settings?.typography?.textColumns;
119
123
  }
120
124
 
125
+ function useHasTextIndentControl( settings ) {
126
+ return settings?.typography?.textIndent;
127
+ }
128
+
121
129
  /**
122
130
  * Concatenate all the font sizes into a single list for the font size picker.
123
131
  *
@@ -169,6 +177,7 @@ const DEFAULT_CONTROLS = {
169
177
  textAlign: true,
170
178
  textTransform: true,
171
179
  textDecoration: true,
180
+ textIndent: true,
172
181
  writingMode: true,
173
182
  textColumns: true,
174
183
  };
@@ -181,6 +190,7 @@ export default function TypographyPanel( {
181
190
  settings,
182
191
  panelId,
183
192
  defaultControls = DEFAULT_CONTROLS,
193
+ isGlobalStyles = false,
184
194
  } ) {
185
195
  const decodeValue = ( rawValue ) =>
186
196
  getValueFromVariable( { settings }, '', rawValue );
@@ -358,6 +368,48 @@ export default function TypographyPanel( {
358
368
  const hasLetterSpacing = () => !! value?.typography?.letterSpacing;
359
369
  const resetLetterSpacing = () => setLetterSpacing( undefined );
360
370
 
371
+ // Text Indent
372
+ const hasTextIndentControl = useHasTextIndentControl( settings );
373
+ const textIndent = decodeValue( inheritedValue?.typography?.textIndent );
374
+
375
+ // Get the setting value - can be 'subsequent' (default), 'all', or false.
376
+ // The setting determines which CSS selector is used for the text-indent style.
377
+ const textIndentSetting = settings?.typography?.textIndent ?? 'subsequent';
378
+ const isTextIndentAll = textIndentSetting === 'all';
379
+
380
+ const setTextIndentValue = ( newValue ) => {
381
+ onChange(
382
+ setImmutably(
383
+ value,
384
+ [ 'typography', 'textIndent' ],
385
+ newValue || undefined
386
+ )
387
+ );
388
+ };
389
+
390
+ const onToggleTextIndentAll = ( newValue ) => {
391
+ // Toggle between 'all' and 'subsequent' for the setting.
392
+ // Include the settings change so it can be handled atomically by the parent.
393
+ onChange( {
394
+ ...value,
395
+ settings: {
396
+ typography: {
397
+ textIndent: newValue ? 'all' : 'subsequent',
398
+ },
399
+ },
400
+ } );
401
+ };
402
+
403
+ const hasTextIndent = () => !! value?.typography?.textIndent;
404
+ const resetTextIndent = () => {
405
+ onChange(
406
+ setImmutably( value, [ 'typography', 'textIndent' ], undefined )
407
+ );
408
+ };
409
+ const textIndentHelp = isTextIndentAll
410
+ ? __( 'Indents the first line of all paragraphs.' )
411
+ : __( 'Indents the first line of each paragraph after the first one.' );
412
+
361
413
  // Text Columns
362
414
  const hasTextColumnsControl = useHasTextColumnsControl( settings );
363
415
  const textColumns = decodeValue( inheritedValue?.typography?.textColumns );
@@ -490,7 +542,6 @@ export default function TypographyPanel( {
490
542
  ) }
491
543
  { hasAppearanceControl && (
492
544
  <ToolsPanelItem
493
- className="single-column"
494
545
  label={ appearanceControlLabel }
495
546
  hasValue={ hasFontAppearance }
496
547
  onDeselect={ resetFontAppearance }
@@ -544,6 +595,32 @@ export default function TypographyPanel( {
544
595
  />
545
596
  </ToolsPanelItem>
546
597
  ) }
598
+ { hasTextIndentControl && (
599
+ <ToolsPanelItem
600
+ label={ __( 'Line indent' ) }
601
+ hasValue={ hasTextIndent }
602
+ onDeselect={ resetTextIndent }
603
+ isShownByDefault={ defaultControls.textIndent }
604
+ panelId={ panelId }
605
+ >
606
+ <TextIndentControl
607
+ value={ textIndent }
608
+ onChange={ setTextIndentValue }
609
+ size="__unstable-large"
610
+ __unstableInputWidth="auto"
611
+ withSlider
612
+ hasBottomMargin={ isGlobalStyles }
613
+ />
614
+ { isGlobalStyles && (
615
+ <ToggleControl
616
+ label={ __( 'Indent all paragraphs' ) }
617
+ checked={ isTextIndentAll }
618
+ onChange={ onToggleTextIndentAll }
619
+ help={ textIndentHelp }
620
+ />
621
+ ) }
622
+ </ToolsPanelItem>
623
+ ) }
547
624
  { hasTextColumnsControl && (
548
625
  <ToolsPanelItem
549
626
  className="single-column"
@@ -15,13 +15,18 @@ import { __experimentalUseDropZone as useDropZone } from '@wordpress/compose';
15
15
  */
16
16
  import { useBlockElement } from '../block-list/use-block-props/use-block-refs';
17
17
  import BlockPopoverCover from '../block-popover/cover';
18
- import { range, GridRect, getGridInfo } from './utils';
18
+ import { range, GridRect, getGridInfo, getGridItemRect } from './utils';
19
19
  import { store as blockEditorStore } from '../../store';
20
20
  import { useGetNumberOfBlocksBeforeCell } from './use-get-number-of-blocks-before-cell';
21
21
  import ButtonBlockAppender from '../button-block-appender';
22
22
  import { unlock } from '../../lock-unlock';
23
23
 
24
- export function GridVisualizer( { clientId, contentRef, parentLayout } ) {
24
+ export function GridVisualizer( {
25
+ clientId,
26
+ contentRef,
27
+ parentLayout,
28
+ childGridClientId,
29
+ } ) {
25
30
  const isDistractionFree = useSelect(
26
31
  ( select ) =>
27
32
  select( blockEditorStore ).getSettings().isDistractionFree,
@@ -42,17 +47,31 @@ export function GridVisualizer( { clientId, contentRef, parentLayout } ) {
42
47
  gridElement={ gridElement }
43
48
  isManualGrid={ isManualGrid }
44
49
  ref={ contentRef }
50
+ childGridClientId={ childGridClientId }
45
51
  />
46
52
  );
47
53
  }
48
54
 
49
55
  const GridVisualizerGrid = forwardRef(
50
- ( { gridClientId, gridElement, isManualGrid }, ref ) => {
56
+ ( { gridClientId, gridElement, isManualGrid, childGridClientId }, ref ) => {
51
57
  const [ gridInfo, setGridInfo ] = useState( () =>
52
58
  getGridInfo( gridElement )
53
59
  );
54
60
  const [ isDroppingAllowed, setIsDroppingAllowed ] = useState( false );
55
61
 
62
+ // Get the element for the child grid block so we can
63
+ // compute its position and hide overlapping visualizer cells.
64
+ const childGridElement = useBlockElement( childGridClientId );
65
+
66
+ // Compute the child grid block's rect from its position in the grid.
67
+ // This works for both manual and non-manual grids.
68
+ const childGridRect = useMemo( () => {
69
+ if ( ! childGridElement ) {
70
+ return null;
71
+ }
72
+ return getGridItemRect( childGridElement );
73
+ }, [ childGridElement ] );
74
+
56
75
  useEffect( () => {
57
76
  const resizeCallback = () =>
58
77
  setGridInfo( getGridInfo( gridElement ) );
@@ -101,14 +120,13 @@ const GridVisualizerGrid = forwardRef(
101
120
  <ManualGridVisualizer
102
121
  gridClientId={ gridClientId }
103
122
  gridInfo={ gridInfo }
123
+ childGridRect={ childGridRect }
104
124
  />
105
125
  ) : (
106
- Array.from( { length: gridInfo.numItems }, ( _, i ) => (
107
- <GridVisualizerCell
108
- key={ i }
109
- color={ gridInfo.currentColor }
110
- />
111
- ) )
126
+ <AutoGridVisualizer
127
+ gridInfo={ gridInfo }
128
+ childGridRect={ childGridRect }
129
+ />
112
130
  ) }
113
131
  </div>
114
132
  </BlockPopoverCover>
@@ -116,7 +134,27 @@ const GridVisualizerGrid = forwardRef(
116
134
  }
117
135
  );
118
136
 
119
- function ManualGridVisualizer( { gridClientId, gridInfo } ) {
137
+ function AutoGridVisualizer( { gridInfo, childGridRect } ) {
138
+ return range( 1, gridInfo.numRows ).map( ( row ) =>
139
+ range( 1, gridInfo.numColumns ).map( ( column ) => {
140
+ // Don't render visualizer cells for a selected child block
141
+ // that is itself a grid, so that only the child's grid
142
+ // visualizer is visible.
143
+ let color = gridInfo.currentColor;
144
+ if ( childGridRect?.contains( column, row ) ) {
145
+ color = 'transparent';
146
+ }
147
+ return (
148
+ <GridVisualizerCell
149
+ key={ `${ row }-${ column }` }
150
+ color={ color }
151
+ />
152
+ );
153
+ } )
154
+ );
155
+ }
156
+
157
+ function ManualGridVisualizer( { gridClientId, gridInfo, childGridRect } ) {
120
158
  const [ highlightedRect, setHighlightedRect ] = useState( null );
121
159
 
122
160
  const gridItemStyles = useSelect(
@@ -155,6 +193,14 @@ function ManualGridVisualizer( { gridClientId, gridInfo } ) {
155
193
 
156
194
  return range( 1, gridInfo.numRows ).map( ( row ) =>
157
195
  range( 1, gridInfo.numColumns ).map( ( column ) => {
196
+ // Don't render visualizer cells for a selected child block
197
+ // that is itself a grid, so that only the child's grid
198
+ // visualizer is visible.
199
+ const isChildGridCell = childGridRect?.contains( column, row );
200
+ let color = gridInfo.currentColor;
201
+ if ( isChildGridCell ) {
202
+ color = 'transparent';
203
+ }
158
204
  const isCellOccupied = occupiedRects.some( ( rect ) =>
159
205
  rect.contains( column, row )
160
206
  );
@@ -163,10 +209,10 @@ function ManualGridVisualizer( { gridClientId, gridInfo } ) {
163
209
  return (
164
210
  <GridVisualizerCell
165
211
  key={ `${ row }-${ column }` }
166
- color={ gridInfo.currentColor }
212
+ color={ color }
167
213
  className={ isHighlighted && 'is-highlighted' }
168
214
  >
169
- { isCellOccupied ? (
215
+ { isCellOccupied && ! isChildGridCell ? (
170
216
  <GridVisualizerDropZone
171
217
  column={ column }
172
218
  row={ row }
@@ -14,7 +14,12 @@ import {
14
14
  useEffect,
15
15
  } from '@wordpress/element';
16
16
  import { __ } from '@wordpress/i18n';
17
- import { useMergeRefs, useRefEffect, useDisabled } from '@wordpress/compose';
17
+ import {
18
+ useMergeRefs,
19
+ useRefEffect,
20
+ useDisabled,
21
+ useViewportMatch,
22
+ } from '@wordpress/compose';
18
23
  import { __experimentalStyleProvider as StyleProvider } from '@wordpress/components';
19
24
  import { useSelect } from '@wordpress/data';
20
25
 
@@ -26,6 +31,8 @@ import { getCompatibilityStyles } from './get-compatibility-styles';
26
31
  import { useScaleCanvas } from './use-scale-canvas';
27
32
  import { store as blockEditorStore } from '../../store';
28
33
 
34
+ const ViewportWidthProvider = useViewportMatch.__experimentalWidthProvider;
35
+
29
36
  function bubbleEvent( event, Constructor, frame ) {
30
37
  const init = {};
31
38
 
@@ -227,6 +234,7 @@ function Iframe( {
227
234
  const {
228
235
  contentResizeListener,
229
236
  containerResizeListener,
237
+ containerWidth,
230
238
  isZoomedOut,
231
239
  scaleContainerWidth,
232
240
  } = useScaleCanvas( {
@@ -347,7 +355,9 @@ function Iframe( {
347
355
  >
348
356
  { contentResizeListener }
349
357
  <StyleProvider document={ iframeDocument }>
350
- { children }
358
+ <ViewportWidthProvider value={ containerWidth }>
359
+ { children }
360
+ </ViewportWidthProvider>
351
361
  </StyleProvider>
352
362
  </body>,
353
363
  iframeDocument.documentElement
@@ -484,6 +484,7 @@ export function useScaleCanvas( {
484
484
  return {
485
485
  isZoomedOut,
486
486
  scaleContainerWidth,
487
+ containerWidth,
487
488
  contentResizeListener,
488
489
  containerResizeListener,
489
490
  };
@@ -129,6 +129,7 @@ function InserterMenu( {
129
129
  insertionIndex,
130
130
  destinationRootClientId,
131
131
  true,
132
+ 0,
132
133
  { source: 'inserter_menu' }
133
134
  );
134
135
  },