diff --git a/.changeset/brown-games-jog.md b/.changeset/brown-games-jog.md new file mode 100644 index 000000000..64708c9dc --- /dev/null +++ b/.changeset/brown-games-jog.md @@ -0,0 +1,52 @@ +--- +'@faustwp/blocks': minor +--- + +### WHAT + +Refactor: Added CoreListItem block to fix repeating sublist issue + +- Added CoreListItem block +- Updated CoreList block +- Updated Corelist.test to accomodate new HTML structure +- Added a new scenario to test nested lists + +### WHY + +CoreList was rendering values attribute, which happens to return nested list items multiple times. + +### HOW + +You need to add new CoreListItem fragments to your queries: + +```javascript +gql` + ${blocks.CoreListItem.fragments.entry} +`; +``` + +Example query: + +```javascript +SingleTemplate.query = gql` + ${blocks.CoreList.fragments.entry} + ${blocks.CoreListItem.fragments.entry} + query GetPost( + $uri: ID! + ) { + post(id: $uri, idType: URI) { + title + content + editorBlocks { + name + __typename + renderedHtml + id: clientId + parentId: parentClientId + ...${blocks.CoreList.fragments.key} + ...${blocks.CoreListItem.fragments.key} + } + } + } +`; +``` diff --git a/packages/blocks/src/blocks/CoreList.tsx b/packages/blocks/src/blocks/CoreList.tsx index ea8176c8f..2b4858274 100644 --- a/packages/blocks/src/blocks/CoreList.tsx +++ b/packages/blocks/src/blocks/CoreList.tsx @@ -1,10 +1,15 @@ import { gql } from '@apollo/client'; import React from 'react'; import { useBlocksTheme } from '../components/WordPressBlocksProvider.js'; -import { ContentBlock } from '../components/WordPressBlocksViewer.js'; +import { + BlockWithAttributes, + ContentBlock, + WordPressBlocksViewer, +} from '../components/WordPressBlocksViewer.js'; import { getStyles } from '../utils/index.js'; +import { CoreListItemFragmentProps } from './CoreListItem.js'; -export type CoreListFragmentProps = ContentBlock & { +export type CoreListFragmentProps = Omit & { attributes?: { anchor?: string; backgroundColor?: string; @@ -22,6 +27,7 @@ export type CoreListFragmentProps = ContentBlock & { values?: string; cssClassName?: string; }; + innerBlocks?: Array; }; export function CoreList(props: CoreListFragmentProps) { @@ -33,18 +39,42 @@ export function CoreList(props: CoreListFragmentProps) { return null; } + const innerBlocks = props?.innerBlocks ?? []; + const hasInnerBlocks = innerBlocks.some((ib) => ib?.attributes); + const ListLevel = attributes?.ordered ? 'ol' : 'ul'; + if (process.env.NODE_ENV === 'development' && !hasInnerBlocks) { + console.warn(`[Faust.js] To ensure compatibility with the next major release, update your template queries to use 'blocks.CoreListItem.fragments'. + +Without this update, CoreList will NOT render list items in the next major version. + +This warning is shown only in development mode. + `); + } + + const listProps = { + style, + className: attributes?.cssClassName, + reversed: + attributes?.ordered && attributes?.reversed === true ? true : undefined, + start: + attributes?.ordered && attributes?.start ? attributes?.start : undefined, + }; + + if (hasInnerBlocks) { + return ( + // eslint-disable-next-line react/jsx-props-no-spreading + + + + ); + } + return ( diff --git a/packages/blocks/src/blocks/CoreListItem.tsx b/packages/blocks/src/blocks/CoreListItem.tsx new file mode 100644 index 000000000..1849a5a82 --- /dev/null +++ b/packages/blocks/src/blocks/CoreListItem.tsx @@ -0,0 +1,82 @@ +import { gql } from '@apollo/client'; +import React from 'react'; +import { useBlocksTheme } from '../components/WordPressBlocksProvider.js'; +import { + BlockWithAttributes, + ContentBlock, + WordPressBlocksViewer, +} from '../components/WordPressBlocksViewer.js'; +import { getStyles } from '../utils/index.js'; + +export type CoreListItemFragmentProps = ContentBlock & { + attributes?: { + content?: string; + anchor?: string; + backgroundColor?: string; + borderColor?: string; + className?: string; + fontFamily?: string; + fontSize?: string; + gradient?: string; + lock?: string; + metadata?: string; + placeholder?: string; + style?: string; + textColor?: string; + }; + innerBlocks?: Array; +}; + +export function CoreListItem(props: CoreListItemFragmentProps) { + const theme = useBlocksTheme(); + const style = getStyles(theme, { ...props }); + const { attributes, innerBlocks } = props; + + const content = attributes?.content; + + if (!content) { + return null; + } + + const ownContent = content ? content.split('\n')[0] : ''; + + return ( +
  • +
    + + +
  • + ); +} + +CoreListItem.fragments = { + key: `CoreListItemFragment`, + entry: gql` + fragment CoreListItemFragment on CoreListItem { + attributes { + content + anchor + backgroundColor + borderColor + className + fontFamily + fontSize + gradient + lock + metadata + placeholder + style + textColor + } + } + `, +}; + +CoreListItem.config = { + name: 'CoreListItem', +}; + +CoreListItem.displayName = 'CoreListItem'; diff --git a/packages/blocks/src/blocks/index.ts b/packages/blocks/src/blocks/index.ts index 6d9ad4a42..ad09097e6 100644 --- a/packages/blocks/src/blocks/index.ts +++ b/packages/blocks/src/blocks/index.ts @@ -10,6 +10,7 @@ import { CoreImage } from './CoreImage.js'; import { CoreSeparator } from './CoreSeparator.js'; import { CoreList } from './CoreList.js'; import { CoreHeading } from './CoreHeading.js'; +import { CoreListItem } from './CoreListItem.js'; export default { CoreParagraph: CoreParagraph, @@ -20,6 +21,7 @@ export default { CoreImage: CoreImage, CoreSeparator: CoreSeparator, CoreList: CoreList, + CoreListItem: CoreListItem, CoreButton: CoreButton, CoreButtons: CoreButtons, CoreHeading: CoreHeading, diff --git a/packages/blocks/src/types/blocks.ts b/packages/blocks/src/types/blocks.ts index a76870063..30d0cd6c7 100644 --- a/packages/blocks/src/types/blocks.ts +++ b/packages/blocks/src/types/blocks.ts @@ -6,6 +6,7 @@ type BlockKeys = | 'core/heading' | 'core/gallery' | 'core/list' + | 'core/list-item' | 'core/quote' | 'core/shortcode' | 'core/archives' diff --git a/packages/blocks/tests/blocks/CoreList.test.tsx b/packages/blocks/tests/blocks/CoreList.test.tsx index 2b3ce86d2..f9eb555c6 100644 --- a/packages/blocks/tests/blocks/CoreList.test.tsx +++ b/packages/blocks/tests/blocks/CoreList.test.tsx @@ -4,10 +4,16 @@ import '@testing-library/jest-dom'; import { render, screen } from '@testing-library/react'; import { WordPressBlocksProvider } from '../../src/components/WordPressBlocksProvider'; import { CoreList, CoreListFragmentProps } from '../../src/blocks/CoreList'; +import { CoreListItem } from '../../src/blocks/CoreListItem'; function renderProvider(props: CoreListFragmentProps) { + const blocks = { + CoreListItem, + CoreList, + }; + return render( - + , ); @@ -18,15 +24,126 @@ describe('', () => { renderProvider({ attributes: { values: '
  • Some Item
  • Another Item
  • One more item
  • ', + ordered: true, }, + innerBlocks: [ + { + __typename: 'CoreListItem', + attributes: { + content: 'Some Item', + }, + }, + { + __typename: 'CoreListItem', + attributes: { + content: 'Another Item', + }, + }, + { + __typename: 'CoreListItem', + attributes: { + content: 'One more item', + }, + }, + ], }); expect(screen.getByRole('list')).toBeInTheDocument(); + expect(screen.getByRole('list')).toHaveProperty('nodeName', 'OL'); expect( - screen.getAllByRole('listitem').map((el) => el.textContent), + screen + .getAllByRole('listitem') + .map((el) => el.querySelector('div')?.textContent), ).toStrictEqual(['Some Item', 'Another Item', 'One more item']); }); + test('renders deep lists', () => { + const tree = renderProvider({ + attributes: { + values: + '
  • Level 1\n
      \n
    • Level 2\n
        \n
      • Level 3
      • \n
      \n
    • \n
    \n
  • Level 2\n
      \n
    • Level 3
    • \n
    \n
  • Level 3
  • Level 2\n
      \n
    • Level 3
    • \n
    \n
  • Level 3
  • Level 3
  • ', + cssClassName: 'wp-block-list', + }, + innerBlocks: [ + { + __typename: 'CoreListItem', + attributes: { + content: + 'Level 1\n
      \n
    • Level 2\n
        \n
      • Level 3
      • \n
      \n
    • \n
    \nLevel 2\n
      \n
    • Level 3
    • \n
    \nLevel 3', + }, + innerBlocks: [ + { + __typename: 'CoreList', + attributes: { + values: + '
  • Level 2\n
      \n
    • Level 3
    • \n
    \n
  • Level 3
  • Level 3
  • ', + cssClassName: 'wp-block-list', + }, + innerBlocks: [ + { + __typename: 'CoreListItem', + attributes: { + content: + 'Level 2\n
      \n
    • Level 3
    • \n
    \nLevel 3', + }, + innerBlocks: [ + { + __typename: 'CoreList', + attributes: { + values: '
  • Level 3
  • ', + cssClassName: 'wp-block-list', + }, + innerBlocks: [ + { + __typename: 'CoreListItem', + attributes: { + content: 'Level 3', + }, + }, + ], + }, + ], + }, + ], + }, + ], + }, + ], + }); + + expect(tree.container).toMatchInlineSnapshot(` +
    +
      +
    • +
      + Level 1 +
      +
        +
      • +
        + Level 2 +
        +
          +
        • +
          + Level 3 +
          +
        • +
        +
      • +
      +
    • +
    +
    + `); + }); + test('applies the correct styles', () => { const tree = renderProvider({ attributes: { @@ -40,6 +157,26 @@ describe('', () => { cssClassName: 'has-background-color has-text-color has-background has-large-font-size', }, + innerBlocks: [ + { + __typename: 'CoreListItem', + attributes: { + content: 'My', + }, + }, + { + __typename: 'CoreListItem', + attributes: { + content: 'Ordered', + }, + }, + { + __typename: 'CoreListItem', + attributes: { + content: 'List', + }, + }, + ], }); expect(tree.container).toMatchInlineSnapshot(` @@ -51,13 +188,19 @@ describe('', () => { style="background-color: rgb(91, 43, 43);" >
  • - My +
    + My +
  • - Unordered +
    + Ordered +
  • - List +
    + List +