Skip to content

Commit dd8c6af

Browse files
authored
Add Heading level variations (#73823)
Adds h1-h6 as block variations for the heading block - Heading level shown in sidebar inspector variation picker - Level-specific icons in the sidebar picker - Proper variation detection via isActive callbacks The variations use scope ['block', 'transform'] to appear in the sidebar picker and transforms, while keeping the inserter clean with just the base "Heading" block. Extends the inserter to include block-scope variations in search results, enabling slash commands like `/h3` to find variations that appear in the sidebar variation picker.
1 parent b91397c commit dd8c6af

File tree

16 files changed

+120
-52
lines changed

16 files changed

+120
-52
lines changed

packages/block-editor/src/components/block-variation-transforms/index.js

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -229,16 +229,13 @@ function __experimentalBlockVariationTransforms( { blockClientId } ) {
229229
( v ) => v.name !== 'stretchy-paragraph'
230230
);
231231
} else if ( blockName === 'core/heading' ) {
232-
if (
233-
activeBlockVariation?.name === 'stretchy-heading' ||
234-
unfilteredVariations.every( ( v ) =>
235-
[ 'heading', 'stretchy-heading' ].includes( v.name )
236-
)
237-
) {
232+
// Hide variations picker when stretchy-heading is active.
233+
if ( activeBlockVariation?.name === 'stretchy-heading' ) {
238234
return [];
239235
}
236+
// Filter out stretchy-heading.
240237
return unfilteredVariations.filter(
241-
( v ) => v.name !== 'stretchy-heading'
238+
( variation ) => variation.name !== 'stretchy-heading'
242239
);
243240
}
244241
return unfilteredVariations;

packages/block-editor/src/components/block-variation-transforms/style.scss

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,10 @@
66
padding: 0 $grid-unit-20 $grid-unit-20 52px;
77

88
&:where(fieldset) {
9-
// Reset `fieldset` browser defaults.
9+
// Reset `fieldset` browser defaults, keeping intentional padding.
1010
border: 0;
11-
padding: 0;
1211
margin: 0;
12+
min-inline-size: 0;
1313
}
1414
}
1515

packages/block-editor/src/components/inserter/block-types-tab.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,11 @@ export function BlockTypesTab(
186186
continue;
187187
}
188188

189+
// Skip search-only items from browse view (they're still searchable).
190+
if ( item.isSearchOnly ) {
191+
continue;
192+
}
193+
189194
if ( item.isAllowedInCurrentRoot ) {
190195
itemsForCurrentRoot.push( item );
191196
} else {

packages/block-editor/src/store/selectors.js

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2078,6 +2078,8 @@ const getItemFromVariation = ( state, item ) => ( variation ) => {
20782078
innerBlocks: variation.innerBlocks,
20792079
keywords: variation.keywords || item.keywords,
20802080
frecency: calculateFrecency( time, count ),
2081+
// Pass through search-only flag for block-scope variations.
2082+
isSearchOnly: variation.isSearchOnly,
20812083
};
20822084
};
20832085

@@ -2151,6 +2153,27 @@ const buildBlockTypeItem =
21512153
blockType.name,
21522154
'inserter'
21532155
);
2156+
const blockVariations = getBlockVariations( blockType.name, 'block' );
2157+
// Combine inserter and block variations. Block-scope variations without
2158+
// inserter scope are searchable via slash commands but hidden from browse.
2159+
const inserterVariationNames = new Set(
2160+
inserterVariations.map( ( variation ) => variation.name )
2161+
);
2162+
const allVariations = [
2163+
...inserterVariations,
2164+
...blockVariations
2165+
.filter(
2166+
( variation ) =>
2167+
! inserterVariationNames.has( variation.name )
2168+
)
2169+
.map( ( variation ) => ( {
2170+
...variation,
2171+
isSearchOnly: true,
2172+
// Block-scope `isDefault` is for the placeholder picker,
2173+
// not for the inserter, so don't carry it over.
2174+
isDefault: false,
2175+
} ) ),
2176+
];
21542177
return {
21552178
...blockItemBase,
21562179
initialAttributes: {},
@@ -2159,7 +2182,7 @@ const buildBlockTypeItem =
21592182
keywords: blockType.keywords,
21602183
parent: blockType.parent,
21612184
ancestor: blockType.ancestor,
2162-
variations: inserterVariations,
2185+
variations: allVariations,
21632186
example: blockType.example,
21642187
utility: 1, // Deprecated.
21652188
};

packages/block-library/src/heading/edit.js

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ import {
1515
RichText,
1616
useBlockProps,
1717
store as blockEditorStore,
18-
HeadingLevelDropdown,
1918
useBlockEditingMode,
2019
} from '@wordpress/block-editor';
2120

