diff --git a/packages/edit-site/src/components/style-book/categories.ts b/packages/edit-site/src/components/style-book/categories.ts deleted file mode 100644 index 2c1b627c6d0c60..00000000000000 --- a/packages/edit-site/src/components/style-book/categories.ts +++ /dev/null @@ -1,91 +0,0 @@ -/** - * WordPress dependencies - */ -import { getCategories } from '@wordpress/blocks'; - -/** - * Internal dependencies - */ -import type { - BlockExample, - StyleBookCategory, - CategoryExamples, -} from './types'; -import { - STYLE_BOOK_CATEGORIES, - STYLE_BOOK_THEME_SUBCATEGORIES, -} from './constants'; - -/** - * Returns category examples for a given category definition and list of examples. - * @param {StyleBookCategory} categoryDefinition The category definition. - * @param {BlockExample[]} examples An array of block examples. - * @return {CategoryExamples|undefined} An object containing the category examples. - */ -export function getExamplesByCategory( - categoryDefinition: StyleBookCategory, - examples: BlockExample[] -): CategoryExamples | undefined { - if ( ! categoryDefinition?.slug || ! examples?.length ) { - return; - } - - if ( categoryDefinition?.subcategories?.length ) { - return categoryDefinition.subcategories.reduce( - ( acc, subcategoryDefinition ) => { - const subcategoryExamples = getExamplesByCategory( - subcategoryDefinition, - examples - ); - if ( subcategoryExamples ) { - acc.subcategories = [ - ...acc.subcategories, - subcategoryExamples, - ]; - } - return acc; - }, - { - title: categoryDefinition.title, - slug: categoryDefinition.slug, - subcategories: [], - } - ); - } - - const blocksToInclude = categoryDefinition?.blocks || []; - const blocksToExclude = categoryDefinition?.exclude || []; - const categoryExamples = examples.filter( ( example ) => { - return ( - ! blocksToExclude.includes( example.name ) && - ( example.category === categoryDefinition.slug || - blocksToInclude.includes( example.name ) ) - ); - } ); - - if ( ! categoryExamples.length ) { - return; - } - - return { - title: categoryDefinition.title, - slug: categoryDefinition.slug, - examples: categoryExamples, - }; -} - -/** - * Returns category examples for a given category definition and list of examples. - * - * @return {StyleBookCategory[]} An array of top-level category definitions. - */ -export function getTopLevelStyleBookCategories(): StyleBookCategory[] { - const reservedCategories = [ - ...STYLE_BOOK_THEME_SUBCATEGORIES, - ...STYLE_BOOK_CATEGORIES, - ].map( ( { slug } ) => slug ); - const extraCategories = getCategories().filter( - ( { slug } ) => ! reservedCategories.includes( slug ) - ); - return [ ...STYLE_BOOK_CATEGORIES, ...extraCategories ]; -} diff --git a/packages/edit-site/src/components/style-book/constants.ts b/packages/edit-site/src/components/style-book/constants.ts deleted file mode 100644 index fc06d8f1409f0d..00000000000000 --- a/packages/edit-site/src/components/style-book/constants.ts +++ /dev/null @@ -1,191 +0,0 @@ -/** - * WordPress dependencies - */ -import { __ } from '@wordpress/i18n'; - -/** - * Internal dependencies - */ -import type { StyleBookCategory } from './types'; - -export const STYLE_BOOK_THEME_SUBCATEGORIES: Omit< - StyleBookCategory, - 'subcategories' ->[] = [ - { - slug: 'site-identity', - title: __( 'Site Identity' ), - blocks: [ 'core/site-logo', 'core/site-title', 'core/site-tagline' ], - }, - { - slug: 'design', - title: __( 'Design' ), - blocks: [ 'core/navigation', 'core/avatar', 'core/post-time-to-read' ], - exclude: [ 'core/home-link', 'core/navigation-link' ], - }, - { - slug: 'posts', - title: __( 'Posts' ), - blocks: [ - 'core/post-title', - 'core/post-excerpt', - 'core/post-author', - 'core/post-author-name', - 'core/post-author-biography', - 'core/post-date', - 'core/post-terms', - 'core/term-description', - 'core/query-title', - 'core/query-no-results', - 'core/query-pagination', - 'core/query-numbers', - ], - }, - { - slug: 'comments', - title: __( 'Comments' ), - blocks: [ - 'core/comments-title', - 'core/comments-pagination', - 'core/comments-pagination-numbers', - 'core/comments', - 'core/comments-author-name', - 'core/comment-content', - 'core/comment-date', - 'core/comment-edit-link', - 'core/comment-reply-link', - 'core/comment-template', - 'core/post-comments-count', - 'core/post-comments-link', - ], - }, -]; - -export const STYLE_BOOK_CATEGORIES: StyleBookCategory[] = [ - { - slug: 'text', - title: __( 'Text' ), - blocks: [ - 'core/post-content', - 'core/home-link', - 'core/navigation-link', - ], - }, - { - slug: 'colors', - title: __( 'Colors' ), - blocks: [ 'custom/colors' ], - }, - { - slug: 'theme', - title: __( 'Theme' ), - subcategories: STYLE_BOOK_THEME_SUBCATEGORIES, - }, - { - slug: 'media', - title: __( 'Media' ), - blocks: [ 'core/post-featured-image' ], - }, - { - slug: 'widgets', - title: __( 'Widgets' ), - blocks: [], - }, - { - slug: 'embed', - title: __( 'Embeds' ), - include: [], - }, -]; - -// 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. -export const STYLE_BOOK_IFRAME_STYLES = ` - // 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; - } - - .edit-site-style-book__examples { - max-width: 1200px; - margin: 0 auto; - } - - .edit-site-style-book__example { - max-width: 900px; - border-radius: 2px; - cursor: pointer; - display: flex; - flex-direction: column; - gap: 40px; - padding: 16px; - width: 100%; - box-sizing: border-box; - scroll-margin-top: 32px; - scroll-margin-bottom: 32px; - margin: 0 auto 40px auto; - } - - .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__subcategory-title, - .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__subcategory-title { - font-size: 16px; - margin-bottom: 40px; - border-bottom: 1px solid #ddd; - padding-bottom: 8px; - } - - .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; - } -`; diff --git a/packages/edit-site/src/components/style-book/examples.ts b/packages/edit-site/src/components/style-book/examples.ts deleted file mode 100644 index 80807b10374c68..00000000000000 --- a/packages/edit-site/src/components/style-book/examples.ts +++ /dev/null @@ -1,63 +0,0 @@ -/** - * WordPress dependencies - */ -import { __, sprintf } from '@wordpress/i18n'; -import { - getBlockType, - getBlockTypes, - getBlockFromExample, - createBlock, -} from '@wordpress/blocks'; - -/** - * Internal dependencies - */ -import type { BlockExample } from './types'; - -/** - * Returns a list of examples for registered block types. - * - * @return {BlockExample[]} An array of block examples. - */ -export function getExamples(): BlockExample[] { - const nonHeadingBlockExamples = getBlockTypes() - .filter( ( blockType ) => { - const { name, example, supports } = blockType; - return ( - name !== 'core/heading' && - !! example && - supports.inserter !== false - ); - } ) - .map( ( blockType ) => ( { - name: blockType.name, - title: blockType.title, - category: blockType.category, - blocks: getBlockFromExample( blockType.name, blockType.example ), - } ) ); - const isHeadingBlockRegistered = !! getBlockType( 'core/heading' ); - - if ( ! isHeadingBlockRegistered ) { - return nonHeadingBlockExamples; - } - - // Use our own example for the Heading block so that we can show multiple - // heading levels. - const headingsExample = { - name: 'core/heading', - title: __( 'Headings' ), - category: 'text', - blocks: [ 1, 2, 3, 4, 5, 6 ].map( ( level ) => { - return createBlock( 'core/heading', { - content: sprintf( - // translators: %d: heading level e.g: "1", "2", "3" - __( 'Heading %d' ), - level - ), - level, - } ); - } ), - }; - - return [ headingsExample, ...nonHeadingBlockExamples ]; -} diff --git a/packages/edit-site/src/components/style-book/index.js b/packages/edit-site/src/components/style-book/index.js index e68474e19f407f..64503dcf7a6dbb 100644 --- a/packages/edit-site/src/components/style-book/index.js +++ b/packages/edit-site/src/components/style-book/index.js @@ -12,6 +12,13 @@ import { privateApis as componentsPrivateApis, } from '@wordpress/components'; import { __, sprintf } from '@wordpress/i18n'; +import { + getCategories, + getBlockType, + getBlockTypes, + getBlockFromExample, + createBlock, +} from '@wordpress/blocks'; import { BlockList, privateApis as blockEditorPrivateApis, @@ -30,12 +37,6 @@ import { ENTER, SPACE } from '@wordpress/keycodes'; */ import { unlock } from '../../lock-unlock'; import EditorCanvasContainer from '../editor-canvas-container'; -import { STYLE_BOOK_IFRAME_STYLES } from './constants'; -import { - getExamplesByCategory, - getTopLevelStyleBookCategories, -} from './categories'; -import { getExamples } from './examples'; const { ExperimentalBlockEditorProvider, @@ -47,10 +48,126 @@ const { mergeBaseAndUserConfigs } = unlock( editorPrivateApis ); const { Tabs } = unlock( componentsPrivateApis ); +// 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; + scroll-margin-top: 32px; + scroll-margin-bottom: 32px; + } + + .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 isObjectEmpty( object ) { return ! object || Object.keys( object ).length === 0; } +function getExamples() { + const nonHeadingBlockExamples = getBlockTypes() + .filter( ( blockType ) => { + const { name, example, supports } = blockType; + return ( + name !== 'core/heading' && + !! example && + supports.inserter !== false + ); + } ) + .map( ( blockType ) => ( { + name: blockType.name, + title: blockType.title, + category: blockType.category, + blocks: getBlockFromExample( blockType.name, blockType.example ), + } ) ); + + const isHeadingBlockRegistered = !! getBlockType( 'core/heading' ); + + if ( ! isHeadingBlockRegistered ) { + return nonHeadingBlockExamples; + } + + // Use our own example for the Heading block so that we can show multiple + // heading levels. + const headingsExample = { + name: 'core/heading', + title: __( 'Headings' ), + category: 'text', + blocks: [ 1, 2, 3, 4, 5, 6 ].map( ( level ) => { + return createBlock( 'core/heading', { + content: sprintf( + // translators: %d: heading level e.g: "1", "2", "3" + __( 'Heading %d' ), + level + ), + level, + } ); + } ), + }; + + return [ headingsExample, ...nonHeadingBlockExamples ]; +} + function StyleBook( { enableResizing = true, isSelected, @@ -67,11 +184,17 @@ function StyleBook( { const [ examples ] = useState( getExamples ); const tabs = useMemo( () => - getTopLevelStyleBookCategories().filter( ( category ) => - examples.some( - ( example ) => example.category === category.slug + getCategories() + .filter( ( category ) => + examples.some( + ( example ) => example.category === category.slug + ) ) - ), + .map( ( category ) => ( { + name: category.slug, + title: category.title, + icon: category.icon, + } ) ), [ examples ] ); const { base: baseConfig } = useContext( GlobalStylesContext ); @@ -125,8 +248,8 @@ function StyleBook( { { tabs.map( ( tab ) => ( { tab.title } @@ -134,12 +257,12 @@ function StyleBook( { { tabs.map( ( tab ) => ( { - const categoryDefinition = category - ? getTopLevelStyleBookCategories().find( - ( _category ) => _category.slug === category - ) - : null; - - const filteredExamples = categoryDefinition - ? getExamplesByCategory( categoryDefinition, examples ) - : { examples }; - return ( - { !! filteredExamples?.examples?.length && - filteredExamples.examples.map( ( example ) => ( + { examples + .filter( ( example ) => + category ? example.category === category : true + ) + .map( ( example ) => ( ) ) } - { !! filteredExamples?.subcategories?.length && - filteredExamples.subcategories.map( ( subcategory ) => ( - - -

