浏览代码

Feat/tab item (#1374)

* feat: Newly added TabItem component, which can be displayed separately and used to generate TabItem variants in C2D

* fix: [Tabs] fix styles error

* fix: [Tabs] Fix errors in TabItem usage

* fix: [TabItem] code optimize

Co-authored-by: pointhalo <[email protected]>
YyumeiZhang 2 年之前
父节点
当前提交
ed3f5780cc

+ 326 - 17
packages/semi-foundation/tabs/tabs.scss

@@ -46,7 +46,7 @@ $module: #{$prefix}-tabs;
                 margin-right: $spacing-tabs_tab_icon-marginRight;
                 top: $spacing-tabs_tab_icon-top;
                 color: $color-tabs_tab-icon-default;
-                transition: color $transition_duration-tabs_tab_line-text $transition_function-tabs_tab_line-text $transition_delay-tabs_tab_line-text;//线条式tabs的 color的transition
+                transition: color $transition_duration-tabs_tab_line-text $transition_function-tabs_tab_line-text $transition_delay-tabs_tab_line-text; //线条式tabs的 color的transition
 
             }
 
@@ -109,6 +109,88 @@ $module: #{$prefix}-tabs;
         }
     }
 
+    &-tab-single {
+        &.#{$module}-tab {
+            @include font-size-regular;
+            cursor: pointer;
+            box-sizing: border-box;
+            position: relative;
+            display: inline-block;
+            // float: left;
+
+            font-weight: $font-tabs_tab-fontWeight;
+            color: $color-tabs_tab_line_default-text-default;
+
+            user-select: none;
+
+            .#{$prefix}-icon {
+                position: relative;
+                margin-right: $spacing-tabs_tab_icon-marginRight;
+                top: $spacing-tabs_tab_icon-top;
+                color: $color-tabs_tab-icon-default;
+                transition: color $transition_duration-tabs_tab_line-text $transition_function-tabs_tab_line-text $transition_delay-tabs_tab_line-text;//线条式tabs的 color的transition
+
+            }
+
+            .#{$prefix}-icon-close {
+                margin-right: 0;
+                font-size: 14px;
+                color: var(--semi-color-text-2);
+                margin-left: 10px;
+                cursor: pointer;
+            }
+
+            &:hover {
+                color: $color-tabs_tab_line_default-text-hover;
+
+                .#{$prefix}-icon {
+                    color: $color-tabs_tab-icon-hover;
+                }
+            }
+
+            &:active {
+                color: $color-tabs_tab_line_default-text-active;
+
+                .#{$prefix}-icon {
+                    color: $color-tabs_tab-icon-active;
+                }
+            }
+        }
+
+        &.#{$module}-tab-active {
+
+            &,
+            &:hover {
+                cursor: default;
+                // border-bottom: 2px solid $color-tabs_tab_line_indicator_selected-icon-default;
+                font-weight: $font-tabs_tab_active-fontWeight;
+                color: $color-tabs_tab_line_selected-text-default;
+
+                .#{$prefix}-icon {
+                    color: $color-tabs_tab_selected-icon-default;
+                }
+
+                .#{$prefix}-icon-close {
+                    color: var(--semi-color-text-2);
+                }
+            }
+
+            .#{$prefix}-icon-close:hover {
+                color: var(--semi-color-text-1);
+            }
+        }
+
+        &.#{$module}-tab-disabled {
+            cursor: not-allowed;
+            color: $color-tabs_tab_line_disabled-text-default;
+
+            &:hover {
+                color: $color-tabs_tab_line_disabled-text-hover;
+                border-bottom: none;
+            }
+        }
+    }
+
     &-bar-collapse {
         &,
         .#{$module}-bar-overflow-list {
@@ -134,10 +216,10 @@ $module: #{$prefix}-tabs;
                     outline-offset: $width-tabs-outline-offset;
                 }
             }
-            & > .#{$prefix}-button-disabled{
+            & > .#{$prefix}-button-disabled {
                 color: $color-tabs_tab-pane_arrow_disabled-text-default;
                 background-color: $color-tabs_tab-pane_arrow_disabled-bg-default;
