Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Style Book: Move iframe to root of content area to support styles that overflow block previews #48664

Merged
Merged
263 changes: 209 additions & 54 deletions packages/edit-site/src/components/style-book/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
Expand All @@ -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.
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -156,65 +245,131 @@ function StyleBook( { isSelected, onSelect, onClose } ) {
tabs={ tabs }
>
{ ( tab ) => (
<Examples
examples={ examples }
category={ tab.name }
isSelected={ isSelected }
onSelect={ onSelect }
/>
<Iframe
className="edit-site-style-book__iframe"
head={
<>
<EditorStyles styles={ settings.styles } />
<style>
{
// Forming a "block formatting context" to prevent margin collapsing.
// @see https://developer.mozilla.org/en-US/docs/Web/Guide/CSS/Block_formatting_context
`.is-root-container { display: flow-root; }
body { position: relative; padding: 32px !important; }` +
STYLE_BOOK_IFRAME_STYLES
}
</style>
</>
}
name="style-book-canvas"
tabIndex={ 0 }
>
{ /* Filters need to be rendered before children to avoid Safari rendering issues. */ }
{ settings.svgFilters }
<Examples
className={ classnames(
'edit-site-style-book__examples',
{
'is-wide': sizes.width > 600,
}
) }
examples={ examples }
category={ tab.name }
label={ sprintf(
// translators: %s: Category of blocks, e.g. Text.
__(
'Examples of blocks in the %s category'
),
tab.title
) }
isSelected={ isSelected }
onSelect={ onSelect }
/>
</Iframe>
) }
</TabPanel>
</section>
</StyleBookFill>
);
}

const Examples = memo( ( { examples, category, isSelected, onSelect } ) => (
<div className="edit-site-style-book__examples">
{ examples
.filter( ( example ) => example.category === category )
.map( ( example ) => (
<Example
key={ example.name }
title={ example.title }
blocks={ example.blocks }
isSelected={ isSelected( example.name ) }
onClick={ () => {
onSelect( example.name );
} }
/>
) ) }
</div>
) );

const Example = memo( ( { title, blocks, isSelected, onClick } ) => (
<button
className={ classnames( 'edit-site-style-book__example', {
'is-selected': isSelected,
} ) }
aria-label={ sprintf(
// translators: %s: Title of a block, e.g. Heading.
__( 'Open %s styles in Styles panel' ),
title
) }
onClick={ onClick }
>
<span className="edit-site-style-book__example-title">{ title }</span>
<div className="edit-site-style-book__example-preview">
<BlockPreview
blocks={ blocks }
viewportWidth={ 0 }
additionalStyles={ [
{
css:
'.wp-block:first-child { margin-top: 0; }' +
'.wp-block:last-child { margin-bottom: 0; }',
},
] }
/>
</div>
</button>
) );
const Examples = memo(
( { className, examples, category, label, isSelected, onSelect } ) => {
const composite = useCompositeState( { orientation: 'vertical' } );
return (
<Composite
{ ...composite }
className={ className }
aria-label={ label }
>
{ examples
.filter( ( example ) => example.category === category )
.map( ( example ) => (
<Example
key={ example.name }
id={ `example-${ example.name }` }
composite={ composite }
title={ example.title }
blocks={ example.blocks }
isSelected={ isSelected( example.name ) }
onClick={ () => {
onSelect( example.name );
} }
/>
) ) }
</Composite>
);
}
);

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 (
<CompositeItem
{ ...composite }
className={ classnames( 'edit-site-style-book__example', {
'is-selected': isSelected,
} ) }
id={ id }
aria-label={ sprintf(
// translators: %s: Title of a block, e.g. Heading.
__( 'Open %s styles in Styles panel' ),
title
) }
onClick={ onClick }
role="button"
as="div"
>
<span className="edit-site-style-book__example-title">
{ title }
</span>
<div className="edit-site-style-book__example-preview" aria-hidden>
<Disabled className="edit-site-style-book__example-preview__content">
<ExperimentalBlockEditorProvider
value={ renderedBlocks }
settings={ settings }
>
<BlockList renderAppender={ false } />
</ExperimentalBlockEditorProvider>
</Disabled>
</div>
</CompositeItem>
);
};

function useHasStyleBook() {
const fills = useSlotFills( SLOT_FILL_NAME );
Expand Down
46 changes: 1 addition & 45 deletions packages/edit-site/src/components/style-book/style.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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%;
}
Loading