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"]' );