<div id="example--right-aligned--3" class="cwf-tabs cwf-tabs--right">
<nav class="cwf-tabs__nav" role="tablist" aria-label="Tabs">
<div class="cwf-tabs__tabs">
<button id="cwf-tabs__tab--example--right-aligned--1" class="cwf-tabs__tab" aria-selected="true" aria-controls="cwf-tabs__panel--example--right-aligned--1" role="tab" tabindex="0">
Right tab 1
</button>
<button id="cwf-tabs__tab--example--right-aligned--2" class="cwf-tabs__tab" aria-selected="false" aria-controls="cwf-tabs__panel--example--right-aligned--2" role="tab" tabindex="-1">
Right tab 2
</button>
<button id="cwf-tabs__tab--example--right-aligned--3" class="cwf-tabs__tab" aria-selected="false" aria-controls="cwf-tabs__panel--example--right-aligned--3" role="tab" tabindex="-1">
Right tab 3
</button>
</div>
</nav>
<section id="cwf-tabs__panel--example--right-aligned--1" class="cwf-tabs__panel" aria-labelledby="cwf-tabs__tab--example--right-aligned--1" aria-hidden="false" role="tabpanel" tabindex="0">
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Morbi
condimentum egestas mi a rhoncus. Praesent at vehicula est. Curabitur
faucibus, mi ut commodo fringilla, urna magna hendrerit ex, eget
vehicula justo elit vel nisi. Aenean quam eros, dictum ullamcorper augue
eu, sodales accumsan risus. Praesent mollis ipsum eget nisl posuere
rutrum. Quisque lacus nunc, porttitor et ex at, posuere molestie turpis.
Integer sodales auctor quam, nec placerat elit eleifend et. Pellentesque
lacinia pulvinar leo et tincidunt. Aenean volutpat ipsum leo, a ornare
lectus eleifend nec. In at risus sit amet lorem consectetur laoreet quis
non libero. Quisque vulputate tellus ut vestibulum dignissim.
</p>
</section>
<section id="cwf-tabs__panel--example--right-aligned--2" class="cwf-tabs__panel" aria-labelledby="cwf-tabs__tab--example--right-aligned--2" aria-hidden="true" role="tabpanel" tabindex="-1">
<p>
Fusce rhoncus, nisl sit amet porta pharetra, ex nibh aliquet purus, ac
pellentesque justo ante vel ante. Duis et rhoncus risus. Etiam sagittis
risus et dui volutpat tristique. Orci varius natoque penatibus et magnis
dis parturient montes, nascetur ridiculus mus. Aenean rhoncus ac massa
non faucibus. Cras dui sapien, vehicula et felis id, dapibus auctor
neque. <a href="https://www.vcu.edu/">
Virginia Commonwealth University
</a> Vestibulum posuere sem pharetra erat sodales, ut rutrum orci dignissim.
Phasellus id lacus faucibus urna porttitor ultricies at id magna. Maecenas
malesuada nisi ut sollicitudin scelerisque. Vivamus hendrerit nulla a sapien
facilisis efficitur. Nulla commodo euismod urna.
</p>
</section>
<section id="cwf-tabs__panel--example--right-aligned--3" class="cwf-tabs__panel" aria-labelledby="cwf-tabs__tab--example--right-aligned--3" aria-hidden="true" role="tabpanel" tabindex="-1">
<p>
Integer mollis consectetur dolor, a gravida magna egestas sed. Donec
iaculis in velit nec lobortis. Maecenas cursus ornare fermentum. Duis ut
nulla ut libero consequat bibendum a et justo. Aenean id ligula tempus,
interdum magna et, pulvinar nunc. Praesent condimentum tristique
maximus. Mauris rhoncus mi in odio dapibus eleifend. Aenean bibendum
ullamcorper egestas. Cras nec imperdiet velit. Nulla pellentesque,
mauris quis vehicula vestibulum, mauris purus vehicula risus, in
accumsan mauris sem eget diam. Mauris sed venenatis nisl. Suspendisse
sit amet velit volutpat, sagittis quam id, sodales erat. Ut iaculis
pharetra leo. Praesent tincidunt sit amet purus ut tristique. Aliquam
sed metus diam.
</p>
</section>
</div>
{# Tabs nav and panels #}
{%- set tabs_nav = [] -%}
{%- set tabs_panels = [] -%}
{%- for panel in panels -%}
{% set id = panel.id ?? loop.index %}
{% set tab_id = 'cwf-tabs__tab--' ~ id %}
{% set panel_id = 'cwf-tabs__panel--' ~ id %}
{% set tabindex = loop.first ? 0 : '-1' %}
{% set tabs_nav =
tabs_nav|merge(
[
{
id: tab_id,
selected: loop.first,
panel: panel_id,
tabindex: tabindex,
text: panel.title
}
]
)
%}
{% set tabs_panels =
tabs_panels|merge(
[
{
id: panel_id,
tab: tab_id,
hidden: not loop.first,
tabindex: tabindex,
content: panel.content
}
]
)
%}
{%- endfor -%}
{%- set classes = ['cwf-tabs'] -%}
{%- if vertical and vertical in [true, 'left', 'right'] -%}
{% if vertical == true %}
{% set vertical = 'left' %}
{% endif %}
{% set classes = classes|merge(['cwf-tabs--' ~ vertical]) %}
{%- endif -%}
{%- if alignment and alignment in [true, 'start', 'center', 'end'] -%}
{% if alignment == true %}
{% set alignment = 'start' %}
{% endif %}
{% set classes = classes|merge(['cwf-tabs--' ~ alignment]) %}
{%- endif -%}
<div id="{{ id ?? 'cwf-tabs' }}" class="{{ classes|join(' ') }}">
<nav class="cwf-tabs__nav"
role="tablist"
aria-label="{{ label ?? 'Tabs' }}">
<div class="cwf-tabs__tabs">
{% for button in tabs_nav %}
<button id="{{ button.id }}"
class="cwf-tabs__tab"
aria-selected="{{ button.selected }}"
aria-controls="{{ button.panel }}"
role="tab"
tabindex="{{ button.tabindex }}">
{{ button.text }}
</button>
{% endfor %}
</div>
</nav>
{% for panel in tabs_panels %}
<section id="{{ panel.id }}"
class="cwf-tabs__panel"
aria-labelledby="{{ panel.tab }}"
aria-hidden="{{ panel.hidden }}"
role="tabpanel"
tabindex="{{ panel.tabindex }}">
{{ panel.content }}
</section>
{% endfor %}
</div>
{
"id": "example-tabs--right-aligned",
"vertical": "right",
"panels": [
{
"id": "example--right-aligned--1",
"title": "Right tab 1",
"content": "<p>\n Lorem ipsum dolor sit amet, consectetur adipiscing elit. Morbi\n condimentum egestas mi a rhoncus. Praesent at vehicula est. Curabitur\n faucibus, mi ut commodo fringilla, urna magna hendrerit ex, eget\n vehicula justo elit vel nisi. Aenean quam eros, dictum ullamcorper augue\n eu, sodales accumsan risus. Praesent mollis ipsum eget nisl posuere\n rutrum. Quisque lacus nunc, porttitor et ex at, posuere molestie turpis.\n Integer sodales auctor quam, nec placerat elit eleifend et. Pellentesque\n lacinia pulvinar leo et tincidunt. Aenean volutpat ipsum leo, a ornare\n lectus eleifend nec. In at risus sit amet lorem consectetur laoreet quis\n non libero. Quisque vulputate tellus ut vestibulum dignissim.\n</p>"
},
{
"id": "example--right-aligned--2",
"title": "Right tab 2",
"content": "<p>\n Fusce rhoncus, nisl sit amet porta pharetra, ex nibh aliquet purus, ac\n pellentesque justo ante vel ante. Duis et rhoncus risus. Etiam sagittis\n risus et dui volutpat tristique. Orci varius natoque penatibus et magnis\n dis parturient montes, nascetur ridiculus mus. Aenean rhoncus ac massa\n non faucibus. Cras dui sapien, vehicula et felis id, dapibus auctor\n neque. <a href=\"https://www.vcu.edu/\">\n Virginia Commonwealth University\n </a> Vestibulum posuere sem pharetra erat sodales, ut rutrum orci dignissim.\n Phasellus id lacus faucibus urna porttitor ultricies at id magna. Maecenas\n malesuada nisi ut sollicitudin scelerisque. Vivamus hendrerit nulla a sapien\n facilisis efficitur. Nulla commodo euismod urna.\n</p>"
},
{
"id": "example--right-aligned--3",
"title": "Right tab 3",
"content": "<p>\n Integer mollis consectetur dolor, a gravida magna egestas sed. Donec\n iaculis in velit nec lobortis. Maecenas cursus ornare fermentum. Duis ut\n nulla ut libero consequat bibendum a et justo. Aenean id ligula tempus,\n interdum magna et, pulvinar nunc. Praesent condimentum tristique\n maximus. Mauris rhoncus mi in odio dapibus eleifend. Aenean bibendum\n ullamcorper egestas. Cras nec imperdiet velit. Nulla pellentesque,\n mauris quis vehicula vestibulum, mauris purus vehicula risus, in\n accumsan mauris sem eget diam. Mauris sed venenatis nisl. Suspendisse\n sit amet velit volutpat, sagittis quam id, sodales erat. Ut iaculis\n pharetra leo. Praesent tincidunt sit amet purus ut tristique. Aliquam\n sed metus diam.\n</p>"
}
]
}
// Tabs component styles
// ---- Modules ----
@use "../../shared/media";
@use "../../shared/style";
@use "../../shared/theme";
@use "sass:list";
// Selector prefix
$prefix: "cwf" !default;
// ---- Wrapper ----
$display: flex !default;
$flex-direction: column !default;
$align-items: flex-start !default;
$border-color: style.lighten("white-darkest", 33%) !default;
$color--active: theme.accent--foreground() !default;
$background-color--inactive: style.color("white-dark") !default;
$color--inactive: style.color("gray-light") !default;
$color--accent: theme.accent--background() !default;
.#{$prefix}-tabs {
--cwf-tabs--display: #{$display};
--cwf-tabs--flex-direction: #{$flex-direction};
--cwf-tabs--align-items: #{$align-items};
--cwf-tabs--border-color: #{$border-color};
--cwf-tabs--active-color: #{$color--active};
--cwf-tabs--inactive-background-color: #{$background-color--inactive};
--cwf-tabs--inactive-color: #{$color--inactive};
--cwf-tabs--accent-color: #{$color--accent};
display: var(--cwf-tabs--display);
flex-direction: var(--cwf-tabs--flex-direction);
align-items: var(--cwf-tabs--align-items);
@include style.spacing;
// Left
$flex-direction--left: row !default;
&--left {
@include media.breakpoint {
--cwf-tabs--flex-direction: #{$flex-direction--left};
}
}
// Left & right
$align-items--vertical: stretch !default;
&--left,
&--right {
@include media.breakpoint {
--cwf-tabs--align-items: #{$align-items--vertical};
}
}
// Right
$flex-direction--right: row-reverse !default;
&--right {
@include media.breakpoint {
--cwf-tabs--flex-direction: #{$flex-direction--right};
}
}
}
// ---- Utilities ----
$vertical--directions: left, right !default;
@function vertical--validate($directions) {
@return $directions == $vertical--directions or
list.index($vertical--directions, $directions);
}
@mixin vertical($directions: $vertical--directions) {
@if not vertical--validate($directions) {
@warn "#{$directions} is not a valid vertical tab direction! Must be #{$vertical--directions}, or both. Falling back to #{$vertical--directions}.";
$directions: $vertical--directions;
}
@if list.length($directions) > 1 {
.#{$prefix}-tabs--#{list.nth($directions, 1)} &,
.#{$prefix}-tabs--#{list.nth($directions, 2)} & {
@include media.breakpoint {
@content;
}
}
} @else {
.#{$prefix}-tabs--#{$directions} & {
@include media.breakpoint {
@content;
}
}
}
}
$alignment--values: start, center, end !default;
@function alignment--validate($alignment) {
@return list.index($alignment--values, $alignment);
}
@mixin alignment($alignment) {
@if alignment--validate($alignment) {
.#{$prefix}-tabs--#{$alignment} & {
@include media.breakpoint {
@content;
}
}
} @else {
@warn "#{$alignment} is not a valid tab alignment value! Must be one of the following: #{$alignment--values}.";
}
}
// ---- Nav ----
$nav__position: relative !default;
$nav__display: flex !default;
$nav__justify-content: flex-start !default;
$nav__width: 100% !default;
$nav__margin: 0 0 -1px 0 !default;
$nav__overflow-x: scroll !default;
$nav__scrollbar-display: none !default;
.#{$prefix}-tabs__nav {
--cwf-tabs__nav--position: #{$nav__position};
--cwf-tabs__nav--display: #{$nav__display};
--cwf-tabs__nav--justify-content: #{$nav__justify-content};
--cwf-tabs__nav--width: #{$nav__width};
--cwf-tabs__nav--margin: #{$nav__margin};
--cwf-tabs__nav--overflow-x: #{$nav__overflow-x};
position: var(--cwf-tabs__nav--position);
display: var(--cwf-tabs__nav--display);
justify-content: var(--cwf-tabs__nav--justify-content);
width: var(--cwf-tabs__nav--width);
margin: var(--cwf-tabs__nav--margin);
overflow-x: var(--cwf-tabs__nav--overflow-x);
scrollbar-width: $nav__scrollbar-display;
&::-webkit-scrollbar {
display: $nav__scrollbar-display;
}
// ---- Vertical tabs ----
// Left vertical
$nav__margin--left: 0 -1px 0 0 !default;
@include vertical(left) {
--cwf-tabs__nav--margin: #{$nav__margin--left};
}
// Left & right vertical
$nav__width--vertical: auto !default;
$nav__overflow-x--vertical: none !default;
@include vertical {
--cwf-tabs__nav--width: #{$nav__width--vertical};
--cwf-tabs__nav--overflow-x: #{$nav__overflow-x--vertical};
}
// Right vertical
$nav__margin--right: 0 0 0 -1px !default;
@include vertical(right) {
--cwf-tabs__nav--margin: #{$nav__margin--right};
}
// ---- Tabs alignment ----
// Start aligned
$nav__justify-content--start: flex-start !default;
@include alignment(start) {
--cwf-tabs__nav--justify-content: #{$nav__justify-content--start};
}
// Center aligned
$nav__justify-content--center: center !default;
@include alignment(center) {
--cwf-tabs__nav--justify-content: #{$nav__justify-content--center};
}
// End aligned
$nav__justify-content--end: flex-end !default;
@include alignment(end) {
--cwf-tabs__nav--justify-content: #{$nav__justify-content--end};
}
}
// ---- Tabs wrapper ----
$tabs__display: flex !default;
$tabs__flex-direction: row !default;
$tabs__align-self: flex-start !default;
$tabs__align-items: flex-end !default;
.#{$prefix}-tabs__tabs {
--cwf-tabs__tabs--display: #{$tabs__display};
--cwf-tabs__tabs--flex-direction: #{$tabs__flex-direction};
--cwf-tabs__tabs--align-self: #{$tabs__align-self};
--cwf-tabs__tabs--align-items: #{$tabs__align-items};
display: var(--cwf-tabs__tabs--display);
flex-direction: var(--cwf-tabs__tabs--flex-direction);
align-self: var(--cwf-tabs__tabs--align-self);
align-items: var(--cwf-tabs__tabs--align-items);
// --- Vertical tabs ---
$tabs__padding--vertical: 0.5rem !default;
// Left
@include vertical(left) {
padding-left: $tabs__padding--vertical;
}
// Left & right
$tabs__flex-direction--vertical: column !default;
$tabs__align-self--vertical: center !default;
@include vertical {
--cwf-tabs__tabs--flex-direction: #{$tabs__flex-direction--vertical};
--cwf-tabs__tabs--align-self: #{$tabs__align-self--vertical};
}
// Right
$tabs__align-items--right: flex-start !default;
@include vertical(right) {
--cwf-tabs__tabs--align-items: #{$tabs__align-items--right};
padding-right: $tabs__padding--vertical;
}
// ---- Tabs alignment ----
// Start aligned
$tabs__align-items--start: flex-start !default;
@include alignment(start) {
--cwf-tabs__tabs--align-self: #{$tabs__align-items--start};
}
// Center aligned
$tabs__align-items--center: center !default;
@include alignment(center) {
--cwf-tabs__tabs--align-self: #{$tabs__align-items--center};
}
// End aligned
$tabs__align-items--end: flex-end !default;
@include alignment(end) {
--cwf-tabs__tabs--align-self: #{$tabs__align-items--end};
}
}
// ---- Tabs ----
$tab__flex-shrink: 0 !default;
$tab__max-width: 75% !default;
$tab__padding: 1rem 1.5rem !default;
$tab__border-width: 1px !default;
$tab__border-style: solid !default;
.#{$prefix}-tabs__tab {
--cwf-tabs__tab--flex-shrink: #{$tab__flex-shrink};
--cwf-tabs__tab--max-width: #{$tab__max-width};
--cwf-tabs__tab--padding: #{$tab__padding};
--cwf-tabs__tab--border-width: #{$tab__border-width};
--cwf-tabs__tab--border-style: #{$tab__border-style};
flex-shrink: var(--cwf-tabs__tab--flex-shrink);
max-width: var(--cwf-tabs__tab--max-width);
padding: var(--cwf-tabs__tab--padding);
border-width: var(--cwf-tabs__tab--border-width);
border-style: var(--cwf-tabs__tab--border-style);
border-color: var(--cwf-tabs__tab--border-color);
background-color: var(--cwf-tabs__tab--background-color);
font-family: theme.font--sans-serif();
color: var(--cwf-tabs__tab--color);
// Left and right vertical tab
$tab__max-width--vertical: auto !default;
@include vertical {
--cwf-tabs__tab--max-width: #{$tab__max-width--vertical};
}
// Inactive tab
$tab__border-color--inactive: var(--cwf-tabs--inactive-background-color)
var(--cwf-tabs--inactive-background-color) var(--cwf-tabs--border-color)
var(--cwf-tabs--inactive-background-color) !default;
$tab__background-color--inactive: var(
--cwf-tabs--inactive-background-color
) !default;
$tab__color--inactive: var(--cwf-tabs--inactive-color) !default;
&[tabindex="-1"] {
--cwf-tabs__tab--border-color: #{$tab__border-color--inactive};
--cwf-tabs__tab--background-color: #{$tab__background-color--inactive};
--cwf-tabs__tab--color: #{$tab__color--inactive};
// Left inactive tab
$tab__border-color--inactive--left: var(
--cwf-tabs--inactive-background-color
)
var(--cwf-tabs--border-color)
var(--cwf-tabs--inactive-background-color)
var(--cwf-tabs--inactive-background-color) !default;
@include vertical(left) {
--cwf-tabs__tab--border-color: #{$tab__border-color--inactive--left};
}
// Left & right inactive tab
$tab__width--inactive--vertical: 100% !default;
@include vertical {
width: $tab__width--inactive--vertical;
}
// Right inactive tab
$tab__border-color--inactive--right: var(
--cwf-tabs--inactive-background-color
)
var(--cwf-tabs--inactive-background-color)
var(--cwf-tabs--inactive-background-color)
var(--cwf-tabs--border-color) !default;
@include vertical(right) {
--cwf-tabs__tab--border-color: #{$tab__border-color--inactive--right};
}
// Inactive tab hover
$tab__background-color--inactive--hover: var(
--cwf-tabs--accent-color
) !default;
$tab__color--inactive--hover: var(--cwf-tabs--active-color) !default;
$tab__border-color--inactive--hover: var(
--cwf-tabs--accent-color
) !default;
&:hover {
--cwf-tabs__tab--border-color: #{$tab__border-color--inactive--hover};
--cwf-tabs__tab--background-color: #{$tab__background-color--inactive--hover};
--cwf-tabs__tab--color: #{$tab__color--inactive--hover};
}
}
// Active tab
$tab__padding--active: 1.5rem 1.5rem 1rem 1.5rem !default;
$tab__border-color--active: var(--cwf-tabs--border-color)
var(--cwf-tabs--border-color) theme.color--white()
var(--cwf-tabs--border-color) !default;
$tab__background-color--active: transparent !default;
$tab__color--active: unset !default;
$tab__position--active: relative !default;
$tab__border-bottom-color--active: var(--cwf-tabs--active-color) !default;
&:focus,
&:focus:hover,
&[tabindex="0"],
&[tabindex="0"]:hover {
--cwf-tabs__tab--padding: #{$tab__padding--active};
--cwf-tabs__tab--border-color: #{$tab__border-color--active};
--cwf-tabs__tab--background-color: #{$tab__background-color--active};
--cwf-tabs__tab--color: #{$tab__color--active};
position: $tab__position--active;
// Left active tab
$tab__padding--active--left: 1rem 1.5rem 1rem 2rem !default;
$tab__border-color--active--left: var(--cwf-tabs--border-color)
theme.color--white() var(--cwf-tabs--border-color)
var(--cwf-tabs--border-color) !default;
@include vertical(left) {
--cwf-tabs__tab--padding: #{$tab__padding--active--left};
--cwf-tabs__tab--border-color: #{$tab__border-color--active--left};
}
// Left & right active tab
$tab__width--active--vertical: calc(100% + 0.5rem) !default;
@include vertical {
width: $tab__width--active--vertical;
}
// Right active tab
$tab__padding--active--right: 1rem 2rem 1rem 1.5rem !default;
$tab__border-color--active--right: var(--cwf-tabs--border-color)
var(--cwf-tabs--border-color) var(--cwf-tabs--border-color)
theme.color--white() !default;
@include vertical(right) {
--cwf-tabs__tab--padding: #{$tab__padding--active--right};
--cwf-tabs__tab--border-color: #{$tab__border-color--active--right};
}
// Active tab indicator
$tab--indicator__content--active: "" !default;
$tab--indicator__position--active: absolute !default;
$tab--indicator__direction--active: -1px !default;
$tab--indicator__display--active: block !default;
$tab--indicator__width--active: calc(100% + 2px) !default;
$tab--indicator__height--active: 0.5rem !default;
$tab--indicator__background-color--active: var(
--cwf-tabs--accent-color
) !default;
&:before {
content: $tab--indicator__content--active;
position: $tab--indicator__position--active;
left: $tab--indicator__direction--active;
top: $tab--indicator__direction--active;
display: $tab--indicator__display--active;
width: $tab--indicator__width--active;
height: $tab--indicator__height--active;
background-color: $tab--indicator__background-color--active;
// Left & right active tab indicator
$tab--indicator__width--active--vertical: 0.5rem !default;
$tab--indicator__height--active--vertical: calc(
100% + 2px
) !default;
@include vertical {
width: $tab--indicator__width--active--vertical;
height: $tab--indicator__height--active--vertical;
}
// Right active tab indicator
$tab--indicator__left--active--right: calc(
100% - 0.5rem + 1px
) !default;
@include vertical(right) {
left: $tab--indicator__left--active--right;
}
}
}
// Focused tab
&:focus:before {
@include style.z-index("hidden");
}
}
// ---- Panels ----
.#{$prefix}-tabs__panel {
display: var(--cwf-tabs__panel--display);
min-width: var(--cwf-tabs__panel--min-width);
max-width: var(--cwf-tabs__panel--max-width);
padding: var(--cwf-tabs__panel--padding);
border: var(--cwf-tabs__panel--border);
// Inactive panel
$panel__display--inactive: none !default;
&[aria-hidden="true"] {
--cwf-tabs__panel--display: #{$panel__display--inactive};
}
// Active panel
$panel__display--active: block !default;
$panel__width--active: 100% !default;
$panel__padding--active: 1.5rem !default;
$panel__border--active: 1px solid var(--cwf-tabs--border-color) !default;
&[aria-hidden="false"] {
--cwf-tabs__panel--display: #{$panel__display--active};
--cwf-tabs__panel--min-width: #{$panel__width--active};
--cwf-tabs__panel--max-width: #{$panel__width--active};
--cwf-tabs__panel--padding: #{$panel__padding--active};
--cwf-tabs__panel--border: #{$panel__border--active};
// Active panel content
@include style.children;
// Left & right
$panel__flex--vertical: 1 !default;
$panel__min-width--vertical: 0 !default;
@include vertical {
--cwf-tabs__panel--min-width: #{$panel__min-width--vertical};
flex: $panel__flex--vertical;
}
}
}
// The default component class
import { Component } from '../../shared/component.js';
// Check whether reduced motion is enabled locally or globally
import { reducedMotion } from '../../shared/media/index.js';
// Find focusable descendants and toggle an element's tab order
import {
descendants as getFocusableDescendants,
toggle as toggleTabOrder
} from '../../shared/focus.js';
// Provide functionality to all tabs
export class Tabs extends Component {
constructor({
prefix = 'cwf',
tabs = 'tabs',
nav = 'tabs__nav',
tab = 'tabs__tab',
panel = 'tabs__panel'
} = {}) {
super({
prefix,
classes: {
tabs,
nav,
tab,
panel
}
});
// Initialize an object for the current tabs group
this.current = {};
// Finally, bind all relevant functions to the class
this.keyDown = this.keyDown.bind(this);
this.focus = this.focus.bind(this);
this.blur = this.blur.bind(this);
this.interaction = this.interaction.bind(this);
this.hash = this.hash.bind(this);
this.hashchange = this.hashchange.bind(this);
this.store = this.store.bind(this);
}
// Finds the group of a given element
group(element) {
return this.references.find(({ tabs, panels }) => {
return (
tabs.find((tab) => tab === element) ||
panels.find(({ panel }) => panel === element)
);
});
}
// Handler for when a key is pressed while a tab is focused
keyDown(event) {
// Grab the key pressed...
const { key } = event;
// ... and if nots a navigation key, do nothing else
if (
![
'ArrowLeft',
'ArrowUp',
'ArrowRight',
'ArrowDown',
'Home',
'End'
].includes(key)
)
return;
// Prevent the default behavior
event.preventDefault();
// Grab the current tab...
const { currentTarget: tab } = event;
// ... and the tabs of the current group
const { tabs } = this.current;
// Next, define the first...
const first = 0;
// ... and last indexes
const last = tabs.length - 1;
// Handle first/last navigation
switch (key) {
case 'Home': // Home = Focus first tab
return tabs[first].focus();
case 'End': // End = Focus last tab
return tabs[last].focus();
}
// Get the index of the current,...
const index = tabs.indexOf(tab);
// ... previous,...
let previous = index - 1;
// ... and next tabs
let next = index + 1;
// If the current tab is the first, set the previous index to that of the last tab
if (previous < first) previous = last;
// If the current tab is the last, set the next index to that of the first tab
if (next > last) next = first;
// Define the previous/next arrows
let previousArrow = 'ArrowLeft';
let nextArrow = 'ArrowRight';
// Grab this tab's nav...
const { nav } = this.current;
// ... and if it's horizontal or vertical
const direction = window.getComputedStyle(nav)['overflow-x'];
// If the tabs are displayed vertically,...
if (direction === 'visible') {
// Set the previous/next arrows to up/down respectively
previousArrow = 'ArrowUp';
nextArrow = 'ArrowDown';
}
// Handle previous/next navigation
switch (key) {
case previousArrow: // Left or up arrow = Focus the previous tab
return tabs[previous].focus();
case nextArrow: // Right or down arrow = Focus the next tab
return tabs[next].focus();
}
}
// Returns half of the given number rounded up
half(number) {
return Math.ceil(number) / 2;
}
// Scroll to center a given tab within its nav
scrollToTab(tab) {
// Grab the given tab's nav...
const { nav } = this.group(tab);
// ... and if it doesn't exist or is not scrollable, do nothing else
if (!nav || !nav.scrollWidth) return;
// Next, grab the nav's width...
const { width: navWidth } = nav.getBoundingClientRect();
// ... and the tab's width
const { width: tabWidth } = tab.getBoundingClientRect();
// Next, define how far left we need to scroll to center the tab...
let left = tab.offsetLeft - (this.half(navWidth) - this.half(tabWidth));
// ... and if it's negative, set it to zero
if (!left) left = 0;
// Finally, smooth scroll to center the tab in the nav
return nav.scrollTo({
left,
behavior: reducedMotion({
reduced: 'auto',
noPreference: 'smooth'
})
});
}
// Toggle tab activation
toggleTabs(id) {
// For each tab,...
this.current.tabs.forEach((tab) => {
// ... activate it if its ID matches the given one,...
// ... otherwise, hide it
const match = tab.id === id;
tab.setAttribute('aria-selected', match);
toggleTabOrder(match, tab);
});
}
// Toggle panel visibility
togglePanels(id) {
// For each panel,...
this.current.panels.forEach(({ panel, focusables }) => {
// ... show it if its ID matches the given one,...
// ... otherwise, hide it
const match = panel.id === id;
panel.setAttribute('aria-hidden', !match);
toggleTabOrder(match, panel, ...focusables);
});
}
// Handler for when a tab is clicked
click({ currentTarget: tab }) {
// Focus the clicked tab
return tab.focus();
}
// Handler for when a tab is focused
focus({ currentTarget: tab }) {
// Grab the tab's ID,...
const { id: tabId } = tab;
// ... and the ID of the panel the tab controls
const panelId = tab.getAttribute('aria-controls');
// Next, find the group this tab belongs to...
const group = this.group(tab);
// ... and store it as the current group
this.current = group;
// Scroll to center th given tab within its nav
this.scrollToTab(tab);
// Next, toggle the tabs and panels of that group
this.toggleTabs(tabId);
this.togglePanels(panelId);
// Finally, begin listening for the tab's key-down event
tab.addEventListener('keydown', this.keyDown);
}
// Handler for when a tab loses focus
blur({ currentTarget: tab }) {
// Stop listening for the tab's key-down event
tab.removeEventListener('keydown', this.keyDown);
}
// Binds to tab's interaction (focus/blur) events
interaction(tab) {
tab[this.listener]('click', this.click);
tab[this.listener]('focus', this.focus);
tab[this.listener]('blur', this.blur);
}
// Store a tab group
store(group) {
// Grab the given group's nav,...
const nav = group.querySelector(this.selectors.nav);
// ... tabs,...
const tabs = Array.from(group.querySelectorAll(this.selectors.tab));
// ... and panels and their focusable decendants
const panels = Array.from(
group.querySelectorAll(this.selectors.panel)
).map((panel) => {
// Get the panel's focusable elements...
const focusables = getFocusableDescendants(panel);
// ... and if the panel is hidden, remove its focusable elements from the tab order
if (panel.getAttribute('tabindex') === '-1')
toggleTabOrder(false, ...focusables);
// Finally, return the panel and its focusable elements
return {
panel,
focusables
};
});
// For each tab, handle its focus/blur events
tabs.forEach(this.interaction);
// Finally, add a reference to the group's tabs and panels
this.references.push({
nav,
tabs,
panels
});
}
// Deactivate tab functionality
deactivate({ tabs }) {
tabs.forEach(this.interaction);
}
// Open a tab if the location hash matches a panel's ID
hash(event) {
// Grab the location hash...
const id = window.location.hash.substring(1);
// ... and attempt to to grab the corresponding panel
const panel = document.getElementById(id);
// If not panel was found, do nothing else
if (!panel) return;
// Next, attempt to find the group of the given panel
const group = this.group(panel);
// If no group was found, do nothing else
if (!group) return;
// If an event was triggered, prevent the default behavior
if (event) event.preventDefault();
// Store the current group
this.current = group;
// Get the ID of the tab that control's the given panel
const tab = panel.getAttribute('aria-labelledby');
// Finally, toggle the tabs and panels of that group...
this.toggleTabs(tab);
this.togglePanels(id);
// ... and focus the panel
panel.focus();
}
// Bind to the document's hash changes
hashchange() {
this.hash();
window[this.listener]('hashchange', this.hash);
}
// Mount/unmount tabs functionality
mount(run) {
super.mount(run);
// If mounting,...
if (run) {
// ... attempt to grab all tab groups from the page,...
const groups = Array.from(
document.querySelectorAll(this.selectors.tabs)
);
// ... do nothing else if none exist,...
if (!groups.length) return;
// ... or store each tab group...
groups.forEach(this.store);
} else {
// ... otherwise, deactivate all references...
this.references.forEach(this.deactivate.bind(this));
// ... and reset all references and the current tab group
this.references = [];
this.current = {};
}
// Finally, start/stop listening to hash changes
this.hashchange();
}
}
The tabs component compartmentalizes content accessible by a tabbed button interface. Inspiration and technical guidance from the W3’s example of tabs with automatic activation.
A tabs component is comprised of one or many tab/panel pairs. A tabs component is a wrapping div.cwf-tabs
element with a nav.cwf-tabs__nav
navigation element containing every tab (button.cwf-tabs__tab
) and every panel (section.cwf-tabs__panel
). A tab is tied to a panel via an aria-controls
and aria-labelledby
attribute respectively.
If the sum of the tab widths exceeds the width of the nav, it will begin horizontally scrolling.
If a modifier class of .cwf-tabs--left
or .cwf-tabs--right
is added to the wrapping div.cwf-tabs
element, it will vertically align the tabs to the left or right respectively. On mobile devices, vertical tabs will revert back to the default (horizontal).
If a modifier class of .cwf-tabs--start
, .cwf-tabs--center
, or .cwf-tabs--end
is added to the wrapping div.cwf-tabs
element, it will align the tabs to the start (left when horizontal, top when vertical), center, or end (right when horizontal, bottom when vertical) of the nav respectively. On mobile devices, the tab alignment will revert back to the default.
Each tab within a tabs component is listening for focus events. When a tab is focused, it will open its corresponding panel and if the nav is scrollable, it will smooth scroll the tab to the center of the nav. It will also begin listening for keydown events when focused for the following keyboard navigation with its group:
In addition, this component is listening for hash changes. If the hash changes to the ID of a panel, it will open and focus it; This allows for linking to a given tabs panel on the same or different page without usability concerns.
Tabs are implemented in T4 as the “Tab Panel” plugin, meaning its classes are .plugin-
prefixed instead of .cwf-
prefixed.
The plugin outputs a single tab/panel pair, and using 1 or more sequential plugins group them together as an entire tabs component.
Global or local areas are not supported by this plugin, meaning this plugin can only be used where normal content goes (e.g. the text/html
content layout).
In the “Injectors” field of the “Tab Panel” plugin, the following injectors can be used:
id:{custom_id}
- Overrides the default, T4 ID of the tab panel with a custom ID.vertical:{left|right}
- Vertically aligns the tabs to the left or the right on the entire tabs group. This injector only works on the first tab panel of the group.alignment:{start|center|end}
- Aligns the tabs to the start, center, or end of the nav on the entire tabs group. This injector only works on the first tab panel of the group.class:{custom_classes}
- Adds custom classes to the tab panel.style:{custom_styles}
- Adds custom styles to a style
attribute of the tab panel.before:{custom_html}
- Adds custom HTML before the tab panel.after:{custom_html}
- Adds custom HTML after the tab panel.