Skip to content
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
75e0f10
fix: tab: added a prop to enable standard key navigation for tabs com…
mbhagat-c-eightfold Feb 26, 2025
6339c1d
fix: tabs: updated tabs component story for enabling standard key nav…
mbhagat-c-eightfold Feb 27, 2025
fefebce
Merge branch 'main' of github.com:EightfoldAI/octuple into mbhagat/no…
mbhagat-c-eightfold Mar 7, 2025
23577d9
Merge branch 'main' into mbhagat/no-std-key-navigation-for-tabs
ypatadia-eightfold Mar 10, 2025
7e9aec5
Merge branch 'main' into mbhagat/no-std-key-navigation-for-tabs
ypatadia-eightfold Mar 10, 2025
27814e2
fix: the tab navigation using arrow keys
mbhagat-c-eightfold Mar 11, 2025
83b25d2
Merge branch 'main' of github.com:EightfoldAI/octuple into mbhagat/no…
mbhagat-c-eightfold Mar 11, 2025
a19e291
Merge branch 'mbhagat/no-std-key-navigation-for-tabs' of github.com:E…
mbhagat-c-eightfold Mar 11, 2025
63eb35b
Merge branch 'main' of github.com:EightfoldAI/octuple into mbhagat/no…
mbhagat-c-eightfold Mar 18, 2025
f353b11
Merge branch 'main' of github.com:EightfoldAI/octuple into mbhagat/no…
mbhagat-c-eightfold Mar 19, 2025
eb37975
Merge branch 'main' of github.com:EightfoldAI/octuple into mbhagat/no…
mbhagat-c-eightfold Mar 26, 2025
aca958f
fix: tabs:removed console statements
mbhagat-c-eightfold Mar 26, 2025
693783c
fix: removed console log
mbhagat-c-eightfold Mar 26, 2025
42072c5
Merge branch 'main' into mbhagat/no-std-key-navigation-for-tabs
mbhagat-c-eightfold Apr 23, 2025
f96f053
Merge branch 'main' of github.com:EightfoldAI/octuple into mbhagat/no…
mbhagat-c-eightfold Apr 25, 2025
e1b18d0
Merge branch 'main' of github.com:EightfoldAI/octuple into mbhagat/no…
mbhagat-c-eightfold May 2, 2025
c07f733
Merge branch 'mbhagat/no-std-key-navigation-for-tabs' of github.com:E…
mbhagat-c-eightfold May 2, 2025
6f87574
Merge branch 'main' of github.com:EightfoldAI/octuple into mbhagat/no…
mbhagat-c-eightfold May 21, 2025
83a967b
Merge branch 'main' into mbhagat/no-std-key-navigation-for-tabs
ypatadia-eightfold Jun 5, 2025
808619b
Merge branch 'main' of github.com:EightfoldAI/octuple into mbhagat/no…
mbhagat-c-eightfold Jun 8, 2025
4ee3f58
fixed indentations
mbhagat-c-eightfold Jun 8, 2025
bba4659
Merge branch 'mbhagat/no-std-key-navigation-for-tabs' of github.com:E…
mbhagat-c-eightfold Jun 8, 2025
afaf4e7
fix: variable name for disable tab indexes
mbhagat-c-eightfold Jun 10, 2025
dab9a96
Merge branch 'main' of github.com:EightfoldAI/octuple into mbhagat/no…
mbhagat-c-eightfold Jun 10, 2025
77eeb03
Merge branch 'main' into mbhagat/no-std-key-navigation-for-tabs
rmotwani-c-eightfold Jun 11, 2025
bcbbd2b
Merge branch 'main' of github.com:EightfoldAI/octuple into mbhagat/no…
mbhagat-c-eightfold Jun 12, 2025
9dc9dfc
fix: added filter count on accordion title for collapsible section
mbhagat-c-eightfold Jun 16, 2025
62d6c74
Merge branch 'main' of github.com:EightfoldAI/octuple into mbhagat/no…
mbhagat-c-eightfold Jul 1, 2025
25c48cf
fix: added test cases to enable standard key navigation for tab compo…
mbhagat-c-eightfold Jul 1, 2025
15b4a3e
fix: updated the tests for tabs component to enable std key navigation
mbhagat-c-eightfold Jul 4, 2025
cddba2f
fix: updated test cases for Tabs file
mbhagat-c-eightfold Jul 14, 2025
3dcfb75
Merge branch 'main' of github.com:EightfoldAI/octuple into mbhagat/no…
mbhagat-c-eightfold Jul 22, 2025
e1188f6
fix: updated test snapshots for Tabs component
mbhagat-c-eightfold Jul 22, 2025
1dd902d
fix: updated test snapshots for Tabs component
mbhagat-c-eightfold Jul 22, 2025
7decc29
Merge branch 'main' into mbhagat/no-std-key-navigation-for-tabs
ypatadia-eightfold Jul 22, 2025
e3a9114
fix: duplicate children issue
ypatadia-eightfold Jul 22, 2025
3b9d998
Merge branch 'main' into mbhagat/no-std-key-navigation-for-tabs
ypatadia-eightfold Jul 22, 2025
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
1 change: 0 additions & 1 deletion src/components/InfoBar/InfoBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,6 @@ export const InfoBar: FC<InfoBarsProps> = React.forwardRef(
}, 1000);
}, [closeButtonRef, closable]);


