diff --git a/lib/block-patterns.php b/lib/block-patterns.php index 56fd179750e960..1f85699a851622 100644 --- a/lib/block-patterns.php +++ b/lib/block-patterns.php @@ -93,7 +93,7 @@ function register_gutenberg_patterns() {
- +
diff --git a/lib/class-wp-theme-json-gutenberg.php b/lib/class-wp-theme-json-gutenberg.php index 292f50f5145965..efe68d31ddd086 100644 --- a/lib/class-wp-theme-json-gutenberg.php +++ b/lib/class-wp-theme-json-gutenberg.php @@ -1393,7 +1393,7 @@ public static function get_from_editor_settings( $settings ) { $theme_settings['settings']['spacing'] = array(); } $theme_settings['settings']['spacing']['units'] = ( true === $settings['enableCustomUnits'] ) ? - array( 'px', 'em', 'rem', 'vh', 'vw' ) : + array( 'px', 'em', 'rem', 'vh', 'vw', '%' ) : $settings['enableCustomUnits']; } diff --git a/lib/experimental-default-theme.json b/lib/experimental-default-theme.json index 72ca42dbaa968d..3ed9c916e9f202 100644 --- a/lib/experimental-default-theme.json +++ b/lib/experimental-default-theme.json @@ -212,7 +212,7 @@ "spacing": { "customMargin": false, "customPadding": false, - "units": [ "px", "em", "rem", "vh", "vw" ] + "units": [ "px", "em", "rem", "vh", "vw", "%" ] }, "border": { "customColor": false, diff --git a/packages/block-editor/src/components/iframe/index.js b/packages/block-editor/src/components/iframe/index.js index d089925b3846aa..f06f140e028491 100644 --- a/packages/block-editor/src/components/iframe/index.js +++ b/packages/block-editor/src/components/iframe/index.js @@ -49,6 +49,19 @@ function styleSheetsCompat( doc ) { return; } + // Generally, ignore inline styles. We add inline styles belonging to a + // stylesheet later, which may or may not match the selectors. + if ( ownerNode.tagName !== 'LINK' ) { + return; + } + + // Don't try to add the reset styles, which were removed as a dependency + // from `edit-blocks` for the iframe since we don't need to reset admin + // styles. + if ( ownerNode.id === 'wp-reset-editor-styles-css' ) { + return; + } + const isMatch = Array.from( cssRules ).find( ( { selectorText } ) => selectorText && @@ -62,9 +75,17 @@ function styleSheetsCompat( doc ) { `Stylesheet ${ ownerNode.id } was not properly added. For blocks, use the block API's style (https://developer.wordpress.org/block-editor/reference-guides/block-api/block-metadata/#style) or editorStyle (https://developer.wordpress.org/block-editor/reference-guides/block-api/block-metadata/#editor-style). For themes, use add_editor_style (https://developer.wordpress.org/block-editor/how-to-guides/themes/theme-support/#editor-styles).`, - ownerNode + ownerNode.outerHTML ); doc.head.appendChild( ownerNode.cloneNode( true ) ); + + // Add inline styles belonging to the stylesheet. + const inlineCssId = ownerNode.id.replace( '-css', '-inline-css' ); + const inlineCssElement = document.getElementById( inlineCssId ); + + if ( inlineCssElement ) { + doc.head.appendChild( inlineCssElement.cloneNode( true ) ); + } } } ); } @@ -232,12 +253,23 @@ function Iframe( { contentRef, children, head, ...props }, ref ) { head = ( <> - { styles.map( ( { tagName, href, id, rel, media }, index ) => { - const TagName = tagName.toLowerCase(); - return ( - - ); - } ) } + { styles.map( + ( { tagName, href, id, rel, media, textContent } ) => { + const TagName = tagName.toLowerCase(); + + if ( TagName === 'style' ) { + return ( + + { textContent } + + ); + } + + return ( + + ); + } + ) } { head } ); diff --git a/packages/block-editor/src/components/rich-text/use-input-rules.js b/packages/block-editor/src/components/rich-text/use-input-rules.js index 8715960ba32f44..7e770d5f55ac90 100644 --- a/packages/block-editor/src/components/rich-text/use-input-rules.js +++ b/packages/block-editor/src/components/rich-text/use-input-rules.js @@ -60,7 +60,7 @@ export function useInputRules( props ) { } function onInput( event ) { - const { inputType } = event; + const { inputType, type } = event; const { value, onChange, @@ -69,7 +69,7 @@ export function useInputRules( props ) { } = propsRef.current; // Only run input rules when inserting text. - if ( inputType !== 'insertText' ) { + if ( inputType !== 'insertText' && type !== 'compositionend' ) { return; } @@ -99,8 +99,10 @@ export function useInputRules( props ) { } element.addEventListener( 'input', onInput ); + element.addEventListener( 'compositionend', onInput ); return () => { element.removeEventListener( 'input', onInput ); + element.removeEventListener( 'compositionend', onInput ); }; }, [] ); } diff --git a/packages/block-editor/src/components/use-setting/index.js b/packages/block-editor/src/components/use-setting/index.js index d302ceb277425f..f82b7e38f0d2ec 100644 --- a/packages/block-editor/src/components/use-setting/index.js +++ b/packages/block-editor/src/components/use-setting/index.js @@ -41,7 +41,7 @@ const deprecatedFlags = { } if ( settings.enableCustomUnits === true ) { - return [ 'px', 'em', 'rem', 'vh', 'vw' ]; + return [ 'px', 'em', 'rem', 'vh', 'vw', '%' ]; } return settings.enableCustomUnits; diff --git a/packages/block-editor/src/components/writing-flow/index.js b/packages/block-editor/src/components/writing-flow/index.js index a421238bd23142..96043a8d931972 100644 --- a/packages/block-editor/src/components/writing-flow/index.js +++ b/packages/block-editor/src/components/writing-flow/index.js @@ -1,9 +1,15 @@ +/** + * External dependencies + */ +import classNames from 'classnames'; + /** * WordPress dependencies */ import { useSelect } from '@wordpress/data'; import { __ } from '@wordpress/i18n'; import { useMergeRefs } from '@wordpress/compose'; +import { forwardRef } from '@wordpress/element'; /** * Internal dependencies @@ -14,14 +20,7 @@ import useArrowNav from './use-arrow-nav'; import useSelectAll from './use-select-all'; import { store as blockEditorStore } from '../../store'; -/** - * Handles selection and navigation across blocks. This component should be - * wrapped around BlockList. - * - * @param {Object} props Component properties. - * @param {WPElement} props.children Children to be rendered. - */ -export default function WritingFlow( { children } ) { +function WritingFlow( { children, ...props }, forwardedRef ) { const [ before, ref, after ] = useTabNav(); const hasMultiSelection = useSelect( ( select ) => select( blockEditorStore ).hasMultiSelection(), @@ -31,14 +30,19 @@ export default function WritingFlow( { children } ) { <> { before }
); } + +/** + * Handles selection and navigation across blocks. This component should be + * wrapped around BlockList. + * + * @param {Object} props Component properties. + * @param {WPElement} props.children Children to be rendered. + */ +export default forwardRef( WritingFlow ); diff --git a/packages/block-editor/src/components/writing-flow/use-select-all.js b/packages/block-editor/src/components/writing-flow/use-select-all.js index f8bb4350bea342..b708d198c2966a 100644 --- a/packages/block-editor/src/components/writing-flow/use-select-all.js +++ b/packages/block-editor/src/components/writing-flow/use-select-all.js @@ -7,9 +7,9 @@ import { first, last } from 'lodash'; * WordPress dependencies */ import { isEntirelySelected } from '@wordpress/dom'; -import { useRef, useCallback } from '@wordpress/element'; import { useSelect, useDispatch } from '@wordpress/data'; -import { useShortcut } from '@wordpress/keyboard-shortcuts'; +import { __unstableUseShortcutEventMatch as useShortcutEventMatch } from '@wordpress/keyboard-shortcuts'; +import { useRefEffect } from '@wordpress/compose'; /** * Internal dependencies @@ -17,54 +17,56 @@ import { useShortcut } from '@wordpress/keyboard-shortcuts'; import { store as blockEditorStore } from '../../store'; export default function useSelectAll() { - const ref = useRef(); const { getBlockOrder, getSelectedBlockClientIds, getBlockRootClientId, } = useSelect( blockEditorStore ); const { multiSelect } = useDispatch( blockEditorStore ); + const isMatch = useShortcutEventMatch(); - const callback = useCallback( ( event ) => { - const selectedClientIds = getSelectedBlockClientIds(); + return useRefEffect( ( node ) => { + function onKeyDown( event ) { + if ( ! isMatch( 'core/block-editor/select-all', event ) ) { + return; + } - if ( ! selectedClientIds.length ) { - return; - } + const selectedClientIds = getSelectedBlockClientIds(); - if ( - selectedClientIds.length === 1 && - ! isEntirelySelected( event.target ) - ) { - return; - } + if ( + selectedClientIds.length === 1 && + ! isEntirelySelected( event.target ) + ) { + return; + } - const [ firstSelectedClientId ] = selectedClientIds; - const rootClientId = getBlockRootClientId( firstSelectedClientId ); - let blockClientIds = getBlockOrder( rootClientId ); + const [ firstSelectedClientId ] = selectedClientIds; + const rootClientId = getBlockRootClientId( firstSelectedClientId ); + let blockClientIds = getBlockOrder( rootClientId ); - // If we have selected all sibling nested blocks, try selecting up a - // level. See: https://github.com/WordPress/gutenberg/pull/31859/ - if ( selectedClientIds.length === blockClientIds.length ) { - blockClientIds = getBlockOrder( - getBlockRootClientId( rootClientId ) - ); - } + // If we have selected all sibling nested blocks, try selecting up a + // level. See: https://github.com/WordPress/gutenberg/pull/31859/ + if ( selectedClientIds.length === blockClientIds.length ) { + blockClientIds = getBlockOrder( + getBlockRootClientId( rootClientId ) + ); + } - const firstClientId = first( blockClientIds ); - const lastClientId = last( blockClientIds ); + const firstClientId = first( blockClientIds ); + const lastClientId = last( blockClientIds ); - if ( firstClientId === lastClientId ) { - return; + if ( firstClientId === lastClientId ) { + return; + } + + multiSelect( firstClientId, lastClientId ); + event.preventDefault(); } - multiSelect( firstClientId, lastClientId ); - event.preventDefault(); - }, [] ); + node.addEventListener( 'keydown', onKeyDown ); - useShortcut( 'core/block-editor/select-all', callback, { - target: ref, + return () => { + node.removeEventListener( 'keydown', onKeyDown ); + }; } ); - - return ref; } diff --git a/packages/block-editor/src/components/writing-flow/use-tab-nav.js b/packages/block-editor/src/components/writing-flow/use-tab-nav.js index 04456706a6bb4e..880b395288be80 100644 --- a/packages/block-editor/src/components/writing-flow/use-tab-nav.js +++ b/packages/block-editor/src/components/writing-flow/use-tab-nav.js @@ -84,6 +84,7 @@ export default function useTabNav() { function onKeyDown( event ) { if ( event.keyCode === ESCAPE && ! hasMultiSelection() ) { event.stopPropagation(); + event.preventDefault(); setNavigationMode( true ); return; } @@ -102,6 +103,13 @@ export default function useTabNav() { const direction = isShift ? 'findPrevious' : 'findNext'; if ( ! hasMultiSelection() && ! getSelectedBlockClientId() ) { + // Preserve the behaviour of entering navigation mode when + // tabbing into the content without a block selection. + // `onFocusCapture` already did this previously, but we need to + // do it again here because after clearing block selection, + // focus land on the writing flow container and pressing Tab + // will no longer send focus through the focus capture element. + if ( event.target === node ) setNavigationMode( true ); return; } diff --git a/packages/block-library/src/post-excerpt/block.json b/packages/block-library/src/post-excerpt/block.json index 9010f9f486235f..a066c4250a88ca 100644 --- a/packages/block-library/src/post-excerpt/block.json +++ b/packages/block-library/src/post-excerpt/block.json @@ -9,10 +9,6 @@ "textAlign": { "type": "string" }, - "wordCount": { - "type": "number", - "default": 55 - }, "moreText": { "type": "string" }, diff --git a/packages/block-library/src/post-excerpt/edit.js b/packages/block-library/src/post-excerpt/edit.js index 0df53eb1879235..4c541f4c391276 100644 --- a/packages/block-library/src/post-excerpt/edit.js +++ b/packages/block-library/src/post-excerpt/edit.js @@ -16,7 +16,7 @@ import { Warning, useBlockProps, } from '@wordpress/block-editor'; -import { PanelBody, RangeControl, ToggleControl } from '@wordpress/components'; +import { PanelBody, ToggleControl, Disabled } from '@wordpress/components'; import { __ } from '@wordpress/i18n'; /** @@ -24,29 +24,8 @@ import { __ } from '@wordpress/i18n'; */ import { useCanEditEntity } from '../utils/hooks'; -function usePostContentExcerpt( wordCount, postId, postType ) { - // Don't destrcuture items from content here, it can be undefined. - const [ , , content ] = useEntityProp( - 'postType', - postType, - 'content', - postId - ); - const renderedPostContent = content?.rendered; - return useMemo( () => { - if ( ! renderedPostContent ) { - return ''; - } - const excerptElement = document.createElement( 'div' ); - excerptElement.innerHTML = renderedPostContent; - const excerpt = - excerptElement.textContent || excerptElement.innerText || ''; - return excerpt.trim().split( ' ', wordCount ).join( ' ' ); - }, [ renderedPostContent, wordCount ] ); -} - export default function PostExcerptEditor( { - attributes: { textAlign, wordCount, moreText, showMoreOnNewLine }, + attributes: { textAlign, moreText, showMoreOnNewLine }, setAttributes, isSelected, context: { postId, postType, queryId }, @@ -59,16 +38,24 @@ export default function PostExcerptEditor( { setExcerpt, { rendered: renderedExcerpt, protected: isProtected } = {}, ] = useEntityProp( 'postType', postType, 'excerpt', postId ); - const postContentExcerpt = usePostContentExcerpt( - wordCount, - postId, - postType - ); const blockProps = useBlockProps( { className: classnames( { [ `has-text-align-${ textAlign }` ]: textAlign, } ), } ); + /** + * When excerpt is editable, strip the html tags from + * rendered excerpt. This will be used if the entity's + * excerpt has been produced from the content. + */ + const strippedRenderedExcerpt = useMemo( () => { + if ( ! renderedExcerpt ) return ''; + const document = new window.DOMParser().parseFromString( + renderedExcerpt, + 'text/html' + ); + return document.body.textContent || document.body.innerText || ''; + }, [ renderedExcerpt ] ); if ( ! postType || ! postId ) { return (
@@ -110,16 +97,17 @@ export default function PostExcerptEditor( { aria-label={ __( 'Post excerpt text' ) } value={ rawExcerpt || - postContentExcerpt || + strippedRenderedExcerpt || ( isSelected ? '' : __( 'No post excerpt found' ) ) } onChange={ setExcerpt } /> ) : ( ( renderedExcerpt && ( - { renderedExcerpt } + + { renderedExcerpt } + ) ) || - postContentExcerpt || __( 'No post excerpt found' ) ); return ( @@ -134,17 +122,6 @@ export default function PostExcerptEditor( { - { ! renderedExcerpt && ( - - setAttributes( { wordCount: newExcerptLength } ) - } - min={ 10 } - max={ 100 } - /> - ) } ' . $attributes['moreText'] . '' : ''; - - $filter_excerpt_length = function() use ( $attributes ) { - return isset( $attributes['wordCount'] ) ? $attributes['wordCount'] : 55; + $more_text = ! empty( $attributes['moreText'] ) ? '' . $attributes['moreText'] . '' : ''; + $filter_excerpt_more = function( $more ) use ( $more_text ) { + return empty( $more_text ) ? $more : ''; }; - add_filter( - 'excerpt_length', - $filter_excerpt_length - ); - + /** + * Some themes might use `excerpt_more` filter to handle the + * `more` link displayed after a trimmed excerpt. Since the + * block has a `more text` attribute we have to check and + * override if needed the return value from this filter. + * So if the block's attribute is not empty override the + * `excerpt_more` filter and return nothing. This will + * result in showing only one `read more` link at a time. + */ + add_filter( 'excerpt_more', $filter_excerpt_more ); $classes = ''; if ( isset( $attributes['textAlign'] ) ) { - $classes .= 'has-text-align-' . $attributes['textAlign']; + $classes .= "has-text-align-{$attributes['textAlign']}"; } $wrapper_attributes = get_block_wrapper_attributes( array( 'class' => $classes ) ); - $content = '

' . get_the_excerpt( $block->context['postId'] ); - if ( ! isset( $attributes['showMoreOnNewLine'] ) || $attributes['showMoreOnNewLine'] ) { + $content = '

' . get_the_excerpt( $block->context['postId'] ); + $show_more_on_new_line = ! isset( $attributes['showMoreOnNewLine'] ) || $attributes['showMoreOnNewLine']; + if ( $show_more_on_new_line && ! empty( $more_text ) ) { $content .= '

' . $more_text . '

'; } else { $content .= " $more_text

"; } - - remove_filter( - 'excerpt_length', - $filter_excerpt_length - ); - + remove_filter( 'excerpt_more', $filter_excerpt_more ); return sprintf( '
%2$s
', $wrapper_attributes, $content ); } diff --git a/packages/components/src/autocomplete/index.js b/packages/components/src/autocomplete/index.js index 95094f261cff93..ee07dea9ef7717 100644 --- a/packages/components/src/autocomplete/index.js +++ b/packages/components/src/autocomplete/index.js @@ -443,6 +443,7 @@ function useAutocomplete( { useEffect( () => { if ( ! textContent ) { + reset(); return; } diff --git a/packages/components/src/form-token-field/suggestions-list.js b/packages/components/src/form-token-field/suggestions-list.js index 0f8b44c4068a0e..1ad0ace0a3b89e 100644 --- a/packages/components/src/form-token-field/suggestions-list.js +++ b/packages/components/src/form-token-field/suggestions-list.js @@ -21,7 +21,11 @@ class SuggestionsList extends Component { componentDidUpdate() { // only have to worry about scrolling selected suggestion into view // when already expanded - if ( this.props.selectedIndex > -1 && this.props.scrollIntoView ) { + if ( + this.props.selectedIndex > -1 && + this.props.scrollIntoView && + this.list.children[ this.props.selectedIndex ] + ) { this.scrollingIntoView = true; scrollIntoView( this.list.children[ this.props.selectedIndex ], diff --git a/packages/e2e-tests/fixtures/blocks/core__post-excerpt.json b/packages/e2e-tests/fixtures/blocks/core__post-excerpt.json index 57d658a4423d05..4378d6e27b6071 100644 --- a/packages/e2e-tests/fixtures/blocks/core__post-excerpt.json +++ b/packages/e2e-tests/fixtures/blocks/core__post-excerpt.json @@ -4,7 +4,6 @@ "name": "core/post-excerpt", "isValid": true, "attributes": { - "wordCount": 55, "showMoreOnNewLine": true }, "innerBlocks": [], diff --git a/packages/e2e-tests/plugins/iframed-inline-styles.php b/packages/e2e-tests/plugins/iframed-inline-styles.php new file mode 100644 index 00000000000000..f54c8eb83e6233 --- /dev/null +++ b/packages/e2e-tests/plugins/iframed-inline-styles.php @@ -0,0 +1,52 @@ + { + const { createElement: el } = element; + const { registerBlockType } = blocks; + const { useBlockProps } = blockEditor; + + registerBlockType( 'test/iframed-inline-styles', { + apiVersion: 2, + edit: function Edit() { + return el( 'div', useBlockProps(), 'Edit' ); + }, + save: function Save() { + return el( 'div', useBlockProps.save(), 'Save' ); + }, + } ); +} )( window ); diff --git a/packages/e2e-tests/plugins/iframed-inline-styles/style.css b/packages/e2e-tests/plugins/iframed-inline-styles/style.css new file mode 100644 index 00000000000000..23ddfa85290811 --- /dev/null +++ b/packages/e2e-tests/plugins/iframed-inline-styles/style.css @@ -0,0 +1,9 @@ +/** + * The following styles get applied both on the front of your site and in the + * editor. + */ +.wp-block-test-iframed-inline-styles { + background-color: #21759b; + color: #fff; + padding: 2px; +} diff --git a/packages/e2e-tests/plugins/iframed-masonry-block/editor.js b/packages/e2e-tests/plugins/iframed-masonry-block/editor.js index a6d2697fe3930c..2833815c0a1aef 100644 --- a/packages/e2e-tests/plugins/iframed-masonry-block/editor.js +++ b/packages/e2e-tests/plugins/iframed-masonry-block/editor.js @@ -28,7 +28,7 @@ el( 'div', { className: 'grid-item' } ), el( 'div', { className: 'grid-item' } ), el( 'div', { className: 'grid-item grid-item--height2' } ), - ] + ]; registerBlockType( 'test/iframed-masonry-block', { edit: function Edit() { diff --git a/packages/e2e-tests/specs/editor/plugins/__snapshots__/iframed-inline-styles.test.js.snap b/packages/e2e-tests/specs/editor/plugins/__snapshots__/iframed-inline-styles.test.js.snap new file mode 100644 index 00000000000000..46d3e807bff832 --- /dev/null +++ b/packages/e2e-tests/specs/editor/plugins/__snapshots__/iframed-inline-styles.test.js.snap @@ -0,0 +1,7 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`iframed inline styles should load inline styles in iframe 1`] = ` +" +
Save
+" +`; diff --git a/packages/e2e-tests/specs/editor/plugins/__snapshots__/iframed-masonry-block.test.js.snap b/packages/e2e-tests/specs/editor/plugins/__snapshots__/iframed-masonry-block.test.js.snap index 1c0d56386164f5..0033b3aff44d12 100644 --- a/packages/e2e-tests/specs/editor/plugins/__snapshots__/iframed-masonry-block.test.js.snap +++ b/packages/e2e-tests/specs/editor/plugins/__snapshots__/iframed-masonry-block.test.js.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`changing image size should load script and dependencies in iframe 1`] = ` +exports[`iframed masonry block should load script and dependencies in iframe 1`] = ` "
" diff --git a/packages/e2e-tests/specs/editor/plugins/iframed-inline-styles.test.js b/packages/e2e-tests/specs/editor/plugins/iframed-inline-styles.test.js new file mode 100644 index 00000000000000..298674e96b7701 --- /dev/null +++ b/packages/e2e-tests/specs/editor/plugins/iframed-inline-styles.test.js @@ -0,0 +1,67 @@ +/** + * WordPress dependencies + */ +import { + activatePlugin, + createNewPost, + deactivatePlugin, + insertBlock, + getEditedPostContent, + openDocumentSettingsSidebar, + clickButton, + canvas, +} from '@wordpress/e2e-test-utils'; + +async function getComputedStyle( context, property ) { + await context.waitForSelector( '.wp-block-test-iframed-inline-styles' ); + return await context.evaluate( ( prop ) => { + const container = document.querySelector( + '.wp-block-test-iframed-inline-styles' + ); + return window.getComputedStyle( container )[ prop ]; + }, property ); +} + +describe( 'iframed inline styles', () => { + beforeEach( async () => { + await activatePlugin( 'gutenberg-test-iframed-inline-styles' ); + await createNewPost( { postType: 'page' } ); + } ); + + afterEach( async () => { + await deactivatePlugin( 'gutenberg-test-iframed-inline-styles' ); + } ); + + it( 'should load inline styles in iframe', async () => { + await insertBlock( 'Iframed Inline Styles' ); + + expect( await getEditedPostContent() ).toMatchSnapshot(); + expect( await getComputedStyle( page, 'padding' ) ).toBe( '20px' ); + expect( await getComputedStyle( page, 'border-width' ) ).toBe( '2px' ); + + await openDocumentSettingsSidebar(); + await clickButton( 'Page' ); + await clickButton( 'Template' ); + await clickButton( 'New' ); + await page.keyboard.press( 'Tab' ); + await page.keyboard.press( 'Tab' ); + await page.keyboard.type( 'Iframed Test' ); + await clickButton( 'Create' ); + await page.waitForSelector( 'iframe[name="editor-canvas"]' ); + + // Inline styles of properly enqueued stylesheet should load. + expect( await getComputedStyle( canvas(), 'padding' ) ).toBe( '20px' ); + + // Inline styles of stylesheet loaded with the compatibility layer + // should load. + expect( await getComputedStyle( canvas(), 'border-width' ) ).toBe( + '2px' + ); + + expect( console ).toHaveErrored( + `Stylesheet iframed-inline-styles-compat-style-css was not properly added. +For blocks, use the block API's style (https://developer.wordpress.org/block-editor/reference-guides/block-api/block-metadata/#style) or editorStyle (https://developer.wordpress.org/block-editor/reference-guides/block-api/block-metadata/#editor-style). +For themes, use add_editor_style (https://developer.wordpress.org/block-editor/how-to-guides/themes/theme-support/#editor-styles). ` + ); + } ); +} ); diff --git a/packages/e2e-tests/specs/editor/plugins/iframed-masonry-block.test.js b/packages/e2e-tests/specs/editor/plugins/iframed-masonry-block.test.js index 5c12f2abb1b6aa..7877442e5695ef 100644 --- a/packages/e2e-tests/specs/editor/plugins/iframed-masonry-block.test.js +++ b/packages/e2e-tests/specs/editor/plugins/iframed-masonry-block.test.js @@ -26,7 +26,7 @@ async function didMasonryLoadCorrectly( context ) { } ); } -describe( 'changing image size', () => { +describe( 'iframed masonry block', () => { beforeEach( async () => { await activatePlugin( 'gutenberg-test-iframed-masonry-block' ); await createNewPost( { postType: 'page' } ); diff --git a/packages/e2e-tests/specs/editor/various/__snapshots__/multi-block-selection.test.js.snap b/packages/e2e-tests/specs/editor/various/__snapshots__/multi-block-selection.test.js.snap index fd0466cb4b3bb8..ab04f0b8689245 100644 --- a/packages/e2e-tests/specs/editor/various/__snapshots__/multi-block-selection.test.js.snap +++ b/packages/e2e-tests/specs/editor/various/__snapshots__/multi-block-selection.test.js.snap @@ -162,6 +162,18 @@ exports[`Multi-block selection should return original focus after failed multi s " `; +exports[`Multi-block selection should select all from empty selection 1`] = ` +" +

1

+ + + +

2

+" +`; + +exports[`Multi-block selection should select all from empty selection 2`] = `""`; + exports[`Multi-block selection should set attributes for multiple paragraphs 1`] = ` "

1

diff --git a/packages/e2e-tests/specs/editor/various/__snapshots__/rich-text.test.js.snap b/packages/e2e-tests/specs/editor/various/__snapshots__/rich-text.test.js.snap index 873b1e305b39b3..bb3517f9c28324 100644 --- a/packages/e2e-tests/specs/editor/various/__snapshots__/rich-text.test.js.snap +++ b/packages/e2e-tests/specs/editor/various/__snapshots__/rich-text.test.js.snap @@ -110,6 +110,12 @@ exports[`RichText should return focus when pressing formatting button 1`] = ` " `; +exports[`RichText should run input rules after composition end 1`] = ` +" +

a

+" +`; + exports[`RichText should split rich text on paste 1`] = ` "

a

diff --git a/packages/e2e-tests/specs/editor/various/multi-block-selection.test.js b/packages/e2e-tests/specs/editor/various/multi-block-selection.test.js index 8eef794c3f5a8a..fded2c6d7e9146 100644 --- a/packages/e2e-tests/specs/editor/various/multi-block-selection.test.js +++ b/packages/e2e-tests/specs/editor/various/multi-block-selection.test.js @@ -636,4 +636,27 @@ describe( 'Multi-block selection', () => { '[data-type="core/paragraph"].is-multi-selected' ); } ); + + it( 'should select all from empty selection', async () => { + await clickBlockAppender(); + + await page.keyboard.type( '1' ); + await page.keyboard.press( 'Enter' ); + await page.keyboard.type( '2' ); + + // Confirm setup. + expect( await getEditedPostContent() ).toMatchSnapshot(); + + // Clear the selected block. + const paragraph = await page.$( '[data-type="core/paragraph"]' ); + const box = await paragraph.boundingBox(); + await page.mouse.click( box.x - 1, box.y ); + + await pressKeyWithModifier( 'primary', 'a' ); + + await page.keyboard.press( 'Backspace' ); + + // Expect both paragraphs to be deleted. + expect( await getEditedPostContent() ).toMatchSnapshot(); + } ); } ); diff --git a/packages/e2e-tests/specs/editor/various/rich-text.test.js b/packages/e2e-tests/specs/editor/various/rich-text.test.js index 39e38297d5bdee..33e0bc64317efd 100644 --- a/packages/e2e-tests/specs/editor/various/rich-text.test.js +++ b/packages/e2e-tests/specs/editor/various/rich-text.test.js @@ -451,4 +451,23 @@ describe( 'RichText', () => { await page.keyboard.press( 'ArrowLeft' ); expect( await page.$( blockToolbarSelector ) ).toBe( null ); } ); + + it( 'should run input rules after composition end', async () => { + await clickBlockAppender(); + // Puppeteer doesn't support composition, so emulate it by inserting + // text in the DOM directly, setting selection in the right place, and + // firing `compositionend`. + // See https://github.com/puppeteer/puppeteer/issues/4981. + await page.evaluate( () => { + document.activeElement.textContent = '`a`'; + const selection = window.getSelection(); + selection.selectAllChildren( document.activeElement ); + selection.collapseToEnd(); + document.activeElement.dispatchEvent( + new CompositionEvent( 'compositionend' ) + ); + } ); + + expect( await getEditedPostContent() ).toMatchSnapshot(); + } ); } ); diff --git a/packages/edit-post/src/components/layout/index.js b/packages/edit-post/src/components/layout/index.js index 01e2847f1b7f9c..85104e58dca95d 100644 --- a/packages/edit-post/src/components/layout/index.js +++ b/packages/edit-post/src/components/layout/index.js @@ -12,6 +12,7 @@ import { UnsavedChangesWarning, EditorNotices, EditorKeyboardShortcutsRegister, + EditorSnackbars, store as editorStore, } from '@wordpress/editor'; import { AsyncModeProvider, useSelect, useDispatch } from '@wordpress/data'; @@ -220,6 +221,7 @@ function Layout( { styles } ) { ) } + notices={ } content={ <> diff --git a/packages/edit-post/src/components/visual-editor/index.js b/packages/edit-post/src/components/visual-editor/index.js index c8f6898ffaed7e..9718bbaad697a4 100644 --- a/packages/edit-post/src/components/visual-editor/index.js +++ b/packages/edit-post/src/components/visual-editor/index.js @@ -57,13 +57,14 @@ function MaybeIframe( { return ( <> -
{ children } -
+ ); } @@ -75,7 +76,7 @@ function MaybeIframe( { contentRef={ contentRef } style={ { width: '100%', height: '100%', display: 'block' } } > - { children } + { children } ); } @@ -232,18 +233,14 @@ export default function VisualEditor( { styles } ) { layout={ defaultLayout } /> ) } - - { ! isTemplateMode && ( -
- -
- ) } - - - -
+ { ! isTemplateMode && ( +
+ +
+ ) } + + + diff --git a/packages/edit-site/src/components/editor/index.js b/packages/edit-site/src/components/editor/index.js index f02348d10f8d7d..6c729c518211e1 100644 --- a/packages/edit-site/src/components/editor/index.js +++ b/packages/edit-site/src/components/editor/index.js @@ -19,6 +19,7 @@ import { } from '@wordpress/interface'; import { EditorNotices, + EditorSnackbars, EntitiesSavedStates, UnsavedChangesWarning, store as editorStore, @@ -213,6 +214,7 @@ function Editor( { initialSettings } ) { } /> } + notices={ } content={ <> diff --git a/packages/edit-widgets/src/blocks/widget-area/edit/index.js b/packages/edit-widgets/src/blocks/widget-area/edit/index.js index 1957361586e5a6..d9c62a7153e9a6 100644 --- a/packages/edit-widgets/src/blocks/widget-area/edit/index.js +++ b/packages/edit-widgets/src/blocks/widget-area/edit/index.js @@ -78,7 +78,7 @@ export default function WidgetAreaEdit( { type="postType" id={ `widget-area-${ id }` } > - + ) } diff --git a/packages/edit-widgets/src/blocks/widget-area/edit/inner-blocks.js b/packages/edit-widgets/src/blocks/widget-area/edit/inner-blocks.js index 38d17428fac5f2..83800d875e62cd 100644 --- a/packages/edit-widgets/src/blocks/widget-area/edit/inner-blocks.js +++ b/packages/edit-widgets/src/blocks/widget-area/edit/inner-blocks.js @@ -18,7 +18,7 @@ import { useRef } from '@wordpress/element'; */ import useIsDraggingWithin from './use-is-dragging-within'; -export default function WidgetAreaInnerBlocks() { +export default function WidgetAreaInnerBlocks( { id } ) { const [ blocks, onInput, onChange ] = useEntityBlockEditor( 'root', 'postType' @@ -40,6 +40,7 @@ export default function WidgetAreaInnerBlocks() { return (
@@ -42,11 +39,6 @@ export function EditorNotices( { notices, onRemove } ) { > - ); } diff --git a/packages/editor/src/components/editor-snackbars/index.js b/packages/editor/src/components/editor-snackbars/index.js new file mode 100644 index 00000000000000..6dc01435eb4b45 --- /dev/null +++ b/packages/editor/src/components/editor-snackbars/index.js @@ -0,0 +1,30 @@ +/** + * External dependencies + */ +import { filter } from 'lodash'; + +/** + * WordPress dependencies + */ +import { SnackbarList } from '@wordpress/components'; +import { useSelect, useDispatch } from '@wordpress/data'; +import { store as noticesStore } from '@wordpress/notices'; + +export default function EditorSnackbars() { + const notices = useSelect( + ( select ) => select( noticesStore ).getNotices(), + [] + ); + const { removeNotice } = useDispatch( noticesStore ); + const snackbarNotices = filter( notices, { + type: 'snackbar', + } ); + + return ( + + ); +} diff --git a/packages/editor/src/components/index.js b/packages/editor/src/components/index.js index c2a281781a2ca6..50dfd6885166d2 100644 --- a/packages/editor/src/components/index.js +++ b/packages/editor/src/components/index.js @@ -11,6 +11,7 @@ export { default as EditorKeyboardShortcutsRegister } from './global-keyboard-sh export { default as EditorHistoryRedo } from './editor-history/redo'; export { default as EditorHistoryUndo } from './editor-history/undo'; export { default as EditorNotices } from './editor-notices'; +export { default as EditorSnackbars } from './editor-snackbars'; export { default as EntitiesSavedStates } from './entities-saved-states'; export { default as ErrorBoundary } from './error-boundary'; export { default as LocalAutosaveMonitor } from './local-autosave-monitor'; diff --git a/packages/interface/src/components/interface-skeleton/index.js b/packages/interface/src/components/interface-skeleton/index.js index 184f0ddde30056..26bf20681f659a 100644 --- a/packages/interface/src/components/interface-skeleton/index.js +++ b/packages/interface/src/components/interface-skeleton/index.js @@ -3,6 +3,9 @@ */ import classnames from 'classnames'; +/** + * WordPress dependencies + */ /** * WordPress dependencies */ @@ -31,6 +34,7 @@ function InterfaceSkeleton( header, sidebar, secondarySidebar, + notices, content, drawer, actions, @@ -105,6 +109,11 @@ function InterfaceSkeleton( { secondarySidebar }
) } + { !! notices && ( +
+ { notices } +
+ ) }
{ + return isKeyboardEvent[ modifier ]( event, character ); + } + ); + } + + return isMatch; +} diff --git a/packages/keyboard-shortcuts/src/index.js b/packages/keyboard-shortcuts/src/index.js index a57718af9c566e..26f2b004b6e04b 100644 --- a/packages/keyboard-shortcuts/src/index.js +++ b/packages/keyboard-shortcuts/src/index.js @@ -1,2 +1,3 @@ export { store } from './store'; export { default as useShortcut } from './hooks/use-shortcut'; +export { default as __unstableUseShortcutEventMatch } from './hooks/use-shortcut-event-match'; diff --git a/packages/keyboard-shortcuts/src/store/selectors.js b/packages/keyboard-shortcuts/src/store/selectors.js index 4bf4c4c9b9c5aa..c4a286e954e11c 100644 --- a/packages/keyboard-shortcuts/src/store/selectors.js +++ b/packages/keyboard-shortcuts/src/store/selectors.js @@ -116,6 +116,16 @@ export function getShortcutAliases( state, name ) { : EMPTY_ARRAY; } +export const getAllShortcutKeyCombinations = createSelector( + ( state, name ) => { + return compact( [ + getShortcutKeyCombination( state, name ), + ...getShortcutAliases( state, name ), + ] ); + }, + ( state, name ) => [ state[ name ] ] +); + /** * Returns the raw representation of all the keyboard combinations of a given shortcut name. * @@ -126,15 +136,12 @@ export function getShortcutAliases( state, name ) { */ export const getAllShortcutRawKeyCombinations = createSelector( ( state, name ) => { - return compact( [ - getKeyCombinationRepresentation( - getShortcutKeyCombination( state, name ), - 'raw' - ), - ...getShortcutAliases( state, name ).map( ( combination ) => - getKeyCombinationRepresentation( combination, 'raw' ) - ), - ] ); + return getAllShortcutKeyCombinations( + state, + name + ).map( ( combination ) => + getKeyCombinationRepresentation( combination, 'raw' ) + ); }, ( state, name ) => [ state[ name ] ] ); diff --git a/phpunit/class-wp-theme-json-test.php b/phpunit/class-wp-theme-json-test.php index 7498e298443493..d5221e455bfe90 100644 --- a/phpunit/class-wp-theme-json-test.php +++ b/phpunit/class-wp-theme-json-test.php @@ -1387,7 +1387,7 @@ function test_get_from_editor_settings() { ), ), 'spacing' => array( - 'units' => array( 'px', 'em', 'rem', 'vh', 'vw' ), + 'units' => array( 'px', 'em', 'rem', 'vh', 'vw', '%' ), ), 'typography' => array( 'customFontSize' => false, @@ -1489,7 +1489,7 @@ function test_get_editor_settings_custom_units_can_be_enabled() { $input = gutenberg_get_default_block_editor_settings(); $expected = array( - 'units' => array( 'px', 'em', 'rem', 'vh', 'vw' ), + 'units' => array( 'px', 'em', 'rem', 'vh', 'vw', '%' ), 'customPadding' => false, );