index.tsx 5.8 KB

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