diff --git a/packages/edit-site/src/components/style-book/index.js b/packages/edit-site/src/components/style-book/index.js index b9b6b63cfe4a9e..5eca935ad0d257 100644 --- a/packages/edit-site/src/components/style-book/index.js +++ b/packages/edit-site/src/components/style-book/index.js @@ -8,6 +8,10 @@ import classnames from 'classnames'; */ import { Button, + __unstableComposite as Composite, + __unstableUseCompositeState as useCompositeState, + __unstableCompositeItem as CompositeItem, + Disabled, TabPanel, createSlotFill, __experimentalUseSlotFills as useSlotFills, @@ -20,9 +24,13 @@ import { createBlock, } from '@wordpress/blocks'; import { - BlockPreview, + BlockList, privateApis as blockEditorPrivateApis, + store as blockEditorStore, + __unstableEditorStyles as EditorStyles, + __unstableIframe as Iframe, } from '@wordpress/block-editor'; +import { useSelect } from '@wordpress/data'; import { closeSmall } from '@wordpress/icons'; import { useResizeObserver, @@ -38,12 +46,84 @@ import { ESCAPE } from '@wordpress/keycodes'; */ import { unlock } from '../../private-apis'; -const { useGlobalStyle } = unlock( blockEditorPrivateApis ); +const { ExperimentalBlockEditorProvider, useGlobalStyle } = unlock( + blockEditorPrivateApis +); const SLOT_FILL_NAME = 'EditSiteStyleBook'; const { Slot: StyleBookSlot, Fill: StyleBookFill } = createSlotFill( SLOT_FILL_NAME ); +// The content area of the Style Book is rendered within an iframe so that global styles +// are applied to elements within the entire content area. To support elements that are +// not part of the block previews, such as headings and layout for the block previews, +// additional CSS rules need to be passed into the iframe. These are hard-coded below. +// Note that button styles are unset, and then focus rules from the `Button` component are +// applied to the `button` element, targeted via `.edit-site-style-book__example`. +// This is to ensure that browser default styles for buttons are not applied to the previews. +const STYLE_BOOK_IFRAME_STYLES = ` + .edit-site-style-book__examples { + max-width: 900px; + margin: 0 auto; + } + + .edit-site-style-book__example { + border-radius: 2px; + cursor: pointer; + display: flex; + flex-direction: column; + gap: 40px; + margin-bottom: 40px; + padding: 16px; + width: 100%; + box-sizing: border-box; + } + + .edit-site-style-book__example.is-selected { + box-shadow: 0 0 0 1px var(--wp-components-color-accent, var(--wp-admin-theme-color, #007cba)); + } + + .edit-site-style-book__example:focus:not(:disabled) { + box-shadow: 0 0 0 var(--wp-admin-border-width-focus) var(--wp-components-color-accent, var(--wp-admin-theme-color, #007cba)); + outline: 3px solid transparent; + } + + .edit-site-style-book__examples.is-wide .edit-site-style-book__example { + flex-direction: row; + } + + .edit-site-style-book__example-title { + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; + font-size: 11px; + font-weight: 500; + line-height: normal; + margin: 0; + text-align: left; + text-transform: uppercase; + } + + .edit-site-style-book__examples.is-wide .edit-site-style-book__example-title { + text-align: right; + width: 120px; + } + + .edit-site-style-book__example-preview { + width: 100%; + } + + .edit-site-style-book__example-preview .block-editor-block-list__insertion-point, + .edit-site-style-book__example-preview .block-list-appender { + display: none; + } + + .edit-site-style-book__example-preview .is-root-container > .wp-block:first-child { + margin-top: 0; + } + .edit-site-style-book__example-preview .is-root-container > .wp-block:last-child { + margin-bottom: 0; + } +`; + function getExamples() { // Use our own example for the Heading block so that we can show multiple // heading levels. @@ -118,6 +198,15 @@ function StyleBook( { isSelected, onSelect, onClose } ) { [ examples ] ); + const originalSettings = useSelect( + ( select ) => select( blockEditorStore ).getSettings(), + [] + ); + const settings = useMemo( + () => ( { ...originalSettings, __unstableIsPreviewMode: true } ), + [ originalSettings ] + ); + function closeOnEscape( event ) { if ( event.keyCode === ESCAPE && ! event.defaultPrevented ) { event.preventDefault(); @@ -156,12 +245,47 @@ function StyleBook( { isSelected, onSelect, onClose } ) { tabs={ tabs } > { ( tab ) => ( - + ) } @@ -169,52 +293,83 @@ function StyleBook( { isSelected, onSelect, onClose } ) { ); } -const Examples = memo( ( { examples, category, isSelected, onSelect } ) => ( -
- { examples - .filter( ( example ) => example.category === category ) - .map( ( example ) => ( - { - onSelect( example.name ); - } } - /> - ) ) } -
-) ); - -const Example = memo( ( { title, blocks, isSelected, onClick } ) => ( - -) ); +const Examples = memo( + ( { className, examples, category, label, isSelected, onSelect } ) => { + const composite = useCompositeState( { orientation: 'vertical' } ); + return ( + + { examples + .filter( ( example ) => example.category === category ) + .map( ( example ) => ( + { + onSelect( example.name ); + } } + /> + ) ) } + + ); + } +); + +const Example = ( { composite, id, title, blocks, isSelected, onClick } ) => { + const originalSettings = useSelect( + ( select ) => select( blockEditorStore ).getSettings(), + [] + ); + const settings = useMemo( + () => ( { ...originalSettings, __unstableIsPreviewMode: true } ), + [ originalSettings ] + ); + + // Cache the list of blocks to avoid additional processing when the component is re-rendered. + const renderedBlocks = useMemo( + () => ( Array.isArray( blocks ) ? blocks : [ blocks ] ), + [ blocks ] + ); + + return ( + + + { title } + +
+ + + + + +
+
+ ); +}; function useHasStyleBook() { const fills = useSlotFills( SLOT_FILL_NAME ); diff --git a/packages/edit-site/src/components/style-book/style.scss b/packages/edit-site/src/components/style-book/style.scss index fc88c399cac207..881b117a75ccb3 100644 --- a/packages/edit-site/src/components/style-book/style.scss +++ b/packages/edit-site/src/components/style-book/style.scss @@ -26,53 +26,9 @@ bottom: 0; left: 0; overflow: auto; - padding: $grid-unit-40; + padding: 0; 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: 11px; - 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/test/e2e/specs/site-editor/style-book.spec.js b/test/e2e/specs/site-editor/style-book.spec.js index f3c06308b92154..56d0ca0cf20f1b 100644 --- a/test/e2e/specs/site-editor/style-book.spec.js +++ b/test/e2e/specs/site-editor/style-book.spec.js @@ -59,37 +59,45 @@ test.describe( 'Style Book', () => { ).toBeVisible(); await expect( page.locator( 'role=tab[name="Theme"i]' ) ).toBeVisible(); + // Buttons to select block examples are rendered within the Style Book iframe. + const styleBookIframe = page.frameLocator( + '[name="style-book-canvas"]' + ); + await expect( - page.locator( - 'role=button[name="Open Headings styles in Styles panel"i]' - ) + styleBookIframe.getByRole( 'button', { + name: 'Open Headings styles in Styles panel', + } ) ).toBeVisible(); await expect( - page.locator( - 'role=button[name="Open Paragraph styles in Styles panel"i]' - ) + styleBookIframe.getByRole( 'button', { + name: 'Open Paragraph styles in Styles panel', + } ) ).toBeVisible(); await page.click( 'role=tab[name="Media"i]' ); await expect( - page.locator( - 'role=button[name="Open Image styles in Styles panel"i]' - ) + styleBookIframe.getByRole( 'button', { + name: 'Open Image styles in Styles panel', + } ) ).toBeVisible(); await expect( - page.locator( - 'role=button[name="Open Gallery styles in Styles panel"i]' - ) + styleBookIframe.getByRole( 'button', { + name: 'Open Gallery styles in Styles panel', + } ) ).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 page + .frameLocator( '[name="style-book-canvas"]' ) + .getByRole( 'button', { + name: 'Open Headings styles in Styles panel', + } ) + .click(); await expect( page.locator( @@ -105,9 +113,12 @@ test.describe( 'Style Book', () => { await page.click( 'role=button[name="Heading block styles"]' ); await page.click( 'role=button[name="Typography styles"]' ); - await page.click( - 'role=button[name="Open Quote styles in Styles panel"i]' - ); + await page + .frameLocator( '[name="style-book-canvas"]' ) + .getByRole( 'button', { + name: 'Open Quote styles in Styles panel', + } ) + .click(); await page.click( 'role=button[name="Navigate to the previous view"]' ); await page.click( 'role=button[name="Navigate to the previous view"]' );