Skip to content

Commit

Permalink
feat: add vertical tabs
Browse files Browse the repository at this point in the history
  • Loading branch information
v8tenko committed Jun 7, 2024
1 parent 22ab294 commit 6bf98d9
Show file tree
Hide file tree
Showing 5 changed files with 218 additions and 20 deletions.
2 changes: 2 additions & 0 deletions src/common.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type {TabsController} from './runtime/TabsController';

export const TABS_CLASSNAME = 'yfm-tabs';
export const TABS_VERTICAL_CLASSNAME = 'yfm-tabs-vertical';
export const TABS_LIST_CLASSNAME = 'yfm-tab-list';
export const TAB_CLASSNAME = 'yfm-tab';
export const TAB_PANEL_CLASSNAME = 'yfm-tab-panel';
Expand All @@ -10,6 +11,7 @@ export const GROUP_DATA_KEY = 'data-diplodoc-group';
export const TAB_DATA_KEY = 'data-diplodoc-key';
export const TAB_DATA_ID = 'data-diplodoc-id';
export const TAB_ACTIVE_KEY = 'data-diplodoc-is-active';
export const TAB_RADIO_KEY = 'data-diplodoc-input'

Check failure on line 14 in src/common.ts

View workflow job for this annotation

GitHub Actions / build (18.x)

Insert `;`

export const DEFAULT_TABS_GROUP_PREFIX = 'defaultTabsGroup-';

Expand Down
65 changes: 55 additions & 10 deletions src/plugin/transform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
GROUP_DATA_KEY,
TABS_CLASSNAME,
TABS_LIST_CLASSNAME,
TABS_VERTICAL_CLASSNAME,
TAB_ACTIVE_KEY,
TAB_CLASSNAME,
TAB_DATA_ID,
Expand All @@ -25,7 +26,9 @@ export type PluginOptions = {
bundle: boolean;
};

const TAB_RE = /`?{% list tabs( group=([^ ]*))? %}`?/;
export type TabsOrientation = 'vertical' | 'horizontal';

const TAB_RE = /`?{% list tabs( group=([^ ]*))?( (vertical)|(horizontal))? %}`?/;

let runsCounter = 0;

