From 9e1c69f0d4b571a9bb7eb70f6897471c87d8c0a5 Mon Sep 17 00:00:00 2001 From: Pranay Kothapalli Date: Sun, 3 Nov 2024 19:09:21 +0530 Subject: [PATCH] Tabs Improvements and Refactor (#521) --- src/components/ui/Tabs/Tabs.tsx | 9 ++- src/components/ui/Tabs/fragments/TabList.tsx | 21 ++---- src/components/ui/Tabs/fragments/TabRoot.tsx | 42 ++++++------ .../ui/Tabs/fragments/TabTrigger.tsx | 52 +++++++-------- .../ui/Tabs/stories/Tabs.stories.js | 30 +++++++++ styles/themes/components/tabs.scss | 66 +++++++++---------- 6 files changed, 120 insertions(+), 100 deletions(-) diff --git a/src/components/ui/Tabs/Tabs.tsx b/src/components/ui/Tabs/Tabs.tsx index 876cdfd8..60fc9b50 100644 --- a/src/components/ui/Tabs/Tabs.tsx +++ b/src/components/ui/Tabs/Tabs.tsx @@ -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'; @@ -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 ( - + + {tabs.map((tab) => ( + + ))} + ); diff --git a/src/components/ui/Tabs/fragments/TabList.tsx b/src/components/ui/Tabs/fragments/TabList.tsx index acdb9b73..6aeca48f 100644 --- a/src/components/ui/Tabs/fragments/TabList.tsx +++ b/src/components/ui/Tabs/fragments/TabList.tsx @@ -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'; @@ -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 ; - // }) - // 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
- {tabs.map((tab, index) => { - return ; - }) - } +const TabList = ({ className = '', children }: TabListProps) => { + const { rootClass } = useContext(TabsRootContext); + return
+ {children}
; }; diff --git a/src/components/ui/Tabs/fragments/TabRoot.tsx b/src/components/ui/Tabs/fragments/TabRoot.tsx index 10b7b0b6..721c4d84 100644 --- a/src/components/ui/Tabs/fragments/TabRoot.tsx +++ b/src/components/ui/Tabs/fragments/TabRoot.tsx @@ -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 ( -
+ value={contextValues}> +
{children}
diff --git a/src/components/ui/Tabs/fragments/TabTrigger.tsx b/src/components/ui/Tabs/fragments/TabTrigger.tsx index 280a40f4..5f481930 100644 --- a/src/components/ui/Tabs/fragments/TabTrigger.tsx +++ b/src/components/ui/Tabs/fragments/TabTrigger.tsx @@ -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; @@ -17,28 +15,14 @@ export type TabTriggerProps = { props?: Record[] } -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(null); const isActive = activeTab === tab.value; - const handleClick = (tab: Tab) => { + const handleClick = (tab: TabProps) => { setActiveTab(tab.value); }; @@ -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 ( ); }; diff --git a/src/components/ui/Tabs/stories/Tabs.stories.js b/src/components/ui/Tabs/stories/Tabs.stories.js index 78680584..23a5dfea 100644 --- a/src/components/ui/Tabs/stories/Tabs.stories.js +++ b/src/components/ui/Tabs/stories/Tabs.stories.js @@ -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', @@ -37,3 +39,31 @@ export const All = { ] } }; + +const TabInTabOutTemplate = (args) => { + return + + + + ; +}; + +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:
Tab Out
, + value: 'tab_out_2', + content:
+ Once you tab out of the Tab component, you will be focused on the second button. +
+ } + + ] + +}; diff --git a/styles/themes/components/tabs.scss b/styles/themes/components/tabs.scss index eba65f24..02ccbdfa 100644 --- a/styles/themes/components/tabs.scss +++ b/styles/themes/components/tabs.scss @@ -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); + } + } + + +