diff --git a/blockparty-faq.php b/blockparty-faq.php index a0c11f4..dca702f 100644 --- a/blockparty-faq.php +++ b/blockparty-faq.php @@ -83,9 +83,9 @@ function blockparty_faq_init(): void { 'forceExpand' => false, 'hasAnimation' => true, 'openMultiple' => false, - 'panelSelector' => '.faq__panel', + 'panelSelector' => '.wp-block-blockparty-faq-answer', 'prefixId' => 'block-faq', - 'triggerSelector' => '.faq__trigger', + 'triggerSelector' => '.wp-block-blockparty-faq-trigger', ] ), ]; diff --git a/languages/blockparty-faq-fr_FR-4ad5d476d786190bd86e6dbedf4bb86e.json b/languages/blockparty-faq-fr_FR-4ad5d476d786190bd86e6dbedf4bb86e.json new file mode 100644 index 0000000..0b47103 --- /dev/null +++ b/languages/blockparty-faq-fr_FR-4ad5d476d786190bd86e6dbedf4bb86e.json @@ -0,0 +1 @@ +{"translation-revision-date":"2026-06-26 15:28+0200","generator":"WP-CLI\/2.11.0","source":"build\/faq-answer\/index.js","domain":"messages","locale_data":{"messages":{"":{"domain":"messages","lang":"fr_FR","plural-forms":"nplurals=2; plural=(n != 1);"},"Answer\u2026":["R\u00e9ponse\u2026"]}}} \ No newline at end of file diff --git a/languages/blockparty-faq-fr_FR-79fbb2fc2e001b2243c4fd8903190184.json b/languages/blockparty-faq-fr_FR-79fbb2fc2e001b2243c4fd8903190184.json new file mode 100644 index 0000000..738315e --- /dev/null +++ b/languages/blockparty-faq-fr_FR-79fbb2fc2e001b2243c4fd8903190184.json @@ -0,0 +1 @@ +{"translation-revision-date":"2026-06-26 15:28+0200","generator":"WP-CLI\/2.11.0","source":"build\/faq-item\/index.js","domain":"messages","locale_data":{"messages":{"":{"domain":"messages","lang":"fr_FR","plural-forms":"nplurals=2; plural=(n != 1);"},"Add FAQ item":["Ajouter un \u00e9l\u00e9ment"],"Remove FAQ item":["Supprimer l\u2019\u00e9l\u00e9ment"]}}} \ No newline at end of file diff --git a/languages/blockparty-faq-fr_FR-8adf06b76d6c09cb7e7b053a4d516d16.json b/languages/blockparty-faq-fr_FR-8adf06b76d6c09cb7e7b053a4d516d16.json new file mode 100644 index 0000000..e22a3df --- /dev/null +++ b/languages/blockparty-faq-fr_FR-8adf06b76d6c09cb7e7b053a4d516d16.json @@ -0,0 +1 @@ +{"translation-revision-date":"2026-06-26 15:28+0200","generator":"WP-CLI\/2.11.0","source":"build\/faq-question\/index.js","domain":"messages","locale_data":{"messages":{"":{"domain":"messages","lang":"fr_FR","plural-forms":"nplurals=2; plural=(n != 1);"},"Question\u2026?":["Question\u2026?"]}}} \ No newline at end of file diff --git a/languages/blockparty-faq-fr_FR-a3c855d2ec3455f2223f2d3db0d980f8.json b/languages/blockparty-faq-fr_FR-a3c855d2ec3455f2223f2d3db0d980f8.json new file mode 100644 index 0000000..478f0df --- /dev/null +++ b/languages/blockparty-faq-fr_FR-a3c855d2ec3455f2223f2d3db0d980f8.json @@ -0,0 +1,39 @@ +{ + "translation-revision-date": "2026-06-26 15:28+0200", + "generator": "WP-CLI\/2.11.0", + "source": "build\/faq\/index.js", + "domain": "messages", + "locale_data": { + "messages": { + "": { + "domain": "messages", + "lang": "fr_FR", + "plural-forms": "nplurals=2; plural=(n != 1);" + }, + "Add FAQ item": [ + "Ajouter un \u00e9l\u00e9ment" + ], + "Remove FAQ item": [ + "Supprimer l\u2019\u00e9l\u00e9ment" + ], + "Settings": [ + "R\u00e9glages" + ], + "Accordion behavior": [ + "Comportement d\u2019accord\u00e9on" + ], + "If enabled, the FAQ will be displayed as an accordion component.": [ + "Si cette option est activ\u00e9e, la FAQ s'affichera sous forme d'accord\u00e9on." + ], + "Question heading level": [ + "Niveau de titre de la question" + ], + "Define the heading level for each FAQ question.": [ + "D\u00e9finissez le niveau de titre pour chaque question de la FAQ." + ], + "Heading level %d": [ + "Niveau de titre %d" + ] + } + } +} diff --git a/languages/blockparty-faq-fr_FR-dfbff627e6c248bcb3b61d7d06da9ca9.json b/languages/blockparty-faq-fr_FR-dfbff627e6c248bcb3b61d7d06da9ca9.json deleted file mode 100644 index 0553b08..0000000 --- a/languages/blockparty-faq-fr_FR-dfbff627e6c248bcb3b61d7d06da9ca9.json +++ /dev/null @@ -1 +0,0 @@ -{"translation-revision-date":"2026-01-26 16:50+0100","generator":"WP-CLI\/2.11.0","source":"build\/index.js","domain":"messages","locale_data":{"messages":{"":{"domain":"messages","lang":"fr_FR","plural-forms":"nplurals=2; plural=(n != 1);"},"Add FAQ item":["Ajouter un \u00e9l\u00e9ment"],"Remove FAQ item":["Supprimer l\u2019\u00e9l\u00e9ment"],"FAQ Settings":["R\u00e9glages de FAQ"],"Accordion behavior":["Comportement d\u2019accord\u00e9on"],"If enabled, the HTML structure will be interpreted as an accordion from screen readers.":["Si c\u2019est activ\u00e9, la structure HTML du bloc sera interpr\u00e9t\u00e9 en tant qu\u2019accord\u00e9on pour les technologies d\u2019assistance."],"Question\u2026?":["Question\u2026?"],"Answer\u2026":["R\u00e9ponse\u2026"]}}} \ No newline at end of file diff --git a/languages/blockparty-faq-fr_FR.mo b/languages/blockparty-faq-fr_FR.mo index 239529e..1ff2012 100644 Binary files a/languages/blockparty-faq-fr_FR.mo and b/languages/blockparty-faq-fr_FR.mo differ diff --git a/languages/blockparty-faq-fr_FR.po b/languages/blockparty-faq-fr_FR.po index 7d38da3..273bf5b 100644 --- a/languages/blockparty-faq-fr_FR.po +++ b/languages/blockparty-faq-fr_FR.po @@ -4,15 +4,15 @@ msgid "" msgstr "" "Project-Id-Version: Blockparty Faq 1.0.2\n" "Report-Msgid-Bugs-To: https://wordpress.org/support/plugin/blockparty-faq\n" -"POT-Creation-Date: 2026-01-26T15:49:49+00:00\n" -"PO-Revision-Date: 2026-01-26 16:50+0100\n" +"POT-Creation-Date: 2026-06-26T13:28:07+00:00\n" +"PO-Revision-Date: 2026-06-26 15:28+0200\n" "Last-Translator: \n" "Language-Team: \n" "Language: fr_FR\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"X-Generator: Poedit 3.8\n" +"X-Generator: Poedit 3.9\n" "X-Domain: blockparty-faq\n" #. Plugin Name of the plugin @@ -40,51 +40,47 @@ msgstr "" msgid "Be API Technical team" msgstr "L’équipe technique de Be API" -#: build/index.js:1 +#: build/faq-answer/index.js:1 +msgid "Answer…" +msgstr "Réponse…" + +#: build/faq-item/index.js:1 build/faq/index.js:1 msgid "Add FAQ item" msgstr "Ajouter un élément" -#: build/index.js:1 +#: build/faq-item/index.js:1 build/faq/index.js:1 msgid "Remove FAQ item" msgstr "Supprimer l’élément" -#: build/index.js:1 -msgid "FAQ Settings" -msgstr "Réglages de FAQ" +#: build/faq-question/index.js:1 +msgid "Question…?" +msgstr "Question…?" -#: build/index.js:1 +#: build/faq/index.js:1 +msgid "Settings" +msgstr "Réglages" + +#: build/faq/index.js:1 msgid "Accordion behavior" msgstr "Comportement d’accordéon" -#: build/index.js:1 -msgid "" -"If enabled, the HTML structure will be interpreted as an accordion from " -"screen readers." +#: build/faq/index.js:1 +msgid "If enabled, the FAQ will be displayed as an accordion component." msgstr "" -"Si c’est activé, la structure HTML du bloc sera interprété en tant " -"qu’accordéon pour les technologies d’assistance." +"Si cette option est activée, la FAQ s'affichera sous forme d'accordéon." -#: build/index.js:1 -msgid "Question…?" -msgstr "Question…?" +#: build/faq/index.js:1 +msgid "Question heading level" +msgstr "Niveau de titre de la question" -#: build/index.js:1 -msgid "Answer…" -msgstr "Réponse…" - -#: block.json -msgctxt "block title" -msgid "FAQ" -msgstr "FAQ" +#: build/faq/index.js:1 +msgid "Define the heading level for each FAQ question." +msgstr "Définissez le niveau de titre pour chaque question de la FAQ." -#: block.json -msgctxt "block description" -msgid "" -"A FAQ block for WordPress Editor that provided structured data based on FAQ " -"schema." -msgstr "" -"A FAQ block for WordPress Editor that provided structured data based on FAQ " -"schema." +#. translators: %d: heading level number (2–6). +#: build/faq/index.js:2 +msgid "Heading level %d" +msgstr "Niveau de titre %d" #: build/faq-answer/block.json msgctxt "block title" @@ -115,3 +111,17 @@ msgstr "Question de FAQ" msgctxt "block description" msgid "Content of the FAQ question." msgstr "Contenu de la question FAQ." + +#: build/faq/block.json +msgctxt "block title" +msgid "FAQ" +msgstr "FAQ" + +#: build/faq/block.json +msgctxt "block description" +msgid "" +"A FAQ block for WordPress Editor that provided structured data based on FAQ " +"schema." +msgstr "" +"A FAQ block for WordPress Editor that provided structured data based on FAQ " +"schema." diff --git a/languages/blockparty-faq.pot b/languages/blockparty-faq.pot index 9d514cd..3bf6be4 100644 --- a/languages/blockparty-faq.pot +++ b/languages/blockparty-faq.pot @@ -2,14 +2,14 @@ # This file is distributed under the GPL-2.0-or-later. msgid "" msgstr "" -"Project-Id-Version: Blockparty FAQ 1.0.2\n" +"Project-Id-Version: Blockparty FAQ 2.0.3\n" "Report-Msgid-Bugs-To: https://wordpress.org/support/plugin/blockparty-faq\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"POT-Creation-Date: 2026-01-26T15:49:49+00:00\n" +"POT-Creation-Date: 2026-06-26T13:28:07+00:00\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "X-Generator: WP-CLI 2.11.0\n" "X-Domain: blockparty-faq\n" @@ -35,42 +35,47 @@ msgstr "" msgid "Be API Technical team" msgstr "" -#: build/index.js:1 +#: build/faq-answer/index.js:1 +msgid "Answer…" +msgstr "" + +#: build/faq-item/index.js:1 +#: build/faq/index.js:1 msgid "Add FAQ item" msgstr "" -#: build/index.js:1 +#: build/faq-item/index.js:1 +#: build/faq/index.js:1 msgid "Remove FAQ item" msgstr "" -#: build/index.js:1 -msgid "FAQ Settings" +#: build/faq-question/index.js:1 +msgid "Question…?" msgstr "" -#: build/index.js:1 -msgid "Accordion behavior" +#: build/faq/index.js:1 +msgid "Settings" msgstr "" -#: build/index.js:1 -msgid "If enabled, the HTML structure will be interpreted as an accordion from screen readers." +#: build/faq/index.js:1 +msgid "Accordion behavior" msgstr "" -#: build/index.js:1 -msgid "Question…?" +#: build/faq/index.js:1 +msgid "If enabled, the FAQ will be displayed as an accordion component." msgstr "" -#: build/index.js:1 -msgid "Answer…" +#: build/faq/index.js:1 +msgid "Question heading level" msgstr "" -#: block.json -msgctxt "block title" -msgid "FAQ" +#: build/faq/index.js:1 +msgid "Define the heading level for each FAQ question." msgstr "" -#: block.json -msgctxt "block description" -msgid "A FAQ block for WordPress Editor that provided structured data based on FAQ schema." +#. translators: %d: heading level number (2–6). +#: build/faq/index.js:2 +msgid "Heading level %d" msgstr "" #: build/faq-answer/block.json @@ -102,3 +107,13 @@ msgstr "" msgctxt "block description" msgid "Content of the FAQ question." msgstr "" + +#: build/faq/block.json +msgctxt "block title" +msgid "FAQ" +msgstr "" + +#: build/faq/block.json +msgctxt "block description" +msgid "A FAQ block for WordPress Editor that provided structured data based on FAQ schema." +msgstr "" diff --git a/package-lock.json b/package-lock.json index b42f093..57185aa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "blockparty-faq", - "version": "1.0.2", + "version": "2.0.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "blockparty-faq", - "version": "1.0.2", + "version": "2.0.3", "license": "GPL-2.0-or-later", "dependencies": { "@beapi/be-a11y": "^1.7.2", diff --git a/src/faq-answer/deprecated.js b/src/faq-answer/deprecated.js new file mode 100644 index 0000000..73a69df --- /dev/null +++ b/src/faq-answer/deprecated.js @@ -0,0 +1,42 @@ +/** + * WordPress dependencies + */ +import { useBlockProps, InnerBlocks } from '@wordpress/block-editor'; + +/** + * Save function for deprecated 2.0.x markup with faq__panel class. + * + * @param {Object} props Component props. + * @param {Object} props.attributes Block attributes. + * @return {JSX.Element} Saved block markup. + */ +function deprecatedSave( { attributes } ) { + const { isAccordion = true } = attributes; + const blockProps = useBlockProps.save( { + className: 'faq__panel', + } ); + + const divProps = isAccordion + ? { ...blockProps, role: 'region' } + : blockProps; + + return ( +
+ +
+ ); +} + +const deprecated = [ + { + attributes: { + isAccordion: { + type: 'boolean', + default: true, + }, + }, + save: deprecatedSave, + }, +]; + +export default deprecated; diff --git a/src/faq-answer/edit.js b/src/faq-answer/edit.js index e5ae80b..f86ab76 100644 --- a/src/faq-answer/edit.js +++ b/src/faq-answer/edit.js @@ -13,24 +13,24 @@ const ALLOWED_BLOCKS = [ ]; export default function Edit() { - const blockProps = useBlockProps( { - className: 'faq__panel', - } ); + const blockProps = useBlockProps(); return ( -
- +
+
+ +
); } diff --git a/src/faq-answer/index.js b/src/faq-answer/index.js index cb97459..66c6552 100644 --- a/src/faq-answer/index.js +++ b/src/faq-answer/index.js @@ -11,6 +11,7 @@ import './style.scss'; import './editor.scss'; import Edit from './edit'; import save from './save'; +import deprecated from './deprecated'; import metadata from './block.json'; registerBlockType( metadata.name, { @@ -24,5 +25,10 @@ registerBlockType( metadata.name, { * @see ./save.js */ save, + + /** + * @see ./deprecated.js + */ + deprecated, icon: postContent, } ); diff --git a/src/faq-answer/save.js b/src/faq-answer/save.js index 7a5afe0..8cff587 100644 --- a/src/faq-answer/save.js +++ b/src/faq-answer/save.js @@ -1,21 +1,20 @@ /** * WordPress dependencies */ -import { useBlockProps, InnerBlocks } from '@wordpress/block-editor'; +import { useBlockProps, useInnerBlocksProps } from '@wordpress/block-editor'; export default function save( { attributes } ) { const { isAccordion = true } = attributes; - const blockProps = useBlockProps.save( { - className: 'faq__panel', + const blockProps = useBlockProps.save( + isAccordion ? { role: 'region' } : {} + ); + const innerBlocksProps = useInnerBlocksProps.save( { + className: 'wp-block-blockparty-faq-answer__inner', } ); - const divProps = isAccordion - ? { ...blockProps, role: 'region' } - : blockProps; - return ( -
- +
+
); } diff --git a/src/faq-item/deprecated.js b/src/faq-item/deprecated.js new file mode 100644 index 0000000..63f4a2e --- /dev/null +++ b/src/faq-item/deprecated.js @@ -0,0 +1,29 @@ +/** + * WordPress dependencies + */ +import { useBlockProps, InnerBlocks } from '@wordpress/block-editor'; + +/** + * Save function for deprecated 2.0.x markup with faq__item class. + * + * @return {JSX.Element} Saved block markup. + */ +function deprecatedSave() { + const blockProps = useBlockProps.save( { + className: 'faq__item', + } ); + + return ( +
+ +
+ ); +} + +const deprecated = [ + { + save: deprecatedSave, + }, +]; + +export default deprecated; diff --git a/src/faq-item/edit.js b/src/faq-item/edit.js index a5d1b94..039b97c 100644 --- a/src/faq-item/edit.js +++ b/src/faq-item/edit.js @@ -3,8 +3,8 @@ */ import { useBlockProps, + useInnerBlocksProps, BlockControls, - InnerBlocks, } from '@wordpress/block-editor'; import { ToolbarGroup, ToolbarButton } from '@wordpress/components'; import { addCard, trash } from '@wordpress/icons'; @@ -13,8 +13,14 @@ import { createBlock } from '@wordpress/blocks'; import { __ } from '@wordpress/i18n'; export default function Edit( { clientId } ) { - const blockProps = useBlockProps( { - className: 'faq__item', + const blockProps = useBlockProps(); + const innerBlocksProps = useInnerBlocksProps( blockProps, { + allowedBlocks: [ 'blockparty/faq-question', 'blockparty/faq-answer' ], + template: [ + [ 'blockparty/faq-question' ], + [ 'blockparty/faq-answer' ], + ], + templateLock: 'all', } ); const { insertBlock, removeBlock } = useDispatch( 'core/block-editor' ); @@ -26,16 +32,19 @@ export default function Edit( { clientId } ) { const rootClientId = getBlockRootClientId( clientId ); const blockIndex = getBlockIndex( clientId ); - // Get isAccordion from parent block (faq) const parentBlock = rootClientId ? getBlock( rootClientId ) : null; const isAccordion = parentBlock?.attributes?.isAccordion !== undefined ? parentBlock.attributes.isAccordion : true; + const headingLevel = parentBlock?.attributes?.headingLevel ?? 3; const onAddItem = () => { const newItem = createBlock( 'blockparty/faq-item', {}, [ - createBlock( 'blockparty/faq-question', { isAccordion } ), + createBlock( 'blockparty/faq-question', { + isAccordion, + headingLevel, + } ), createBlock( 'blockparty/faq-answer', { isAccordion } ), ] ); insertBlock( newItem, blockIndex + 1, rootClientId ); @@ -61,19 +70,7 @@ export default function Edit( { clientId } ) { /> -
- -
+
); } diff --git a/src/faq-item/index.js b/src/faq-item/index.js index 1d35264..1ebbd4d 100644 --- a/src/faq-item/index.js +++ b/src/faq-item/index.js @@ -11,6 +11,7 @@ import './style.scss'; import './editor.scss'; import Edit from './edit'; import save from './save'; +import deprecated from './deprecated'; import metadata from './block.json'; registerBlockType( metadata.name, { @@ -24,5 +25,10 @@ registerBlockType( metadata.name, { * @see ./save.js */ save, + + /** + * @see ./deprecated.js + */ + deprecated, icon: list, } ); diff --git a/src/faq-item/save.js b/src/faq-item/save.js index a3fd3fc..5314afe 100644 --- a/src/faq-item/save.js +++ b/src/faq-item/save.js @@ -4,9 +4,7 @@ import { useBlockProps, InnerBlocks } from '@wordpress/block-editor'; export default function save() { - const blockProps = useBlockProps.save( { - className: 'faq__item', - } ); + const blockProps = useBlockProps.save(); return (
diff --git a/src/faq-question/block.json b/src/faq-question/block.json index 907f8a1..b28917f 100644 --- a/src/faq-question/block.json +++ b/src/faq-question/block.json @@ -7,6 +7,7 @@ "category": "design", "parent": [ "blockparty/faq-item" ], "description": "Content of the FAQ question.", + "usesContext": [ "blockparty/headingLevel" ], "supports": { "html": false, "inserter": false, @@ -23,6 +24,11 @@ "isAccordion": { "type": "boolean", "default": true + }, + "headingLevel": { + "type": "number", + "default": 3, + "enum": [ 2, 3, 4, 5, 6 ] } }, "textdomain": "blockparty-faq", diff --git a/src/faq-question/deprecated.js b/src/faq-question/deprecated.js new file mode 100644 index 0000000..935b0fd --- /dev/null +++ b/src/faq-question/deprecated.js @@ -0,0 +1,53 @@ +/** + * WordPress dependencies + */ +import { useBlockProps, RichText, InnerBlocks } from '@wordpress/block-editor'; + +/** + * Save function for deprecated 2.0.x markup with faq__title and faq__trigger classes. + * + * @param {Object} props Component props. + * @param {Object} props.attributes Block attributes. + * @return {JSX.Element} Saved block markup. + */ +function deprecatedSave( { attributes } ) { + const { question, isAccordion = true } = attributes; + const blockProps = useBlockProps.save( { + className: 'faq__title', + } ); + + const TriggerTag = isAccordion ? 'button' : 'span'; + const triggerProps = isAccordion + ? { className: 'faq__trigger', 'aria-expanded': 'false' } + : { className: 'faq__trigger' }; + + return ( +

+ + { isAccordion ? ( + + ) : ( + + ) } + +

+ ); +} + +const deprecated = [ + { + attributes: { + question: { + type: 'string', + default: '', + }, + isAccordion: { + type: 'boolean', + default: true, + }, + }, + save: deprecatedSave, + }, +]; + +export default deprecated; diff --git a/src/faq-question/edit.js b/src/faq-question/edit.js index 2b13fff..024725b 100644 --- a/src/faq-question/edit.js +++ b/src/faq-question/edit.js @@ -13,11 +13,21 @@ import { __ } from '@wordpress/i18n'; const ALLOWED_BLOCKS_SIMPLE = [ 'core/heading', 'core/paragraph' ]; -export default function Edit( { attributes, setAttributes, clientId } ) { - const { question, isAccordion = true } = attributes; - const blockProps = useBlockProps( { - className: 'faq__title', - } ); +export default function Edit( { + attributes, + setAttributes, + clientId, + context = {}, +} ) { + const { + question, + isAccordion = true, + headingLevel: savedHeadingLevel, + } = attributes; + const headingLevel = + context[ 'blockparty/headingLevel' ] ?? savedHeadingLevel ?? 3; + const HeadingTag = `h${ headingLevel }`; + const blockProps = useBlockProps(); const innerBlocks = useSelect( ( select ) => select( 'core/block-editor' ).getBlocks( clientId ), @@ -26,21 +36,14 @@ export default function Edit( { attributes, setAttributes, clientId } ) { const { replaceInnerBlocks } = useDispatch( 'core/block-editor' ); - // When isAccordion changes from true to false and innerBlocks are empty, - // create a core/heading block with the question attribute value - // When isAccordion changes from false to true, remove innerBlocks and - // transfer their content to the question attribute useEffect( () => { if ( ! isAccordion && innerBlocks.length === 0 && question ) { - // Switch from accordion to non-accordion: create heading block const headingBlock = createBlock( 'core/heading', { level: 3, content: question, } ); replaceInnerBlocks( clientId, [ headingBlock ] ); } else if ( isAccordion && innerBlocks.length > 0 ) { - // Switch from non-accordion to accordion: extract content from innerBlocks - // Get the text content from the first block (usually a heading or paragraph) const firstBlock = innerBlocks[ 0 ]; let extractedContent = ''; @@ -50,12 +53,10 @@ export default function Edit( { attributes, setAttributes, clientId } ) { extractedContent = firstBlock.attributes?.content || ''; } - // Update the question attribute with the extracted content if ( extractedContent && extractedContent !== question ) { setAttributes( { question: extractedContent } ); } - // Remove innerBlocks replaceInnerBlocks( clientId, [] ); } }, [ @@ -67,38 +68,41 @@ export default function Edit( { attributes, setAttributes, clientId } ) { setAttributes, ] ); + if ( ! isAccordion ) { + return ( +

+ +

+ ); + } + return ( -

-
- { isAccordion ? ( - - setAttributes( { question: content } ) - } - placeholder={ __( 'Question…?', 'blockparty-faq' ) } - allowedFormats={ [] } - /> - ) : ( - - ) } -
-

+ + + setAttributes( { question: content } ) + } + placeholder={ __( 'Question…?', 'blockparty-faq' ) } + allowedFormats={ [] } + /> + ); } diff --git a/src/faq-question/index.js b/src/faq-question/index.js index 4ef60a3..c5bb7e1 100644 --- a/src/faq-question/index.js +++ b/src/faq-question/index.js @@ -11,6 +11,7 @@ import './style.scss'; import './editor.scss'; import Edit from './edit'; import save from './save'; +import deprecated from './deprecated'; import metadata from './block.json'; registerBlockType( metadata.name, { @@ -24,5 +25,10 @@ registerBlockType( metadata.name, { * @see ./save.js */ save, + + /** + * @see ./deprecated.js + */ + deprecated, icon: listItem, } ); diff --git a/src/faq-question/save.js b/src/faq-question/save.js index 4ae0e3b..22276db 100644 --- a/src/faq-question/save.js +++ b/src/faq-question/save.js @@ -4,25 +4,30 @@ import { useBlockProps, RichText, InnerBlocks } from '@wordpress/block-editor'; export default function save( { attributes } ) { - const { question, isAccordion = true } = attributes; - const blockProps = useBlockProps.save( { - className: 'faq__title', - } ); + const { question, isAccordion = true, headingLevel = 3 } = attributes; + const blockProps = useBlockProps.save(); + const HeadingTag = `h${ headingLevel }`; - const TriggerTag = isAccordion ? 'button' : 'span'; - const triggerProps = isAccordion - ? { className: 'faq__trigger', 'aria-expanded': 'false' } - : { className: 'faq__trigger' }; + if ( ! isAccordion ) { + return ( +

+ +

+ ); + } return ( -

- - { isAccordion ? ( - - ) : ( - - ) } - -

+ + + ); } diff --git a/src/faq/block.json b/src/faq/block.json index 372550f..e744445 100644 --- a/src/faq/block.json +++ b/src/faq/block.json @@ -14,8 +14,16 @@ "isAccordion": { "type": "boolean", "default": true + }, + "headingLevel": { + "type": "number", + "default": 3, + "enum": [ 2, 3, 4, 5, 6 ] } }, + "providesContext": { + "blockparty/headingLevel": "headingLevel" + }, "example": { "attributes": { "isAccordion": true diff --git a/src/faq/deprecated.js b/src/faq/deprecated.js index 89f2059..146cc5e 100644 --- a/src/faq/deprecated.js +++ b/src/faq/deprecated.js @@ -2,7 +2,7 @@ * WordPress dependencies */ import { createBlock, parse } from '@wordpress/blocks'; -import { useBlockProps, RichText } from '@wordpress/block-editor'; +import { useBlockProps, RichText, InnerBlocks } from '@wordpress/block-editor'; /** * Migration script to convert old FAQ format to new InnerBlocks format. @@ -184,6 +184,23 @@ function deprecatedSave( { attributes } ) { ); } +/** + * Save function for deprecated 2.0.x nested InnerBlocks format with faq__accordion wrapper. + * + * @return {JSX.Element} Saved block markup. + */ +function deprecatedNestedSave() { + const blockProps = useBlockProps.save(); + + return ( +
+
+ +
+
+ ); +} + /** * Deprecated block configuration for migration from old format. * @@ -191,6 +208,19 @@ function deprecatedSave( { attributes } ) { * New format: InnerBlocks with faq-item blocks */ const deprecated = [ + { + attributes: { + isAccordion: { + type: 'boolean', + default: true, + }, + }, + supports: { + html: false, + innerBlocks: true, + }, + save: deprecatedNestedSave, + }, { attributes: { questions: { diff --git a/src/faq/edit.js b/src/faq/edit.js index f8ec1ba..09d2423 100644 --- a/src/faq/edit.js +++ b/src/faq/edit.js @@ -3,25 +3,90 @@ */ import { BlockControls, - InnerBlocks, useBlockProps, + useInnerBlocksProps, InspectorControls, + useBlockEditContext, + store as blockEditorStore, } from '@wordpress/block-editor'; import { ToolbarGroup, ToolbarButton, PanelBody, ToggleControl, + // eslint-disable-next-line @wordpress/no-unsafe-wp-apis -- ToggleGroupControl is not yet a stable export in @wordpress/components 27. + __experimentalToggleGroupControl as ToggleGroupControl, + // eslint-disable-next-line @wordpress/no-unsafe-wp-apis + __experimentalToggleGroupControlOptionIcon as ToggleGroupControlOptionIcon, } from '@wordpress/components'; -import { addCard } from '@wordpress/icons'; +import { + addCard, + headingLevel2, + headingLevel3, + headingLevel4, + headingLevel5, + headingLevel6, +} from '@wordpress/icons'; import { useDispatch, useSelect } from '@wordpress/data'; import { createBlock } from '@wordpress/blocks'; -import { __ } from '@wordpress/i18n'; +import { __, sprintf } from '@wordpress/i18n'; import { useEffect } from '@wordpress/element'; +const QUESTION_BLOCK = 'blockparty/faq-question'; +const HEADING_LEVELS = [ 2, 3, 4, 5, 6 ]; + +const HEADING_LEVEL_ICONS = { + 2: headingLevel2, + 3: headingLevel3, + 4: headingLevel4, + 5: headingLevel5, + 6: headingLevel6, +}; + +function collectQuestionBlocks( blocks ) { + return blocks.flatMap( ( block ) => { + const questions = block.name === QUESTION_BLOCK ? [ block ] : []; + + return questions.concat( + collectQuestionBlocks( block.innerBlocks || [] ) + ); + } ); +} + +function useSyncQuestionHeadingLevels( headingLevel, isAccordion ) { + const { clientId } = useBlockEditContext(); + const { updateBlockAttributes } = useDispatch( blockEditorStore ); + const questionBlocks = useSelect( + ( select ) => { + const { getBlocksByClientId } = select( blockEditorStore ); + const [ faqBlock ] = getBlocksByClientId( clientId ); + + return collectQuestionBlocks( faqBlock?.innerBlocks || [] ); + }, + [ clientId ] + ); + + useEffect( () => { + if ( ! isAccordion ) { + return; + } + + questionBlocks.forEach( ( block ) => { + if ( block.attributes.headingLevel !== headingLevel ) { + updateBlockAttributes( block.clientId, { headingLevel } ); + } + } ); + }, [ headingLevel, isAccordion, questionBlocks, updateBlockAttributes ] ); +} + export default function Edit( { clientId, attributes, setAttributes } ) { - const { isAccordion = true } = attributes; + const { isAccordion = true, headingLevel = 3 } = attributes; const blockProps = useBlockProps(); + const innerBlocksProps = useInnerBlocksProps( blockProps, { + allowedBlocks: [ 'blockparty/faq-item' ], + template: [ [ 'blockparty/faq-item' ] ], + templateLock: false, + } ); const { insertBlock, updateBlockAttributes } = useDispatch( 'core/block-editor' ); @@ -30,11 +95,12 @@ export default function Edit( { clientId, attributes, setAttributes } ) { [] ); + useSyncQuestionHeadingLevels( headingLevel, isAccordion ); + // Synchronize isAccordion attribute to all child blocks useEffect( () => { const innerBlocks = getBlocks( clientId ); innerBlocks.forEach( ( block ) => { - // Update faq-item blocks if ( 'blockparty/faq-item' === block.name ) { const itemInnerBlocks = getBlocks( block.clientId ); itemInnerBlocks.forEach( ( itemBlock ) => { @@ -54,7 +120,10 @@ export default function Edit( { clientId, attributes, setAttributes } ) { const onAddItem = () => { const newItem = createBlock( 'blockparty/faq-item', {}, [ - createBlock( 'blockparty/faq-question', { isAccordion } ), + createBlock( 'blockparty/faq-question', { + isAccordion, + headingLevel, + } ), createBlock( 'blockparty/faq-answer', { isAccordion } ), ] ); insertBlock( newItem, undefined, clientId ); @@ -72,11 +141,11 @@ export default function Edit( { clientId, attributes, setAttributes } ) { - + + { isAccordion && ( + + setAttributes( { + headingLevel: Number( value ), + } ) + } + > + { HEADING_LEVELS.map( ( level ) => ( + + ) ) } + + ) } -
-
- -
-
+
); } diff --git a/src/faq/editor.scss b/src/faq/editor.scss index a3367df..edb80b3 100644 --- a/src/faq/editor.scss +++ b/src/faq/editor.scss @@ -1,24 +1,34 @@ -.wp-block-blockparty-faq-block { +/* editor */ +.wp-block-blockparty-faq { - .faq-list { + &-item, + &-answer, + .wp-block { + max-width: 100% !important; + } - &__button { - float: right; - opacity: 0; - z-index: 999; - position: relative; + &-item, + &-answer { + margin-top: 0; + margin-bottom: 0; + } - &:hover { - color: #007cba; - } - } + &-answer { + display: none; + } - &__item:hover .faq-list__button { /* stylelint-disable-line */ - opacity: 1; - } + &-item.has-child-selected .wp-block-blockparty-faq-answer { + display: block; + } + + &-item.has-child-selected .wp-block-blockparty-faq-trigger::after { + transform: translateY(-50%) rotate(-135deg); + } + + &-question { - &__add-item svg { - margin-right: 0.25em; + .rich-text { + white-space: nowrap; } } } diff --git a/src/faq/save.js b/src/faq/save.js index 718f494..9b56627 100644 --- a/src/faq/save.js +++ b/src/faq/save.js @@ -1,16 +1,11 @@ /** * WordPress dependencies */ -import { useBlockProps, InnerBlocks } from '@wordpress/block-editor'; +import { useBlockProps, useInnerBlocksProps } from '@wordpress/block-editor'; export default function save() { const blockProps = useBlockProps.save(); + const innerBlocksProps = useInnerBlocksProps.save( blockProps ); - return ( -
-
- -
-
- ); + return
; } diff --git a/src/faq/script.js b/src/faq/script.js index 9990707..9e2a33f 100644 --- a/src/faq/script.js +++ b/src/faq/script.js @@ -3,10 +3,15 @@ import { Accordion } from '@beapi/be-a11y'; // eslint-disable-next-line no-undef const accordionConfig = beapiFaqBlock.accordionConfig; -// Initialize beapi-accordion window.addEventListener( 'load', function () { Accordion.init( - '.wp-block-blockparty-faq:has(button.faq__trigger)', + '.wp-block-blockparty-faq:has(button.wp-block-blockparty-faq-trigger)', accordionConfig ); + + Accordion.init( '.wp-block-blockparty-faq:has(button.faq__trigger)', { + ...accordionConfig, + panelSelector: '.faq__panel', + triggerSelector: '.faq__trigger', + } ); } ); diff --git a/src/faq/style.scss b/src/faq/style.scss index 6cec69c..1c13bb9 100644 --- a/src/faq/style.scss +++ b/src/faq/style.scss @@ -1,13 +1,115 @@ -.wp-block-blockparty-faq-block { +/* style */ +.wp-block-blockparty-faq { + --wp--blockparty--faq--border: 1px solid #000; + --wp--blockparty--faq--font-size: var(--wp--preset--font-size--h-3); + --wp--blockparty--faq--font-weight: 400; + --wp--blockparty--faq--padding: 1rem 0; + --wp--blockparty--faq--padding-with-background: 1rem; + --wp--blockparty--faq--icon-size: 24px; - .faq { + &-question { + position: relative; + display: flex; + align-items: center; + padding: var(--wp--blockparty--faq--padding) !important; + border-bottom: var(--wp--blockparty--faq--border); + } + + &-item:where(:has([aria-expanded="false"])) &-answer { + display: none; + } + + &-answer__inner { + padding: var(--wp--blockparty--faq--padding); + } + + &-trigger { + position: relative; + display: flex; + align-items: center; + width: 100%; + padding: 0; + font-size: var(--wp--blockparty--faq--font-size); + font-weight: var(--wp--blockparty--faq--font-weight); + text-align: left; + background-color: transparent; + border: 0; + border-radius: 0; + box-shadow: none; + appearance: none; + + &::after { + position: absolute; + top: 50%; + right: 0.5rem; + width: 0.5rem; + height: 0.5rem; + pointer-events: none; + content: ""; + border: solid currentcolor; + border-width: 0 2px 2px 0; + transform: translateY(-60%) rotate(45deg); + } + + &[aria-expanded="true"]::after { + transform: translateY(-50%) rotate(-135deg); + } + } + + &-question.has-background, + .has-background &-question, + .has-background &-answer__inner { + padding: var(--wp--blockparty--faq--padding-with-background) !important; + } + + // Legacy markup (2.0.x) + .faq__title { + position: relative; + display: flex; + align-items: center; + padding: var(--wp--blockparty--faq--padding) !important; + border-bottom: var(--wp--blockparty--faq--border); + } + + .faq__item:where(:has([aria-expanded="false"])) .faq__panel { + display: none; + } + + .faq__panel { + padding: var(--wp--blockparty--faq--padding); + } + + .faq__trigger { + position: relative; + display: flex; + align-items: center; + width: 100%; + padding: 0; + font-size: var(--wp--blockparty--faq--font-size); + font-weight: var(--wp--blockparty--faq--font-weight); + text-align: left; + background-color: transparent; + border: 0; + border-radius: 0; + box-shadow: none; + appearance: none; + cursor: pointer; + + &::after { + position: absolute; + top: 50%; + right: 0.5rem; + width: 0.5rem; + height: 0.5rem; + pointer-events: none; + content: ""; + border: solid currentcolor; + border-width: 0 2px 2px 0; + transform: translateY(-60%) rotate(45deg); + } - &__trigger { - appearance: none; - background: transparent; - border: 0; - cursor: pointer; - width: 100%; + &[aria-expanded="true"]::after { + transform: translateY(-50%) rotate(-135deg); } } } diff --git a/src/index.js b/src/index.js deleted file mode 100644 index 78bd1c0..0000000 --- a/src/index.js +++ /dev/null @@ -1,11 +0,0 @@ -/** - * WordPress dependencies - */ - -/** - * Internal dependencies - */ -import './faq'; -import './faq-item'; -import './faq-question'; -import './faq-answer';