Expand Down Expand Up @@ -119,6 +122,7 @@ function findTabs(tokens: Token[], idx: number) {
function insertTabs(
tabs: Tab[],
state: StateCore,
align: TabsOrientation,
{start, end}: {start: number; end: number},
{
containerClasses,
Expand Down Expand Up @@ -155,11 +159,17 @@ function insertTabs(
tabListOpen.block = true;
tabListClose.block = true;

tabsOpen.attrSet('class', [TABS_CLASSNAME, containerClasses].filter(Boolean).join(' '));
const areTabsVerticalClass = align === 'vertical' && TABS_VERTICAL_CLASSNAME;

tabsOpen.attrSet('class', [TABS_CLASSNAME, containerClasses, areTabsVerticalClass].filter(Boolean).join(' '));

Check failure on line 164 in src/plugin/transform.ts

View workflow job for this annotation

GitHub Actions / build (18.x)

Replace `'class',·[TABS_CLASSNAME,·containerClasses,·areTabsVerticalClass].filter(Boolean).join('·')` with `⏎········'class',⏎········[TABS_CLASSNAME,·containerClasses,·areTabsVerticalClass].filter(Boolean).join('·'),⏎····`
tabsOpen.attrSet(GROUP_DATA_KEY, tabsGroup);
tabListOpen.attrSet('class', TABS_LIST_CLASSNAME);
tabListOpen.attrSet('role', 'tablist');

if (align === 'vertical') {
tabsTokens.push(tabsOpen);
}

for (let i = 0; i < tabs.length; i++) {
const tabOpen = new state.Token('tab_open', 'div', 1);
const tabInline = new state.Token('inline', '', 0);
Expand All @@ -168,13 +178,25 @@ function insertTabs(
const tabPanelOpen = new state.Token('tab-panel_open', 'div', 1);
const tabPanelClose = new state.Token('tab-panel_close', 'div', -1);

const verticalTabOpen = new state.Token('tab_open', 'input', 0);
const verticalTabLabelOpen = new state.Token('label_open', 'label', 1)

Check failure on line 182 in src/plugin/transform.ts

View workflow job for this annotation

GitHub Actions / build (18.x)

Replace `⏎········` with `;`


tabOpen.map = tabs[i].listItem.map;
tabOpen.markup = tabs[i].listItem.markup;

const tab = tabs[i];
const tabId = getTabId(tab, {runId});
const tabKey = getTabKey(tab);
tab.name = getName(tab);

const tabPanelId = generateID();

verticalTabOpen.block = true;

verticalTabOpen.attrJoin('class', 'radio')

Check failure on line 197 in src/plugin/transform.ts

View workflow job for this annotation

GitHub Actions / build (18.x)

Insert `;`
verticalTabOpen.attrSet('type', 'radio');

tabOpen.map = tabs[i].listItem.map;
tabOpen.markup = tabs[i].listItem.markup;
tabText.content = tabs[i].name;
Expand All @@ -187,6 +209,7 @@ function insertTabs(
tabOpen.attrSet(TAB_DATA_KEY, tabKey);
tabOpen.attrSet(TAB_ACTIVE_KEY, i === 0 ? 'true' : 'false');
tabOpen.attrSet('class', TAB_CLASSNAME);
tabOpen.attrJoin('class', 'yfm-tab-group')

Check failure on line 212 in src/plugin/transform.ts

View workflow job for this annotation

GitHub Actions / build (18.x)

Insert `;`
tabOpen.attrSet('role', 'tab');
tabOpen.attrSet('aria-controls', tabPanelId);
tabOpen.attrSet('aria-selected', 'false');
Expand All @@ -197,21 +220,39 @@ function insertTabs(
tabPanelOpen.attrSet('aria-labelledby', tabId);
tabPanelOpen.attrSet('data-title', tab.name);

if (align === 'vertical') {
tabOpen.attrSet('data-diplodoc-vertical-tab', 'true')

Check failure on line 224 in src/plugin/transform.ts

View workflow job for this annotation

GitHub Actions / build (18.x)

Insert `;`
tabOpen.attrJoin('class', 'yfm-vertical-tab')

Check failure on line 225 in src/plugin/transform.ts

View workflow job for this annotation

GitHub Actions / build (18.x)

Insert `;`
}

if (i === 0) {
if (align === 'horizontal') {

Check failure on line 229 in src/plugin/transform.ts

View workflow job for this annotation

GitHub Actions / build (18.x)

Insert `·`
tabOpen.attrJoin('class', ACTIVE_CLASSNAME);

Check failure on line 230 in src/plugin/transform.ts

View workflow job for this annotation

GitHub Actions / build (18.x)

Replace `············` with `················`
tabOpen.attrSet('aria-selected', 'true');

Check failure on line 231 in src/plugin/transform.ts

View workflow job for this annotation

GitHub Actions / build (18.x)

Insert `····`
tabPanelOpen.attrJoin('class', ACTIVE_CLASSNAME);
} else {
verticalTabOpen.attrSet('checked', 'true');
}

tabPanelOpen.attrJoin('class', ACTIVE_CLASSNAME);
}

tabListTokens.push(tabOpen, tabInline, tabClose);
tabPanelsTokens.push(tabPanelOpen, ...tabs[i].tokens, tabPanelClose);
if (align === 'vertical') {
tabsTokens.push(tabOpen, verticalTabOpen, verticalTabLabelOpen, tabInline, tabClose);
tabsTokens.push(tabPanelOpen, ...tabs[i].tokens, tabPanelClose);
} else {
tabListTokens.push(tabOpen, tabInline, tabClose);
tabPanelsTokens.push(tabPanelOpen, ...tabs[i].tokens, tabPanelClose);
}
}

if (align === 'horizontal') {
tabsTokens.push(tabsOpen)
tabsTokens.push(tabListOpen);
tabsTokens.push(...tabListTokens);
tabsTokens.push(tabListClose);
tabsTokens.push(...tabPanelsTokens);
}

tabsTokens.push(tabsOpen);
tabsTokens.push(tabListOpen);
tabsTokens.push(...tabListTokens);
tabsTokens.push(tabListClose);
tabsTokens.push(...tabPanelsTokens);
tabsTokens.push(tabsClose);

state.tokens.splice(start, end - start + 1, ...tabsTokens);
Expand Down Expand Up @@ -290,13 +331,17 @@ export function transform({
}

const tabsGroup = match[2] || `${DEFAULT_TABS_GROUP_PREFIX}${generateID()}`;
const orientation = (match[4] || 'horizontal') as TabsOrientation;

const {tabs, index} = findTabs(state.tokens, i + 3);

debugger;

if (tabs.length > 0) {
insertTabs(
tabs,
state,
orientation,
{start: i, end: index + 3},
{
containerClasses,
Expand Down
3 changes: 2 additions & 1 deletion src/react/useDiplodocTabs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export function useDiplodocTabs(callback: UseDiplodocTabsCallback) {
window[GLOBAL_SYMBOL].selectTabById(tabId, options),
[],
),
selectTab: useCallback((tab: Tab) => window[GLOBAL_SYMBOL].selectTab(tab), []),
// @todo remove
selectTab: useCallback((tab: Tab) => window[GLOBAL_SYMBOL].selectTab(tab, 'horizontal'), []),
};
}
104 changes: 95 additions & 9 deletions src/runtime/TabsController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,14 @@ import {
SelectedTabEvent,
TABS_CLASSNAME,
TABS_LIST_CLASSNAME,
TABS_VERTICAL_CLASSNAME,
TAB_CLASSNAME,
TAB_DATA_ID,
TAB_DATA_KEY,
TAB_PANEL_CLASSNAME,
Tab,
} from '../common';
import { TabsOrientation } from '../plugin/transform';
import {
ElementOffset,
getClosestScrollableParent,
Expand All @@ -24,6 +26,7 @@ const Selector = {
TAB_LIST: `.${TABS_LIST_CLASSNAME}`,
TAB: `.${TAB_CLASSNAME}`,
TAB_PANEL: `.${TAB_PANEL_CLASSNAME}`,
VERTICAL_TABS: `.${TABS_VERTICAL_CLASSNAME}`
};

export interface ISelectTabByIdOptions {
Expand All @@ -43,14 +46,23 @@ export class TabsController {
this._document = document;
this._document.addEventListener('click', (event) => {
const target = getEventTarget(event) as HTMLElement;
const areVertical = this.areTabsVertical(target);

if (isCustom(event) || !this.isValidTabElement(target)) {
if (isCustom(event)) {
return;
}

if (!(this.isValidTabElement(target) || areVertical)) {
return;
}

const tab = this.getTabDataFromHTMLElement(target);
const align: TabsOrientation = areVertical ? 'vertical' : 'horizontal';

console.log(tab)

Check warning on line 62 in src/runtime/TabsController.ts

View workflow job for this annotation

GitHub Actions / build (18.x)

Unexpected console statement

if (tab) {
this._selectTab(tab, target);
this._selectTab(tab, align,target);
}
});
this._document.addEventListener('keydown', (event) => {
Expand Down Expand Up @@ -84,10 +96,12 @@ export class TabsController {
return;
}

const align = this.getTabsOrientation(target);

const newIndex =
(currentTabIndex + (direction === 'left' ? -1 : 1) + tabs.length) % tabs.length;

this.selectTab(tabs[newIndex]);
this.selectTab(tabs[newIndex], align);
nodes[newIndex].focus();
});
}
Expand All @@ -108,22 +122,24 @@ export class TabsController {
if (!target || !this.isValidTabElement(target)) {
return;
}

const align = this.getTabsOrientation(target);

const tab = this.getTabDataFromHTMLElement(target);
if (tab) {
this._selectTab(tab, target);
this._selectTab(tab, align, target);
}

if (options?.scrollToElement) {
target.scrollIntoView();
}
}

selectTab(tab: Tab) {
this._selectTab(tab);
selectTab(tab: Tab, align: TabsOrientation) {
this._selectTab(tab, align);
}

private _selectTab(tab: Tab, targetTab?: HTMLElement) {
private _selectTab(tab: Tab, align: TabsOrientation, targetTab?: HTMLElement) {
const {group, key} = tab;

if (!group) {
Expand All @@ -134,7 +150,7 @@ export class TabsController {
const previousTargetOffset =
scrollableParent && getOffsetByScrollableParent(targetTab, scrollableParent);

const updatedTabs = this.updateHTML({group, key});
const updatedTabs = this.updateHTML({group, key}, align);

if (updatedTabs > 0) {
this.fireSelectTabEvent({group, key}, targetTab?.dataset.diplodocId);
Expand All @@ -145,7 +161,55 @@ export class TabsController {
}
}

private updateHTML(tab: Required<Tab>) {
private updateHTML(tab: Required<Tab>, align: TabsOrientation) {

Check warning on line 164 in src/runtime/TabsController.ts

View workflow job for this annotation

GitHub Actions / build (18.x)

Expected to return a value at the end of method 'updateHTML'
switch (align) {
case 'vertical': {
return this.updateHTMLVertical(tab)
}
case 'horizontal': {
return this.updateHTMLHorizontal(tab);
}
}

}

private updateHTMLVertical(tab: Required<Tab>) {
const {group, key} = tab;

const [tabs] = this._document.querySelectorAll(
`${Selector.TABS}[${GROUP_DATA_KEY}="${group}"] ${Selector.TAB}[${TAB_DATA_KEY}="${key}"]`,
);


let updated = 0;
const root = tabs.parentNode!;

Check warning on line 185 in src/runtime/TabsController.ts

View workflow job for this annotation

GitHub Actions / build (18.x)

Forbidden non-null assertion
const elements = root.children;

for (let i = 0; i < elements.length; i += 2) {
const [title, content] = [elements.item(i), elements.item(i + 1)] as HTMLElement[];

const input = title.children.item(0) as HTMLInputElement;


if (input.hasAttribute('checked')) {
title.classList.remove('active');
content?.classList.remove('active');
input.removeAttribute('checked')
}

if (title === tabs) {
title.classList.add('active');
content?.classList.add('active');
input.setAttribute('checked', 'true');
}

updated++;
}

return updated;
}

private updateHTMLHorizontal(tab: Required<Tab>) {
const {group, key} = tab;

const tabs = this._document.querySelectorAll(
Expand Down Expand Up @@ -208,6 +272,7 @@ export class TabsController {
const {group, key} = tab;

const eventTab: Tab = group.startsWith(DEFAULT_TABS_GROUP_PREFIX) ? {key} : tab;
console.log(eventTab, this._onSelectTabHandlers);

Check warning on line 275 in src/runtime/TabsController.ts

View workflow job for this annotation

GitHub Actions / build (18.x)

Unexpected console statement

this._onSelectTabHandlers.forEach((handler) => {
handler({tab: eventTab, currentTabId: diplodocId});
Expand All @@ -219,10 +284,31 @@ export class TabsController {
element.matches(Selector.TAB) && element.dataset.diplodocId
? element.closest(Selector.TAB_LIST)
: null;

return tabList?.closest(Selector.TABS);
}

private areTabsVertical(target: HTMLElement) {
const parent = target.parentElement;

return target.dataset.diplodocVerticalTab || Boolean(parent?.dataset.diplodocVerticalTab);
}

private getTabsOrientation(target: HTMLElement): TabsOrientation {
const areVertical = this.areTabsVertical(target);

return areVertical ? 'vertical' : 'horizontal';
}

private getTabDataFromHTMLElement(target: HTMLElement): Tab | null {
if (this.areTabsVertical(target)) {
const tab = target.dataset.diplodocVerticalTab ? target : target.parentElement!;

Check warning on line 305 in src/runtime/TabsController.ts

View workflow job for this annotation

GitHub Actions / build (18.x)

Forbidden non-null assertion

const key = tab.dataset.diplodocKey;
const group = (tab.closest(Selector.TABS) as HTMLElement)?.dataset.diplodocGroup;
return key && group ? {group, key} : null;
}

const key = target.dataset.diplodocKey;
const group = (target.closest(Selector.TABS) as HTMLElement)?.dataset.diplodocGroup;
return key && group ? {group, key} : null;
Expand Down
Loading

0 comments on commit 6bf98d9

Please sign in to comment.