TabBar.tsx 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250
  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. // eslint-disable-next-line max-len
  84. const tabItem = document.querySelector(`[data-uuid="${this.state.uuid}"] .${cssClasses.TABS_TAB}[data-scrollkey="${key}"]`);
  85. tabItem.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'nearest' });
  86. }
  87. };
  88. handleKeyDown = (event: React.KeyboardEvent, itemKey: string, closable: boolean) => {
  89. this.props.handleKeyDown(event, itemKey, closable);
  90. }
  91. renderTabItem = (panel: PlainTab): ReactNode => {
  92. const { size, type, deleteTabItem, handleKeyDown, tabPosition } = this.props;
  93. const isSelected = this._isActive(panel.itemKey);
  94. return (
  95. <TabItem
  96. {...pick(panel, ['disabled', 'icon', 'itemKey', 'tab', 'closable'])}
  97. key={this._getItemKey(panel.itemKey)}
  98. selected={isSelected}
  99. size={size}
  100. type={type}
  101. tabPosition={tabPosition}
  102. handleKeyDown={handleKeyDown}
  103. deleteTabItem={deleteTabItem}
  104. onClick={this.handleItemClick}
  105. />
  106. );
  107. };
  108. renderTabComponents = (list: Array<PlainTab>): Array<ReactNode> => list.map(panel => this.renderTabItem(panel));
  109. handleArrowClick = (items: Array<OverflowItem>, pos: 'start' | 'end'): void => {
  110. const lastItem = pos === 'start' ? items.pop() : items.shift();
  111. if (!lastItem) {
  112. return;
  113. }
  114. const key = this._getItemKey(lastItem.itemKey);
  115. // eslint-disable-next-line max-len
  116. const tabItem = document.querySelector(`[data-uuid="${this.state.uuid}"] .${cssClasses.TABS_TAB}[data-scrollkey="${key}"]`);
  117. tabItem.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'nearest' });
  118. };
  119. renderCollapse = (items: Array<OverflowItem>, icon: ReactNode, pos: 'start' | 'end'): ReactNode => {
  120. if (isEmpty(items)) {
  121. return (
  122. <Button
  123. disabled={true}
  124. icon={icon}
  125. theme="borderless"
  126. />
  127. );
  128. }
  129. const { dropdownClassName, dropdownStyle } = this.props;
  130. const { rePosKey } = this.state;
  131. const disabled = !items.length;
  132. const menu = (
  133. <Dropdown.Menu>
  134. {items.map(panel => {
  135. const { icon: i, tab, itemKey } = panel;
  136. const panelIcon = i ? this.renderIcon(panel.icon) : null;
  137. return (
  138. <Dropdown.Item
  139. key={itemKey}
  140. onClick={(e): void => this.handleItemClick(itemKey, e)}
  141. active={this._isActive(itemKey)}
  142. >
  143. {panelIcon}
  144. {tab}
  145. </Dropdown.Item>
  146. );
  147. })}
  148. </Dropdown.Menu>
  149. );
  150. const arrowCls = cls({
  151. [`${cssClasses.TABS_BAR}-arrow-${pos}`]: pos,
  152. [`${cssClasses.TABS_BAR}-arrow`]: true,
  153. });
  154. const dropdownCls = cls(dropdownClassName, {
  155. [`${cssClasses.TABS_BAR}-dropdown`]: true,
  156. });
  157. return (
  158. <Dropdown
  159. className={dropdownCls}
  160. clickToHide
  161. clickTriggerToHide
  162. key={`${rePosKey}-${pos}`}
  163. position={pos === 'start' ? 'bottomLeft' : 'bottomRight'}
  164. render={disabled ? null : menu}
  165. showTick
  166. style={dropdownStyle}
  167. trigger={'hover'}
  168. disableFocusListener // prevent the panel from popping up again after clicking
  169. >
  170. <div role="presentation" className={arrowCls} onClick={(e): void => this.handleArrowClick(items, pos)}>
  171. <Button
  172. disabled={disabled}
  173. icon={icon}
  174. // size="small"
  175. theme="borderless"
  176. />
  177. </div>
  178. </Dropdown>
  179. );
  180. };
  181. renderOverflow = (items: any[]): Array<ReactNode> => items.map((item, ind) => {
  182. const icon = ind === 0 ? <IconChevronLeft /> : <IconChevronRight />;
  183. const pos = ind === 0 ? 'start' : 'end';
  184. return this.renderCollapse(item, icon, pos);
  185. });
  186. renderCollapsedTab = (): ReactNode => {
  187. const { list } = this.props;
  188. const renderedList = list.map(item => {
  189. const { itemKey } = item;
  190. return { key: this._getItemKey(itemKey), active: this._isActive(itemKey), ...item };
  191. });
  192. return (
  193. <OverflowList
  194. items={renderedList}
  195. overflowRenderer={this.renderOverflow}
  196. renderMode="scroll"
  197. className={`${cssClasses.TABS_BAR}-overflow-list`}
  198. visibleItemRenderer={this.renderTabItem as any}
  199. />
  200. );
  201. };
  202. render(): ReactNode {
  203. const { type, style, className, list, tabPosition, collapsible, ...restProps } = this.props;
  204. const classNames = cls(className, {
  205. [cssClasses.TABS_BAR]: true,
  206. [cssClasses.TABS_BAR_LINE]: type === 'line',
  207. [cssClasses.TABS_BAR_CARD]: type === 'card',
  208. [cssClasses.TABS_BAR_BUTTON]: type === 'button',
  209. [`${cssClasses.TABS_BAR}-${tabPosition}`]: tabPosition,
  210. [`${cssClasses.TABS_BAR}-collapse`]: collapsible,
  211. });
  212. const extra = this.renderExtra();
  213. const contents = collapsible ? this.renderCollapsedTab() : this.renderTabComponents(list);
  214. return (
  215. <div role="tablist" aria-orientation={tabPosition === "left" ? "vertical" : "horizontal"} className={classNames} style={style} {...getDataAttr(restProps)} data-uuid={this.state.uuid}>
  216. {contents}
  217. {extra}
  218. </div>
  219. );
  220. }
  221. private _isActive = (key: string): boolean => key === this.props.activeKey;
  222. private _getItemKey = (key: string): string => `${key}-bar`;
  223. }
  224. export default TabBar;