/**
* HTZ A11Y TABS
*
* JavaScript scaffolding for accessible tab interfaces
*
* @module htz-a11y-tabs
* @license MIT
*/
import { initialize, destroyInstance, gotoTab, nextTab, prevTab } from './lib/utils';
/**
* Initialize a tab interface.
* Depends on semantic markup, in which the tablist is a `ul` element, and each
* tab contains an clickable tag pointing to its respective tabpanel.
* @param {HTMLElement} container - The wrapper element around the tabs and tab panels
* @param {Boolean} rtl - Determine if the tab interface should be right-to-left
* @param {String} [tablistSelector='ul'] - The tablist's selector.
* @param {String} [tabpanelSelector='section'] - The tabpanels' selector.
* @param {Integer} [activeTab=0] - The tab number to have initially activated. Zero based.
*
* @fires module:htz-a11y-tabs~a11y-tabs:init - Fired from `container` after a
* tab interface has been initialized
* @fires module:htz-a11y-tabs~a11y-tabs:destroy - Fired from `container` after
* a tab interface has been destroyed
* @fires module:htz-a11y-tabs~a11y-tabs:before-select - Fired from `container`
* before a tab selection is applied. If the event handler executes
* `event.preventDefault()`, the selection will not be applied.
* @fires module:htz-a11y-tabs~a11y-tabs:after-select - Fired from `container`
* after a tab selection is applied.
*
* @return {module:htz-a11y-tabs#API}
*/
export default function htzA11yTabs(
container,
rtl,
tablistSelector = 'ul',
tabpanelSelector = 'section',
activeTab = 0
) {
// State
const state = {
isInitialized: false,
activeTab,
};
let [tabs, tabpanels] = initialize(
container,
tablistSelector,
tabpanelSelector,
clickHandler,
keyHandler,
activeTab
);
init(activeTab);
/* ---- EVENT HANDLERS ----- */
/**
* Change focus between tabs with arrow keys
*
* @param {Object} evt - A keyboard event object.
*/
function keyHandler(evt) {
const key = evt.keyCode;
const active = state.activeTab;
const back = key === (rtl ? 39 : 37) || key === 38;
const forward = key === (rtl ? 37 : 39) || key === 40;
if (back) state.activeTab = prevTab(container, tabs, tabpanels, active, false);
else if (forward) state.activeTab = nextTab(container, tabs, tabpanels, active, false);
if ([undefined, null, false].indexOf(state.activeTab) >= 0) state.activeTab = active;
}
function clickHandler(evt) {
evt.preventDefault();
const targetIndex = tabs.indexOf(evt.target.closest('a, button'));
const active = state.activeTab;
state.activeTab = gotoTab(targetIndex, container, tabs, tabpanels, active, true);
if ([undefined, null, false].indexOf(state.activeTab) >= 0) state.activeTab = active;
}
/* ---- Instance methods ----- */
/**
* Initialize an instance
* @callback module:htz-a11y-tabs#init
*
* @param {Integer} [activate] - The tab number to activate. Zero based.
*/
function init(activate = activeTab) {
if (state.isInitialized) destroy();
([tabs, tabpanels] = initialize(
container,
tablistSelector,
tabpanelSelector,
clickHandler,
keyHandler,
activate
));
state.isInitialized = true;
state.activeTab = activate;
}
/**
* Destroy an instance. Removes event listeners and, optionally,
* accessibility attributes from the DOM.
*
* @callback module:htz-a11y-tabs#destroy
*
* @param {Boolean} [removeAttrs] - Determine if attributes should be remove
*/
function destroy(removeAttrs) {
destroyInstance(
container,
tablistSelector,
tabpanelSelector,
clickHandler,
keyHandler,
removeAttrs
);
state.isInitialized = false;
}
/**
* Go to a tab
*
* @callback module:htz-a11y-tabs#goto
*
* @param {Integer} index - The index of the tab to be activated
* @param {Boolean} [focus] - Determine if the newly activated tab should be focused.
*/
function goto(index, focus) {
const active = state.activeTab;
state.activeTab = gotoTab(index, container, tabs, tabpanels, active, focus);
if ([undefined, null, false].indexOf(state.activeTab) >= 0) state.activeTab = active;
}
/**
* Go to the next tab
*
* @callback module:htz-a11y-tabs#next
*
* @param {Boolean} [focus] - Determine if the newly activated tab should be focused.
*/
function next(focus) {
const active = state.activeTab;
state.activeTab = nextTab(container, tabs, tabpanels, active, focus);
if ([undefined, null, false].indexOf(state.activeTab) >= 0) state.activeTab = active;
}
/**
* Go to the previous tab
*
* @callback module:htz-a11y-tabs#prev
*
* @param {Boolean} [focus] - Determine if the newly activated tab should be focused.
*/
function prev(focus) {
const active = state.activeTab;
state.activeTab = prevTab(container, tabs, tabpanels, active, focus);
if ([undefined, null, false].indexOf(state.activeTab) >= 0) state.activeTab = active;
}
/**
* Instance API
*
* @typedef {Object} module:htz-a11y-tabs#API
*
* @prop {Method} isInitialized - Returns true if the instance is initialized
* @prop {Method} visibleTab - Returns the number of the active tab. Zero based.
* @prop {module:htz-a11y-tabs#init} init - Initialize an instance.
* @prop {module:htz-a11y-tabs#destroy} destroy - Destroy an instance.
* @prop {module:htz-a11y-tabs#goto} goto - Go to a specific tab
* @prop {module:htz-a11y-tabs#next} next - Go to the next tab
* @prop {module:htz-a11y-tabs#prev} previous - Go to the next tab
*/
return {
// Status
isInitialized() { return state.isInitialized; },
visibleTab() { return state.activeTab; },
// Instance handling
init,
destroy,
// Instance control
goto,
next,
prev,
};
}