diff --git a/web/app/themes/gds/app/Providers/PerformanceServiceProvider.php b/web/app/themes/gds/app/Providers/PerformanceServiceProvider.php
index 1afe5a07..138c78d7 100644
--- a/web/app/themes/gds/app/Providers/PerformanceServiceProvider.php
+++ b/web/app/themes/gds/app/Providers/PerformanceServiceProvider.php
@@ -31,6 +31,8 @@ public function earlyEnqeueueBlockStyles(): void
{
render_block(['blockName' => 'core/heading']);
render_block(['blockName' => 'core/paragraph']);
+ render_block(['blockName' => 'core/buttons']);
+ render_block(['blockName' => 'core/button']);
// Enqeueue stylesheets of the firt block.
if (is_singular() && $post = get_post()) {
diff --git a/web/app/themes/gds/app/setup.php b/web/app/themes/gds/app/setup.php
index b3895517..463e59a3 100755
--- a/web/app/themes/gds/app/setup.php
+++ b/web/app/themes/gds/app/setup.php
@@ -36,23 +36,6 @@
echo app(GoogleFonts::class)->load()->toHtml();
}, 7);
-/**
- * Always enqueue stylesheets for the following blocks in the
- */
-add_action('wp_enqueue_scripts', function () {
- render_block(['blockName' => 'core/heading']);
- render_block(['blockName' => 'core/paragraph']);
-
- // Enqeueue stylesheets of the first block.
- if (is_singular() && $post = get_post()) {
- $blocks = parse_blocks($post->post_content);
- render_block($blocks[0]);
- if ($blocks = parse_blocks($post->post_content)) {
- render_block($blocks[0]);
- }
- }
-}, 9);
-
/**
* Register the theme assets with the block editor.
*
diff --git a/web/app/themes/gds/resources/blocks/tab/block.json b/web/app/themes/gds/resources/blocks/tab/block.json
new file mode 100644
index 00000000..e708b003
--- /dev/null
+++ b/web/app/themes/gds/resources/blocks/tab/block.json
@@ -0,0 +1,22 @@
+{
+ "$schema": "https://schemas.wp.org/trunk/block.json",
+ "apiVersion": 2,
+ "name": "gds/tab",
+ "title": "Tab",
+ "description": "",
+ "icon": "plus-alt2",
+ "category": "layout",
+ "parent": ["gds/tabs"],
+ "attributes": {
+ "label": {
+ "type": "string"
+ }
+ },
+ "supports": {
+ "html": false,
+ "color": {}
+ },
+ "editorScript": "file:./index.js",
+ "editorStyle": "file:./editor.css",
+ "style": "file:./style.css"
+}
diff --git a/web/app/themes/gds/resources/blocks/tab/edit.js b/web/app/themes/gds/resources/blocks/tab/edit.js
new file mode 100644
index 00000000..cee22d0d
--- /dev/null
+++ b/web/app/themes/gds/resources/blocks/tab/edit.js
@@ -0,0 +1,38 @@
+import {
+ InnerBlocks,
+} from '@wordpress/block-editor';
+import {
+ useBlockProps,
+ useInnerBlocksProps,
+} from '@wordpress/block-editor';
+import {useSelect} from '@wordpress/data';
+
+const Edit = (props) => {
+ const {clientId} = props;
+
+ const hasInnerBlocks = useSelect(select => {
+ const {getBlock} = select('core/block-editor');
+ const block = getBlock(clientId);
+
+ return !!(block && block.innerBlocks.length);
+ }, [clientId]);
+
+ const blockProps = useBlockProps({
+ className: '',
+ });
+
+ const innerBlocksProps = useInnerBlocksProps({
+ ref: blockProps.ref,
+ className: 'wp-block-gds-tab__content',
+ }, {
+ renderAppender: hasInnerBlocks ? undefined : InnerBlocks.ButtonBlockAppender,
+ });
+
+ return (
+
+ );
+};
+
+export default Edit;
diff --git a/web/app/themes/gds/resources/blocks/tab/editor.scss b/web/app/themes/gds/resources/blocks/tab/editor.scss
new file mode 100644
index 00000000..2932ba67
--- /dev/null
+++ b/web/app/themes/gds/resources/blocks/tab/editor.scss
@@ -0,0 +1,11 @@
+.wp-block-gds-tab {
+ display: block;
+
+ &__content {
+ display: none;
+
+ &.active {
+ display: block;
+ }
+ }
+}
diff --git a/web/app/themes/gds/resources/blocks/tab/index.js b/web/app/themes/gds/resources/blocks/tab/index.js
new file mode 100644
index 00000000..30e9dcd9
--- /dev/null
+++ b/web/app/themes/gds/resources/blocks/tab/index.js
@@ -0,0 +1,14 @@
+/** @wordpress */
+import { registerBlockType } from '@wordpress/blocks'
+import { InnerBlocks } from '@wordpress/block-editor'
+
+import edit from './edit'
+import meta from './block.json';
+
+registerBlockType(meta.name, {
+ ...meta,
+ edit,
+ save() {
+ return ;
+ },
+});
diff --git a/web/app/themes/gds/resources/blocks/tab/style.scss b/web/app/themes/gds/resources/blocks/tab/style.scss
new file mode 100644
index 00000000..688245ab
--- /dev/null
+++ b/web/app/themes/gds/resources/blocks/tab/style.scss
@@ -0,0 +1,11 @@
+.wp-block-gds-tab {
+ display: none;
+
+ &.active {
+ display: block;
+ }
+
+ &__content {
+ padding-top: var(--block-gutter-s);
+ }
+}
diff --git a/web/app/themes/gds/resources/blocks/tab/tab.blade.php b/web/app/themes/gds/resources/blocks/tab/tab.blade.php
new file mode 100644
index 00000000..6d38a352
--- /dev/null
+++ b/web/app/themes/gds/resources/blocks/tab/tab.blade.php
@@ -0,0 +1,11 @@
+ ($index === 0 ? 'active' : ''),
+ 'id' => 'tabpanel-' . $uid . '-' . $index,
+ 'role' => 'tabpanel',
+ 'aria-labelledby' => 'tab-' . $uid . '-' . $index,
+ ]) !!}
+>
+
+ {!! $content !!}
+
+
diff --git a/web/app/themes/gds/resources/blocks/tab/tab.php b/web/app/themes/gds/resources/blocks/tab/tab.php
new file mode 100644
index 00000000..0a5d8235
--- /dev/null
+++ b/web/app/themes/gds/resources/blocks/tab/tab.php
@@ -0,0 +1,19 @@
+path(), [
+ 'render_callback' => function (array $attributes, string $content, $block) {
+ $attributes = (object) $attributes;
+
+ return view('blocks::tab.tab', [
+ 'attributes' => $attributes,
+ 'content' => $content,
+ 'block' => $block,
+ 'index' => $attributes->index ?? 0,
+ 'uid' => $attributes->uid ?? 0,
+ ]);
+ }
+]);
diff --git a/web/app/themes/gds/resources/blocks/tabs/block.json b/web/app/themes/gds/resources/blocks/tabs/block.json
new file mode 100644
index 00000000..3c88f31e
--- /dev/null
+++ b/web/app/themes/gds/resources/blocks/tabs/block.json
@@ -0,0 +1,17 @@
+{
+ "$schema": "https://schemas.wp.org/trunk/block.json",
+ "apiVersion": 2,
+ "name": "gds/tabs",
+ "title": "Tabs",
+ "description": "",
+ "category": "layout",
+ "icon": "welcome-widgets-menus",
+ "attributes": {
+ },
+ "supports": {
+ "html": false,
+ "color": {}
+ },
+ "editorScript": "file:./index.js",
+ "script": "file:/script.js"
+}
diff --git a/web/app/themes/gds/resources/blocks/tabs/edit.js b/web/app/themes/gds/resources/blocks/tabs/edit.js
new file mode 100644
index 00000000..34850282
--- /dev/null
+++ b/web/app/themes/gds/resources/blocks/tabs/edit.js
@@ -0,0 +1,185 @@
+import classnames from 'classnames';
+import {__, sprintf} from '@wordpress/i18n';
+import {
+ RichText,
+ useBlockProps,
+ useInnerBlocksProps,
+ BlockControls,
+} from '@wordpress/block-editor';
+import {
+ Icon,
+ ToolbarButton,
+ ToolbarGroup,
+} from '@wordpress/components';
+import {createBlock} from '@wordpress/blocks';
+import {useSelect, useDispatch} from '@wordpress/data';
+import {useEffect, useState, useRef} from '@wordpress/element';
+
+const Edit = (props) => {
+ const {isSelected, clientId} = props;
+ const contentRef = useRef(null);
+ const [activeTab, setActiveTab] = useState('');
+
+ const children = useSelect(select => {
+ const {getBlock} = select('core/block-editor');
+ return getBlock(clientId).innerBlocks;
+ });
+
+ const isParentOfSelectedBlock = useSelect(
+ (select) => select('core/block-editor').hasSelectedInnerBlock(clientId, true),
+ );
+
+ const {
+ insertBlock,
+ removeBlock,
+ // selectBlock,
+ moveBlockToPosition,
+ updateBlockAttributes,
+ } = useDispatch('core/block-editor');
+
+ // const selectTab = (blockId) => {
+ // if (0 < children?.length) {
+ // const block = children.filter(block => block.clientId === blockId)[0];
+ // selectBlock(block.clientId);
+ // }
+ // };
+
+ const toggleActiveTab = (blockId) => {
+ if (contentRef.current) {
+ children.forEach(block => {
+ const blockContent = contentRef.current.querySelector(`#block-${block.clientId} .wp-block-gds-tab__content`);
+ blockContent?.classList.toggle('active', block.clientId === blockId);
+ });
+
+ setActiveTab(blockId);
+ }
+ };
+
+ const moveTab = (blockId, position) => {
+ const blockClientId = children.filter(block => block.clientId === blockId)[0]?.clientId;
+ if (blockClientId) {
+ moveBlockToPosition(blockClientId, clientId, clientId, position);
+ }
+ };
+
+ const deleteTab = (blockId) => {
+ if (0 < children?.length) {
+ const block = children.filter(block => block.clientId === blockId)[0];
+ removeBlock(block.clientId, false);
+ if (activeTab === blockId) {
+ setActiveTab('');
+ }
+ }
+ };
+
+ const addTab = () => {
+ const itemBlock = createBlock('gds/tab', {label: sprintf(__('Tab #%d'), children.length + 1)});
+ insertBlock(itemBlock, (children?.length) || 0, clientId, false);
+ };
+
+ useEffect(() => {
+ // Handle breakpoint
+ const activeContent = contentRef.current.querySelector(`#block-${activeTab} .wp-block-gds-tab__content.active`)
+
+ if (activeTab && !activeContent) {
+ toggleActiveTab(activeTab);
+ }
+ });
+
+ useEffect(() => {
+ // Activate the first tab when no tabs are selected
+ if (0 < children?.length && ('' === activeTab || 0 === children?.filter(block => block.clientId === activeTab).length)) {
+ toggleActiveTab(children[0].clientId);
+ }
+ }, [children]);
+
+ const Controls = () => {
+ const index = children?.findIndex(({clientId}) => clientId === activeTab);
+
+ return (
+
+
+ deleteTab(activeTab)}
+ />
+
+ moveTab(activeTab, index - 1)}
+ />
+
+ moveTab(activeTab, index + 1)}
+ />
+
+
+ );
+ };
+
+ const blockProps = useBlockProps({
+ className: '',
+ });
+
+ const innerBlocksProps = useInnerBlocksProps({
+ ref: contentRef,
+ className: 'wp-block-gds-tabs__panels',
+ }, {
+ orientation: 'horizontal',
+ renderAppender: false,
+ allowedBlocks: ['gds/tab'],
+ template: [
+ ['gds/tab', {label: sprintf(__('Tab #%d'), 1)}],
+ ],
+ });
+
+ return (
+ <>
+
+
+
+
+ {children?.map((tab, index) => {
+ return (
+
+
+
+ );
+ }) || ''}
+
+ {(isSelected || isParentOfSelectedBlock || 0 === children.length) && (
+
+
+
+ )}
+
+
+
+
+ >
+ );
+};
+
+export default Edit;
diff --git a/web/app/themes/gds/resources/blocks/tabs/index.js b/web/app/themes/gds/resources/blocks/tabs/index.js
new file mode 100644
index 00000000..30e9dcd9
--- /dev/null
+++ b/web/app/themes/gds/resources/blocks/tabs/index.js
@@ -0,0 +1,14 @@
+/** @wordpress */
+import { registerBlockType } from '@wordpress/blocks'
+import { InnerBlocks } from '@wordpress/block-editor'
+
+import edit from './edit'
+import meta from './block.json';
+
+registerBlockType(meta.name, {
+ ...meta,
+ edit,
+ save() {
+ return ;
+ },
+});
diff --git a/web/app/themes/gds/resources/blocks/tabs/script.js b/web/app/themes/gds/resources/blocks/tabs/script.js
new file mode 100644
index 00000000..53883274
--- /dev/null
+++ b/web/app/themes/gds/resources/blocks/tabs/script.js
@@ -0,0 +1,36 @@
+import {ready} from '~/utils';
+
+const init = (block) => {
+ const tabs = block.querySelectorAll('button[role="tab"]');
+ const panels = block.querySelectorAll('.wp-block-gds-tab');
+
+ if (!tabs) {
+ return;
+ }
+
+ for (const tab of tabs) {
+ tab.addEventListener('click', (e) => {
+ e.preventDefault();
+
+ tabs.forEach((tab) => {
+ tab.setAttribute('aria-selected', 'false');
+ });
+
+ panels.forEach((panel) => {
+ panel.classList.remove('active');
+ });
+
+ e.currentTarget.setAttribute('aria-selected', 'true');
+
+ const activePanelId = e.currentTarget.getAttribute('aria-controls');
+ const activePanel = block.querySelector(`#${activePanelId}.wp-block-gds-tab`);
+ activePanel?.classList.add('active');
+ });
+ }
+}
+
+ready(()=> {
+ for (const block of document.querySelectorAll('.wp-block-gds-tabs')) {
+ init(block);
+ }
+});
diff --git a/web/app/themes/gds/resources/blocks/tabs/tabs.blade.php b/web/app/themes/gds/resources/blocks/tabs/tabs.blade.php
new file mode 100644
index 00000000..40f2f3a8
--- /dev/null
+++ b/web/app/themes/gds/resources/blocks/tabs/tabs.blade.php
@@ -0,0 +1,28 @@
+@if( $block->inner_blocks ?? false )
+
+
+ @foreach( $block->inner_blocks as $index => $inner_block )
+
+ @endforeach
+
+
+
+ @foreach( $block->inner_blocks as $index => $inner_block )
+ @php
+ $inner_block->attributes['index'] = $index;
+ $inner_block->attributes['uid'] = $uid;
+ @endphp
+
+ {!! $inner_block->render() !!}
+ @endforeach
+
+
+@endif
diff --git a/web/app/themes/gds/resources/blocks/tabs/tabs.php b/web/app/themes/gds/resources/blocks/tabs/tabs.php
new file mode 100644
index 00000000..6050549c
--- /dev/null
+++ b/web/app/themes/gds/resources/blocks/tabs/tabs.php
@@ -0,0 +1,20 @@
+path(), [
+ 'render_callback' => function (array $attributes, string $content, WP_Block $block) {
+ return view('blocks::tabs.tabs', [
+ 'attributes' => (object) $attributes,
+ 'content' => $content,
+ 'block' => $block,
+ 'uid' => wp_unique_id(),
+ ]);
+ }
+]);
+
+add_action('wp_enqueue_scripts', function () {
+ wp_script_add_data('gds-tabs-item-script', 'strategy', 'async');
+});
diff --git a/web/app/themes/gds/resources/styles/blocks/core-buttons.scss b/web/app/themes/gds/resources/styles/blocks/core-buttons.scss
index 8727856f..5eb70355 100644
--- a/web/app/themes/gds/resources/styles/blocks/core-buttons.scss
+++ b/web/app/themes/gds/resources/styles/blocks/core-buttons.scss
@@ -1,5 +1,6 @@
.wp-block-buttons {
display: flex;
+ flex-wrap: wrap;
// Set the block gap so button width settings work
--wp--style--block-gap: var(--grid-gutter);