@@ -32,8 +31,7 @@ function HeadingEdit( {
3231
style,
3332
clientId,
3433
} ) {
35-
const { textAlign, content, level, levelOptions, placeholder, anchor } =
36-
attributes;
34+
const { textAlign, content, level, placeholder, anchor } = attributes;
3735
const tagName = 'h' + level;
3836
const blockProps = useBlockProps( {
3937
className: clsx( {
@@ -94,13 +92,6 @@ function HeadingEdit( {
9492
<>
9593
{ blockEditingMode === 'default' && (
9694
<BlockControls group="block">
97-
<HeadingLevelDropdown
98-
value={ level }
99-
options={ levelOptions }
100-
onChange={ ( newLevel ) =>
101-
setAttributes( { level: newLevel } )
102-
}
103-
/>
10495
<AlignmentControl
10596
value={ textAlign }
10697
onChange={ ( nextAlign ) => {

packages/block-library/src/heading/index.js

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,11 @@
33
*/
44
import { heading as icon } from '@wordpress/icons';
55
import { __, sprintf } from '@wordpress/i18n';
6-
import { privateApis as blocksPrivateApis } from '@wordpress/blocks';
6+
import {
7+
privateApis as blocksPrivateApis,
8+
getBlockType,
9+
unregisterBlockVariation,
10+
} from '@wordpress/blocks';
711

812
/**
913
* Internal dependencies
@@ -86,4 +90,21 @@ if ( window.__experimentalContentOnlyInspectorFields ) {
8690
};
8791
}
8892

89-
export const init = () => initBlock( { name, metadata, settings } );
93+
export const init = () => {
94+
const block = initBlock( { name, metadata, settings } );
95+
96+
// Unregister heading level variations based on `levelOptions` attribute.
97+
// This is for backwards compatibility, as extenders can now unregister the
98+
// variation directly: `wp.blocks.unregisterBlockVariation( 'core/heading', 'h1' )`.
99+
const levelOptions =
100+
getBlockType( name )?.attributes?.levelOptions?.default;
101+
if ( levelOptions ) {
102+
[ 1, 2, 3, 4, 5, 6 ].forEach( ( level ) => {
103+
if ( ! levelOptions.includes( level ) ) {
104+
unregisterBlockVariation( name, `h${ level }` );
105+
}
106+
} );
107+
}
108+
109+
return block;
110+
};

packages/block-library/src/heading/variations.js

Lines changed: 32 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,44 @@
11
/**
22
* WordPress dependencies
33
*/
4-
import { __ } from '@wordpress/i18n';
4+
import { __, sprintf } from '@wordpress/i18n';
55
import { Path, SVG } from '@wordpress/primitives';
6-
import { heading } from '@wordpress/icons';
6+
import {
7+
headingLevel1,
8+
headingLevel2,
9+
headingLevel3,
10+
headingLevel4,
11+
headingLevel5,
12+
headingLevel6,
13+
} from '@wordpress/icons';
14+
15+
const LEVEL_ICONS = [
16+
headingLevel1,
17+
headingLevel2,
18+
headingLevel3,
19+
headingLevel4,
20+
headingLevel5,
21+
headingLevel6,
22+
];
723

824
const variations = [
9-
{
10-
name: 'heading',
11-
title: __( 'Heading' ),
25+
...[ 1, 2, 3, 4, 5, 6 ].map( ( level ) => ( {
26+
name: `h${ level }`,
27+
title: sprintf(
28+
/* translators: %d: heading level e.g: "1", "2", "3" */
29+
__( 'Heading %d' ),
30+
level
31+
),
1232
description: __(
1333
'Introduce new sections and organize content to help visitors (and search engines) understand the structure of your content.'
1434
),
15-
isDefault: true,
16-
scope: [ 'inserter', 'transform' ],
17-
attributes: { fitText: undefined },
18-
icon: heading,
19-
},
35+
icon: LEVEL_ICONS[ level - 1 ],
36+
attributes: { level },
37+
scope: [ 'block', 'transform' ],
38+
keywords: [ `h${ level }` ],
39+
isActive: ( blockAttributes ) =>
40+
! blockAttributes.fitText && blockAttributes.level === level,
41+
} ) ),
2042
// There is a hardcoded workaround in packages/block-editor/src/store/selectors.js
2143
// to make Stretchy variations appear as the last of their sections in the inserter.
2244
{

test/e2e/specs/editor/blocks/heading.spec.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -365,8 +365,8 @@ test.describe( 'Heading', () => {
365365

366366
await expect(
367367
headingListViewItem,
368-
'should show default block name if the content is empty'
369-
).toHaveText( 'Heading' );
368+
'should show variation name if the content is empty'
369+
).toHaveText( 'Heading 2' );
370370

371371
await editor.canvas
372372
.getByRole( 'document', {

test/e2e/specs/editor/plugins/block-variations.spec.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,9 @@ test.describe( 'Block variations', () => {
112112
.locator( 'role=button[name="Add default block"i]' )
113113
.click();
114114
await page.keyboard.type( '/Columns' );
115-
await page.getByRole( 'option', { name: 'Columns' } ).click();
115+
await page
116+
.getByRole( 'option', { name: 'Columns', exact: true } )
117+
.click();
116118

117119
await editor.canvas
118120
.getByRole( 'list', { name: 'Block variations' } )

test/e2e/specs/editor/various/autocomplete-and-mentions.spec.js

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -566,13 +566,11 @@ test.describe( 'Autocomplete (@firefox, @webkit)', () => {
566566

567567
await page.keyboard.type( 'heading' );
568568
await expect(
569-
page.locator( `role=option[name="Heading"i]` )
569+
page.getByRole( 'option', { name: 'Heading', exact: true } )
570570
).toBeVisible();
571571
// Get the assertive live region screen reader announcement.
572572
await expect(
573-
page.getByText(
574-
'3 results found, use up and down arrow keys to navigate.'
575-
)
573+
page.getByText( 'use up and down arrow keys to navigate.' )
576574
).toBeVisible();
577575
} );
578576
} );

0 commit comments

Comments
 (0)