const infoBarClassNames: string = mergeClasses([
styles.infoBar,
{ [styles.bordered]: !!bordered },
Expand Down
44 changes: 42 additions & 2 deletions src/components/Tabs/Tab/Tab.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
'use client';

import React, { FC, Ref } from 'react';
import React, { FC, Ref, useEffect, useRef } from 'react';
import { mergeClasses } from '../../../shared/utilities';
import { TabIconAlign, TabProps, TabSize, TabVariant } from '../Tabs.types';
import { useTabs } from '../Tabs.context';
Expand All @@ -13,6 +13,7 @@ import { Loader } from '../../Loader';
import { useCanvasDirection } from '../../../hooks/useCanvasDirection';

import styles from '../tabs.module.scss';
import { useMergedRefs } from '../../../hooks/useMergedRefs';

export const Tab: FC<TabProps> = React.forwardRef(
(
Expand All @@ -25,11 +26,15 @@ export const Tab: FC<TabProps> = React.forwardRef(
label,
loading,
value,
ariaControls,
index = 0,
...rest
},
ref: Ref<HTMLButtonElement>
) => {
const htmlDir: string = useCanvasDirection();
const tabRef = useRef(null);
const combinedRef = useMergedRefs(ref, tabRef);

const {
alignIcon,
Expand All @@ -38,6 +43,10 @@ export const Tab: FC<TabProps> = React.forwardRef(
currentActiveTab,
size,
variant,
enableArrowNav,
handleKeyDown,
registerTab,
focusedTabIndex,
theme,
} = useTabs();

Expand All @@ -63,6 +72,12 @@ export const Tab: FC<TabProps> = React.forwardRef(
[TabSize.XSmall, IconSize.Small],
]);

useEffect(() => {
if (registerTab) {
registerTab(tabRef.current, index);
}
}, [registerTab, index]);

const getIcon = (): JSX.Element =>
// TODO: Once sizes are implemented for other variants, use the mapping.
// For now, a ternary determines if mapping vs using the default icon size (medium).
Expand Down Expand Up @@ -106,16 +121,41 @@ export const Tab: FC<TabProps> = React.forwardRef(
const getLoader = (): JSX.Element =>
loading && <Loader classNames={styles.loader} />;

const handleTabKeyDown = (e: React.KeyboardEvent<HTMLButtonElement>) => {
if (enableArrowNav && index !== undefined) {
handleKeyDown?.(e, index);
}
};

const currentActiveTabIndex =
parseInt(currentActiveTab.match(/\d+/)?.[0] || '0', 10) - 1;

const getTabIndex = () => {
if (
currentActiveTabIndex !== undefined &&
currentActiveTabIndex !== null &&
index === currentActiveTabIndex
) {
return 0;
}
return -1;
};

return (
<button
{...rest}
ref={ref}
aria-controls={ariaControls}
ref={combinedRef}
className={tabClassNames}
aria-label={ariaLabel}
aria-selected={isActive}
role="tab"
disabled={disabled}
onClick={(e) => onTabClick(value, e)}
onKeyDown={handleTabKeyDown}
tabIndex={getTabIndex()}
data-index={index}
data-value={value}
>
{alignIcon === TabIconAlign.Start && getIcon()}
{getLabel()}
Expand Down
193 changes: 188 additions & 5 deletions src/components/Tabs/Tabs.context.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
import React, { createContext, useEffect, useState } from 'react';
import React, {
createContext,
useCallback,
useEffect,
useState,
useRef,
} from 'react';
import {
TabsContextProps,
ITabsContext,
Expand Down Expand Up @@ -29,17 +35,188 @@ const TabsProvider = ({
themeContainerId,
statgrouptheme,
value,
disabledTabIndexes = [],
enableArrowNav = true,
variant = TabVariant.default,
}: TabsContextProps) => {
const [currentActiveTab, setCurrentActiveTab] = useState<TabValue>(value);
const [currentActiveTab, setCurrentActiveTab] = useState(value);
const [focusedTabIndex, setFocusedTabIndex] = useState<number | null>(null);
const tabsRef = useRef<HTMLElement[]>([]);
const tabListRef = useRef<HTMLElement | null>(null);
const tabsValuesRef = useRef<TabValue[]>([]);

useEffect(() => {
setCurrentActiveTab(value);
}, [value]);

const onTabClick = (value: TabValue, e: SelectTabEvent) => {
onChange(value, e);
};
const registerTab = useCallback(
(tabElement: HTMLElement | null, index: number) => {
if (tabElement) {
tabsRef.current[index] = tabElement;
const tabValue = tabElement.getAttribute('data-value');
if (tabValue) {
tabsValuesRef.current[index] = tabValue;
}
}
},
[]
);

const registerTablist = useCallback((tabListElement: HTMLElement | null) => {
tabListRef.current = tabListElement;
}, []);

const onTabClick = useCallback(
(value: TabValue, e: SelectTabEvent) => {
if (!readOnly) {
setCurrentActiveTab(value);
onChange(value, e);
}
},
[onChange, readOnly]
);

const moveFocusToNextTab = useCallback(() => {
const tabValues = tabsValuesRef.current.filter(Boolean);
if (tabValues.length === 0) return;

let currentIndex =
focusedTabIndex !== null
? focusedTabIndex
: tabValues.indexOf(currentActiveTab);
if (currentIndex === -1) currentIndex = 0;

let nextIndex = currentIndex;
do {
nextIndex = (nextIndex + 1) % tabValues.length;
if (nextIndex === currentIndex) break;
} while (disabledTabIndexes.includes(nextIndex));

const nextTab = tabsRef.current[nextIndex];
if (nextTab && !disabledTabIndexes.includes(nextIndex)) {
nextTab.focus();
setFocusedTabIndex(nextIndex);
}
}, [focusedTabIndex, currentActiveTab, disabledTabIndexes]);

const moveFocusToPreviousTab = useCallback(() => {
const tabValues = tabsValuesRef.current.filter(Boolean);
if (tabValues.length == 0) return;

let currentIndex =
focusedTabIndex !== null
? focusedTabIndex
: tabValues.indexOf(currentActiveTab);
if (currentIndex === -1) currentIndex = 0;

let prevIndex = currentIndex;
do {
prevIndex = (prevIndex - 1 + tabValues.length) % tabValues.length;
if (prevIndex === currentIndex) break;
} while (disabledTabIndexes.includes(prevIndex));

const prevTab = tabsRef.current[prevIndex];
if (prevTab && !disabledTabIndexes.includes(prevIndex)) {
prevTab.focus();
setFocusedTabIndex(prevIndex);
}
}, [focusedTabIndex, currentActiveTab, disabledTabIndexes]);

useEffect(() => {
const handleKeyDown = (event: globalThis.KeyboardEvent) => {
if (enableArrowNav) return;
if (event.key == 'Tab') {
const activeElement = document.activeElement;
const tabList = tabListRef.current;

if (
tabList &&
tabList.contains(activeElement) &&
activeElement?.getAttribute('role') === 'tab'
) {
event.preventDefault();
if (event.shiftKey) {
moveFocusToPreviousTab();
} else {
moveFocusToNextTab();
}
}
}
};
document.addEventListener('keydown', handleKeyDown);
return () => {
document.removeEventListener('keydown', handleKeyDown);
};
}, [moveFocusToNextTab, moveFocusToPreviousTab]);

const handleKeyDown = useCallback(
(event: React.KeyboardEvent, tabIndex: number) => {
if (!enableArrowNav || readOnly) return;

if (event.key === 'Tab') {
return;
}

const enableTabIndexes = tabsRef.current
.map((_, index) => index)
.filter((index) => !disabledTabIndexes.includes(index));

const currentEnabledIndex = enableTabIndexes.indexOf(tabIndex);

if (currentEnabledIndex === -1) return;

let nextFocusIndex: number | null = null;

switch (event.key) {
case 'ArrowLeft':
nextFocusIndex =
currentEnabledIndex === 0
? enableTabIndexes[enableTabIndexes.length - 1]
: enableTabIndexes[currentEnabledIndex - 1];
event.preventDefault();
break;
case 'ArrowRight':
nextFocusIndex =
currentEnabledIndex === enableTabIndexes.length - 1
? enableTabIndexes[0]
: enableTabIndexes[currentEnabledIndex + 1];
event.preventDefault();
break;
case 'Home':
nextFocusIndex = enableTabIndexes[0];
event.preventDefault();
break;
case 'End':
nextFocusIndex = enableTabIndexes[enableTabIndexes.length - 1];
event.preventDefault();
break;
case 'Enter':
const currentTab = tabsRef.current[tabIndex];
if (currentTab) {
const tabValue = currentTab.getAttribute('data-value');
if (tabValue) {
setCurrentActiveTab(tabValue);
onChange?.(tabValue, {
currentTarget: currentTab,
} as SelectTabEvent);
}
}
event.preventDefault();
return;
default:
return;
}

if (nextFocusIndex !== null) {
const nextTab = tabsRef.current[nextFocusIndex];
if (nextTab) {
nextTab.focus();
setFocusedTabIndex(nextFocusIndex);
}
}
},
[enableArrowNav, disabledTabIndexes, readOnly, onChange]
);

return (
<TabsContext.Provider
Expand All @@ -59,6 +236,12 @@ const TabsProvider = ({
theme,
themeContainerId,
variant,
registerTab,
registerTablist,
handleKeyDown,
enableArrowNav,
disabledTabIndexes,
focusedTabIndex,
}}
>
{children}
Expand Down
11 changes: 11 additions & 0 deletions src/components/Tabs/Tabs.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -73,13 +73,15 @@ const tabs = [1, 2, 3, 4].map((i) => ({
value: `tab${i}`,
label: `Tab ${i}`,
ariaLabel: `Tab ${i}`,
id: `tab-${i}`,
...(i === 4 ? { disabled: true } : {}),
}));

const badgeTabs = [1, 2, 3, 4].map((i) => ({
value: `tab${i}`,
label: `Tab ${i}`,
ariaLabel: `Tab ${i}`,
id: `tab-${i}`,
badgeContent: i,
...(i === 4 ? { disabled: true } : {}),
}));
Expand All @@ -88,6 +90,7 @@ const iconTabs = [1, 2, 3, 4].map((i) => ({
value: `tab${i}`,
icon: IconName.mdiCardsHeart,
ariaLabel: `Tab ${i}`,
id: `tab-${i}`,
...(i === 4 ? { disabled: true } : {}),
}));

Expand All @@ -96,16 +99,22 @@ const iconLabelTabs = [1, 2, 3, 4].map((i) => ({
icon: IconName.mdiCardsHeart,
label: `Tab ${i}`,
ariaLabel: `Tab ${i}`,
id: `tab-${i}`,
...(i === 4 ? { disabled: true } : {}),
}));

const scrollableTabs = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map((i) => ({
value: `tab${i}`,
label: `Tab ${i}`,
ariaLabel: `Tab ${i}`,
id: `tab-${i}`,
...(i === 4 ? { disabled: true } : {}),
}));

const disabledTabIndexes = tabs
.map((tab, index) => (tab.disabled ? index : -1))
.filter((index) => index !== -1);

const Tabs_Story: ComponentStory<typeof Tabs> = (args) => {
const [activeTabs, setActiveTabs] = useState({ defaultTab: 'tab1' });
return (
Expand All @@ -123,6 +132,7 @@ const Tabs_Story: ComponentStory<typeof Tabs> = (args) => {
{...args}
onChange={(tab) => setActiveTabs({ ...activeTabs, defaultTab: tab })}
value={activeTabs.defaultTab}
disabledTabIndexes={disabledTabIndexes}
/>
</div>
);
Expand Down Expand Up @@ -170,6 +180,7 @@ const tabsArgs: Object = {
variant: TabVariant.default,
size: TabSize.Medium,
underlined: false,
enableArrowNav: false,
children: tabs.map((tab) => <Tab key={tab.value} {...tab} />),
style: {},
};
Expand Down
Loading
Loading