diff --git a/packages/block-editor/src/components/block-preview/README.md b/packages/block-editor/src/components/block-preview/README.md index 0da7348817cd85..6ae8cfe97514a2 100644 --- a/packages/block-editor/src/components/block-preview/README.md +++ b/packages/block-editor/src/components/block-preview/README.md @@ -28,9 +28,28 @@ Width of the preview container in pixels. Controls at what size the blocks will `viewportWidth` can be used to simulate how blocks look on different device sizes or to make sure make sure multiple previews will be rendered with the same scale, regardless of their content. +Set `viewportWidth` to `0` to make the viewport the same width as the container. + ### `__experimentalPadding` - **Type** `Int` - **Default** `undefined` Padding for the preview container body. + +### `__experimentalStyles` + +List of additional editor styles to load into the preview iframe. Each object +should contain a `css` attribute. See `EditorStyles` for more info. + +```jsx + +``` + +- **Type** `Int` +- **Default** `[]` diff --git a/packages/block-editor/src/components/block-preview/auto.js b/packages/block-editor/src/components/block-preview/auto.js index ddf4ee0ec75abd..587a264f407ca4 100644 --- a/packages/block-editor/src/components/block-preview/auto.js +++ b/packages/block-editor/src/components/block-preview/auto.js @@ -25,7 +25,12 @@ function ScaledBlockPreview( { containerWidth, __experimentalPadding, __experimentalMinHeight, + __experimentalStyles, } ) { + if ( ! viewportWidth ) { + viewportWidth = containerWidth; + } + const [ contentResizeListener, { height: contentHeight } ] = useResizeObserver(); const { styles, assets, duotone } = useSelect( ( select ) => { @@ -42,6 +47,7 @@ function ScaledBlockPreview( { if ( styles ) { return [ ...styles, + ...__experimentalStyles, { css: 'body{height:auto;overflow:hidden;}', __unstableType: 'presets', @@ -50,7 +56,7 @@ function ScaledBlockPreview( { } return styles; - }, [ styles ] ); + }, [ styles, __experimentalStyles ] ); const svgFilters = useMemo( () => { return [ ...( duotone?.default ?? [] ), ...( duotone?.theme ?? [] ) ]; diff --git a/packages/block-editor/src/components/block-preview/index.js b/packages/block-editor/src/components/block-preview/index.js index 535a67438cad8e..78f38d390f53cd 100644 --- a/packages/block-editor/src/components/block-preview/index.js +++ b/packages/block-editor/src/components/block-preview/index.js @@ -23,6 +23,7 @@ export function BlockPreview( { __experimentalPadding = 0, viewportWidth = 1200, __experimentalMinHeight, + __experimentalStyles = [], } ) { const originalSettings = useSelect( ( select ) => select( blockEditorStore ).getSettings(), @@ -45,6 +46,7 @@ export function BlockPreview( { viewportWidth={ viewportWidth } __experimentalPadding={ __experimentalPadding } __experimentalMinHeight={ __experimentalMinHeight } + __experimentalStyles={ __experimentalStyles } /> ); diff --git a/packages/edit-site/src/components/block-editor/editor-canvas.js b/packages/edit-site/src/components/block-editor/editor-canvas.js new file mode 100644 index 00000000000000..2ab7ee4c66579b --- /dev/null +++ b/packages/edit-site/src/components/block-editor/editor-canvas.js @@ -0,0 +1,69 @@ +/** + * WordPress dependencies + */ +import { + __experimentalUseResizeCanvas as useResizeCanvas, + __unstableEditorStyles as EditorStyles, + __unstableIframe as Iframe, + __unstableUseMouseMoveTypingReset as useMouseMoveTypingReset, + store as blockEditorStore, +} from '@wordpress/block-editor'; +import { useSelect } from '@wordpress/data'; + +/** + * Internal dependencies + */ +import { store as editSiteStore } from '../../store'; + +function EditorCanvas( { enableResizing, settings, children, ...props } ) { + const { deviceType, isZoomOutMode } = useSelect( + ( select ) => ( { + deviceType: + select( editSiteStore ).__experimentalGetPreviewDeviceType(), + isZoomOutMode: + select( blockEditorStore ).__unstableGetEditorMode() === + 'zoom-out', + } ), + [] + ); + const deviceStyles = useResizeCanvas( deviceType ); + const mouseMoveTypingRef = useMouseMoveTypingReset(); + return ( + + ); +} + +export default EditorCanvas; diff --git a/packages/edit-site/src/components/block-editor/index.js b/packages/edit-site/src/components/block-editor/index.js index f990ee7f15f697..b3b2b9212fd6bc 100644 --- a/packages/edit-site/src/components/block-editor/index.js +++ b/packages/edit-site/src/components/block-editor/index.js @@ -26,7 +26,11 @@ import { store as blockEditorStore, __unstableBlockNameContext, } from '@wordpress/block-editor'; -import { useMergeRefs, useViewportMatch } from '@wordpress/compose'; +import { + useMergeRefs, + useViewportMatch, + useResizeObserver, +} from '@wordpress/compose'; import { ReusableBlocksMenuItems } from '@wordpress/reusable-blocks'; import { listView } from '@wordpress/icons'; import { ToolbarButton, ToolbarGroup } from '@wordpress/components'; @@ -43,6 +47,8 @@ import { store as editSiteStore } from '../../store'; import BlockInspectorButton from './block-inspector-button'; import BackButton from './back-button'; import ResizableEditor from './resizable-editor'; +import EditorCanvas from './editor-canvas'; +import StyleBook from '../style-book'; const LAYOUT = { type: 'default', @@ -59,10 +65,16 @@ export default function BlockEditor( { setIsInserterOpen } ) { templateId, page, isNavigationSidebarOpen, + canvasMode, } = useSelect( ( select ) => { - const { getSettings, getEditedPostType, getEditedPostId, getPage } = - select( editSiteStore ); + const { + getSettings, + getEditedPostType, + getEditedPostId, + getPage, + __unstableGetCanvasMode, + } = select( editSiteStore ); return { storedSettings: getSettings( setIsInserterOpen ), @@ -73,6 +85,7 @@ export default function BlockEditor( { setIsInserterOpen } ) { select( interfaceStore ).getActiveComplementaryArea( editSiteStore.name ) === NAVIGATION_SIDEBAR_NAME, + canvasMode: __unstableGetCanvasMode(), }; }, [ setIsInserterOpen ] @@ -158,9 +171,15 @@ export default function BlockEditor( { setIsInserterOpen } ) { const mergedRefs = useMergeRefs( [ contentRef, useTypingObserver() ] ); const isMobileViewport = useViewportMatch( 'small', '<' ); const { clearSelectedBlock } = useDispatch( blockEditorStore ); + const [ resizeObserver, sizes ] = useResizeObserver(); const isTemplatePart = templateType === 'wp_template_part'; const hasBlocks = blocks.length !== 0; + const enableResizing = + isTemplatePart && + canvasMode !== 'view' && + // Disable resizing in mobile viewport. + ! isMobileViewport; const NavMenuSidebarToggle = () => ( @@ -211,54 +230,72 @@ export default function BlockEditor( { setIsInserterOpen } ) { - { - // Clear selected block when clicking on the gray background. - if ( event.target === event.currentTarget ) { - clearSelectedBlock(); - } - } } - > - - - - - - <__unstableBlockSettingsMenuFirstItem> - { ( { onClose } ) => ( - - ) } - - <__unstableBlockToolbarLastItem> - <__unstableBlockNameContext.Consumer> - { ( blockName ) => - blockName === 'core/navigation' && ( - - ) - } - - - + { /* Potentially this could be a generic slot (e.g. EditorCanvas.Slot) if there are other uses for it. */ } + + { ( [ styleBook ] ) => + styleBook ? ( +
+ + { styleBook } + +
+ ) : ( + { + // Clear selected block when clicking on the gray background. + if ( event.target === event.currentTarget ) { + clearSelectedBlock(); + } + } } + > + + + + + { resizeObserver } + + + + <__unstableBlockSettingsMenuFirstItem> + { ( { onClose } ) => ( + + ) } + + <__unstableBlockToolbarLastItem> + <__unstableBlockNameContext.Consumer> + { ( blockName ) => + blockName === 'core/navigation' && ( + + ) + } + + + + ) + } +
); diff --git a/packages/edit-site/src/components/block-editor/resizable-editor.js b/packages/edit-site/src/components/block-editor/resizable-editor.js index 6cdf3811f729c0..20fdd972528439 100644 --- a/packages/edit-site/src/components/block-editor/resizable-editor.js +++ b/packages/edit-site/src/components/block-editor/resizable-editor.js @@ -3,20 +3,10 @@ */ import { useState, useRef, useCallback } from '@wordpress/element'; import { ResizableBox } from '@wordpress/components'; -import { - __experimentalUseResizeCanvas as useResizeCanvas, - __unstableEditorStyles as EditorStyles, - __unstableIframe as Iframe, - __unstableUseMouseMoveTypingReset as useMouseMoveTypingReset, - store as blockEditorStore, -} from '@wordpress/block-editor'; -import { useSelect } from '@wordpress/data'; -import { useMergeRefs, useResizeObserver } from '@wordpress/compose'; /** * Internal dependencies */ -import { store as editSiteStore } from '../../store'; import ResizeHandle from './resize-handle'; // Removes the inline styles in the drag handles. @@ -32,37 +22,22 @@ const HANDLE_STYLES_OVERRIDE = { left: undefined, }; -function ResizableEditor( { enableResizing, settings, children, ...props } ) { - const [ resizeObserver, sizes ] = useResizeObserver(); - const { deviceType, isZoomOutMode, canvasMode } = useSelect( - ( select ) => ( { - deviceType: - select( editSiteStore ).__experimentalGetPreviewDeviceType(), - canvasMode: select( editSiteStore ).__unstableGetCanvasMode(), - isZoomOutMode: - select( blockEditorStore ).__unstableGetEditorMode() === - 'zoom-out', - } ), - [] - ); - const deviceStyles = useResizeCanvas( deviceType ); +function ResizableEditor( { enableResizing, height, children } ) { const [ width, setWidth ] = useState( '100%' ); - const iframeRef = useRef(); - const mouseMoveTypingResetRef = useMouseMoveTypingReset(); - const ref = useMergeRefs( [ iframeRef, mouseMoveTypingResetRef ] ); - const isResizingEnabled = enableResizing && canvasMode !== 'view'; + const resizableRef = useRef(); const resizeWidthBy = useCallback( ( deltaPixels ) => { - if ( iframeRef.current ) { - setWidth( iframeRef.current.offsetWidth + deltaPixels ); + if ( resizableRef.current ) { + setWidth( resizableRef.current.offsetWidth + deltaPixels ); } }, [] ); - return ( { + resizableRef.current = api?.resizable; + } } size={ { - width: isResizingEnabled ? width : '100%', - height: - isResizingEnabled && sizes.height ? sizes.height : '100%', + width: enableResizing ? width : '100%', + height: enableResizing && height ? height : '100%', } } onResizeStop={ ( event, direction, element ) => { setWidth( element.style.width ); @@ -71,10 +46,10 @@ function ResizableEditor( { enableResizing, settings, children, ...props } ) { maxWidth="100%" maxHeight="100%" enable={ { - right: isResizingEnabled, - left: isResizingEnabled, + right: enableResizing, + left: enableResizing, } } - showHandle={ isResizingEnabled } + showHandle={ enableResizing } // The editor is centered horizontally, resizing it only // moves half the distance. Hence double the ratio to correctly // align the cursor to the resizer handle. @@ -99,42 +74,7 @@ function ResizableEditor( { enableResizing, settings, children, ...props } ) { right: HANDLE_STYLES_OVERRIDE, } } > - + { children } ); } diff --git a/packages/edit-site/src/components/global-styles/ui.js b/packages/edit-site/src/components/global-styles/ui.js index 1daf72dbf1a883..8f7fa28e93eee6 100644 --- a/packages/edit-site/src/components/global-styles/ui.js +++ b/packages/edit-site/src/components/global-styles/ui.js @@ -4,6 +4,7 @@ import { __experimentalNavigatorProvider as NavigatorProvider, __experimentalNavigatorScreen as NavigatorScreen, + __experimentalUseNavigator as useNavigator, } from '@wordpress/components'; import { getBlockTypes } from '@wordpress/blocks'; @@ -25,6 +26,7 @@ import ScreenButtonColor from './screen-button-color'; import ScreenLayout from './screen-layout'; import ScreenStyleVariations from './screen-style-variations'; import ScreenBorder from './screen-border'; +import StyleBook from '../style-book'; function GlobalStylesNavigationScreen( { className, ...props } ) { return ( @@ -120,9 +122,23 @@ function ContextScreens( { name } ) { ); } -function GlobalStylesUI() { - const blocks = getBlockTypes(); +function GlobalStylesStyleBook( { onClose } ) { + const navigator = useNavigator(); + return ( + + navigator.location.path.startsWith( '/blocks/' + blockName ) + } + onSelect={ ( blockName ) => + navigator.goTo( '/blocks/' + blockName ) + } + onClose={ onClose } + /> + ); +} +function GlobalStylesUI( { isStyleBookOpened, onCloseStyleBook } ) { + const blocks = getBlockTypes(); return ( ) ) } + + { isStyleBookOpened && ( + + ) } ); } diff --git a/packages/edit-site/src/components/header-edit-mode/index.js b/packages/edit-site/src/components/header-edit-mode/index.js index c67a00069aa8db..8c104ddd3f9b2e 100644 --- a/packages/edit-site/src/components/header-edit-mode/index.js +++ b/packages/edit-site/src/components/header-edit-mode/index.js @@ -38,6 +38,7 @@ import UndoButton from './undo-redo/undo'; import RedoButton from './undo-redo/redo'; import DocumentActions from './document-actions'; import { store as editSiteStore } from '../../store'; +import { useHasStyleBook } from '../style-book'; const preventDefault = ( event ) => { event.preventDefault(); @@ -113,6 +114,8 @@ export default function HeaderEditMode() { [ setIsListViewOpened, isListViewOpen ] ); + const hasStyleBook = useHasStyleBook(); + const isFocusMode = templateType === 'wp_template_part'; /* translators: button label text should, if possible, be under 16 characters. */ @@ -132,93 +135,97 @@ export default function HeaderEditMode() { 'show-icon-labels': showIconLabels, } ) } > - -
- - { isLargeViewport && ( - <> - - - - - { isZoomedOutViewExperimentEnabled && ( + { ! hasStyleBook && ( + +
+ + { isLargeViewport && ( + <> + + + { - setPreviewDeviceType( 'desktop' ); - __unstableSetEditorMode( - isZoomedOutView - ? 'edit' - : 'zoom-out' - ); - } } + label={ __( 'List View' ) } + onClick={ toggleListView } + shortcut={ listViewShortcut } + showTooltip={ ! showIconLabels } + variant={ + showIconLabels ? 'tertiary' : undefined + } /> - ) } - - ) } -
-
+ { isZoomedOutViewExperimentEnabled && ( + { + setPreviewDeviceType( 'desktop' ); + __unstableSetEditorMode( + isZoomedOutView + ? 'edit' + : 'zoom-out' + ); + } } + /> + ) } + + ) } +
+
+ ) }
- + { hasStyleBook ? __( 'Style Book' ) : }
- { ! isFocusMode && ( + { ! isFocusMode && ! hasStyleBook && (
select( editSiteStore ).getEditorMode(), + [] + ); + useEffect( () => { + if ( editorMode !== 'visual' ) { + setIsStyleBookOpened( false ); + } + }, [ editorMode ] ); return ( { __( 'Styles' ) } + + +) ); + +function useHasStyleBook() { + const fills = useSlotFills( SLOT_FILL_NAME ); + return !! fills?.length; +} + +StyleBook.Slot = StyleBookSlot; +export default StyleBook; +export { useHasStyleBook }; diff --git a/packages/edit-site/src/components/style-book/style.scss b/packages/edit-site/src/components/style-book/style.scss new file mode 100644 index 00000000000000..abcff368dd4bd6 --- /dev/null +++ b/packages/edit-site/src/components/style-book/style.scss @@ -0,0 +1,78 @@ +.edit-site-style-book { + background: $white; // Fallback color, overriden by JavaScript. + border-radius: $radius-block-ui; + bottom: 0; + left: 0; + overflow: hidden; + position: absolute; + right: 0; + top: 0; + transition: all 0.3s; // Match .block-editor-iframe__body transition. +} + +.edit-site-style-book__close-button { + position: absolute; + right: $grid-unit-10; + top: math.div($grid-unit-60 - $button-size, 2); // ( tab height - button size ) / 2 +} + +.edit-site-style-book__tab-panel { + .components-tab-panel__tabs { + background: $white; + color: $gray-900; + } + + .components-tab-panel__tab-content { + bottom: 0; + left: 0; + overflow: auto; + padding: $grid-unit-40; + position: absolute; + right: 0; + top: $grid-unit-60; // Height of tabs. + } +} + +.edit-site-style-book__examples { + max-width: 900px; + margin: 0 auto; +} + +.edit-site-style-book__example { + background: none; + border-radius: $radius-block-ui; + border: none; + color: inherit; + cursor: pointer; + display: flex; + flex-direction: column; + gap: $grid-unit-50; + margin-bottom: $grid-unit-50; + padding: $grid-unit-20; + width: 100%; + + &.is-selected { + box-shadow: 0 0 0 1px var(--wp-admin-theme-color); + } + + .edit-site-style-book.is-wide & { + flex-direction: row; + } +} + +.edit-site-style-book__example-title { + font-size: $default-font-size; + font-weight: 500; + margin: 0; + text-align: left; + text-transform: uppercase; + + .edit-site-style-book.is-wide & { + text-align: right; + width: 120px; + } +} + +.edit-site-style-book__example-preview { + width: 100%; +} diff --git a/packages/edit-site/src/style.scss b/packages/edit-site/src/style.scss index efc6187a39a6b0..9b871cff4f2d50 100644 --- a/packages/edit-site/src/style.scss +++ b/packages/edit-site/src/style.scss @@ -23,6 +23,7 @@ @import "./components/sidebar-navigation-root/style.scss"; @import "./components/sidebar-navigation-title/style.scss"; @import "./components/site-icon/style.scss"; +@import "./components/style-book/style.scss"; html #wpadminbar { display: none; diff --git a/packages/icons/src/index.js b/packages/icons/src/index.js index 80bdc23994c4b8..92bc48c185cc99 100644 --- a/packages/icons/src/index.js +++ b/packages/icons/src/index.js @@ -199,6 +199,7 @@ export { default as rotateLeft } from './library/rotate-left'; export { default as rotateRight } from './library/rotate-right'; export { default as rss } from './library/rss'; export { default as search } from './library/search'; +export { default as seen } from './library/seen'; export { default as separator } from './library/separator'; export { default as settings } from './library/settings'; export { default as shadow } from './library/shadow'; diff --git a/packages/icons/src/library/seen.js b/packages/icons/src/library/seen.js new file mode 100644 index 00000000000000..4bb328271f834e --- /dev/null +++ b/packages/icons/src/library/seen.js @@ -0,0 +1,12 @@ +/** + * WordPress dependencies + */ +import { SVG, Path } from '@wordpress/primitives'; + +const seen = ( + + + +); + +export default seen; diff --git a/test/e2e/specs/site-editor/style-book.spec.js b/test/e2e/specs/site-editor/style-book.spec.js new file mode 100644 index 00000000000000..779e54322a9102 --- /dev/null +++ b/test/e2e/specs/site-editor/style-book.spec.js @@ -0,0 +1,131 @@ +/** + * WordPress dependencies + */ +const { test, expect } = require( '@wordpress/e2e-test-utils-playwright' ); + +test.use( { + styleBook: async ( { page }, use ) => { + await use( new StyleBook( { page } ) ); + }, +} ); + +test.describe( 'Style Book', () => { + test.beforeAll( async ( { requestUtils } ) => { + await requestUtils.activateTheme( 'emptytheme' ); + } ); + + test.afterAll( async ( { requestUtils } ) => { + await requestUtils.activateTheme( 'twentytwentyone' ); + } ); + + test.beforeEach( async ( { admin, siteEditor, styleBook, page } ) => { + await admin.visitSiteEditor(); + await siteEditor.enterEditMode(); + await styleBook.open(); + await expect( + page.locator( 'role=region[name="Style Book"i]' ) + ).toBeVisible(); + } ); + + test( 'should disable toolbar butons when open', async ( { page } ) => { + await expect( + page.locator( 'role=button[name="Toggle block inserter"i]' ) + ).not.toBeVisible(); + await expect( + page.locator( 'role=button[name="Tools"i]' ) + ).not.toBeVisible(); + await expect( + page.locator( 'role=button[name="Undo"i]' ) + ).not.toBeVisible(); + await expect( + page.locator( 'role=button[name="Redo"i]' ) + ).not.toBeVisible(); + await expect( + page.locator( 'role=button[name="Show template details"i]' ) + ).not.toBeVisible(); + await expect( + page.locator( 'role=button[name="View"i]' ) + ).not.toBeVisible(); + } ); + + test( 'should have tabs containing block examples', async ( { page } ) => { + await expect( page.locator( 'role=tab[name="Text"i]' ) ).toBeVisible(); + await expect( page.locator( 'role=tab[name="Media"i]' ) ).toBeVisible(); + await expect( + page.locator( 'role=tab[name="Design"i]' ) + ).toBeVisible(); + await expect( + page.locator( 'role=tab[name="Widgets"i]' ) + ).toBeVisible(); + await expect( page.locator( 'role=tab[name="Theme"i]' ) ).toBeVisible(); + + await expect( + page.locator( + 'role=button[name="Open Headings styles in Styles panel"i]' + ) + ).toBeVisible(); + await expect( + page.locator( + 'role=button[name="Open Paragraph styles in Styles panel"i]' + ) + ).toBeVisible(); + + await page.click( 'role=tab[name="Media"i]' ); + + await expect( + page.locator( + 'role=button[name="Open Image styles in Styles panel"i]' + ) + ).toBeVisible(); + await expect( + page.locator( + 'role=button[name="Open Gallery styles in Styles panel"i]' + ) + ).toBeVisible(); + } ); + + test( 'should open correct Global Styles panel when example is clicked', async ( { + page, + } ) => { + await page.click( + 'role=button[name="Open Headings styles in Styles panel"i]' + ); + + await expect( + page.locator( + 'role=region[name="Editor settings"i] >> role=heading[name="Heading"i]' + ) + ).toBeVisible(); + } ); + + test( 'should disappear when closed', async ( { page } ) => { + await page.click( + 'role=region[name="Style Book"i] >> role=button[name="Close Style Book"i]' + ); + + await expect( + page.locator( 'role=region[name="Style Book"i]' ) + ).not.toBeVisible(); + } ); +} ); + +class StyleBook { + constructor( { page } ) { + this.page = page; + } + + async disableWelcomeGuide() { + // Turn off the welcome guide. + await this.page.evaluate( () => { + window.wp.data + .dispatch( 'core/preferences' ) + .set( 'core/edit-site', 'welcomeGuideStyles', false ); + } ); + } + + async open() { + await this.disableWelcomeGuide(); + await this.page.click( 'role=button[name="Styles"i]' ); + await this.page.click( 'role=button[name="Open Style Book"i]' ); + } +}