Skip to content

Commit

Permalink
Tabs Improvements and Refactor (#521)
Browse files Browse the repository at this point in the history
  • Loading branch information
kotAPI authored Nov 3, 2024
1 parent 0c39d1f commit 9e1c69f
Show file tree
Hide file tree
Showing 6 changed files with 120 additions and 100 deletions.
9 changes: 7 additions & 2 deletions src/components/ui/Tabs/Tabs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import React, { useState } from 'react';

import TabList from './fragments/TabList';
import TabTrigger from './fragments/TabTrigger';
import TabContent from './fragments/TabContent';
import TabRoot from './fragments/TabRoot';

Expand All @@ -14,12 +15,16 @@ export type TabsProps = {

const Tabs = ({ tabs = [], ...props }: TabsProps) => {
// This should be a value <`tabs.value`> that is passed in from the parent component
const [activeTab, setActiveTab] = useState(tabs[0].value || '');

const defaultActiveTab = tabs[0].value || '';

return (
<TabRoot tabs={tabs} defaultTab={defaultActiveTab} >
<TabList />
<TabList>
{tabs.map((tab) => (
<TabTrigger key={tab.value} tab={tab} />
))}
</TabList>
<TabContent />
</TabRoot>
);
Expand Down
21 changes: 5 additions & 16 deletions src/components/ui/Tabs/fragments/TabList.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
'use client';
import React, { useContext } from 'react';
import { customClassSwitcher } from '~/core';

import TabTrigger from './TabTrigger';
import { TabProps } from '../types';
import TabsRootContext from '../context/TabsRootContext';
Expand All @@ -15,21 +15,10 @@ export type TabListProps = {
activeTab: TabProps;
}

const TabList = ({ className = '', customRootClass = '' }: TabListProps) => {
const rootClass = customClassSwitcher(customRootClass, COMPONENT_NAME);
const { tabs, activeTab, setActiveTab } = useContext(TabsRootContext);

// TODO: in the previous return value of
// {tabs.map((tab, index) => {
// return <TabTrigger activeTab={activeTab} setActiveTab={setActiveTab} key={index} tab={tab} index={index} />;
// })
// idk if React changed anything, but because its mapped, it shouldve worked. maybe it is the wrong prop? look into it.
// ive temporarily added an unnecessary '
return <div role="tablist" aria-orientation='horizontal' aria-label="todo" className={`${rootClass} ${className}`}>
{tabs.map((tab, index) => {
return <TabTrigger activeTab={activeTab} setActiveTab={setActiveTab} key={index} tab={tab} index={index} />;
})
}
const TabList = ({ className = '', children }: TabListProps) => {
const { rootClass } = useContext(TabsRootContext);
return <div role="tablist" aria-orientation='horizontal' aria-label="todo" className={`${rootClass}-list ${className}`}>
{children}
</div>;
};

Expand Down
42 changes: 21 additions & 21 deletions src/components/ui/Tabs/fragments/TabRoot.tsx
Original file line number Diff line number Diff line change
@@ -1,44 +1,44 @@
'use client';
import React, { useState } from 'react';
import React, { useState, useRef } from 'react';
import { customClassSwitcher } from '~/core';

import TabsRootContext from '../context/TabsRootContext';

import { TabRootProps } from '../types';
import { getAllBatchElements, getNextBatchItem, getPrevBatchItem } from '~/core/batches';

const COMPONENT_NAME = 'Tabs';

const TabRoot = ({ children, defaultTab = '', customRootClass, tabs = [], className, color, ...props }: TabRootProps) => {
const rootClass = customClassSwitcher(customRootClass, COMPONENT_NAME);

const tabRef = useRef(null);

const [activeTab, setActiveTab] = useState(defaultTab || tabs[0].value || '');

const nextTab = () => {
const currentIndex = tabs.findIndex((tab) => tab.value === activeTab);
const nextIndex = currentIndex + 1;
if (nextIndex < tabs.length) {
setActiveTab(tabs[nextIndex].value);
}
const batches = getAllBatchElements(tabRef?.current);
const nextItem = getNextBatchItem(batches);
nextItem.focus();
};

const previousTab = () => {
const currentIndex = tabs.findIndex((tab) => tab.value === activeTab);
const previousIndex = currentIndex - 1;
if (previousIndex >= 0) {
setActiveTab(tabs[previousIndex].value);
}
const batches = getAllBatchElements(tabRef?.current);
const prevItem = getPrevBatchItem(batches);
prevItem.focus();
};

const contextValues = {
rootClass,
activeTab,
setActiveTab,
nextTab,
previousTab,
tabs
};

return (
<TabsRootContext.Provider
value={{
activeTab,
setActiveTab,
nextTab,
previousTab,
tabs
}}>
<div className={`${rootClass} ${className}`} data-accent-color={color} {...props} >
value={contextValues}>
<div ref={tabRef} className={`${rootClass} ${className}`} data-accent-color={color} {...props} >
{children}
</div>
</TabsRootContext.Provider>
Expand Down
52 changes: 25 additions & 27 deletions src/components/ui/Tabs/fragments/TabTrigger.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
'use client';
import React, { useContext, useEffect, useRef } from 'react';
import { customClassSwitcher } from '~/core';
import React, { useContext, useRef } from 'react';

import { TabProps } from '../types';

import TabsRootContext from '../context/TabsRootContext';

const COMPONENT_NAME = 'TabTrigger';

export type TabTriggerProps = {
tab: TabProps;
setActiveTab: React.Dispatch<Tab>;
Expand All @@ -17,28 +15,14 @@ export type TabTriggerProps = {
props?: Record<string, any>[]
}

const TabTrigger = ({ tab, setActiveTab, activeTab, className, customRootClass, index, ...props }: TabTriggerProps) => {
const TabTrigger = ({ tab, className = '', ...props }: TabTriggerProps) => {
// use context
const { tabs, previousTab, nextTab } = useContext(TabsRootContext);
const ref = useRef(null);

const handleFocusTabEvent = () => {
// focus on the active tab
if (activeTab === tab.value) {
ref.current.focus();
}
};

useEffect(() => {
handleFocusTabEvent();
}
, [activeTab]);

const rootClass = customClassSwitcher(customRootClass, COMPONENT_NAME);
const { previousTab, nextTab, activeTab, setActiveTab, rootClass } = useContext(TabsRootContext);
const ref = useRef<HTMLButtonElement>(null);

const isActive = activeTab === tab.value;

const handleClick = (tab: Tab) => {
const handleClick = (tab: TabProps) => {
setActiveTab(tab.value);
};

Expand All @@ -51,14 +35,28 @@ const TabTrigger = ({ tab, setActiveTab, activeTab, className, customRootClass,
}
};

const handleFocus = (tab: TabProps) => {
if (ref.current) {
ref.current?.focus();
}
setActiveTab(tab.value);

// This is a good way to manage flow, when a focus event is triggered, we can set the active tab to the tab that is being focused on
// This way, we dont need to keep track of the active tab in the parent component
// This should be the defacto pattern we should follow for all components
};

return (
<button
ref={ref}
role="tab" key={index} className={`${rootClass} ${isActive ? 'active' : ''} ${className}`} {...props} onKeyDown={handleKeyDownEvent}
onClick={() => handleClick(tab)}>
<span className={`${rootClass}-inner`}>
{tab.label}
</span>
role="tab" className={`${rootClass}-trigger ${isActive ? 'active' : ''} ${className}`} {...props} onKeyDown={handleKeyDownEvent}
onClick={() => handleClick(tab)}
onFocus={() => handleFocus(tab)}
tabIndex={isActive ? 0 : -1}
data-rad-ui-batch-element

>
{tab.label}
</button>
);
};
Expand Down
30 changes: 30 additions & 0 deletions src/components/ui/Tabs/stories/Tabs.stories.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import Tabs from '../Tabs';
import SandboxEditor from '~/components/tools/SandboxEditor/SandboxEditor';

import Button from '~/components/ui/Button/Button';

// More on how to set up stories at: https://storybook.js.org/docs/react/writing-stories/introduction#default-export
export default {
title: 'WIP/Tabs',
Expand Down Expand Up @@ -37,3 +39,31 @@ export const All = {
]
}
};

const TabInTabOutTemplate = (args) => {
return <SandboxEditor>
<Button>Click me</Button>
<Tabs {...args} />
<Button>Click me</Button>
</SandboxEditor>;
};

export const TabInTabOut = TabInTabOutTemplate.bind({});
TabInTabOut.args = {
tabs: [{
label: 'Tabbing In',
value: 'tab_in_1',
content: 'Focus on the first button, press tab to move to the Tab component, it tabs into the selected tab. '

},
{
label: <div className='flex items-center space-x-2'> <span>Tab Out</span> <ArrowIcon/></div>,
value: 'tab_out_2',
content: <div className='text-gray-1000'>
Once you tab out of the Tab component, you will be focused on the second button.
</div>
}

]

};
66 changes: 32 additions & 34 deletions styles/themes/components/tabs.scss
Original file line number Diff line number Diff line change
@@ -1,41 +1,39 @@
/** TABS */
.rad-ui-tab-root{
.rad-ui-tabs{
.rad-ui-tabs-list{
display: flex;
box-shadow: inset 0 -1px 0 0 var(--rad-ui-color-gray-500);

}
.rad-ui-tab-list{
display: flex;
box-shadow: inset 0 -1px 0 0 var(--rad-ui-color-gray-500);
}

.rad-ui-tabs-trigger {
color: var(--rad-ui-color-accent-600);
font-family: inherit;
padding: 10px 10px;

display: flex;
align-items: center;
height:42px;
font-size: 15px;
line-height: 1;
cursor: pointer;

.rad-ui-tab-trigger.active{
color: var(--rad-ui-color-accent-950);
border-bottom:2px solid var(--rad-ui-color-accent-900);
}
.rad-ui-tab-trigger {
color: var(--rad-ui-color-accent-600);
font-family: inherit;
padding: 10px 10px;

display: flex;
height:42px;
font-size: 15px;
line-height: 1;
cursor: pointer;
}
.rad-ui-tab-trigger:hover{
color: var(--rad-ui-color-accent-900);
}
&.active{
color: var(--rad-ui-color-accent-950);
border-bottom:2px solid var(--rad-ui-color-accent-900);
}

.rad-ui-tab-trigger-inner{
padding:4px 8px;
}
.rad-ui-tab-trigger:hover > .rad-ui-tab-trigger-inner{
background-color: var(--rad-ui-color-accent-200);
border-radius: 4px;
}
&:hover{
color: var(--rad-ui-color-accent-900);
}
}
}

.rad-ui-tab-content{
padding:20px 0px;
color : var(--rad-ui-color-gray-1000);
.rad-ui-tab-content{
padding:20px 0px;
color : var(--rad-ui-color-gray-1000);
}

}



0 comments on commit 9e1c69f

Please sign in to comment.