TabBar.tsx 9.7 KB

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