-                &:hover{
+                &:hover {
                     color: $color-tabs_tab-pane_arrow_disabled-text-hover;
                     background-color: $color-tabs_tab-pane_arrow_disabled-bg-hover;
                 }
@@ -146,12 +228,12 @@ $module: #{$prefix}-tabs;
 
         .#{$module}-bar-arrow-start {
             margin-right: $spacing-tabs_overflow_icon-marginRight;
-            & > .#{$prefix}-button{
+            & > .#{$prefix}-button {
                 color: $color-tabs_tab-pane_arrow-text-default;
                 padding: $spacing-tabs_tab-pane_arrow;
                 border: $width-tabs_tab-pane_arrow-border solid $color-tabs_tab-pane_arrow-border-default;
                 background-color: $color-tabs_tab-pane_arrow-bg-default;
-                &:hover{
+                &:hover {
                     background-color: var(--semi-color-fill-0);
                     color: $color-tabs_tab-pane_arrow-text-hover;
                     border-color: $color-tabs_tab-pane_arrow-border-hover;
@@ -167,17 +249,17 @@ $module: #{$prefix}-tabs;
 
         .#{$module}-bar-arrow-end {
             margin-left: $spacing-tabs_overflow_icon-marginLeft;
-            & > .#{$prefix}-button{
+            & > .#{$prefix}-button {
                 color: $color-tabs_tab-pane_arrow-text-default;
                 padding: $spacing-tabs_tab-pane_arrow;
                 border: $width-tabs_tab-pane_arrow-border solid $color-tabs_tab-pane_arrow-border-default;
                 background-color: $color-tabs_tab-pane_arrow-bg-default;
-                &:hover{
+                &:hover {
                     background-color: var(--semi-color-fill-0);
                     color: $color-tabs_tab-pane_arrow-text-hover;
                     border-color: $color-tabs_tab-pane_arrow-border-hover;
                 }
-                &:active{
+                &:active {
                     background-color: var(--semi-color-fill-1);
                     color: $color-tabs_tab-pane_arrow-text-active;
                     border-color: $color-tabs_tab-pane_arrow-border-active;
@@ -202,12 +284,13 @@ $module: #{$prefix}-tabs;
         &.#{$module}-bar-top {
             border-bottom: $width-tabs_bar_line-border solid $color-tabs_tab_line_default-border-default;
             transition: color $transition_duration-tabs_tab_line-text $transition_function-tabs_tab_line-text $transition_delay-tabs_tab_line-text; //线条式tabs的 color的transition
-            transform:scale($transform_scale-tabs_tab_line-item);
+            transform: scale($transform_scale-tabs_tab_line-item);
 
             .#{$module}-tab {
                 padding: $spacing-tabs_bar_line_tab-paddingTop $spacing-tabs_bar_line_tab-paddingRight $spacing-tabs_bar_line_tab-paddingBottom $spacing-tabs_bar_line_tab-paddingLeft;
                 transition: border-bottom-color $transition_duration-tabs_tab_line-border $transition_function-tabs_tab_line-border $transition_delay-tabs_tab_line-border,
-                color $transition_duration-tabs_tab_line-text $transition_function-tabs_tab_line-text $transition_delay-tabs_tab_line-text; //线条式tabs的border-color 的 transition
+                    color $transition_duration-tabs_tab_line-text $transition_function-tabs_tab_line-text $transition_delay-tabs_tab_line-text; //线条式tabs的border-color 的 transition
+                
                 &:nth-of-type(1) {
                     padding-left: 0;
                 }
@@ -391,7 +474,7 @@ $module: #{$prefix}-tabs;
             transition: background-color $transition_duration-tabs_tab_card-bg $transition_function-tabs_tab_card-bg $transition_delay-tabs_tab_card-bg, //卡片式tabs的bg的transition
             color $transition_duration-tabs_tab_line-text $transition_function-tabs_tab_line-text $transition_delay-tabs_tab_line-text;//卡片式tabs的 color的transition
 
-            transform:scale($transform_scale-tabs_tab_card-item);
+            transform: scale($transform_scale-tabs_tab_card-item);
 
             &:hover {
                 background: $color-tabs_tab_card-bg-hover;
@@ -435,9 +518,9 @@ $module: #{$prefix}-tabs;
             color: $color-tabs_tab_button-text-default;
             border: none;
             transition: background-color $transition_duration-tabs-tab_button-bg $transition_function-tabs_tab_button-bg $transition_delay-tabs_tab_button-bg,//按钮tabs的背景色的transition
-            color $transition_duration-tabs_tab_line-text $transition_function-tabs_tab_line-text $transition_delay-tabs_tab_line-text;//按钮式tabs的 color的transition
+                color $transition_duration-tabs_tab_line-text $transition_function-tabs_tab_line-text $transition_delay-tabs_tab_line-text;//按钮式tabs的 color的transition
 
-            transform:scale($transform_scale-tabs_tab_button-item);
+            transform: scale($transform_scale-tabs_tab_button-item);
 
             &:hover {
                 border: none;
@@ -496,10 +579,12 @@ $module: #{$prefix}-tabs;
 
 
     @keyframes #{$module}-panel-keyframe-leftShow {
+
         0% {
             transform: translateX($animation_transform_translateX-tabs_tabPanel-leftShow);
             opacity: $animation_opacity-tabs_tabPanel_show;
         }
+
         100% {
             transform: translateX(0);
             opacity: 1;
@@ -507,10 +592,12 @@ $module: #{$prefix}-tabs;
 
     }
     @keyframes #{$module}-panel-keyframe-rightShow {
+
         0% {
             transform: translateX($animation_transform_translateX-tabs_tabPanel-rightShow);
             opacity: $animation_opacity-tabs_tabPanel_show;
         }
+
         100% {
             transform: translateX(0);
             opacity: 1;
@@ -518,10 +605,12 @@ $module: #{$prefix}-tabs;
     }
 
     @keyframes #{$module}-panel-keyframe-topShow {
+        
         0% {
             transform: translateY($animation_transform_translateX-tabs_tabPanel-leftShow);
             opacity: $animation_opacity-tabs_tabPanel_show;
         }
+        
         100% {
             transform: translateY(0);
             opacity: 1;
@@ -529,10 +618,12 @@ $module: #{$prefix}-tabs;
 
     }
     @keyframes #{$module}-panel-keyframe-bottomShow {
+        
         0% {
             transform: translateY($animation_transform_translateX-tabs_tabPanel-rightShow);
             opacity: $animation_opacity-tabs_tabPanel_show;
         }
+         
         100% {
             transform: translateY(0);
             opacity: 1;
@@ -540,23 +631,241 @@ $module: #{$prefix}-tabs;
     }
 
 
-    &-pane-animate-leftShow{
+    &-pane-animate-leftShow {
         animation: $animation_duration-tabs_tabPanel-show #{$module}-panel-keyframe-leftShow $animation_function-tabs_tabPanel-show $animation_delay-tabs_tabPanel-show;
         animation-fill-mode: forwards;
     }
-    &-pane-animate-rightShow{
+    &-pane-animate-rightShow {
         animation: $animation_duration-tabs_tabPanel-show #{$module}-panel-keyframe-rightShow $animation_function-tabs_tabPanel-show $animation_delay-tabs_tabPanel-show;
         animation-fill-mode: forwards;
     }
 
-    &-pane-animate-topShow{
+    &-pane-animate-topShow {
         animation: $animation_duration-tabs_tabPanel-show #{$module}-panel-keyframe-topShow $animation_function-tabs_tabPanel-show $animation_delay-tabs_tabPanel-show;
         animation-fill-mode: forwards;
     }
-    &-pane-animate-bottomShow{
+
+    &-pane-animate-bottomShow {
         animation: $animation_duration-tabs_tabPanel-show #{$module}-panel-keyframe-bottomShow $animation_function-tabs_tabPanel-show $animation_delay-tabs_tabPanel-show;
         animation-fill-mode: forwards;
     }
+
+    &-tab-line {
+        &.#{$module}-tab-top {
+
+            &.#{$module}-tab {
+                padding: $spacing-tabs_bar_line_tab-paddingTop $spacing-tabs_bar_line_tab-paddingRight $spacing-tabs_bar_line_tab-paddingBottom $spacing-tabs_bar_line_tab-paddingLeft;
+                transition: border-bottom-color $transition_duration-tabs_tab_line-border $transition_function-tabs_tab_line-border $transition_delay-tabs_tab_line-border,
+                    color $transition_duration-tabs_tab_line-text $transition_function-tabs_tab_line-text $transition_delay-tabs_tab_line-text; //线条式tabs的border-color 的 transition
+                
+                &:nth-of-type(1) {
+                    padding-left: 0;
+                }
+
+                border-bottom: $width-tabs_bar_line_tab-border solid transparent;
+
+                &:hover {
+                    border-bottom: $width-tabs_bar_line_tab-border solid $color-tabs_tab_line_default-border-hover;
+                }
+
+                &:focus-visible {
+                    outline: $width-tabs-outline solid $color-tabs_tab-outline-focus;
+                    outline-offset: $width-tabs_bar_line-outline-offset;
+                }
+
+                &:active {
+                    border-bottom: $width-tabs_bar_line_tab-border solid $color-tabs_tab_line_default-border-active;
+                }
+
+                // &:not(:last-of-type) {
+                //     margin-right: $spacing-tabs_bar_line_tab-marginRight;
+                // }
+
+                &-small {
+                    padding: $spacing-tabs_bar_line_tab_small-paddingTop $spacing-tabs_bar_line_tab_small-paddingRight $spacing-tabs_bar_line_tab_small-paddingBottom $spacing-tabs_bar_line_tab_small-paddingLeft;
+                }
+
+                &-medium {
+                    padding: $spacing-tabs_bar_line_tab_medium-paddingTop $spacing-tabs_bar_line_tab_medium-paddingRight $spacing-tabs_bar_line_tab_medium-paddingBottom $spacing-tabs_bar_line_tab_medium-paddingLeft;
+                }
+            }
+
+            &.#{$module}-tab-active {
+
+                &,
+                &:hover {
+                    border-bottom: $width-tabs_bar_line_tab-border solid $color-tabs_tab_line_indicator_selected-icon-default;
+                }
+            }
+        }
+
+        &.#{$module}-tab-left {
+
+            &.#{$module}-tab {
+                padding: $spacing-tabs_bar_line_tab_left-padding;
+                border-left: $width-tabs_bar_line_tab-border solid transparent;
+                transition: background-color $transition_duration-tabs-tab_button-bg $transition_function-tabs_tab_button-bg $transition_delay-tabs_tab_button-bg,//按钮tabs的背景色的transition
+                color $transition_duration-tabs_tab_line-text $transition_function-tabs_tab_line-text $transition_delay-tabs_tab_line-text;//按钮式tabs的 color的transition
+
+                &:hover {
+                    border-left: $width-tabs_bar_line_tab-border solid $color-tabs_tab_line_default-border-hover;
+                    background-color: $color-tabs_tab_line_vertical-bg-hover;
+                }
+
+                &:focus-visible {
+                    outline: $width-tabs-outline solid $color-tabs_tab-outline-focus;
+                    outline-offset: $width-tabs-outline-offset;
+                }
+
+                &:active {
+                    border-left: $width-tabs_bar_line_tab-border solid $color-tabs_tab_line_default-border-active;
+                    background-color: $color-tabs_tab_line_vertical-bg-active;
+                }
+
+                &-small {
+                    padding: $spacing-tabs_bar_line_tab_left_small-padding;
+                }
+
+                &-medium {
+                    padding: $spacing-tabs_bar_line_tab_left_medium-padding;
+                }
+            }
+
+            &.#{$module}-tab-active {
+                background-color: $color-tabs_tab_line_vertical_selected-bg-default;
+
+                &,
+                &:hover {
+                    border-left: $width-tabs_bar_line_tab-border solid $color-tabs_tab_line_indicator_selected-icon-default;
+                    background-color: $color-tabs_tab_line_vertical_selected-bg-default;
+                }
+            }
+        }
+    }
+
+    &-tab-card {
+        &.#{$module}-tab-top {
+
+            &.#{$module}-tab {
+                border: $width-tabs_bar_card-border solid transparent;
+                border-bottom: none;
+                border-radius: $radius-tabs_tab_card;
+
+                &:hover {
+                    border-bottom: none;
+                }
+
+            }
+
+            &.#{$module}-tab-active {
+
+                &,
+                &:hover {
+                    padding: $spacing-tabs_bar_card_tab_active-paddingTop $spacing-tabs_bar_card_tab_active-paddingRight $spacing-tabs_bar_card_tab_active-paddingBottom $spacing-tabs_bar_card_tab_active-paddingLeft;
+                    border: $width-tabs_bar_card-border solid $color-tabs_tab_card_indicator_selected-icon-default;
+                    border-bottom: $width-tabs_bar_card-border solid $color-tabs_tab_card_selected-bg-default;
+                    background: transparent;
+                    // padding-bottom: $spacing-tight + 1;
+                }
+            }
+        }
+
+        &.#{$module}-tab-left {
+
+            &.#{$module}-tab {
+                border: $width-tabs_bar_card-border solid transparent;
+                border-right: none;
+                border-radius: $radius-tabs_tab_card_left;
+
+                &:hover {
+                    border-right: none;
+                }
+
+            }
+
+            &.#{$module}-tab-active {
+
+                &:after {
+                    content: " ";
+                    width: 1px;
+                    position: absolute;
+                    right: -1px;
+                    top: 0;
+                    bottom: 0;
+                    background: $color-tabs_tab_card_selected-bg-default;
+                }
+
+                &,
+                &:hover {
+                    padding: $spacing-tabs_bar_card_tab_left_active-paddingY $spacing-tabs_bar_card_tab_left_active-paddingX;
+                    border: $width-tabs_bar_card-border solid $color-tabs_tab_card_indicator_selected-icon-default;
+                    border-right: none;
+                    background: transparent;
+                    // padding-bottom: $spacing-tight + 1;
+                }
+            }
+        }
+
+        &.#{$module}-tab {
+            padding: $spacing-tabs_bar_card_tab-paddingY $spacing-tabs_bar_card_tab-paddingX;
+            transition: background-color $transition_duration-tabs_tab_card-bg $transition_function-tabs_tab_card-bg $transition_delay-tabs_tab_card-bg, //卡片式tabs的bg的transition
+            color $transition_duration-tabs_tab_line-text $transition_function-tabs_tab_line-text $transition_delay-tabs_tab_line-text;//卡片式tabs的 color的transition
+
+            transform: scale($transform_scale-tabs_tab_card-item);
+
+            &:hover {
+                background: $color-tabs_tab_card-bg-hover;
+            }
+
+            &:focus-visible {
+                outline: $width-tabs-outline solid $color-tabs_tab-outline-focus;
+                outline-offset: $width-tabs-outline-offset;
+            }
+
+            &:active {
+                background: $color-tabs_tab_card-bg-active;
+            }
+        }
+    }
+
+    &-tab-button {
+        border: none;
+
+        &.#{$module}-tab {
+            padding: $spacing-tabs_bar_button_tab-paddingY $spacing-tabs_bar_button_tab-paddingX;
+            border-radius: $radius-tabs_tab_button;
+            color: $color-tabs_tab_button-text-default;
+            border: none;
+            transition: background-color $transition_duration-tabs-tab_button-bg $transition_function-tabs_tab_button-bg $transition_delay-tabs_tab_button-bg,//按钮tabs的背景色的transition
+            color $transition_duration-tabs_tab_line-text $transition_function-tabs_tab_line-text $transition_delay-tabs_tab_line-text;//按钮式tabs的 color的transition
+
+            transform: scale($transform_scale-tabs_tab_button-item);
+
+            &:hover {
+                border: none;
+                background-color: $color-tabs_tab_button-bg-hover;
+            }
+
+            &:focus-visible {
+                outline: $width-tabs-outline solid $color-tabs_tab-outline-focus;
+                outline-offset: $width-tabs-outline-offset;
+            }
+
+            &:active {
+                background-color: $color-tabs_tab_button-bg-active;
+            }
+        }
+
+        &.#{$module}-tab-active {
+
+            &,
+            &:hover {
+                color: $color-tabs_tab_button_selected-text-default;
+                border: none;
+                background-color: $color-tabs_tab_button_selected-bg-default;
+            }
+        }
+    }
 }
 
 @import "./rtl.scss";

