Tab 1 title
Tab 2 title
Tab 3 title
Tab 4 title
Tab 5 title
This is a default heading.
This is a default paragraph.
This is a default heading 2.
This is a default paragraph.
This is a default heading.
This is a default paragraph.
This is a default heading.
This is a default paragraph.
This is a default heading.
This is a default paragraph.
Tab 1 title
Tab 2 title
Tab 3 title
Tab 4 title
Tab 5 title
Tab 6 title
This is a default heading 1.
This is a default paragraph.
This is a default heading 2.
This is a default paragraph.
This is a default heading 3.
This is a default paragraph.
This is a default heading 4.
This is a default paragraph.
This is a default heading 5.
This is a default paragraph.
This is a default heading.
This is a default paragraph.
block.json
{
"$schema": "https://schemas.wp.org/trunk/block.json",
"apiVersion": 3,
"name": "wlc/tabs",
"title": "Tabs",
"category": "widgets",
"icon": "index-card",
"description": "A block to display tabs",
"supports": {
"html": false
},
"keywords": [
"tabs"
],
"attributes": {
"tabs": {
"type": "array",
"default": [
{
"index": 0,
"title": "Tab 1"
},
{
"index": 1,
"title": "Tab 2"
}
]
},
"navFontSize": {
"type": "integer",
"default": 16
},
"navHorizontalPadding": {
"type": "integer",
"default": 25
},
"navVerticalPadding": {
"type": "integer",
"default": 15
},
"showActiveIndicator": {
"type": "boolean",
"default": true
},
"position": {
"type": "string",
"default": "horizontal"
},
"navBg": {
"type": "string",
"default": "#f1f1f1"
},
"navColor": {
"type": "string",
"default": "#000000"
},
"navBgActive": {
"type": "string",
"default": "#b1e9b4"
},
"navColorActive": {
"type": "string",
"default": "#ffffff"
}
},
"textdomain": "WLC",
"editorScript": "file:./index.js",
"editorStyle": "file:./index.css",
"style": "file:./style-index.css",
"viewScript": "file:view.js"
}
edit.js
import { __ } from "@wordpress/i18n";
import { useBlockProps, useInnerBlocksProps, InspectorControls, RichText } from "@wordpress/block-editor";
import { PanelBody, SelectControl, RangeControl, ToggleControl, Button, ColorPicker } from "@wordpress/components";
import { useState } from "react";
const { useDispatch, useSelect } = wp.data;
const { createBlock } = wp.blocks;
const ALLOWED_BLOCKS = ["wlc/tabs-item", "wlc/tabs"];
const TEMPLATE = [
["wlc/tabs-item", { tabIndex: 0 }],
["wlc/tabs-item", { tabIndex: 1 }],
];
const edit = ({ attributes, setAttributes, clientId }) => {
const {
tabs,
position,
navFontSize,
navHorizontalPadding,
navVerticalPadding,
showActiveIndicator,
navBg,
navColor,
navBgActive,
navColorActive,
} = attributes;
const blockProps = useBlockProps({
style: {
"--nav-color": navColor,
"--nav-bg": navBg,
"--nav-color-active": navColorActive,
"--nav-bg-active": navBgActive,
"--nav-font-size": `${navFontSize}px`,
"--nav-vertical-padding": `${navVerticalPadding}px`,
"--nav-horizontal-padding": `${navHorizontalPadding}px`,
},
className: "wlc-tabs",
});
const { children } = useInnerBlocksProps(blockProps, {
allowedBlocks: ALLOWED_BLOCKS,
template: TEMPLATE,
});
const [activeTabIndex, setActiveTabIndex] = useState(0);
const { replaceInnerBlocks } = useDispatch("core/block-editor");
const { innerBlocks } = useSelect((select) => ({
innerBlocks: select("core/block-editor").getBlocks(clientId),
}));
const handleAddTab = () => {
const tabIndex = tabs.length;
const newTab = {
index: tabIndex,
title: __(`Tab ${tabIndex + 1} title`, "WLC"),
};
setAttributes({ tabs: [...tabs, newTab] });
const newBlock = createBlock("wlc/tabs-item", { tabIndex });
replaceInnerBlocks(clientId, [...innerBlocks, newBlock], false);
setActiveTabIndex(tabIndex);
};
const handleRemoveTab = (index) => {
if (window.confirm(__("Are you sure you want to remove this tab?", "WLC"))) {
const newTabs = tabs.filter((tab, i) => i !== index).map((tab, i) => ({ ...tab, index: i }));
setAttributes({ tabs: newTabs });
const newBlocks = innerBlocks.filter((block, i) => i !== index);
replaceInnerBlocks(clientId, newBlocks, false);
setActiveTabIndex(index === 0 ? 0 : index - 1);
}
};
const setActiveTab = (index) => {
setActiveTabIndex(index);
};
const handleTitleChange = (value, index) => {
const newTabs = [...tabs];
newTabs[index] = { ...newTabs[index], title: value };
setAttributes({ tabs: newTabs });
};
return (
<div {...blockProps}>
<InspectorControls>
<PanelBody title={__("Tabs Settings", "WLC")}>
<SelectControl
label={__("Position", "WLC")}
value={position}
options={[
{ value: "horizontal", label: __("Horizontal", "WLC") },
{ value: "vertical", label: __("Vertical", "WLC") },
]}
onChange={(value) => setAttributes({ position: value })}
/>
<RangeControl
label={__("Nav Font Size (px)", "WLC")}
value={navFontSize}
onChange={(value) => setAttributes({ navFontSize: value })}
min={10}
max={30}
/>
<RangeControl
label={__("Nav Horizontal Padding (px)", "WLC")}
value={navHorizontalPadding}
onChange={(value) => setAttributes({ navHorizontalPadding: value })}
min={0}
max={50}
/>
<RangeControl
label={__("Nav Vertical Padding (px)", "WLC")}
value={navVerticalPadding}
onChange={(value) => setAttributes({ navVerticalPadding: value })}
min={0}
max={50}
/>
<ToggleControl
label={__("Show Active Indicator", "WLC")}
checked={showActiveIndicator}
onChange={(value) => setAttributes({ showActiveIndicator: value })}
/>
</PanelBody>
<PanelBody title={__("Colors", "WLC")} initialOpen={false}>
<div className="components-base-control">
<div className="components-base-control__field">
<label className="components-base-control__label">{__("Nav Background Color", "WLC")}</label>
<ColorPicker color={navBg} onChangeComplete={(value) => setAttributes({ navBg: value.hex })} />
</div>
</div>
<div className="components-base-control">
<div className="components-base-control__field">
<label className="components-base-control__label">{__("Nav Background Active Color", "WLC")}</label>
<ColorPicker
color={navBgActive}
onChangeComplete={(value) => setAttributes({ navBgActive: value.hex })}
/>
</div>
</div>
</PanelBody>
</InspectorControls>
<div className={`wlc-tabs__inner is-${position}`}>
<div className={`wlc-tabs__nav ${showActiveIndicator === true ? "tabs-indicator" : ""}`}>
{tabs.map((tab, index) => (
<div
key={index}
className={`tab-item${index === activeTabIndex ? " active" : ""}`}
onClick={() => setActiveTab(index)}
>
<RichText
tagName="span"
value={tab.title}
onChange={(value) => handleTitleChange(value, index)}
placeholder={__("Enter tab title...", "WLC")}
/>
<Button className="remove-btn" onClick={() => handleRemoveTab(index)}>
×
</Button>
</div>
))}
<Button className="add-item" onClick={handleAddTab}>
{__("+", "WLC")}
</Button>
</div>
<div className="wlc-tabs__content" data-active-tab={activeTabIndex}>
{children}
</div>
</div>
</div>
);
};
export default edit;
editor.scss
index.js
/**
* WordPress dependencies
*/
import { registerBlockType } from '@wordpress/blocks';
/**
* Internal dependencies
*/
import './editor.scss';
import './style.scss';
import './view.js';
import edit from './edit';
import save from './save';
import metadata from './block.json';
registerBlockType( metadata.name, {
edit,
save,
} );
save.js
import { useBlockProps, RichText, InnerBlocks } from "@wordpress/block-editor";
const save = ({ attributes }) => {
const {
tabs,
position,
navFontSize,
navHorizontalPadding,
navVerticalPadding,
showActiveIndicator,
navBg,
navColor,
navBgActive,
navColorActive,
} = attributes;
const blockProps = useBlockProps.save({
style: {
"--nav-color": navColor,
"--nav-bg": navBg,
"--nav-color-active": navColorActive,
"--nav-bg-active": navBgActive,
"--nav-font-size": `${navFontSize}px`,
"--nav-vertical-padding": `${navVerticalPadding}px`,
"--nav-horizontal-padding": `${navHorizontalPadding}px`,
},
className: "wlc-tabs",
});
return (
<div {...blockProps}>
<div className={`wlc-tabs__inner is-${position}`}>
<div className="wlc-tabs__nav">
{tabs.map((tab, index) => (
<div
key={index}
className={`tab-item${showActiveIndicator === true ? " tab-item--indicator" : ""}`}
data-tab-index={index}
>
<RichText.Content tagName="span" value={tab.title} />
</div>
))}
</div>
<div className="wlc-tabs__content">
<InnerBlocks.Content />
</div>
</div>
</div>
);
};
export default save;
style.scss
.wlc-tabs {
&__inner {
display: flex;
flex-direction: column;
&.is-vertical {
flex-direction: row;
.wlc-tabs__nav {
width: 25%;
margin: 0;
flex-wrap: wrap;
flex-direction: column;
&.tabs-indicator {
.tab-item {
&.active {
&::after {
top: calc(50% - 0.469rem);
left: auto;
border-width: 0.625rem 0 0.625rem 0.938rem;
border-color: transparent transparent transparent var(--nav-bg-active);
transform: rotate(0deg) translateX(100%);
}
}
}
}
.add-item {
width: 100%;
margin: 0;
}
}
.wlc-tabs__content {
flex-grow: 1;
padding-left: 2.5rem;
}
}
}
&__nav {
margin-bottom: 1.5rem;
overflow-x: auto;
display: flex;
@media screen and (min-width: 992px) {
overflow-x: visible;
}
&.tabs-indicator {
.tab-item {
&.active {
&::after {
content: "";
right: 0;
left: 0;
bottom: 0;
width: 0;
height: 0;
border-style: solid;
border-width: 0.938rem 0.625rem 0 0.625rem;
border-color: var(--nav-bg-active) transparent transparent transparent;
transform: rotate(0deg) translateY(100%);
margin: 0 auto;
position: absolute;
}
}
}
}
.tab-item {
cursor: pointer;
width: 100%;
color: red!important;
font-size: var(--nav-font-size);
line-height: 1.2;
box-sizing: border-box;
background: var(--nav-bg);
padding: var(--nav-vertical-padding) var(--nav-horizontal-padding);
display: flex;
align-items: center;
white-space: nowrap;
position: relative;
&.active {
color: var(--nav-color-active);
font-weight: bold;
background: var(--nav-bg-active);
}
.remove-btn {
outline: none;
color: #fff;
font-size: 1.125rem;
line-height: 0.8;
text-align: center;
width: 0.938rem;
height: 0.938rem;
top: 0.5rem;
right: 0.5rem;
padding: 0;
mix-blend-mode: difference;
display: inline-block;
position: absolute;
}
}
.add-item {
font-size: 1.563rem;
line-height: 1;
border: 0.063rem solid #dcdcdc;
margin-left: 1.25rem;
padding: 0 0.75rem 0.125rem;
display: inline-block;
height: auto;
}
}
&__content {
@for $i from 0 through 20 {
&[data-active-tab="#{$i}"] {
> .wp-block-wlc-tabs-item:nth-child(#{$i + 1}) {
display: block;
}
}
}
.wp-block-wlc-tabs-item {
display: none;
&.active {
display: block;
}
}
}
}
view.js
document.addEventListener("DOMContentLoaded", () => {
class WLCTabs {
constructor(tabContainer) {
this.tabContainer = tabContainer;
this.activeClass = "active";
this.navContainer = this.tabContainer.querySelector(".wlc-tabs__nav");
this.contentContainer = this.tabContainer.querySelector(".wlc-tabs__content");
if (!this.navContainer || !this.contentContainer) return;
this.navItems = Array.from(this.navContainer.children).filter((child) => child.classList.contains("tab-item"));
this.contentTabs = Array.from(this.contentContainer.children).filter((child) =>
child.classList.contains("wp-block-wlc-tabs-item"),
);
if (this.navItems.length === 0 || this.contentTabs.length === 0) return;
this.initTabs();
}
initTabs() {
this.navItems.forEach((item, index) => {
item.setAttribute("role", "tab");
item.setAttribute("tabindex", index === 0 ? "0" : "-1");
item.setAttribute("aria-selected", index === 0 ? "true" : "false");
item.setAttribute("aria-controls", `tab-content-${index}`);
item.id = `tab-${index}`;
});
this.contentTabs.forEach((tab, index) => {
tab.setAttribute("role", "tabpanel");
tab.setAttribute("aria-labelledby", `tab-${index}`);
tab.id = `tab-content-${index}`;
tab.setAttribute("tabindex", "0");
tab.hidden = index !== 0;
});
this.setActiveTabByIndex(0);
this.navContainer.addEventListener("click", (event) => {
const clickedItem = event.target.closest(".tab-item");
if (clickedItem) {
const clickedIndex = this.navItems.indexOf(clickedItem);
if (clickedIndex !== -1) {
this.setActiveTabByIndex(clickedIndex);
clickedItem.focus();
}
}
});
this.navContainer.addEventListener("keydown", (event) => {
const currentIndex = this.navItems.findIndex((item) => item.getAttribute("tabindex") === "0");
if (event.key === "ArrowRight") {
const nextIndex = (currentIndex + 1) % this.navItems.length;
this.setActiveTabByIndex(nextIndex);
this.navItems[nextIndex].focus();
} else if (event.key === "ArrowLeft") {
const prevIndex = (currentIndex - 1 + this.navItems.length) % this.navItems.length;
this.setActiveTabByIndex(prevIndex);
this.navItems[prevIndex].focus();
}
});
}
setActiveTabByIndex(index) {
this.navItems.forEach((item, i) => {
item.classList.toggle(this.activeClass, i === index);
item.setAttribute("aria-selected", i === index ? "true" : "false");
item.setAttribute("tabindex", i === index ? "0" : "-1");
});
this.contentTabs.forEach((tab, i) => {
tab.classList.toggle(this.activeClass, i === index);
tab.hidden = i !== index;
});
}
}
const tabContainers = document.querySelectorAll(".wlc-tabs");
tabContainers.forEach((tabContainer) => new WLCTabs(tabContainer));
});