- { subcategory.title } -

-
- -
- ) ) }
); } ); -const Subcategory = ( { examples, isSelected, onSelect } ) => { - return ( - !! examples?.length && - examples.map( ( example ) => ( - { - onSelect?.( example.name ); - } } - /> - ) ) - ); -}; - const Example = ( { id, title, blocks, isSelected, onClick } ) => { const originalSettings = useSelect( ( select ) => select( blockEditorStore ).getSettings(), diff --git a/packages/edit-site/src/components/style-book/test/categories.js b/packages/edit-site/src/components/style-book/test/categories.js deleted file mode 100644 index 5629689e260f89..00000000000000 --- a/packages/edit-site/src/components/style-book/test/categories.js +++ /dev/null @@ -1,171 +0,0 @@ -/** - * Internal dependencies - */ -import { - getExamplesByCategory, - getTopLevelStyleBookCategories, -} from '../categories'; -import { STYLE_BOOK_CATEGORIES } from '../constants'; - -jest.mock( '@wordpress/blocks', () => { - return { - getCategories() { - return [ - { - slug: 'text', - title: 'Text Registered', - icon: 'text', - }, - { - slug: 'design', - title: 'Design Registered', - icon: 'design', - }, - { - slug: 'funky', - title: 'Funky', - icon: 'funky', - }, - ]; - }, - }; -} ); - -// Fixtures -const exampleThemeBlocks = [ - { - name: 'core/post-content', - title: 'Post Content', - category: 'theme', - }, - { - name: 'core/post-terms', - title: 'Post Terms', - category: 'theme', - }, - { - name: 'core/home-link', - title: 'Home Link', - category: 'design', - }, - { - name: 'custom/colors', - title: 'Colors', - category: 'colors', - }, - { - name: 'core/site-logo', - title: 'Site Logo', - category: 'theme', - }, - { - name: 'core/site-title', - title: 'Site Title', - category: 'theme', - }, - { - name: 'core/site-tagline', - title: 'Site Tagline', - category: 'theme', - }, - { - name: 'core/group', - title: 'Group', - category: 'design', - }, - { - name: 'core/comments-pagination-numbers', - title: 'Comments Page Numbers', - category: 'theme', - }, - { - name: 'core/post-featured-image', - title: 'Featured Image', - category: 'theme', - }, -]; - -describe( 'utils', () => { - describe( 'getTopLevelStyleBookCategories', () => { - it( 'returns theme subcategories examples', () => { - expect( getTopLevelStyleBookCategories() ).toEqual( [ - ...STYLE_BOOK_CATEGORIES, - { - slug: 'funky', - title: 'Funky', - icon: 'funky', - }, - ] ); - } ); - } ); - - describe( 'getExamplesByCategory', () => { - it( 'returns theme subcategories examples', () => { - const themeCategory = STYLE_BOOK_CATEGORIES.find( - ( category ) => category.slug === 'theme' - ); - const themeCategoryExamples = getExamplesByCategory( - themeCategory, - exampleThemeBlocks - ); - - expect( themeCategoryExamples.slug ).toEqual( 'theme' ); - - const siteIdentity = themeCategoryExamples.subcategories.find( - ( subcategory ) => subcategory.slug === 'site-identity' - ); - expect( siteIdentity ).toEqual( { - title: 'Site Identity', - slug: 'site-identity', - examples: [ - { - name: 'core/site-logo', - title: 'Site Logo', - category: 'theme', - }, - { - name: 'core/site-title', - title: 'Site Title', - category: 'theme', - }, - { - name: 'core/site-tagline', - title: 'Site Tagline', - category: 'theme', - }, - ], - } ); - - const design = themeCategoryExamples.subcategories.find( - ( subcategory ) => subcategory.slug === 'design' - ); - expect( design ).toEqual( { - title: 'Design', - slug: 'design', - examples: [ - { - name: 'core/group', - title: 'Group', - category: 'design', - }, - ], - } ); - - const posts = themeCategoryExamples.subcategories.find( - ( subcategory ) => subcategory.slug === 'posts' - ); - - expect( posts ).toEqual( { - title: 'Posts', - slug: 'posts', - examples: [ - { - name: 'core/post-terms', - title: 'Post Terms', - category: 'theme', - }, - ], - } ); - } ); - } ); -} ); diff --git a/packages/edit-site/src/components/style-book/types.ts b/packages/edit-site/src/components/style-book/types.ts deleted file mode 100644 index 4729b38b1b2bb1..00000000000000 --- a/packages/edit-site/src/components/style-book/types.ts +++ /dev/null @@ -1,27 +0,0 @@ -type Block = { - name: string; - attributes: Record< string, unknown >; - innerBlocks?: Block[]; -}; - -export type StyleBookCategory = { - title: string; - slug: string; - blocks?: string[]; - exclude?: string[]; - subcategories?: StyleBookCategory[]; -}; - -export type BlockExample = { - name: string; - title: string; - category: string; - blocks: Block | Block[]; -}; - -export type CategoryExamples = { - title: string; - slug: string; - examples?: BlockExample[]; - subcategories?: CategoryExamples[]; -}; diff --git a/test/e2e/specs/site-editor/style-book.spec.js b/test/e2e/specs/site-editor/style-book.spec.js index 3f871d28ef941b..c4e153e9b5e2fa 100644 --- a/test/e2e/specs/site-editor/style-book.spec.js +++ b/test/e2e/specs/site-editor/style-book.spec.js @@ -42,6 +42,9 @@ test.describe( 'Style Book', () => { 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();