+ 2 - 1
packages/semi-ui/overflowList/intersectionObserver.tsx

@@ -1,6 +1,7 @@
 import React, { ReactNode } from 'react';
 import PropTypes from 'prop-types';
 import { isEqual, isEmpty } from 'lodash';
+import { isHTMLElement } from '../_base/reactUtils';
 
 export interface ReactIntersectionObserverProps {
     onIntersect?: IntersectionObserverCallback;
@@ -80,7 +81,7 @@ export default class ReactIntersectionObserver extends React.PureComponent<React
         // observer callback is invoked immediately when observing new elements
         Object.keys(items).forEach(key => {
             const node = items[key];
-            if (!node) {
+            if (!(node && isHTMLElement(node))) {
                 return;
             }
             this.observer.observe(node);

+ 16 - 35
packages/semi-ui/tabs/TabBar.tsx

@@ -7,9 +7,10 @@ import OverflowList from '../overflowList';
 import Dropdown from '../dropdown';
 import Button from '../button';
 import { TabBarProps, PlainTab } from './interface';
-import { isEmpty } from 'lodash';
+import { isEmpty, pick } from 'lodash';
 import { IconChevronRight, IconChevronLeft, IconClose } from '@douyinfe/semi-icons';
 import { getUuidv4 } from '@douyinfe/semi-foundation/utils/uuid';
+import TabItem from './TabItem';
 
 export interface TabBarState {
     endInd: number;
@@ -98,41 +99,21 @@ class TabBar extends React.Component<TabBarProps, TabBarState> {
     }
 
     renderTabItem = (panel: PlainTab): ReactNode => {
-        const { size, type, deleteTabItem } = this.props;
-        const panelIcon = panel.icon ? this.renderIcon(panel.icon) : null;
-        const closableIcon = (type === 'card' && panel.closable) ? <IconClose aria-label="Close" role="button" className={`${cssClasses.TABS_TAB}-icon-close`} onClick={(e: React.MouseEvent<HTMLSpanElement>) => deleteTabItem(panel.itemKey, e)} /> : null;
-        let events = {};
-        const key = panel.itemKey;
-        if (!panel.disabled) {
-            events = {
-                onClick: (e: MouseEvent<HTMLDivElement>): void => this.handleItemClick(key, e),
-            };
-        }
-        const isSelected = this._isActive(key);
-        const className = cls(cssClasses.TABS_TAB, {
-            [cssClasses.TABS_TAB_ACTIVE]: isSelected,
-            [cssClasses.TABS_TAB_DISABLED]: panel.disabled,
-            [`${cssClasses.TABS_TAB}-small`]: size === 'small',
-            [`${cssClasses.TABS_TAB}-medium`]: size === 'medium',
-        });
+        const { size, type, deleteTabItem, handleKeyDown, tabPosition } = this.props;
+        const isSelected = this._isActive(panel.itemKey);
+        
         return (
-            <div
-                role="tab"
-                id={`semiTab${key}`}
-                data-tabkey={`semiTab${key}`}
-                aria-controls={`semiTabPanel${key}`}
-                aria-disabled={panel.disabled ? 'true' : 'false'}
-                aria-selected={isSelected ? 'true' : 'false'}
-                tabIndex={isSelected ? 0 : -1}
-                onKeyDown={e => this.handleKeyDown(e, key, panel.closable)}
-                {...events}
-                className={className}
-                key={this._getItemKey(key)}
-            >
-                {panelIcon}
-                {panel.tab}
-                {closableIcon}
-            </div>
+            <TabItem
+                {...pick(panel, ['disabled', 'icon', 'itemKey', 'tab', 'closable'])}
+                key={this._getItemKey(panel.itemKey)} 
+                selected={isSelected}
+                size={size}
+                type={type}
+                tabPosition={tabPosition}
+                handleKeyDown={handleKeyDown}
+                deleteTabItem={deleteTabItem}
+                onClick={this.handleItemClick}
+            />
         );
     };
 

+ 115 - 0
packages/semi-ui/tabs/TabItem.tsx

@@ -0,0 +1,115 @@
+import React, { ReactNode, MouseEvent, forwardRef, LegacyRef, useCallback, useMemo } from 'react';
+import cls from 'classnames';
+import { cssClasses } from '@douyinfe/semi-foundation/tabs/constants';
+import { IconClose } from '@douyinfe/semi-icons';
+import { TabType, TabSize, TabPosition } from './interface';
+
+export interface TabItemProps {
+    tab?: ReactNode;
+    icon?: ReactNode;
+    size?: TabSize;
+    type?: TabType;
+    tabPosition?: TabPosition;
+    selected?: boolean;
+    closable?: boolean;
+    disabled?: boolean;
+    itemKey?: string;
+    handleKeyDown?: (event: React.KeyboardEvent, itemKey: string, closable: boolean) => void;
+    deleteTabItem?: (tabKey: string, event: MouseEvent<Element>) => void;
+    onClick?: (itemKey: string, e: MouseEvent<Element>) => void 
+}
+
+const TabItem = (props: TabItemProps, ref: LegacyRef<HTMLDivElement>) => {
+    const { 
+        tab, 
+        size, 
+        type, 
+        icon, 
+        selected, 
+        closable, 
+        disabled, 
+        itemKey, 
+        deleteTabItem, 
+        tabPosition, 
+        handleKeyDown,
+        onClick,
+        ...restProps
+    } = props;
+
+    const closableIcon = useMemo(() => {
+        return (type === 'card' && closable) ?
+            <IconClose 
+                aria-label="Close" 
+                role="button" 
+                className={`${cssClasses.TABS_TAB}-icon-close`} 
+                onClick={(e: MouseEvent<HTMLSpanElement>) => deleteTabItem(itemKey, e)} 
+            /> : null;
+    }, [type, closable, deleteTabItem, itemKey]);
+
+    const renderIcon = useCallback(
+        (icon: ReactNode) => (
+            <span>
+                {icon}
+            </span>
+        ), []);
+
+    const handleKeyDownInItem = useCallback(
+        (event: React.KeyboardEvent) => {
+            handleKeyDown && handleKeyDown(event, itemKey, closable);
+        },
+        [handleKeyDown, itemKey, closable],
+    );
+
+    const handleItemClick = useCallback(
+        (e: MouseEvent) => {
+            !disabled && onClick && onClick(itemKey, e);
+        },
+        [itemKey, disabled, onClick],
+    );
+
+    const panelIcon = icon ? renderIcon(icon) : null;
+    const className = cls(
+        cssClasses.TABS_TAB, 
+        `${cssClasses.TABS_TAB}-${type}`,
+        `${cssClasses.TABS_TAB}-${tabPosition}`,
+        `${cssClasses.TABS_TAB}-single`,
+        
+        {
+            [cssClasses.TABS_TAB_ACTIVE]: selected,
+            [cssClasses.TABS_TAB_DISABLED]: disabled,
+            [`${cssClasses.TABS_TAB}-small`]: size === 'small',
+            [`${cssClasses.TABS_TAB}-medium`]: size === 'medium',
+        }
+    );
+    
+    return (
+        <div
+            role="tab"
+            id={`semiTab${itemKey}`}
+            data-tabkey={`semiTab${itemKey}`}
+            aria-controls={`semiTabPanel${itemKey}`}
+            aria-disabled={disabled ? 'true' : 'false'}
+            aria-selected={selected ? 'true' : 'false'}
+            tabIndex={selected ? 0 : -1}
+            onKeyDown={handleKeyDownInItem}
+            onClick={handleItemClick}
+            className={className}
+            {...restProps}
+            ref={ref}
+        >
+            {panelIcon}
+            {tab}
+            {closableIcon}
+        </div>
+    );
+};
+
+// Why is forwardRef needed here?
+// Because TabItem needs to be used in OverflowList (when tabs' type is collapsible), 
+// OverflowList will pass ref to the outermost div DOM node of TabItem
+const ForwardTabItem = forwardRef<HTMLDivElement, TabItemProps>(TabItem);
+
+// @ts-ignore 
+ForwardTabItem.elementType = 'Tabs.TabItem';
+
+export default ForwardTabItem;

+ 64 - 4
packages/semi-ui/tabs/_story/tabs.stories.jsx

@@ -286,7 +286,7 @@ export const RenderTabBar = () => (
     defaultActiveKey="1"
     renderTabBar={(tabBarProps, DefaultTabBar) => {
       return (
-        <div className="tab-bar-box" itemKey="bar">
+        <div className="tab-bar-box" >
           这是二次封装的Tab Bar,当前ActiveKey:{tabBarProps.activeKey}
           <DefaultTabBar {...tabBarProps} />
         </div>
@@ -708,7 +708,7 @@ export const CollapseTabs = () => (
       collapsible
     >
       {[...Array(30).keys()].map(i => (
-        <TabPane tab={`Tab-${i}`} itemKey={`Tab-${i}`}>
+        <TabPane tab={`Tab-${i}`} itemKey={`Tab-${i}`} key={`${i}`}>
           Content of card tab {i}
         </TabPane>
       ))}
@@ -717,7 +717,7 @@ export const CollapseTabs = () => (
     <br />
     <Tabs style={style} type="button" collapsible>
       {[...Array(30).keys()].map(i => (
-        <TabPane tab={`Tab-${i}`} itemKey={`${i}`}>
+        <TabPane tab={`Tab-${i}`} itemKey={`${i}`} key={`${i}`}>
           Content of button tab {i}
         </TabPane>
       ))}
@@ -726,7 +726,7 @@ export const CollapseTabs = () => (
     <br />
     <Tabs style={style} type="line" collapsible>
       {[...Array(30).keys()].map(i => (
-        <TabPane tab={`Tab-${i}`} itemKey={`${i}`}>
+        <TabPane tab={`Tab-${i}`} itemKey={`${i}`} key={`${i}`}>
           Content of line tab {i}
         </TabPane>
       ))}
@@ -903,3 +903,63 @@ export const TabClosable = () => <TabClosableDemo />;
 TabClosable.story = {
   name: 'tab closable',
 };
+
+export const TabItem = () => {
+  const TabItem = Tabs.TabItem;
+  console.log('TabItem', TabItem.elementType, TabItem);
+  
+  const params = [
+    // line 不同size
+    { tab: '标签栏一', type: 'line',   icon: null, size: 'large', tabPosition: 'top', selected: false, closable: false, disabled: false, itemKey: '1' },
+    { tab: '标签栏一', type: 'line',   icon: null, size: 'medium', tabPosition: 'top', selected: false, closable: false, disabled: false, itemKey: '1' },
+    { tab: '标签栏一', type: 'line',   icon: null, size: 'small', tabPosition: 'top', selected: false, closable: false, disabled: false, itemKey: '1' },
+    // with icon
+    { tab: '标签栏一', type: 'line',   icon: <IconFile />, size: 'large', tabPosition: 'top', selected: false, closable: false, disabled: false, itemKey: '1' },
+    { tab: '标签栏一', type: 'line',   icon: <IconFile />, size: 'medium', tabPosition: 'top', selected: false, closable: false, disabled: false, itemKey: '1' },
+    { tab: '标签栏一', type: 'line',   icon: <IconFile />, size: 'small', tabPosition: 'top', selected: false, closable: false, disabled: false, itemKey: '1' },
+    
+    
+    // button card
+    { tab: '标签栏一', type: 'button', icon: null, size: 'large',  tabPosition: 'top', selected: false, closable: false, disabled: false, itemKey: '2' },
+    { tab: '标签栏一', type: 'card', icon: null, size: 'large',  tabPosition: 'top', selected: false, closable: false, disabled: false, itemKey: '3' },
+
+    { tab: '标签栏一', type: 'button', icon: <IconFile />, size: 'large',  tabPosition: 'top', selected: false, closable: false, disabled: false, itemKey: '2' },
+    { tab: '标签栏一', type: 'card', icon: <IconFile />, size: 'large',  tabPosition: 'top', selected: false, closable: false, disabled: false, itemKey: '3' },
+
+    // left
+    { tab: '标签栏一', type: 'line',   icon: null, size: 'large',tabPosition: 'left', selected: false, closable: false, disabled: false, itemKey: '1' },
+    { tab: '标签栏一', type: 'line',   icon: null, size: 'medium',tabPosition: 'left', selected: false, closable: false, disabled: false, itemKey: '1' },
+    { tab: '标签栏一', type: 'line', icon: null, size: 'large',  tabPosition: 'left', selected: false, closable: false, disabled: false, itemKey: '2' },
+
+    { tab: '标签栏一', type: 'line',   icon:  <IconFile />, size: 'large',tabPosition: 'left', selected: false, closable: false, disabled: false, itemKey: '1' },
+    { tab: '标签栏一', type: 'line',   icon:  <IconFile />, size: 'medium',tabPosition: 'left', selected: false, closable: false, disabled: false, itemKey: '1' },
+    { tab: '标签栏一', type: 'line', icon:  <IconFile />, size: 'large',  tabPosition: 'left', selected: false, closable: false, disabled: false, itemKey: '2' },
+
+    { tab: '标签栏一', type: 'card', icon: null, size: 'large',  tabPosition: 'left', selected: false, closable: false, disabled: false, itemKey: '3' },
+    { tab: '标签栏一', type: 'button',   icon: null, size: 'small',tabPosition: 'left', selected: false, closable: false, disabled: false, itemKey: '1' },
+
+    { tab: '标签栏一', type: 'card', icon: <IconFile />, size: 'large',  tabPosition: 'left', selected: false, closable: false, disabled: false, itemKey: '3' },
+    { tab: '标签栏一', type: 'button',   icon:  <IconFile />, size: 'small',tabPosition: 'left', selected: false, closable: false, disabled: false, itemKey: '1' },
+  ]
+
+  return (<div style={{ minWidth: 100, minHeight: 100, border: '1px solid grey' }}>
+    {params.map((param, index) => (
+      <div key={`tab-item-${index}`} style={{ margin: 10 }}>
+        <TabItem {...param} />
+        <span style={{ marginLeft: '20px'}}></span>
+        <TabItem {...param}  selected={true}/>
+        <span style={{ marginLeft: '20px'}}></span>
+        <TabItem {...param}  disabled={true}/>
+        {param.type === 'card' &&
+          <div key={`tab-item-${index}-2`} style={{ margin: 10 }}>
+            <TabItem {...param} closable={true} />
+            <span style={{ marginLeft: '20px'}}></span>
+            <TabItem {...param}  selected={true} closable={true} />
+            <span style={{ marginLeft: '20px'}} closable={true} ></span>
+            <TabItem {...param}  disabled={true} closable={true} />
+          </div>
+        }
+      </div>
+    ))}
+  </div>)
+}

+ 2 - 0
packages/semi-ui/tabs/index.tsx

@@ -11,6 +11,7 @@ import '@douyinfe/semi-foundation/tabs/tabs.scss';
 
 import TabBar from './TabBar';
 import TabPane from './TabPane';
+import TabItem from './TabItem';
 import TabsContext from './tabs-context';
 import { PlainTab, TabBarProps, TabsProps } from './interface';
 
@@ -27,6 +28,7 @@ export interface TabsState {
 
 class Tabs extends BaseComponent<TabsProps, TabsState> {
     static TabPane = TabPane;
+    static TabItem = TabItem;
 
     static propTypes = {
         activeKey: PropTypes.string,