TabBar.tsx 9.3 KB

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