index.tsx 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177
  1. import React, { Component } from 'react';
  2. import classNames from 'classnames';
  3. import PropTypes from 'prop-types';
  4. import { cssClasses, strings } from '@douyinfe/semi-foundation/tag/constants';
  5. import Avatar from '../avatar/index';
  6. import { IconClose } from '@douyinfe/semi-icons';
  7. import { TagProps, TagSize, TagColor, TagType } from './interface';
  8. import { handlePrevent } from '@douyinfe/semi-foundation/utils/a11y';
  9. import '@douyinfe/semi-foundation/tag/tag.scss';
  10. import { isString } from 'lodash';
  11. import cls from 'classnames';
  12. export * from './interface';
  13. const prefixCls = cssClasses.PREFIX;
  14. const tagColors = strings.TAG_COLOR;
  15. const tagSize = strings.TAG_SIZE;
  16. const tagType = strings.TAG_TYPE;
  17. const avatarShapeSet = strings.AVATAR_SHAPE;
  18. export interface TagState {
  19. visible: boolean
  20. }
  21. export default class Tag extends Component<TagProps, TagState> {
  22. static defaultProps: TagProps = {
  23. size: tagSize[0] as TagSize,
  24. color: tagColors[0] as TagColor,
  25. closable: false,
  26. // visible: true,
  27. type: tagType[0] as TagType,
  28. onClose: () => undefined,
  29. onClick: () => undefined,
  30. onMouseEnter: () => undefined,
  31. style: {},
  32. className: '',
  33. shape: 'square',
  34. avatarShape: 'square',
  35. prefixIcon: null,
  36. suffixIcon: null
  37. };
  38. static propTypes = {
  39. children: PropTypes.node,
  40. tagKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
  41. size: PropTypes.oneOf(tagSize),
  42. color: PropTypes.oneOf(tagColors),
  43. type: PropTypes.oneOf(tagType),
  44. closable: PropTypes.bool,
  45. visible: PropTypes.bool,
  46. onClose: PropTypes.func,
  47. onClick: PropTypes.func,
  48. prefixIcon: PropTypes.node,
  49. suffixIcon: PropTypes.node,
  50. style: PropTypes.object,
  51. className: PropTypes.string,
  52. avatarSrc: PropTypes.string,
  53. avatarShape: PropTypes.oneOf(avatarShapeSet),
  54. 'aria-label': PropTypes.string,
  55. };
  56. constructor(props: TagProps) {
  57. super(props);
  58. this.state = {
  59. visible: true,
  60. };
  61. this.close = this.close.bind(this);
  62. this.handleKeyDown = this.handleKeyDown.bind(this);
  63. }
  64. // any other way to achieve this?
  65. static getDerivedStateFromProps(nextProps: TagProps) {
  66. if ('visible' in nextProps) {
  67. return {
  68. visible: nextProps.visible,
  69. };
  70. }
  71. return null;
  72. }
  73. setVisible(visible: boolean) {
  74. if (!('visible' in this.props)) {
  75. this.setState({ visible });
  76. }
  77. }
  78. close(e: React.MouseEvent<HTMLElement>, value: React.ReactNode, tagKey: string | number) {
  79. const { onClose } = this.props;
  80. e.stopPropagation();
  81. e.nativeEvent.stopImmediatePropagation();
  82. onClose && onClose(value, e, tagKey);
  83. // when user call e.preventDefault() in onClick callback, tag will not hidden
  84. if (e.defaultPrevented) {
  85. return;
  86. }
  87. this.setVisible(false);
  88. }
  89. handleKeyDown(event: any) {
  90. const { closable, onClick, onKeyDown } = this.props;
  91. switch (event.key) {
  92. case "Backspace":
  93. case "Delete":
  94. closable && this.close(event, this.props.children, this.props.tagKey);
  95. handlePrevent(event);
  96. break;
  97. case "Enter":
  98. onClick(event);
  99. handlePrevent(event);
  100. break;
  101. case 'Escape':
  102. event.target.blur();
  103. break;
  104. default:
  105. break;
  106. }
  107. onKeyDown && onKeyDown(event);
  108. }
  109. renderAvatar() {
  110. const { avatarShape, avatarSrc } = this.props;
  111. const avatar = <Avatar src={avatarSrc} shape={avatarShape} />;
  112. return avatar;
  113. }
  114. render() {
  115. const { tagKey, children, size, color, closable, visible, onClose, onClick, className, type, shape, avatarSrc, avatarShape, tabIndex, prefixIcon, suffixIcon, ...attr } = this.props;
  116. const { visible: isVisible } = this.state;
  117. const clickable = onClick !== Tag.defaultProps.onClick || closable;
  118. // only when the Tag is clickable or closable, the value of tabIndex is allowed to be passed in.
  119. const a11yProps = { role: 'button', tabIndex: tabIndex || 0, onKeyDown: this.handleKeyDown };
  120. const baseProps = {
  121. ...attr,
  122. onClick,
  123. tabIndex: tabIndex,
  124. className: classNames(
  125. prefixCls,
  126. {
  127. [`${prefixCls}-default`]: size === 'default',
  128. [`${prefixCls}-small`]: size === 'small',
  129. [`${prefixCls}-large`]: size === 'large',
  130. [`${prefixCls}-square`]: shape === 'square',
  131. [`${prefixCls}-circle`]: shape === 'circle',
  132. [`${prefixCls}-${type}`]: type,
  133. [`${prefixCls}-${color}-${type}`]: color && type,
  134. [`${prefixCls}-closable`]: closable,
  135. [`${prefixCls}-invisible`]: !isVisible,
  136. [`${prefixCls}-avatar-${avatarShape}`]: avatarSrc,
  137. },
  138. className
  139. ),
  140. };
  141. const wrapProps = clickable ? ({ ...baseProps, ...a11yProps }) : baseProps;
  142. const closeIcon = closable ? (
  143. // eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions
  144. <div className={`${prefixCls}-close`} onClick={e => this.close(e, children, tagKey)}>
  145. <IconClose size="small" />
  146. </div>
  147. ) : null;
  148. const stringChild = isString(children);
  149. const contentCls = cls(`${prefixCls}-content`, `${prefixCls}-content-${stringChild ? 'ellipsis' : 'center' }`);
  150. return (
  151. <div aria-label={this.props['aria-label'] || stringChild ? `${closable ? 'Closable ' : ''}Tag: ${children}` : '' } {...wrapProps}>
  152. {prefixIcon ? <div className={`${prefixCls}-prefix-icon`}>{prefixIcon}</div> : null}
  153. {avatarSrc ? this.renderAvatar() : null}
  154. <div className={contentCls}>
  155. {children}
  156. </div>
  157. {suffixIcon ? <div className={`${prefixCls}-suffix-icon`}>{suffixIcon}</div> : null}
  158. {closeIcon}
  159. </div>
  160. );
  161. }
  162. }