TabBar.tsx 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248
  1. import React, { MouseEvent, ReactElement, ReactNode } from 'react';
  2. import PropTypes from 'prop-types';
  3. import cls from 'classnames';
  4. import { cssClasses, strings } from '@douyinfe/semi-foundation/tabs/constants';
  5. import getDataAttr from '@douyinfe/semi-foundation/utils/getDataAttr';
  6. import OverflowList from '../overflowList';
  7. import Dropdown from '../dropdown';
  8. import Button from '../button';
  9. import { TabBarProps, PlainTab } from './interface';
  10. import { isEmpty, pick } from 'lodash';
  11. import { IconChevronRight, IconChevronLeft, IconClose } from '@douyinfe/semi-icons';
  12. import { getUuidv4 } from '@douyinfe/semi-foundation/utils/uuid';
  13. import TabItem from './TabItem';
  14. export interface TabBarState {
  15. endInd: number;
  16. rePosKey: number;
  17. startInd: number;
  18. uuid: string
  19. }
  20. export interface OverflowItem extends PlainTab {
  21. key: string;
  22. active: boolean
  23. }
  24. class TabBar extends React.Component<TabBarProps, TabBarState> {
  25. static propTypes = {
  26. activeKey: PropTypes.string,
  27. className: PropTypes.string,
  28. collapsible: PropTypes.bool,
  29. list: PropTypes.array,
  30. onTabClick: PropTypes.func,
  31. size: PropTypes.oneOf(strings.SIZE),
  32. style: PropTypes.object,
  33. tabBarExtraContent: PropTypes.node,
  34. tabPosition: PropTypes.oneOf(strings.POSITION_MAP),
  35. type: PropTypes.oneOf(strings.TYPE_MAP),
  36. closable: PropTypes.bool,
  37. deleteTabItem: PropTypes.func
  38. };
  39. constructor(props: TabBarProps) {
  40. super(props);
  41. this.state = {
  42. endInd: props.list.length,
  43. rePosKey: 0,
  44. startInd: 0,
  45. uuid: '',
  46. };
  47. }
  48. componentDidMount() {
  49. this.setState({
  50. uuid: getUuidv4(),
  51. });
  52. }
  53. renderIcon(icon: ReactNode): ReactNode {
  54. return (
  55. <span>
  56. {icon}
  57. </span>
  58. );
  59. }
  60. renderExtra(): ReactNode {
  61. const { tabBarExtraContent, type, size } = this.props;
  62. const tabBarExtraContentDefaultStyle = { float: 'right' };
  63. const tabBarExtraContentStyle =
  64. tabBarExtraContent && (tabBarExtraContent as ReactElement).props ? (tabBarExtraContent as ReactElement).props.style : {};
  65. const extraCls = cls(cssClasses.TABS_BAR_EXTRA, {
  66. [`${cssClasses.TABS_BAR}-${type}-extra`]: type,
  67. [`${cssClasses.TABS_BAR}-${type}-extra-${size}`]: size,
  68. });
  69. if (tabBarExtraContent) {
  70. const tabBarStyle = { ...tabBarExtraContentDefaultStyle, ...tabBarExtraContentStyle };
  71. return (
  72. <div className={extraCls} style={tabBarStyle} x-semi-prop="tabBarExtraContent">
  73. {tabBarExtraContent}
  74. </div>
  75. );
  76. }
  77. return null;
  78. }
  79. handleItemClick = (itemKey: string, e: MouseEvent<Element>): void => {
  80. this.props.onTabClick(itemKey, e);
  81. if (this.props.collapsible) {
  82. const key = this._getItemKey(itemKey);
  83. const tabItem = document.querySelector(`[data-uuid="${this.state.uuid}"] .${cssClasses.TABS_TAB}[data-scrollkey="${key}"]`);
  84. tabItem.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'nearest' });
  85. }
  86. };
  87. handleKeyDown = (event: React.KeyboardEvent, itemKey: string, closable: boolean) => {
  88. this.props.handleKeyDown(event, itemKey, closable);
  89. }
  90. renderTabItem = (panel: PlainTab): ReactNode => {
  91. const { size, type, deleteTabItem, handleKeyDown, tabPosition } = this.props;
  92. const isSelected = this._isActive(panel.itemKey);
  93. return (
  94. <TabItem
  95. {...pick(panel, ['disabled', 'icon', 'itemKey', 'tab', 'closable'])}
  96. key={this._getItemKey(panel.itemKey)}
  97. selected={isSelected}
  98. size={size}
  99. type={type}
  100. tabPosition={tabPosition}
  101. handleKeyDown={handleKeyDown}
  102. deleteTabItem={deleteTabItem}
  103. onClick={this.handleItemClick}
  104. />
  105. );
  106. };
  107. renderTabComponents = (list: Array<PlainTab>): Array<ReactNode> => list.map(panel => this.renderTabItem(panel));
  108. handleArrowClick = (items: Array<OverflowItem>, pos: 'start' | 'end'): void => {
  109. const lastItem = pos === 'start' ? items.pop() : items.shift();
  110. if (!lastItem) {
  111. return;
  112. }
  113. const key = this._getItemKey(lastItem.itemKey);
  114. const tabItem = document.querySelector(`[data-uuid="${this.state.uuid}"] .${cssClasses.TABS_TAB}[data-scrollkey="${key}"]`);
  115. tabItem.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'nearest' });
  116. };
  117. renderCollapse = (items: Array<OverflowItem>, icon: ReactNode, pos: 'start' | 'end'): ReactNode => {
  118. if (isEmpty(items)) {
  119. return (
  120. <Button
  121. disabled={true}
  122. icon={icon}
  123. theme="borderless"
  124. />
  125. );
  126. }
  127. const { dropdownClassName, dropdownStyle } = this.props;
  128. const { rePosKey } = this.state;
  129. const disabled = !items.length;
  130. const menu = (
  131. <Dropdown.Menu>
  132. {items.map(panel => {
  133. const { icon: i, tab, itemKey } = panel;
  134. const panelIcon = i ? this.renderIcon(panel.icon) : null;
  135. return (
  136. <Dropdown.Item
  137. key={itemKey}
  138. onClick={(e): void => this.handleItemClick(itemKey, e)}
  139. active={this._isActive(itemKey)}
  140. >
  141. {panelIcon}
  142. {tab}
  143. </Dropdown.Item>
  144. );
  145. })}
  146. </Dropdown.Menu>
  147. );
  148. const arrowCls = cls({
  149. [`${cssClasses.TABS_BAR}-arrow-${pos}`]: pos,
  150. [`${cssClasses.TABS_BAR}-arrow`]: true,
  151. });
  152. const dropdownCls = cls(dropdownClassName, {
  153. [`${cssClasses.TABS_BAR}-dropdown`]: true,
  154. });
  155. return (
  156. <Dropdown
  157. className={dropdownCls}
  158. clickToHide
  159. clickTriggerToHide
  160. key={`${rePosKey}-${pos}`}
  161. position={pos === 'start' ? 'bottomLeft' : 'bottomRight'}
  162. render={disabled ? null : menu}
  163. showTick
  164. style={dropdownStyle}
  165. trigger={'hover'}
  166. disableFocusListener // prevent the panel from popping up again after clicking
  167. >
  168. <div role="presentation" className={arrowCls} onClick={(e): void => this.handleArrowClick(items, pos)}>
  169. <Button
  170. disabled={disabled}
  171. icon={icon}
  172. // size="small"
  173. theme="borderless"
  174. />
  175. </div>
  176. </Dropdown>
  177. );
  178. };
  179. renderOverflow = (items: any[]): Array<ReactNode> => items.map((item, ind) => {
  180. const icon = ind === 0 ? <IconChevronLeft /> : <IconChevronRight />;
  181. const pos = ind === 0 ? 'start' : 'end';
  182. return this.renderCollapse(item, icon, pos);
  183. });
  184. renderCollapsedTab = (): ReactNode => {
  185. const { list } = this.props;
  186. const renderedList = list.map(item => {
  187. const { itemKey } = item;
  188. return { key: this._getItemKey(itemKey), active: this._isActive(itemKey), ...item };
  189. });
  190. return (
  191. <OverflowList
  192. items={renderedList}
  193. overflowRenderer={this.renderOverflow}
  194. renderMode="scroll"
  195. className={`${cssClasses.TABS_BAR}-overflow-list`}
  196. visibleItemRenderer={this.renderTabItem as any}
  197. />
  198. );
  199. };
  200. render(): ReactNode {
  201. const { type, style, className, list, tabPosition, collapsible, ...restProps } = this.props;
  202. const classNames = cls(className, {
  203. [cssClasses.TABS_BAR]: true,
  204. [cssClasses.TABS_BAR_LINE]: type === 'line',
  205. [cssClasses.TABS_BAR_CARD]: type === 'card',
  206. [cssClasses.TABS_BAR_BUTTON]: type === 'button',
  207. [`${cssClasses.TABS_BAR}-${tabPosition}`]: tabPosition,
  208. [`${cssClasses.TABS_BAR}-collapse`]: collapsible,
  209. });
  210. const extra = this.renderExtra();
  211. const contents = collapsible ? this.renderCollapsedTab() : this.renderTabComponents(list);
  212. return (
  213. <div role="tablist" aria-orientation={tabPosition === "left" ? "vertical" : "horizontal"} className={classNames} style={style} {...getDataAttr(restProps)} data-uuid={this.state.uuid}>
  214. {contents}
  215. {extra}
  216. </div>
  217. );
  218. }
  219. private _isActive = (key: string): boolean => key === this.props.activeKey;
  220. private _getItemKey = (key: string): string => `${key}-bar`;
  221. }
  222. export default TabBar;