index.tsx 5.3 KB

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