Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add vertical tabs #38

Merged
merged 1 commit into from
Jun 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,25 @@ Example:
{% endlist %}
```

Additionally, you can use radiobatons using a contruction

```
{% list tabs vertical %}

- Tab 1

Text 1.

* You can use list
* And **other** features.

- Tab 2

Text 2.

{% endlist %}
```

The keys for the tabs are generated automatically. They are based on the tab's names using the github anchors style.

You can set your own keys for tabs with this statement:
Expand Down
6 changes: 6 additions & 0 deletions src/common.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,27 @@
import type {TabsOrientation} from './plugin/transform';
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';
export const ACTIVE_CLASSNAME = 'active';
export const VERTICAL_TAB_CLASSNAME = 'yfm-vertical-tab';

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_DATA_VERTICAL_TAB = 'data-diplodoc-vertical-tab';
export const TAB_ACTIVE_KEY = 'data-diplodoc-is-active';
export const TAB_RADIO_KEY = 'data-diplodoc-input';

export const DEFAULT_TABS_GROUP_PREFIX = 'defaultTabsGroup-';

export interface Tab {
group?: string;
key: string;
align: TabsOrientation;
}

export interface SelectedTabEvent {
Expand Down
69 changes: 58 additions & 11 deletions src/plugin/transform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,14 @@ import {
GROUP_DATA_KEY,
TABS_CLASSNAME,
TABS_LIST_CLASSNAME,
TABS_VERTICAL_CLASSNAME,
TAB_ACTIVE_KEY,
TAB_CLASSNAME,
TAB_DATA_ID,
TAB_DATA_KEY,
TAB_DATA_VERTICAL_TAB,
TAB_PANEL_CLASSNAME,
VERTICAL_TAB_CLASSNAME,
} from '../common';

export type PluginOptions = {
Expand All @@ -25,7 +28,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))? %}`?/;
v8tenko marked this conversation as resolved.
Show resolved Hide resolved

let runsCounter = 0;

Expand Down Expand Up @@ -119,6 +124,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 +161,20 @@ 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(' '),
);
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 +183,24 @@ 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);

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');
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 +213,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');
tabOpen.attrSet('role', 'tab');
tabOpen.attrSet('aria-controls', tabPanelId);
tabOpen.attrSet('aria-selected', 'false');
Expand All @@ -197,21 +224,39 @@ function insertTabs(
tabPanelOpen.attrSet('aria-labelledby', tabId);
tabPanelOpen.attrSet('data-title', tab.name);

if (align === 'vertical') {
tabOpen.attrSet(TAB_DATA_VERTICAL_TAB, 'true');
tabOpen.attrJoin('class', VERTICAL_TAB_CLASSNAME);
}

if (i === 0) {
tabOpen.attrJoin('class', ACTIVE_CLASSNAME);
tabOpen.attrSet('aria-selected', 'true');
if (align === 'horizontal') {
tabOpen.attrJoin('class', ACTIVE_CLASSNAME);
tabOpen.attrSet('aria-selected', 'true');
} 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 +335,15 @@ 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);

if (tabs.length > 0) {
insertTabs(
tabs,
state,
orientation,
{start: i, end: index + 3},
{
containerClasses,
Expand Down
1 change: 1 addition & 0 deletions 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),
[],
),
// @todo remove
v8tenko marked this conversation as resolved.
Show resolved Hide resolved
selectTab: useCallback((tab: Tab) => window[GLOBAL_SYMBOL].selectTab(tab), []),
};
}
90 changes: 82 additions & 8 deletions src/runtime/TabsController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,14 @@
SelectedTabEvent,
TABS_CLASSNAME,
TABS_LIST_CLASSNAME,
TABS_VERTICAL_CLASSNAME,
TAB_CLASSNAME,
TAB_DATA_ID,
TAB_DATA_KEY,
TAB_PANEL_CLASSNAME,
Tab,
} from '../common';
import type {TabsOrientation} from '../plugin/transform';
import {
ElementOffset,
getClosestScrollableParent,
Expand All @@ -24,6 +26,7 @@
TAB_LIST: `.${TABS_LIST_CLASSNAME}`,
TAB: `.${TAB_CLASSNAME}`,
TAB_PANEL: `.${TAB_PANEL_CLASSNAME}`,
VERTICAL_TABS: `.${TABS_VERTICAL_CLASSNAME}`,
};

export interface ISelectTabByIdOptions {
Expand All @@ -43,12 +46,18 @@
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);

if (tab) {
this._selectTab(tab, target);
}
Expand Down Expand Up @@ -110,6 +119,7 @@
}

const tab = this.getTabDataFromHTMLElement(target);

if (tab) {
this._selectTab(tab, target);
}
Expand All @@ -124,7 +134,7 @@
}

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

if (!group) {
return;
Expand All @@ -134,18 +144,65 @@
const previousTargetOffset =
scrollableParent && getOffsetByScrollableParent(targetTab, scrollableParent);

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

if (updatedTabs > 0) {
this.fireSelectTabEvent({group, key}, targetTab?.dataset.diplodocId);
this.fireSelectTabEvent({group, key, align}, targetTab?.dataset.diplodocId);

if (previousTargetOffset) {
this.resetScroll(targetTab, scrollableParent, previousTargetOffset);
}
}
}

private updateHTML(tab: Required<Tab>) {
private updateHTML(tab: Required<Tab>, align: TabsOrientation) {
switch (align) {
case 'vertical': {
return this.updateHTMLVertical(tab);
}
case 'horizontal': {
return this.updateHTMLHorizontal(tab);
}
}

return 0;
}

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 179 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 @@ -205,9 +262,9 @@
}

private fireSelectTabEvent(tab: Required<Tab>, diplodocId?: string) {
const {group, key} = tab;
const {group, key, align} = tab;

const eventTab: Tab = group.startsWith(DEFAULT_TABS_GROUP_PREFIX) ? {key} : tab;
const eventTab: Tab = group.startsWith(DEFAULT_TABS_GROUP_PREFIX) ? {key, align} : tab;

this._onSelectTabHandlers.forEach((handler) => {
handler({tab: eventTab, currentTabId: diplodocId});
Expand All @@ -219,13 +276,28 @@
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);
v8tenko marked this conversation as resolved.
Show resolved Hide resolved
}

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

Check warning on line 291 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, align: 'vertical'} : null;
}

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

private getTabs(target: HTMLElement): {tabs: Tab[]; nodes: NodeListOf<HTMLElement>} {
Expand All @@ -241,9 +313,11 @@
return;
}

/** horizontal-only supported feature (used in left/right button click) */
tabs.push({
group,
key,
align: 'horizontal',
});
});

Expand Down
Loading
Loading