# Pattern

## What is it

A tabs component that provides different sections of content that are displayed one at a time when the user selects that information.

## When to use it

The tabbed user interface enables users to jump to their target section quickly. Tabs present like logically group information on the same page. Information should

- be logically chunked and ordered
- be arallel in nature
- show user's context
- obvious where they begin and end

Users should not need to see content of multiple tabs simultaneously and the user should be able to easily recognise where they are within the content.

## How to use it

The structure of the tab set is defined in html. There are two forms supported. Adding a class of `.tab-group` to the container element will work in place of the `tabset` tag, and the tab panels can be defined using either `tab=""` or `data-tab=""`. Passing an optional element to the init function will initialise tabs within that element.

The tab initalize function now takes a new function parameter in the form of an third argument is an object that can take the following callbacks:

- altModifer (When the altKey is used)
- shiftModifier (When the shift key is used)
- metaModifier (When the Windows key or Apple key is used)

You can use these callbacks to create a custom event to get the tab information.

Note: There is a new core function (core.getTabPath) that will generate the query string and url hash for use in DS2 Core. You may wish to update your scaffolding.js file to make use of this functionality.

    tabset#uniqueID
    	div(tab="[tab title]")
    	div(tab="[tab title]")

    .tab-group,tabset{margin:2rem 0 1rem 0}.tab-group [role=tablist],tabset [role=tablist]{display:-webkit-box;display:-ms-flexbox;display:flex;margin:0;padding:0}.tab-group [role=tablist] li.separator,tabset [role=tablist] li.separator{border-bottom:1px solid #7f7f7f;display:inline-block;margin:0.45rem 0 0 0;width:100%}.tab-group [role=tablist] li[role=tab],tabset [role=tablist] li[role=tab]{background-color:#FFF;border-left:1px solid #7f7f7f;border-right:1px solid #7f7f7f;border-radius:0.5rem 0.5rem 0 0;border-top:1px solid #7f7f7f;cursor:pointer;display:inline;margin:0;padding:1rem 1.5rem 0.14rem 1.5rem;z-index:2}.tab-group [role=tablist] li[role=tab]:last-of-type,tabset [role=tablist] li[role=tab]:last-of-type{border-right:1px solid #7f7f7f}.tab-group [role=tablist] li[role=tab]:not(.selected),tabset [role=tablist] li[role=tab]:not(.selected){background-color:#f0f0f0;border-bottom:1px solid #7f7f7f}.tab-group [role=tablist] li[role=tab] span,tabset [role=tablist] li[role=tab] span{display:block;margin:0 0 0.5rem 0}.tab-group .tab-hidden,tabset .tab-hidden{display:none}.tab-group [role=tabpanel],tabset [role=tabpanel]{background-color:#FFF;border:1px solid #7f7f7f;border-top:none;padding:1rem;z-index:1}.tab-group [role=tabpanel]:not(.open),tabset [role=tabpanel]:not(.open){display:none}

    @use "scss/core/tabs/_tabs";
    @include tabs{
    	// optional content block
    };

    //		DS2 core (c) 2024 Alexander McIlwraith
    //		Licensed under CC BY-SA 4.0

    $tab-border: #7f7f7f !default;
    $tab-panel-background-color: #FFF !default;
    $tab-panel-top-border: #7f7f7f !default;
    $tab-panel-top-border-width: 1px !default;
    $tab-selected: #FFF !default;
    $tab-selected-text: #000 !default;
    $tab-notselected: #f0f0f0 !default;
    $tab-notselected-text: #000 !default;

    @mixin tabs {
    	tabset, .tab-group {
    		margin: 2rem 0 1rem 0;

    		[role="tablist"] {
    			display: flex;
    			margin: 0;
    			padding: 0;

    			li {
    				&.separator {
    					border-bottom: 1px solid $tab-border;
    					display: inline-block;
    					margin: .45rem 0 0 0;
    					width: 100%;
    				}

    				&[role="tab"] {
    					background-color: $tab-selected;
    					border-left: 1px solid $tab-border;
    					border-right: 1px solid $tab-border;
    					border-radius: .5rem .5rem 0 0;
    					border-top: 1px solid $tab-border;
    					cursor:pointer;
    					display: inline;
    					margin: 0;
    					padding: 1rem 1.5rem .14rem 1.5rem;
    					z-index: 2;

    					&:last-of-type {
    						border-right: 1px solid $tab-border;
    					}

    					&:not(.selected) {
    						background-color: $tab-notselected;
    						border-bottom: 1px solid $tab-border;
    									/// color
    					}

    					span {
    						display: block;
    						margin: 0 0 .5rem 0;
    					}
    				}
    			}
    		}

    		.tab-hidden {
    			display: none;
    		}

    		[role="tabpanel"] {
    			background-color: $tab-selected;
    			border: 1px solid $tab-border;
    			border-top: none;
    			padding: 1rem;
    			z-index: 1;

    			&:not(.open) {
    				display: none;
    			}
    			@content;

    		}

    	}
    }

    import * as tabs from "./js/core/tabs/_tabs.js";
    tabs.init();

    /*  DS2 core (c) 2024 Alexander McIlwraith
    	Released under Creative Commons Attribution-ShareAlike 4.0 International
     */

    // create a pure JS mouse click event
    const click = new MouseEvent('click', {
    	view: window,
    	bubbles: false,
    	cancelable: true
    });

    const waitForElement = (selector) => {
        return new Promise(resolve => {
            if (document.querySelector(selector)) {
                return resolve(document.querySelector(selector));
            }

            const observer = new MutationObserver(mutations => {
                if (document.querySelector(selector)) {
                    observer.disconnect();
                    resolve(document.querySelector(selector));
                }
            });

            // If you get "parameter 1 is not of type 'Node'" error, see https://stackoverflow.com/a/77855838/492336
            observer.observe(document.body, {
                childList: true,
                subtree: true
            });
        });
    }

    const chooseTab = (tab) => {
    	const siblings = Array.from(tab.parentNode.children);
    	siblings.forEach(sibling => sibling.classList.remove("selected"));
    	tab.classList.add("selected");

    	const tabPanels = Array.from(tab.parentNode.parentNode.children)
    	tabPanels.forEach(panel => panel.classList.remove("open"));

    	const tabPanelId = tab.getAttribute("id").replace("tab", "tab-panel");
    	document.getElementById(tabPanelId).classList.add("open");
    }

    let pushState = 0;
    let tabsetCount = 0;
    module.exports = {
    	"init": (container = document, spacer = true, args = {}) => {
    		container.querySelectorAll(".tab-group, tabset").forEach(tabGroup => {
    			if (tabGroup.querySelector("[role=tablist]") === null) {
    				if (tabGroup.getAttribute("id") == null) {
    					tabGroup.setAttribute("id", "tab-group-" + tabsetCount);
    					tabsetCount++;
    				}

    				const tabgroup = tabGroup.getAttribute("id");
    				let tablist = "";

    				Array.from(tabGroup.children).forEach(child => {

    					// is details?
    					let dtls = child.nodeName == "DETAILS" ? true : false;

    					// get the tab text
    					let tab = dtls ? child.querySelector("summary").innerHTML : child.getAttribute("tab") || child.getAttribute("data-tab");

    					// if the tab text is not blank
    					if (tab !== null) {
    						const tabID = tab.replace(/\W+/g, "-").toLowerCase();

    						// define the tab panel content
    						let tabPanel = null;
    						if (dtls) {
    							tabPanel = child;
    							tabPanel.setAttribute("open", "");
    						} else {
    							tabPanel = document.createElement('div');
    							tabPanel.appendChild(child.cloneNode(true));
    						}

    						tabPanel.id = `tab-panel-${tabgroup}-${tabID}`;
    						tabPanel.className = tablist === "" ? "open" : "";
    						tabPanel.setAttribute("role", "tabpanel");
    						tabPanel.setAttribute("tabindex", "0");
    						tabPanel.setAttribute("aria-labelledby", `tab-${tabgroup}-${tabID}`);
    						child.parentNode.replaceChild(tabPanel, child);
    						let cls = tablist === "" ? "class='selected'" : "";
    						tablist += `${tab}`;
    					} else {
    						child.classList.add("tab-hidden");
    					}
    				});

    				const ul = document.createElement('ul');
    				ul.setAttribute("role", "tablist");
    				ul.innerHTML = spacer != true ? `${tablist}` : `${tablist}`;
    				tabGroup.insertBefore(ul, tabGroup.firstChild);

    				tabGroup.querySelectorAll('[role="tab"]').forEach(tab => {
    					tab.addEventListener("click", (evt) => {
    						if (pushState == 0) {
    							window.history.pushState({rand: Math.random(), pushState: pushState, tab: tab.parentNode.firstChild.getAttribute("id")}, "", `#${tab.parentNode.firstChild.getAttribute("id")}`);
    							pushState++;
    						}

    						chooseTab(evt.currentTarget);
    						window.history.pushState({rand: Math.random(), pushState: pushState, tab: tab.getAttribute("id")}, "", `#${tab.getAttribute("id")}`);
    						pushState++;
    					});

    					tab.addEventListener("keypress", (e) => {
    						e.preventDefault();
    						if( e.which == 32 || e.which == 13 ) {
    							e.currentTarget.dispatchEvent(click);
    						}
    					})
    				});
    			}

    			if (document.location.hash != "" && document.location.hash.substring(0,5) == "#tab-") {
    				waitForElement(document.location.hash).then((el) => {
    					el.scrollIntoView();
    					el.focus();
    					el.dispatchEvent(click);
    				});
    			}
    		});

    		window.addEventListener("popstate", function (e) {
    			e.preventDefault();
    			if (e.state != null) {
    				chooseTab(document.querySelector(`#${e.state.tab}`));
    			} else {
    				history.go(-1);
    			}
    		});

    	}
    }