index.tsx 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238
  1. import React from 'react';
  2. import cls from 'classnames';
  3. import PropTypes from 'prop-types';
  4. import { cssClasses, strings } from '@douyinfe/semi-foundation/avatar/constants';
  5. import AvatarFoundation, { AvatarAdapter } from '@douyinfe/semi-foundation/avatar/foundation';
  6. import '@douyinfe/semi-foundation/avatar/avatar.scss';
  7. import { noop } from '@douyinfe/semi-foundation/utils/function';
  8. import BaseComponent from '../_base/baseComponent';
  9. import { AvatarProps } from './interface';
  10. import { handlePrevent } from '@douyinfe/semi-foundation/utils/a11y';
  11. const sizeSet = strings.SIZE;
  12. const shapeSet = strings.SHAPE;
  13. const colorSet = strings.COLOR;
  14. const prefixCls = cssClasses.PREFIX;
  15. export * from './interface';
  16. export interface AvatarState {
  17. isImgExist: boolean;
  18. hoverContent: React.ReactNode;
  19. focusVisible: boolean
  20. }
  21. export default class Avatar extends BaseComponent<AvatarProps, AvatarState> {
  22. static defaultProps = {
  23. size: 'medium',
  24. color: 'grey',
  25. shape: 'circle',
  26. onClick: noop,
  27. onMouseEnter: noop,
  28. onMouseLeave: noop,
  29. };
  30. static elementType: string;
  31. static propTypes = {
  32. children: PropTypes.node,
  33. color: PropTypes.oneOf(colorSet),
  34. shape: PropTypes.oneOf(shapeSet),
  35. size: PropTypes.oneOf(sizeSet),
  36. hoverMask: PropTypes.node,
  37. className: PropTypes.string,
  38. style: PropTypes.object,
  39. imgAttr: PropTypes.object,
  40. src: PropTypes.string,
  41. srcSet: PropTypes.string,
  42. alt: PropTypes.string,
  43. onError: PropTypes.func,
  44. onClick: PropTypes.func,
  45. onMouseEnter: PropTypes.func,
  46. onMouseLeave: PropTypes.func,
  47. };
  48. foundation!: AvatarFoundation;
  49. constructor(props: AvatarProps) {
  50. super(props);
  51. this.state = {
  52. isImgExist: true,
  53. hoverContent: '',
  54. focusVisible: false,
  55. };
  56. this.onEnter = this.onEnter.bind(this);
  57. this.onLeave = this.onLeave.bind(this);
  58. this.handleError = this.handleError.bind(this);
  59. this.handleKeyDown = this.handleKeyDown.bind(this);
  60. this.getContent = this.getContent.bind(this);
  61. }
  62. get adapter(): AvatarAdapter<AvatarProps, AvatarState> {
  63. return {
  64. ...super.adapter,
  65. notifyImgState: (isImgExist: boolean) => {
  66. this.setState({ isImgExist });
  67. },
  68. notifyEnter: (e: React.MouseEvent) => {
  69. const { hoverMask } = this.props;
  70. const hoverContent = hoverMask;
  71. this.setState({ hoverContent }, () => {
  72. const { onMouseEnter } = this.props;
  73. onMouseEnter && onMouseEnter(e);
  74. });
  75. },
  76. notifyLeave: (e: React.MouseEvent) => {
  77. this.setState({ hoverContent: '' }, () => {
  78. const { onMouseLeave } = this.props;
  79. onMouseLeave && onMouseLeave(e);
  80. });
  81. },
  82. setFocusVisible: (focusVisible: boolean): void => {
  83. this.setState({ focusVisible });
  84. },
  85. };
  86. }
  87. componentDidMount() {
  88. this.foundation = new AvatarFoundation<AvatarProps, AvatarState>(this.adapter);
  89. this.foundation.init();
  90. }
  91. componentDidUpdate(prevProps: AvatarProps) {
  92. if (this.props.src && this.props.src !== prevProps.src) {
  93. const image = new Image(0, 0);
  94. image.src = this.props.src;
  95. image.onload = () => {
  96. this.setState({ isImgExist: true });
  97. };
  98. image.onerror = () => {
  99. this.setState({ isImgExist: false });
  100. };
  101. image.onabort = () => {
  102. this.setState({ isImgExist: false });
  103. };
  104. }
  105. }
  106. componentWillUnmount() {
  107. this.foundation.destroy();
  108. }
  109. onEnter(e: React.MouseEvent) {
  110. this.foundation.handleEnter(e);
  111. }
  112. onLeave(e: React.MouseEvent) {
  113. this.foundation.handleLeave(e);
  114. }
  115. handleError() {
  116. this.foundation.handleImgLoadError();
  117. }
  118. handleKeyDown(event: any) {
  119. const { onClick } = this.props;
  120. switch (event.key) {
  121. case "Enter":
  122. onClick(event);
  123. handlePrevent(event);
  124. break;
  125. case 'Escape':
  126. event.target.blur();
  127. break;
  128. default:
  129. break;
  130. }
  131. }
  132. handleFocusVisible = (event: React.FocusEvent) => {
  133. this.foundation.handleFocusVisible(event);
  134. }
  135. handleBlur = (event: React.FocusEvent) => {
  136. this.foundation.handleBlur();
  137. }
  138. getContent = () => {
  139. const { children, onClick, imgAttr, src, srcSet, alt } = this.props;
  140. const { isImgExist } = this.state;
  141. let content = children;
  142. const clickable = onClick !== noop;
  143. const isImg = src && isImgExist;
  144. const a11yFocusProps = {
  145. tabIndex: 0,
  146. onKeyDown: this.handleKeyDown,
  147. onFocus: this.handleFocusVisible,
  148. onBlur: this.handleBlur,
  149. };
  150. if (isImg) {
  151. const finalAlt = clickable ? `clickable Avatar: ${alt}` : alt;
  152. const imgBasicProps = {
  153. src,
  154. srcSet,
  155. onError: this.handleError,
  156. ...imgAttr,
  157. className: cls({
  158. [`${prefixCls}-no-focus-visible`]: clickable,
  159. }),
  160. };
  161. const imgProps = clickable ? { ...imgBasicProps, ...a11yFocusProps } : imgBasicProps;
  162. content = (
  163. <img alt={finalAlt} {...imgProps}/>
  164. );
  165. } else if (typeof children === 'string') {
  166. const tempAlt = alt ?? children;
  167. const finalAlt = clickable ? `clickable Avatar: ${tempAlt}` : tempAlt;
  168. const props = {
  169. role: 'img',
  170. 'aria-label': finalAlt,
  171. className: cls(`${prefixCls}-label`,
  172. {
  173. [`${prefixCls}-no-focus-visible`]: clickable,
  174. }
  175. ),
  176. };
  177. const finalProps = clickable ? { ...props, ...a11yFocusProps } : props;
  178. content = (
  179. <span className={`${prefixCls}-content`}>
  180. <span {...finalProps} x-semi-prop="children">{children}</span>
  181. </span>
  182. );
  183. }
  184. return content;
  185. }
  186. render() {
  187. // eslint-disable-next-line max-len, no-unused-vars
  188. const { shape, children, size, color, className, hoverMask, onClick, imgAttr, src, srcSet, style, alt, ...others } = this.props;
  189. const { isImgExist, hoverContent, focusVisible } = this.state;
  190. const isImg = src && isImgExist;
  191. const avatarCls = cls(
  192. prefixCls,
  193. {
  194. [`${prefixCls}-${shape}`]: shape,
  195. [`${prefixCls}-${size}`]: size,
  196. [`${prefixCls}-${color}`]: color && !isImg,
  197. [`${prefixCls}-img`]: isImg,
  198. [`${prefixCls}-focus`]: focusVisible,
  199. },
  200. className
  201. );
  202. const hoverRender = hoverContent ? (<div className={`${prefixCls}-hover`} x-semi-prop="hoverContent">{hoverContent}</div>) : null;
  203. return (
  204. <span
  205. {...(others as any)}
  206. style={style}
  207. className={avatarCls}
  208. onClick={onClick as any}
  209. onMouseEnter={this.onEnter as any}
  210. onMouseLeave={this.onLeave as any}
  211. role='listitem'
  212. >
  213. {this.getContent()}
  214. {hoverRender}
  215. </span>
  216. );
  217. }
  218. }
  219. Avatar.elementType = 'Avatar';