diff --git a/accordion/.gitignore b/accordion/.gitignore new file mode 100644 index 0000000..f42e1b1 --- /dev/null +++ b/accordion/.gitignore @@ -0,0 +1,35 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Coverage directory used by tools like istanbul +coverage + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Output of `npm pack` +*.tgz + +# Output of `wp-scripts plugin-zip` +*.zip + +# dotenv environment variables file +.env + +# Build files +build/ + +package-lock.json \ No newline at end of file diff --git a/accordion/accordion.php b/accordion/accordion.php new file mode 100644 index 0000000..03164e2 --- /dev/null +++ b/accordion/accordion.php @@ -0,0 +1,33 @@ + +
+
+ ); +} diff --git a/accordion/src/accordion-content/index.js b/accordion/src/accordion-content/index.js new file mode 100644 index 0000000..d37ee59 --- /dev/null +++ b/accordion/src/accordion-content/index.js @@ -0,0 +1,28 @@ +import { registerBlockType } from '@wordpress/blocks'; +import { SVG, Path } from '@wordpress/components'; +import Edit from './edit'; +import save from './save'; +import metadata from './block.json'; + +const icon = ( + + + +); + +registerBlockType( metadata.name, { + icon, + edit: Edit, + save, +} ); diff --git a/accordion/src/accordion-content/save.js b/accordion/src/accordion-content/save.js new file mode 100644 index 0000000..2698c87 --- /dev/null +++ b/accordion/src/accordion-content/save.js @@ -0,0 +1,46 @@ +import { __ } from '@wordpress/i18n'; +import { + InnerBlocks, + useBlockProps, + __experimentalGetBorderClassesAndStyles as getBorderClassesAndStyles, + __experimentalGetColorClassesAndStyles as getColorClassesAndStyles, + __experimentalGetSpacingClassesAndStyles as getSpacingClassesAndStyles, + __experimentalGetShadowClassesAndStyles as getShadowClassesAndStyles, +} from '@wordpress/block-editor'; +import clsx from 'clsx'; + +export default function save( { attributes } ) { + const blockProps = useBlockProps.save(); + const borderProps = getBorderClassesAndStyles( attributes ); + const colorProps = getColorClassesAndStyles( attributes ); + const spacingProps = getSpacingClassesAndStyles( attributes ); + const shadowProps = getShadowClassesAndStyles( attributes ); + + return ( +
+
+ +
+
+ ); +} diff --git a/accordion/src/accordion-group/block.json b/accordion/src/accordion-group/block.json new file mode 100644 index 0000000..0d035b4 --- /dev/null +++ b/accordion/src/accordion-group/block.json @@ -0,0 +1,52 @@ +{ + "$schema": "https://schemas.wp.org/trunk/block.json", + "apiVersion": 3, + "name": "wpcomsp/accordion-group", + "version": "0.1.0", + "title": "Accordion Group", + "category": "design", + "description": "A group of headers and associated expandable content.", + "example": {}, + "supports": { + "html": false, + "align": [ "wide", "full" ], + "background": { + "backgroundImage": true, + "backgroundSize": true, + "__experimentalDefaultControls": { + "backgroundImage": true + } + }, + "color": { + "background": true, + "gradient": true + }, + "spacing": { + "padding": true, + "margin": [ "top", "bottom" ], + "blockGap": true + }, + "shadow": true, + "layout": true, + "interactivity": true + }, + "attributes": { + "iconPosition": { + "type": "string", + "default": "right" + }, + "autoclose": { + "type": "boolean", + "default": false + }, + "allowedBlocks": { + "type": "array" + } + }, + "allowedBlocks": [ "wpcomsp/accordion-item" ], + "textdomain": "accordion", + "editorScript": "file:./index.js", + "viewScriptModule": "file:./view.js", + "render": "file:./render.php", + "style": "file:./style-index.css" +} diff --git a/accordion/src/accordion-group/edit.js b/accordion/src/accordion-group/edit.js new file mode 100644 index 0000000..2802d5b --- /dev/null +++ b/accordion/src/accordion-group/edit.js @@ -0,0 +1,42 @@ +import { + useBlockProps, + useInnerBlocksProps, + InspectorControls, +} from '@wordpress/block-editor'; +import { __ } from '@wordpress/i18n'; +import { PanelBody, ToggleControl } from '@wordpress/components'; + +const ACCORDION_BLOCK_NAME = 'wpcomsp/accordion-item'; +const ACCORDION_BLOCK = { + name: ACCORDION_BLOCK_NAME, +}; + +export default function Edit( { attributes: { autoclose }, setAttributes } ) { + const blockProps = useBlockProps(); + + const innerBlocksProps = useInnerBlocksProps( blockProps, { + template: [ [ ACCORDION_BLOCK_NAME ], [ ACCORDION_BLOCK_NAME ] ], + defaultBlock: ACCORDION_BLOCK, + directInsert: true, + } ); + + return ( + <> + + + { + setAttributes( { + autoclose: value, + } ); + } } + checked={ autoclose } + /> + + +
+ + ); +} diff --git a/accordion/src/accordion-group/index.js b/accordion/src/accordion-group/index.js new file mode 100644 index 0000000..d954bf5 --- /dev/null +++ b/accordion/src/accordion-group/index.js @@ -0,0 +1,37 @@ +import { registerBlockType } from '@wordpress/blocks'; +import { SVG, Path } from '@wordpress/components'; +import Edit from './edit'; +import save from './save'; +import metadata from './block.json'; +import './style.scss'; + +const icon = ( + + + + + + +); + +registerBlockType( metadata.name, { + icon, + edit: Edit, + save, +} ); diff --git a/accordion/src/accordion-group/render.php b/accordion/src/accordion-group/render.php new file mode 100644 index 0000000..c5668f1 --- /dev/null +++ b/accordion/src/accordion-group/render.php @@ -0,0 +1,12 @@ +next_tag() ){ + if ( $p->has_class( 'wp-block-wpcomsp-accordion-group') ) { + $p->set_attribute( 'data-wp-interactive', 'wpcomsp/accordion' ); + $p->set_attribute( 'data-wp-context', '{"isOpen":[],"autoclose":"' . $autoclose . '"}' ); + } +} + +echo $p->get_updated_html(); \ No newline at end of file diff --git a/accordion/src/accordion-group/save.js b/accordion/src/accordion-group/save.js new file mode 100644 index 0000000..b0d759b --- /dev/null +++ b/accordion/src/accordion-group/save.js @@ -0,0 +1,16 @@ +import clsx from 'clsx'; +import { InnerBlocks, useBlockProps } from '@wordpress/block-editor'; + +export default function save( { attributes } ) { + const { iconPosition } = attributes; + + const className = clsx( { + 'icon-position-left': iconPosition === 'left', + } ); + + return ( +
+ +
+ ); +} diff --git a/accordion/src/accordion-group/style.scss b/accordion/src/accordion-group/style.scss new file mode 100644 index 0000000..2a5d3ac --- /dev/null +++ b/accordion/src/accordion-group/style.scss @@ -0,0 +1,91 @@ +.wp-block-wpcomsp-accordion-item { + display: grid; + grid-template-rows: max-content 0fr; +} + +.wp-block-wpcomsp-accordion-item.is-open { + grid-template-rows: max-content 1fr; +} + +.wp-block-wpcomsp-accordion-item .wpcomsp-accordion-item__heading { + color: inherit; + padding: 0; + margin: 0; +} + +.wpcomsp-accordion-item__toggle { + font-family: inherit; + font-size: inherit; + font-weight: inherit; + line-height: inherit; + letter-spacing: inherit; + text-transform: inherit; + text-decoration: inherit; + word-spacing: inherit; + background: none; + border: none; + color: inherit; + padding: 0; + cursor: pointer; + outline: none; + display: flex; + align-items: center; + text-align: inherit; + position: relative; + width: 100%; +} + +.wpcomsp-accordion-item__toggle > span { + width: 100%; +} + +.is-layout-flow > .wp-block-wpcomsp-accordion-content, +.wp-block-wpcomsp-accordion-content { + overflow: hidden; + margin: 0; +} + + +/* No icon block style */ +.is-style-no-icon .wpcomsp-accordion-item__toggle-icon { + background-color: unset; +} + +.wp-block-wpcomsp-accordion-trigger.icon-position-left .wpcomsp-accordion-item__toggle { + flex-direction: row-reverse; +} + +.wpcomsp-accordion-item__toggle:focus, +.wpcomsp-accordion-item__toggle:focus-visible { + outline: 2px solid -webkit-focus-ring-color; + outline-offset: 2px; +} + +/* Add transitions only for users who do not prefer reduced motion */ +@media (prefers-reduced-motion: no-preference) { + .wp-block-wpcomsp-accordion-item .wpcomsp-accordion-item__toggle-icon { + transition: transform 0.2s ease-in-out; + } + + .wp-block-wpcomsp-accordion-item { + transition: grid-template-rows 0.3s ease-out; + } +} + +.is-open { + .wpcomsp-accordion-item__toggle-icon.has-icon-plus { + transform: rotate(45deg); + } + .wpcomsp-accordion-item__toggle-icon.has-icon-chevron { + transform: rotate(-180deg); + } + .wpcomsp-accordion-item__toggle-icon.has-icon-circlePlus { + transform: rotate(45deg); + } + .wpcomsp-accordion-item__toggle-icon.has-icon-caret { + transform: rotate(-180deg); + } + .wpcomsp-accordion-item__toggle-icon.has-icon-chevronRight { + transform: rotate(90deg); + } +} diff --git a/accordion/src/accordion-group/view.js b/accordion/src/accordion-group/view.js new file mode 100644 index 0000000..e1e2f7c --- /dev/null +++ b/accordion/src/accordion-group/view.js @@ -0,0 +1,44 @@ +import { store, getContext, getElement } from '@wordpress/interactivity'; + +const { state, actions } = store( 'wpcomsp/accordion', { + state: { + get isOpen() { + const { attributes } = getElement(); + const id = + attributes.id || + attributes[ 'aria-controls' ] || + attributes[ 'aria-labelledby' ]; + const context = getContext(); + return context.isOpen.includes( id ); + }, + }, + actions: { + toggle: () => { + const { attributes } = getElement(); + const id = attributes[ 'aria-controls' ]; + const context = getContext(); + if ( context.isOpen.includes( id ) ) { + if ( context.autoclose ) { + context.isOpen = []; + } else { + context.isOpen = context.isOpen.filter( + ( item ) => item !== id + ); + } + } else { + if ( context.autoclose ) { + context.isOpen = [ id ]; + } else { + context.isOpen = [ ...context.isOpen, id ]; + } + } + }, + }, + callbacks: { + open: () => { + const context = getContext(); + const { ref } = getElement(); + context.isOpen.push( ref.id ); + }, + }, +} ); diff --git a/accordion/src/accordion-item/block.json b/accordion/src/accordion-item/block.json new file mode 100644 index 0000000..d9ace5c --- /dev/null +++ b/accordion/src/accordion-item/block.json @@ -0,0 +1,51 @@ +{ + "$schema": "https://schemas.wp.org/trunk/block.json", + "apiVersion": 3, + "name": "wpcomsp/accordion-item", + "version": "0.1.0", + "title": "Accordion", + "category": "design", + "description": "A single accordion that displays a header and expandable content.", + "example": {}, + "parent": [ "wpcomsp/accordion-group" ], + "allowedBlocks": [ + "wpcomsp/accordion-trigger", + "wpcomsp/accordion-content" + ], + "supports": { + "align": [ "wide", "full" ], + "color": { + "background": true, + "gradient": true + }, + "border": true, + "interactivity": true, + "spacing": { + "margin": [ "top", "bottom" ], + "blockGap": true + }, + "__experimentalBorder": { + "color": true, + "radius": true, + "style": true, + "width": true, + "__experimentalDefaultControls": { + "color": true, + "radius": true, + "style": true, + "width": true + } + }, + "shadow": true, + "layout": true + }, + "attributes": { + "openByDefault": { + "type": "boolean", + "default": false + } + }, + "textdomain": "accordion", + "editorScript": "file:./index.js", + "render": "file:./render.php" +} diff --git a/accordion/src/accordion-item/edit.js b/accordion/src/accordion-item/edit.js new file mode 100644 index 0000000..1c2d8af --- /dev/null +++ b/accordion/src/accordion-item/edit.js @@ -0,0 +1,96 @@ +import { __ } from '@wordpress/i18n'; +import { + useBlockProps, + useInnerBlocksProps, + InspectorControls, + store as blockEditorStore, +} from '@wordpress/block-editor'; +import { useDispatch, useSelect } from '@wordpress/data'; +import { useEffect } from '@wordpress/element'; +import { PanelBody, ToggleControl } from '@wordpress/components'; +import clsx from 'clsx'; + +export default function Edit( { + attributes: { openByDefault }, + clientId, + setAttributes, +} ) { + const [ isSelected, getBlockOrder ] = useSelect( + ( select ) => { + const { isBlockSelected, hasSelectedInnerBlock, getBlockOrder } = + select( blockEditorStore ); + return [ + isBlockSelected( clientId ) || + hasSelectedInnerBlock( clientId, true ), + getBlockOrder, + ]; + }, + [ clientId ] + ); + + const contentBlockClientId = getBlockOrder( clientId )[ 1 ]; + const { updateBlockAttributes, __unstableMarkNextChangeAsNotPersistent } = + useDispatch( blockEditorStore ); + + useEffect( () => { + if ( contentBlockClientId ) { + __unstableMarkNextChangeAsNotPersistent(); + updateBlockAttributes( contentBlockClientId, { + isSelected: isSelected, + } ); + } + }, [ + isSelected, + contentBlockClientId, + __unstableMarkNextChangeAsNotPersistent, + updateBlockAttributes, + ] ); + + const blockProps = useBlockProps(); + const innerBlocksProps = useInnerBlocksProps( + { + ...blockProps, + className: clsx( blockProps.className, { + 'is-open': openByDefault || isSelected, + } ), + }, + { + template: [ + [ 'wpcomsp/accordion-trigger', {} ], + [ + 'wpcomsp/accordion-content', + { + isSelected: true, + openByDefault, + }, + ], + ], + templateLock: 'all', + directInsert: true, + } + ); + + return ( + <> + + + { + setAttributes( { + openByDefault: value, + } ); + if ( contentBlockClientId ) { + updateBlockAttributes( contentBlockClientId, { + openByDefault: value, + } ); + } + } } + checked={ openByDefault } + /> + + +
+ + ); +} diff --git a/accordion/src/accordion-item/icons.js b/accordion/src/accordion-item/icons.js new file mode 100644 index 0000000..43017f6 --- /dev/null +++ b/accordion/src/accordion-item/icons.js @@ -0,0 +1,118 @@ +import { SVG, Path } from '@wordpress/components'; + +export const chevron = ( { width, height } ) => { + return ( + + + + ); +}; + +export const plus = ( { width, height } ) => { + return ( + + + + ); +}; + +export const circlePlus = ( { width, height } ) => { + return ( + + + + + ); +}; + +export const circleMinus = ( { width, height } ) => { + return ( + + + + + ); +}; + +export const caret = ( { width, height } ) => { + return ( + + + + ); +}; + +export const chevronRight = ( { width, height } ) => { + return ( + + + + ); +}; diff --git a/accordion/src/accordion-item/index.js b/accordion/src/accordion-item/index.js new file mode 100644 index 0000000..7572151 --- /dev/null +++ b/accordion/src/accordion-item/index.js @@ -0,0 +1,41 @@ +import { registerBlockType } from '@wordpress/blocks'; +import { SVG, Path } from '@wordpress/components'; +import Edit from './edit'; +import save from './save'; +import metadata from './block.json'; + +const icon = ( + + + + + + +); + +registerBlockType( metadata.name, { + icon, + edit: Edit, + save, +} ); diff --git a/accordion/src/accordion-item/render.php b/accordion/src/accordion-item/render.php new file mode 100644 index 0000000..3c1ac38 --- /dev/null +++ b/accordion/src/accordion-item/render.php @@ -0,0 +1,36 @@ +next_tag() ){ + if ( $p->has_class( 'wp-block-wpcomsp-accordion-item') ) { + $p->set_attribute( 'id', $unique_id ); + $p->set_attribute( 'data-wp-class--is-open', 'state.isOpen' ); + if ( $attributes['openByDefault'] ) { + $p->set_attribute( 'data-wp-init', 'callbacks.open' ); + } + } +} + +$content = $p->get_updated_html(); +$p = new WP_HTML_Tag_Processor( $content ); + +while ( $p->next_tag() ){ + if ( $p->has_class( 'wpcomsp-accordion-item__toggle' ) ) { + $p->set_attribute( 'data-wp-on--click', 'actions.toggle' ); + $p->set_attribute( 'aria-controls', $unique_id ); + $p->set_attribute( 'data-wp-bind--aria-expanded', 'state.isOpen' ); + } +} + +$content = $p->get_updated_html(); +$p = new WP_HTML_Tag_Processor( $content ); + +while ( $p->next_tag() ){ + if ( $p->has_class( 'wp-block-wpcomsp-accordion-content' ) ) { + $p->set_attribute( 'aria-labelledby', $unique_id ); + $p->set_attribute( 'data-wp-bind--aria-hidden', '!state.isOpen' ); + } +} + +echo $p->get_updated_html(); \ No newline at end of file diff --git a/accordion/src/accordion-item/save.js b/accordion/src/accordion-item/save.js new file mode 100644 index 0000000..4cd8884 --- /dev/null +++ b/accordion/src/accordion-item/save.js @@ -0,0 +1,11 @@ +import clsx from 'clsx'; +import { __ } from '@wordpress/i18n'; +import { InnerBlocks, useBlockProps } from '@wordpress/block-editor'; + +export default function save( { attributes } ) { + return ( +
+ +
+ ); +} diff --git a/accordion/src/accordion-trigger/block.json b/accordion/src/accordion-trigger/block.json new file mode 100644 index 0000000..25743b4 --- /dev/null +++ b/accordion/src/accordion-trigger/block.json @@ -0,0 +1,95 @@ +{ + "$schema": "https://schemas.wp.org/trunk/block.json", + "apiVersion": 3, + "name": "wpcomsp/accordion-trigger", + "version": "0.1.0", + "title": "Trigger", + "category": "design", + "description": "Accordion item trigger.", + "example": {}, + "parent": [ "wpcomsp/accordion-item" ], + "supports": { + "anchor": true, + "color": { + "background": true, + "gradient": true + }, + "align": false, + "border": true, + "interactivity": true, + "spacing": { + "padding": true, + "margin": [ "top", "bottom" ], + "__experimentalDefaultControls": { + "padding": true, + "margin": true + } + }, + "__experimentalBorder": { + "color": true, + "radius": true, + "style": true, + "width": true, + "__experimentalDefaultControls": { + "color": true, + "radius": true, + "style": true, + "width": true + } + }, + "typography": { + "textAlign": true, + "fontSize": true, + "__experimentalFontFamily": true, + "__experimentalFontWeight": true, + "__experimentalFontStyle": true, + "__experimentalTextTransform": true, + "__experimentalTextDecoration": true, + "__experimentalLetterSpacing": true, + "__experimentalDefaultControls": { + "fontSize": true, + "fontFamily": true + } + }, + "shadow": true, + "layout": true + }, + "attributes": { + "openByDefault": { + "type": "boolean", + "default": false + }, + "title": { + "type": "rich-text", + "source": "rich-text", + "selector": "span" + }, + "level": { + "type": "number", + "default": 3 + }, + "textAlignment": { + "type": "string", + "default": "left" + }, + "icon": { + "type": [ "string", "boolean" ], + "enum": [ + "plus", + "chevron", + "chevronRight", + "caret", + "circlePlus", + false + ], + "default": "plus" + }, + "iconPosition": { + "type": "string", + "enum": [ "left", "right" ], + "default": "right" + } + }, + "textdomain": "accordion", + "editorScript": "file:./index.js" +} diff --git a/accordion/src/accordion-trigger/edit.js b/accordion/src/accordion-trigger/edit.js new file mode 100644 index 0000000..1f8638f --- /dev/null +++ b/accordion/src/accordion-trigger/edit.js @@ -0,0 +1,171 @@ +import clsx from 'clsx'; +import { __ } from '@wordpress/i18n'; +import { + useBlockProps, + __experimentalUseBorderProps as useBorderProps, + __experimentalUseColorProps as useColorProps, + __experimentalGetSpacingClassesAndStyles as useSpacingProps, + __experimentalGetShadowClassesAndStyles as useShadowProps, + BlockControls, + HeadingLevelDropdown, + RichText, + InspectorControls, +} from '@wordpress/block-editor'; +import { + PanelBody, + ToolbarGroup, + __experimentalToggleGroupControl as ToggleGroupControl, + __experimentalToggleGroupControlOption as ToggleGroupControlOption, + __experimentalToggleGroupControlOptionIcon as ToggleGroupControlOptionIcon, +} from '@wordpress/components'; +import { + caret, + chevron, + chevronRight, + circlePlus, + plus, +} from '../accordion-item/icons'; + +const ICONS = { + plus, + circlePlus, + chevron, + chevronRight, + caret, +}; + +export default function Edit( { attributes, setAttributes } ) { + const { level, title, textAlign, icon, iconPosition } = attributes; + const TagName = 'h' + level; + + const blockProps = useBlockProps(); + const borderProps = useBorderProps( attributes ); + const colorProps = useColorProps( attributes ); + const spacingProps = useSpacingProps( attributes ); + const shadowProps = useShadowProps( attributes ); + + const Icon = ICONS[ icon ]; + + return ( + <> + + + + setAttributes( { level: newLevel } ) + } + /> + + + + + + setAttributes( { icon: value } ) + } + > + + + + + + + + { + setAttributes( { iconPosition: value } ); + } } + > + + + + + + + + + + ); +} diff --git a/accordion/src/accordion-trigger/index.js b/accordion/src/accordion-trigger/index.js new file mode 100644 index 0000000..b5d5f1c --- /dev/null +++ b/accordion/src/accordion-trigger/index.js @@ -0,0 +1,29 @@ +import { registerBlockType } from '@wordpress/blocks'; +import { SVG, Path } from '@wordpress/components'; +import Edit from './edit'; +import save from './save'; +import metadata from './block.json'; + +const icon = ( + + + + +); + +registerBlockType( metadata.name, { + icon, + edit: Edit, + save, +} ); diff --git a/accordion/src/accordion-trigger/save.js b/accordion/src/accordion-trigger/save.js new file mode 100644 index 0000000..14adcea --- /dev/null +++ b/accordion/src/accordion-trigger/save.js @@ -0,0 +1,81 @@ +import clsx from 'clsx'; +import { __ } from '@wordpress/i18n'; +import { + useBlockProps, + __experimentalGetBorderClassesAndStyles as getBorderClassesAndStyles, + __experimentalGetColorClassesAndStyles as getColorClassesAndStyles, + __experimentalGetSpacingClassesAndStyles as getSpacingClassesAndStyles, + __experimentalGetShadowClassesAndStyles as getShadowClassesAndStyles, + RichText, +} from '@wordpress/block-editor'; +import { + caret, + chevron, + chevronRight, + circlePlus, + plus, +} from '../accordion-item/icons'; + +const ICONS = { + plus, + circlePlus, + chevron, + chevronRight, + caret, +}; + +export default function save( { attributes } ) { + const { level, title, iconPosition, textAlign, icon } = attributes; + const TagName = 'h' + level; + + const blockProps = useBlockProps.save(); + const borderProps = getBorderClassesAndStyles( attributes ); + const colorProps = getColorClassesAndStyles( attributes ); + const spacingProps = getSpacingClassesAndStyles( attributes ); + const shadowProps = getShadowClassesAndStyles( attributes ); + + const Icon = ICONS[ icon ]; + + return ( + + + + ); +}