/**
* Provides tablist element.
* @module AashTablist
*/
import { defineComponent, PropType, ref, reactive, computed,
onMounted, watch } from 'vue'
import { provideApi } from "../../AashUtil";
/**
* The information about a panel managed by the tablist.
* @memberOf module:AashTablist
*/
export type Panel = {
/** The id of the panel's root node */
id: string;
/** The label to use for the panel */
label: string | Function;
/** A function to call when the panel is removed (optional) */
removeCallback?: () => void
};
/**
* The interface provided by the component.
*
* * `addPanel(panel: Panel): void`: adds another panel.
* * `removePanel(panelId: string): void`: removes the panel with the given id.
* * `selectPanel(panelId: string): void`: activates the panel with the given id.
* * `panels(): Panel[]`: returns the panels.
*
* @memberof module:AashTablist
*/
export interface Api {
addPanel(panel: Panel): void;
removePanel(panelId: string): void;
selectPanel(panelId: string): void;
panels(): Panel[];
}
/**
* @classdesc
* Generates a
* [tab list element](https://www.w3.org/TR/wai-aria-practices-1.1/#tabpanel)
* and its child tab elements with all required ARIA attributes.
* All tab elements have an `aria-controls` attribute that references the
* associated tab panel.
*
* The tab panels controlled by the tab list are made known by objects of
* type {@link module:AashTablist.Panel Panel}. Because the tab panels are
* referenced from the
* tab elements, the tab panel elements need only
* an `id` attribute and `role=tabpanel` `tabindex=0`.
*
* Once created, the component provides the externally invocable methods
* defined by {@link module:AashTablist.Api} through an object in
* a property of the mounted DOM element (see {@link module:AashUtil.getApi}).
*
* The DOM is generated as shown in the
* [WAI-ARIA Authoring Practices 1.1](https://www.w3.org/TR/wai-aria-practices-1.1/examples/tabs/tabs-2/tabs.html)
*
* Example:
* ```html
* <div>
* <div id="sampleTabs" class="aash-tablist" role="tablist">
* <span id="tab-1-tab" role="tab" aria-selected="true"
* aria-controls="tab-1">
* <button type="button" tabindex="0">Tab 1</button>
* </span>
* <span id="tab-2-tab" role="tab" aria-selected="false"
* aria-controls="tab-2">
* <button type="button" tabindex="-1">Tab 2</button>
* </span>
* </div>
* </div>
* <div id="tab-1" role="tabpanel" aria-labelledby="tab-1-tab">This
* is panel One.</div>
* <div id="tab-2" role="tabpanel" aria-labelledby="tab-2-tab" hidden="">
* This is panel Two.</div>
* ```
*
* @class AashTablistComponent
* @param {Object} props the properties
* @param {string} props.id the id for the enclosing `div`
* @param {Panel[]} props.initialPanels the list of initial panels
* @param {function} props.l10n a function invoked with a label
* (of type string) as argument before the label is rendered
*/
export default defineComponent({
props: {
id: { type: String, required: true },
initialPanels: { type: Array as PropType<Array<Panel>> },
l10n: { type: Function as PropType<((key: string) => string)> }
},
setup(props, context) {
const panels = reactive(props.initialPanels || []);
const selected: any = ref(null);
const isVertical = computed(() => {
return context.attrs["aria-orientation"] !== undefined
&& context.attrs["aria-orientation"] === "vertical";
});
const addPanel = (panel: Panel) => {
panels.push(panel);
setupTabpanel(panel)
};
const removePanel = (panelId: string) => {
let prevPanel = 0;
for (let i = 0; i < panels.length; i++) {
if (panels[i].id === panelId) {
panels.splice(i, 1);
break;
}
prevPanel = i;
}
if (panels.length > 0) {
selectPanel(panels[prevPanel].id);
}
};
const selectPanel = (panelId: string) => {
if (selected.value) {
let tabpanel = document.querySelector("[id='" + selected.value + "']");
if (tabpanel) {
tabpanel.setAttribute("hidden", "");
}
}
selected.value = panelId;
let tabpanel = document.querySelector("[id='" + selected.value + "']");
if (tabpanel) {
tabpanel.removeAttribute("hidden");
}
};
const label = (panel: Panel) => {
if (typeof panel.label === 'function') {
return panel.label();
}
if (props.l10n) {
return props.l10n(panel.label);
}
return panel.label;
};
const setupTabpanel = (panel: Panel) => {
let tabpanel: HTMLElement | null = document.querySelector(
"[id='" + panel.id + "']");
if (tabpanel == null) {
return;
}
tabpanel.setAttribute("role", "tabpanel");
tabpanel.setAttribute("aria-labelledby",
tabpanel.getAttribute('id') + '-tab');
if (tabpanel.getAttribute('id') === selected.value) {
tabpanel.removeAttribute("hidden");
} else {
tabpanel.setAttribute("hidden", "");
}
};
const selectedPanel = function(): [Panel | null, number] {
for (let i = 0; i < panels.length; i++) {
let panel = panels[i];
if (panel.id === selected.value) {
return [panel, i];
}
}
return [null, -1];
}
const onKey = (event: KeyboardEvent) => {
if (event.type === "keydown") {
if (isVertical.value
&& ["ArrowUp", "ArrowDown"].includes(event.key)) {
event.preventDefault();
}
return;
}
if (event.type !== "keyup") {
return;
}
let [panel, panelIndex] = selectedPanel();
if (!panel) {
return;
}
let handled = false;
if (isVertical.value ? event.key === "ArrowUp"
: event.key === "ArrowLeft") {
selectPanel(panels[
(panelIndex-1+panels.length)%panels.length].id);
handled = true;
} else if (isVertical.value ? event.key === "ArrowDown"
: event.key === "ArrowRight") {
selectPanel(panels[(panelIndex+1)%panels.length].id);
handled = true;
} else if (event.key === "Delete") {
if (panel.removeCallback) {
panel.removeCallback();
handled = true;
}
} else if (event.key === "Home") {
selectPanel(panels[0].id);
handled = true;
} else if (event.key === "End") {
selectPanel(panels[panels.length-1].id);
handled = true;
}
if (handled) {
event.preventDefault();
let tab: HTMLElement | null = document.querySelector(
"[id='" + selected.value + "-tab'] > button");
tab?.focus();
}
}
const tablist = ref(null);
provideApi(tablist, { addPanel, removePanel, selectPanel,
panels: () => { return panels.slice() } });
onMounted(() => {
if (panels.length > 0) {
selected.value = panels[0].id;
}
for (let panel of panels) {
setupTabpanel(panel);
}
});
watch(panels, (oldValue, newValue) => {
if (selected.value === null && newValue.length > 0) {
selectPanel(panels[0].id);
}
});
return { panels, selected, label, tablist, onKey, selectPanel };
}
});
Source