diff --git a/packages/customize-widgets/src/components/keyboard-shortcut-help-modal/config.js b/packages/customize-widgets/src/components/keyboard-shortcut-help-modal/config.js index 9e8e323a7c70fc..bd8bb53f5453d3 100644 --- a/packages/customize-widgets/src/components/keyboard-shortcut-help-modal/config.js +++ b/packages/customize-widgets/src/components/keyboard-shortcut-help-modal/config.js @@ -36,4 +36,14 @@ export const textFormattingShortcuts = [ keyCombination: { modifier: 'access', character: 'x' }, description: __( 'Make the selected text inline code.' ), }, + { + keyCombination: { modifier: 'access', character: '0' }, + description: __( 'Convert the current heading to a paragraph.' ), + }, + { + keyCombination: { modifier: 'access', character: '1-6' }, + description: __( + 'Convert the current paragraph or heading to a heading of level 1 to 6.' + ), + }, ]; diff --git a/packages/customize-widgets/src/components/keyboard-shortcuts/index.js b/packages/customize-widgets/src/components/keyboard-shortcuts/index.js index 5c5f3ea2bca832..a83b730ddbb701 100644 --- a/packages/customize-widgets/src/components/keyboard-shortcuts/index.js +++ b/packages/customize-widgets/src/components/keyboard-shortcuts/index.js @@ -7,10 +7,44 @@ import { store as keyboardShortcutsStore, } from '@wordpress/keyboard-shortcuts'; import { isAppleOS } from '@wordpress/keycodes'; -import { useDispatch } from '@wordpress/data'; +import { useDispatch, useSelect } from '@wordpress/data'; import { __ } from '@wordpress/i18n'; +import { store as blockEditorStore } from '@wordpress/block-editor'; +import { createBlock } from '@wordpress/blocks'; function KeyboardShortcuts( { undo, redo, save } ) { + const { replaceBlocks } = useDispatch( blockEditorStore ); + const { getBlockName, getSelectedBlockClientId, getBlockAttributes } = + useSelect( blockEditorStore ); + + const handleTextLevelShortcut = ( event, level ) => { + event.preventDefault(); + const destinationBlockName = + level === 0 ? 'core/paragraph' : 'core/heading'; + const currentClientId = getSelectedBlockClientId(); + if ( currentClientId === null ) { + return; + } + const blockName = getBlockName( currentClientId ); + if ( blockName !== 'core/paragraph' && blockName !== 'core/heading' ) { + return; + } + const attributes = getBlockAttributes( currentClientId ); + const textAlign = + blockName === 'core/paragraph' ? 'align' : 'textAlign'; + const destinationTextAlign = + destinationBlockName === 'core/paragraph' ? 'align' : 'textAlign'; + + replaceBlocks( + currentClientId, + createBlock( destinationBlockName, { + level, + content: attributes.content, + ...{ [ destinationTextAlign ]: attributes[ textAlign ] }, + } ) + ); + }; + useShortcut( 'core/customize-widgets/undo', ( event ) => { undo(); event.preventDefault(); @@ -26,6 +60,21 @@ function KeyboardShortcuts( { undo, redo, save } ) { save(); } ); + useShortcut( + 'core/customize-widgets/transform-heading-to-paragraph', + ( event ) => handleTextLevelShortcut( event, 0 ) + ); + + [ 1, 2, 3, 4, 5, 6 ].forEach( ( level ) => { + //the loop is based off on a constant therefore + //the hook will execute the same way every time + //eslint-disable-next-line react-hooks/rules-of-hooks + useShortcut( + `core/customize-widgets/transform-paragraph-to-heading-${ level }`, + ( event ) => handleTextLevelShortcut( event, level ) + ); + } ); + return null; } @@ -77,6 +126,28 @@ function KeyboardShortcutsRegister() { }, } ); + registerShortcut( { + name: `core/customize-widgets/transform-heading-to-paragraph`, + category: 'block-library', + description: __( 'Transform heading to paragraph.' ), + keyCombination: { + modifier: 'access', + character: `0`, + }, + } ); + + [ 1, 2, 3, 4, 5, 6 ].forEach( ( level ) => { + registerShortcut( { + name: `core/customize-widgets/transform-paragraph-to-heading-${ level }`, + category: 'block-library', + description: __( 'Transform paragraph to heading.' ), + keyCombination: { + modifier: 'access', + character: `${ level }`, + }, + } ); + } ); + return () => { unregisterShortcut( 'core/customize-widgets/undo' ); unregisterShortcut( 'core/customize-widgets/redo' ); diff --git a/packages/edit-post/src/components/keyboard-shortcuts/index.js b/packages/edit-post/src/components/keyboard-shortcuts/index.js index f3a8c15addb5b4..13a989de7fd076 100644 --- a/packages/edit-post/src/components/keyboard-shortcuts/index.js +++ b/packages/edit-post/src/components/keyboard-shortcuts/index.js @@ -70,15 +70,18 @@ function KeyboardShortcuts() { if ( blockName !== 'core/paragraph' && blockName !== 'core/heading' ) { return; } - const currentAttributes = getBlockAttributes( currentClientId ); - const { content: currentContent, align: currentAlign } = - currentAttributes; + const attributes = getBlockAttributes( currentClientId ); + const textAlign = + blockName === 'core/paragraph' ? 'align' : 'textAlign'; + const destinationTextAlign = + destinationBlockName === 'core/paragraph' ? 'align' : 'textAlign'; + replaceBlocks( currentClientId, createBlock( destinationBlockName, { level, - content: currentContent, - align: currentAlign, + content: attributes.content, + ...{ [ destinationTextAlign ]: attributes[ textAlign ] }, } ) ); }; @@ -181,7 +184,7 @@ function KeyboardShortcuts() { } ); registerShortcut( { - name: `core/block-editor/transform-heading-to-paragraph`, + name: `core/edit-post/transform-heading-to-paragraph`, category: 'block-library', description: __( 'Transform heading to paragraph.' ), keyCombination: { @@ -192,7 +195,7 @@ function KeyboardShortcuts() { [ 1, 2, 3, 4, 5, 6 ].forEach( ( level ) => { registerShortcut( { - name: `core/block-editor/transform-paragraph-to-heading-${ level }`, + name: `core/edit-post/transform-paragraph-to-heading-${ level }`, category: 'block-library', description: __( 'Transform paragraph to heading.' ), keyCombination: { @@ -257,9 +260,8 @@ function KeyboardShortcuts() { } } ); - useShortcut( - 'core/block-editor/transform-heading-to-paragraph', - ( event ) => handleTextLevelShortcut( event, 0 ) + useShortcut( 'core/edit-post/transform-heading-to-paragraph', ( event ) => + handleTextLevelShortcut( event, 0 ) ); [ 1, 2, 3, 4, 5, 6 ].forEach( ( level ) => { @@ -267,7 +269,7 @@ function KeyboardShortcuts() { //the hook will execute the same way every time //eslint-disable-next-line react-hooks/rules-of-hooks useShortcut( - `core/block-editor/transform-paragraph-to-heading-${ level }`, + `core/edit-post/transform-paragraph-to-heading-${ level }`, ( event ) => handleTextLevelShortcut( event, level ) ); } ); diff --git a/packages/edit-site/src/components/keyboard-shortcut-help-modal/config.js b/packages/edit-site/src/components/keyboard-shortcut-help-modal/config.js index 9e8e323a7c70fc..bd8bb53f5453d3 100644 --- a/packages/edit-site/src/components/keyboard-shortcut-help-modal/config.js +++ b/packages/edit-site/src/components/keyboard-shortcut-help-modal/config.js @@ -36,4 +36,14 @@ export const textFormattingShortcuts = [ keyCombination: { modifier: 'access', character: 'x' }, description: __( 'Make the selected text inline code.' ), }, + { + keyCombination: { modifier: 'access', character: '0' }, + description: __( 'Convert the current heading to a paragraph.' ), + }, + { + keyCombination: { modifier: 'access', character: '1-6' }, + description: __( + 'Convert the current paragraph or heading to a heading of level 1 to 6.' + ), + }, ]; diff --git a/packages/edit-site/src/components/keyboard-shortcuts/index.js b/packages/edit-site/src/components/keyboard-shortcuts/index.js index 8d4337dd097c51..dc246a013f5bd4 100644 --- a/packages/edit-site/src/components/keyboard-shortcuts/index.js +++ b/packages/edit-site/src/components/keyboard-shortcuts/index.js @@ -10,7 +10,9 @@ import { isAppleOS } from '@wordpress/keycodes'; import { useDispatch, useSelect } from '@wordpress/data'; import { __ } from '@wordpress/i18n'; import { store as coreStore } from '@wordpress/core-data'; +import { store as blockEditorStore } from '@wordpress/block-editor'; import { store as interfaceStore } from '@wordpress/interface'; +import { createBlock } from '@wordpress/blocks'; /** * Internal dependencies @@ -41,6 +43,38 @@ function KeyboardShortcuts() { useDispatch( interfaceStore ); const { setIsSaveViewOpened } = useDispatch( editSiteStore ); + const { replaceBlocks } = useDispatch( blockEditorStore ); + const { getBlockName, getSelectedBlockClientId, getBlockAttributes } = + useSelect( blockEditorStore ); + + const handleTextLevelShortcut = ( event, level ) => { + event.preventDefault(); + const destinationBlockName = + level === 0 ? 'core/paragraph' : 'core/heading'; + const currentClientId = getSelectedBlockClientId(); + if ( currentClientId === null ) { + return; + } + const blockName = getBlockName( currentClientId ); + if ( blockName !== 'core/paragraph' && blockName !== 'core/heading' ) { + return; + } + const attributes = getBlockAttributes( currentClientId ); + const textAlign = + blockName === 'core/paragraph' ? 'align' : 'textAlign'; + const destinationTextAlign = + destinationBlockName === 'core/paragraph' ? 'align' : 'textAlign'; + + replaceBlocks( + currentClientId, + createBlock( destinationBlockName, { + level, + content: attributes.content, + ...{ [ destinationTextAlign ]: attributes[ textAlign ] }, + } ) + ); + }; + useShortcut( 'core/edit-site/save', ( event ) => { event.preventDefault(); @@ -85,6 +119,20 @@ function KeyboardShortcuts() { switchEditorMode( getEditorMode() === 'visual' ? 'text' : 'visual' ); } ); + useShortcut( 'core/edit-site/transform-heading-to-paragraph', ( event ) => + handleTextLevelShortcut( event, 0 ) + ); + + [ 1, 2, 3, 4, 5, 6 ].forEach( ( level ) => { + //the loop is based off on a constant therefore + //the hook will execute the same way every time + //eslint-disable-next-line react-hooks/rules-of-hooks + useShortcut( + `core/edit-site/transform-paragraph-to-heading-${ level }`, + ( event ) => handleTextLevelShortcut( event, level ) + ); + } ); + return null; } @@ -208,6 +256,28 @@ function KeyboardShortcutsRegister() { character: 'm', }, } ); + + registerShortcut( { + name: `core/edit-site/transform-heading-to-paragraph`, + category: 'block-library', + description: __( 'Transform heading to paragraph.' ), + keyCombination: { + modifier: 'access', + character: `0`, + }, + } ); + + [ 1, 2, 3, 4, 5, 6 ].forEach( ( level ) => { + registerShortcut( { + name: `core/edit-site/transform-paragraph-to-heading-${ level }`, + category: 'block-library', + description: __( 'Transform paragraph to heading.' ), + keyCombination: { + modifier: 'access', + character: `${ level }`, + }, + } ); + } ); }, [ registerShortcut ] ); return null; diff --git a/packages/edit-widgets/src/components/keyboard-shortcut-help-modal/config.js b/packages/edit-widgets/src/components/keyboard-shortcut-help-modal/config.js index 9e8e323a7c70fc..bd8bb53f5453d3 100644 --- a/packages/edit-widgets/src/components/keyboard-shortcut-help-modal/config.js +++ b/packages/edit-widgets/src/components/keyboard-shortcut-help-modal/config.js @@ -36,4 +36,14 @@ export const textFormattingShortcuts = [ keyCombination: { modifier: 'access', character: 'x' }, description: __( 'Make the selected text inline code.' ), }, + { + keyCombination: { modifier: 'access', character: '0' }, + description: __( 'Convert the current heading to a paragraph.' ), + }, + { + keyCombination: { modifier: 'access', character: '1-6' }, + description: __( + 'Convert the current paragraph or heading to a heading of level 1 to 6.' + ), + }, ]; diff --git a/packages/edit-widgets/src/components/keyboard-shortcuts/index.js b/packages/edit-widgets/src/components/keyboard-shortcuts/index.js index 65ecceeb4aa8c1..7add778094c524 100644 --- a/packages/edit-widgets/src/components/keyboard-shortcuts/index.js +++ b/packages/edit-widgets/src/components/keyboard-shortcuts/index.js @@ -7,9 +7,11 @@ import { store as keyboardShortcutsStore, } from '@wordpress/keyboard-shortcuts'; import { isAppleOS } from '@wordpress/keycodes'; -import { useDispatch } from '@wordpress/data'; +import { useDispatch, useSelect } from '@wordpress/data'; import { __ } from '@wordpress/i18n'; import { store as coreStore } from '@wordpress/core-data'; +import { store as blockEditorStore } from '@wordpress/block-editor'; +import { createBlock } from '@wordpress/blocks'; /** * Internal dependencies @@ -20,6 +22,38 @@ function KeyboardShortcuts() { const { redo, undo } = useDispatch( coreStore ); const { saveEditedWidgetAreas } = useDispatch( editWidgetsStore ); + const { replaceBlocks } = useDispatch( blockEditorStore ); + const { getBlockName, getSelectedBlockClientId, getBlockAttributes } = + useSelect( blockEditorStore ); + + const handleTextLevelShortcut = ( event, level ) => { + event.preventDefault(); + const destinationBlockName = + level === 0 ? 'core/paragraph' : 'core/heading'; + const currentClientId = getSelectedBlockClientId(); + if ( currentClientId === null ) { + return; + } + const blockName = getBlockName( currentClientId ); + if ( blockName !== 'core/paragraph' && blockName !== 'core/heading' ) { + return; + } + const attributes = getBlockAttributes( currentClientId ); + const textAlign = + blockName === 'core/paragraph' ? 'align' : 'textAlign'; + const destinationTextAlign = + destinationBlockName === 'core/paragraph' ? 'align' : 'textAlign'; + + replaceBlocks( + currentClientId, + createBlock( destinationBlockName, { + level, + content: attributes.content, + ...{ [ destinationTextAlign ]: attributes[ textAlign ] }, + } ) + ); + }; + useShortcut( 'core/edit-widgets/undo', ( event ) => { undo(); event.preventDefault(); @@ -35,6 +69,21 @@ function KeyboardShortcuts() { saveEditedWidgetAreas(); } ); + useShortcut( + 'core/edit-widgets//transform-heading-to-paragraph', + ( event ) => handleTextLevelShortcut( event, 0 ) + ); + + [ 1, 2, 3, 4, 5, 6 ].forEach( ( level ) => { + //the loop is based off on a constant therefore + //the hook will execute the same way every time + //eslint-disable-next-line react-hooks/rules-of-hooks + useShortcut( + `core/edit-widgets//transform-paragraph-to-heading-${ level }`, + ( event ) => handleTextLevelShortcut( event, level ) + ); + } ); + return null; } @@ -129,6 +178,28 @@ function KeyboardShortcutsRegister() { }, ], } ); + + registerShortcut( { + name: `core/edit-widgets//transform-heading-to-paragraph`, + category: 'block-library', + description: __( 'Transform heading to paragraph.' ), + keyCombination: { + modifier: 'access', + character: `0`, + }, + } ); + + [ 1, 2, 3, 4, 5, 6 ].forEach( ( level ) => { + registerShortcut( { + name: `core/edit-widgets//transform-paragraph-to-heading-${ level }`, + category: 'block-library', + description: __( 'Transform paragraph to heading.' ), + keyCombination: { + modifier: 'access', + character: `${ level }`, + }, + } ); + } ); }, [ registerShortcut ] ); return null; diff --git a/test/e2e/specs/editor/blocks/heading.spec.js b/test/e2e/specs/editor/blocks/heading.spec.js index 56154f3e668e07..9d6e0db52f816a 100644 --- a/test/e2e/specs/editor/blocks/heading.spec.js +++ b/test/e2e/specs/editor/blocks/heading.spec.js @@ -179,4 +179,98 @@ test.describe( 'Heading', () => { }, ] ); } ); + + test( 'should change heading level with keyboard shortcuts', async ( { + editor, + page, + pageUtils, + } ) => { + await page.click( 'role=button[name="Add default block"i]' ); + await page.keyboard.type( '## Heading' ); + + // Change text alignment + await editor.clickBlockToolbarButton( 'Align text' ); + const textAlignButton = page.locator( + 'role=menuitemradio[name="Align text center"i]' + ); + await textAlignButton.click(); + + // Focus the block content + await page.keyboard.press( 'Tab' ); + + await pageUtils.pressKeyWithModifier( 'access', '4' ); + await expect.poll( editor.getBlocks ).toMatchObject( [ + { + name: 'core/heading', + attributes: { + content: 'Heading', + textAlign: 'center', + level: 4, + }, + }, + ] ); + } ); + + test( 'should be converted from a paragraph to a heading with keyboard shortcuts', async ( { + editor, + page, + pageUtils, + } ) => { + await page.click( 'role=button[name="Add default block"i]' ); + await page.keyboard.type( 'Paragraph' ); + + // Change text alignment + await editor.clickBlockToolbarButton( 'Align text' ); + const textAlignButton = page.locator( + 'role=menuitemradio[name="Align text center"i]' + ); + await textAlignButton.click(); + + // Focus the block content + await page.keyboard.press( 'Tab' ); + + await pageUtils.pressKeyWithModifier( 'access', '2' ); + await expect.poll( editor.getBlocks ).toMatchObject( [ + { + name: 'core/heading', + attributes: { + content: 'Paragraph', + textAlign: 'center', + level: 2, + }, + }, + ] ); + } ); + + test( 'should be converted from a heading to a paragraph with keyboard shortcuts', async ( { + editor, + page, + pageUtils, + } ) => { + await page.click( 'role=button[name="Add default block"i]' ); + await page.keyboard.type( '## Heading' ); + + // Change text alignment + await editor.clickBlockToolbarButton( 'Align text' ); + const textAlignButton = page.locator( + 'role=menuitemradio[name="Align text center"i]' + ); + + await textAlignButton.click(); + + // Focus the block content + await page.keyboard.press( 'Tab' ); + + await pageUtils.pressKeyWithModifier( 'access', '0' ); + + await expect.poll( editor.getBlocks ).toMatchObject( [ + { + name: 'core/paragraph', + attributes: { + content: 'Heading', + align: 'center', + }, + }, + ] ); + } ); } );