index.tsx 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352
  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. import TopSlotSvg from "@douyinfe/semi-ui/avatar/TopSlotSvg";
  12. const sizeSet = strings.SIZE;
  13. const shapeSet = strings.SHAPE;
  14. const colorSet = strings.COLOR;
  15. const prefixCls = cssClasses.PREFIX;
  16. export * from './interface';
  17. export interface AvatarState {
  18. isImgExist: boolean;
  19. hoverContent: React.ReactNode;
  20. focusVisible: boolean;
  21. scale: number
  22. }
  23. export default class Avatar extends BaseComponent<AvatarProps, AvatarState> {
  24. static defaultProps = {
  25. size: 'medium',
  26. color: 'grey',
  27. shape: 'circle',
  28. gap: 3,
  29. onClick: noop,
  30. onMouseEnter: noop,
  31. onMouseLeave: noop,
  32. };
  33. static elementType: string;
  34. static propTypes = {
  35. children: PropTypes.node,
  36. color: PropTypes.oneOf(colorSet),
  37. shape: PropTypes.oneOf(shapeSet),
  38. size: PropTypes.oneOf(sizeSet),
  39. hoverMask: PropTypes.node,
  40. className: PropTypes.string,
  41. style: PropTypes.object,
  42. gap: PropTypes.number,
  43. imgAttr: PropTypes.object,
  44. src: PropTypes.string,
  45. srcSet: PropTypes.string,
  46. alt: PropTypes.string,
  47. onError: PropTypes.func,
  48. onClick: PropTypes.func,
  49. onMouseEnter: PropTypes.func,
  50. onMouseLeave: PropTypes.func,
  51. };
  52. foundation!: AvatarFoundation;
  53. avatarRef: React.RefObject<HTMLElement | null>;
  54. constructor(props: AvatarProps) {
  55. super(props);
  56. this.state = {
  57. isImgExist: true,
  58. hoverContent: '',
  59. focusVisible: false,
  60. scale: 1,
  61. };
  62. this.onEnter = this.onEnter.bind(this);
  63. this.onLeave = this.onLeave.bind(this);
  64. this.handleError = this.handleError.bind(this);
  65. this.handleKeyDown = this.handleKeyDown.bind(this);
  66. this.getContent = this.getContent.bind(this);
  67. this.avatarRef = React.createRef();
  68. }
  69. get adapter(): AvatarAdapter<AvatarProps, AvatarState> {
  70. return {
  71. ...super.adapter,
  72. notifyImgState: (isImgExist: boolean) => {
  73. this.setState({ isImgExist });
  74. },
  75. notifyEnter: (e: React.MouseEvent) => {
  76. const { hoverMask } = this.props;
  77. const hoverContent = hoverMask;
  78. this.setState({ hoverContent }, () => {
  79. const { onMouseEnter } = this.props;
  80. onMouseEnter && onMouseEnter(e);
  81. });
  82. },
  83. notifyLeave: (e: React.MouseEvent) => {
  84. this.setState({ hoverContent: '' }, () => {
  85. const { onMouseLeave } = this.props;
  86. onMouseLeave && onMouseLeave(e);
  87. });
  88. },
  89. setFocusVisible: (focusVisible: boolean): void => {
  90. this.setState({ focusVisible });
  91. },
  92. setScale: (scale: number) => {
  93. this.setState({ scale });
  94. },
  95. getAvatarNode: () => {
  96. return this.avatarRef?.current;
  97. }
  98. };
  99. }
  100. componentDidMount() {
  101. this.foundation = new AvatarFoundation<AvatarProps, AvatarState>(this.adapter);
  102. this.foundation.init();
  103. }
  104. componentDidUpdate(prevProps: AvatarProps) {
  105. if (this.props.src && this.props.src !== prevProps.src) {
  106. const image = new Image(0, 0);
  107. image.src = this.props.src;
  108. image.onload = () => {
  109. this.setState({ isImgExist: true });
  110. };
  111. image.onerror = () => {
  112. this.setState({ isImgExist: false });
  113. };
  114. image.onabort = () => {
  115. this.setState({ isImgExist: false });
  116. };
  117. }
  118. if (typeof this.props.children === "string" && this.props.children !== prevProps.children) {
  119. this.foundation.changeScale();
  120. }
  121. }
  122. componentWillUnmount() {
  123. this.foundation.destroy();
  124. }
  125. onEnter(e: React.MouseEvent) {
  126. this.foundation.handleEnter(e);
  127. }
  128. onLeave(e: React.MouseEvent) {
  129. this.foundation.handleLeave(e);
  130. }
  131. handleError() {
  132. this.foundation.handleImgLoadError();
  133. }
  134. handleKeyDown(event: any) {
  135. const { onClick } = this.props;
  136. switch (event.key) {
  137. case "Enter":
  138. onClick(event);
  139. handlePrevent(event);
  140. break;
  141. case 'Escape':
  142. event.target.blur();
  143. break;
  144. default:
  145. break;
  146. }
  147. }
  148. handleFocusVisible = (event: React.FocusEvent) => {
  149. this.foundation.handleFocusVisible(event);
  150. }
  151. handleBlur = (event: React.FocusEvent) => {
  152. this.foundation.handleBlur();
  153. }
  154. getContent = () => {
  155. const { children, onClick, imgAttr, src, srcSet, alt } = this.props;
  156. const { isImgExist } = this.state;
  157. let content = children;
  158. const clickable = onClick !== noop;
  159. const isImg = src && isImgExist;
  160. const a11yFocusProps = {
  161. tabIndex: 0,
  162. onKeyDown: this.handleKeyDown,
  163. onFocus: this.handleFocusVisible,
  164. onBlur: this.handleBlur,
  165. };
  166. if (isImg) {
  167. const finalAlt = clickable ? `clickable Avatar: ${alt}` : alt;
  168. const imgBasicProps = {
  169. src,
  170. srcSet,
  171. onError: this.handleError,
  172. ...imgAttr,
  173. className: cls({
  174. [`${prefixCls}-no-focus-visible`]: clickable,
  175. }),
  176. };
  177. const imgProps = clickable ? { ...imgBasicProps, ...a11yFocusProps } : imgBasicProps;
  178. content = (
  179. <img alt={finalAlt} {...imgProps}/>
  180. );
  181. } else if (typeof children === 'string') {
  182. const tempAlt = alt ?? children;
  183. const finalAlt = clickable ? `clickable Avatar: ${tempAlt}` : tempAlt;
  184. const props = {
  185. role: 'img',
  186. 'aria-label': finalAlt,
  187. className: cls(`${prefixCls}-label`,
  188. {
  189. [`${prefixCls}-no-focus-visible`]: clickable,
  190. }
  191. ),
  192. };
  193. const finalProps = clickable ? { ...props, ...a11yFocusProps } : props;
  194. const stringStyle: React.CSSProperties = {
  195. transform: `scale(${this.state.scale})`,
  196. };
  197. content = (
  198. <span className={`${prefixCls}-content`} style={stringStyle}>
  199. <span {...finalProps} x-semi-prop="children">{children}</span>
  200. </span>
  201. );
  202. }
  203. return content;
  204. }
  205. renderBottomSlot = ()=>{
  206. if (!this.props.bottomSlot) {
  207. return null;
  208. }
  209. if (this.props.bottomSlot.render) {
  210. return this.props.bottomSlot.render();
  211. }
  212. const renderContent = this.props.bottomSlot.render ??(()=>{
  213. return <span className={cls(`${prefixCls}-bottom_slot-shape_${this.props.bottomSlot.shape}`, `${prefixCls}-bottom_slot-shape_${this.props.bottomSlot.shape}-${this.props.size}`)}>
  214. {this.props.bottomSlot.content}
  215. </span>;
  216. });
  217. return <div className={cls([`${prefixCls}-bottom_slot`])}>
  218. {renderContent()}
  219. </div>;
  220. }
  221. renderTopSlot = ()=> {
  222. return <div className={cls([`${prefixCls}-top_slot-wrapper`, {
  223. [`${prefixCls}-animated`]: this.props.contentMotion,
  224. }])}>
  225. <div className={cls([`${prefixCls}-top_slot-bg`, `${prefixCls}-top_slot-bg-${this.props.size}`])}>
  226. <div className={cls([`${prefixCls}-top_slot-bg-svg`, `${prefixCls}-top_slot-bg-svg-${this.props.size}`])}>
  227. <TopSlotSvg gradientStart={this.props.topSlot.gradientStart ?? "#FF1764"} gradientEnd={this.props.topSlot.gradientStart ?? "#ED3494"}/>
  228. </div>
  229. </div>
  230. <div className={cls([`${prefixCls}-top_slot`])}>
  231. <div
  232. className={cls([`${prefixCls}-top_slot-content`, `${prefixCls}-top_slot-content-${this.props.size}`])}>{this.props.topSlot.content}</div>
  233. </div>
  234. </div>;
  235. }
  236. render() {
  237. const {
  238. shape,
  239. children,
  240. size,
  241. color,
  242. className,
  243. hoverMask,
  244. onClick,
  245. imgAttr,
  246. src,
  247. srcSet,
  248. style,
  249. alt,
  250. gap,
  251. bottomSlot,
  252. topSlot,
  253. border,
  254. contentMotion,
  255. ...others
  256. } = this.props;
  257. const { isImgExist, hoverContent, focusVisible } = this.state;
  258. const isImg = src && isImgExist;
  259. const avatarCls = cls(
  260. prefixCls,
  261. {
  262. [`${prefixCls}-${shape}`]: shape,
  263. [`${prefixCls}-${size}`]: size,
  264. [`${prefixCls}-${color}`]: color && !isImg,
  265. [`${prefixCls}-img`]: isImg,
  266. [`${prefixCls}-focus`]: focusVisible,
  267. [`${prefixCls}-animated`]: contentMotion,
  268. },
  269. className
  270. );
  271. const hoverRender = hoverContent ? (
  272. <div className={`${prefixCls}-hover`} x-semi-prop="hoverContent">{hoverContent}</div>) : null;
  273. let avatar = <span
  274. {...(others as any)}
  275. style={style}
  276. className={avatarCls}
  277. onClick={onClick as any}
  278. onMouseEnter={this.onEnter as any}
  279. onMouseLeave={this.onLeave as any}
  280. role='listitem'
  281. ref={this.avatarRef}>
  282. {this.getContent()}
  283. {hoverRender}
  284. </span>;
  285. if (border) {
  286. avatar = <>
  287. {avatar}
  288. <span className={cls([
  289. `${prefixCls}-border`,
  290. `${prefixCls}-border-${size}`,
  291. {
  292. [`${prefixCls}-${shape}`]: shape,
  293. },
  294. ])}>
  295. </span>
  296. {
  297. this.props.borderMotion && <span className={cls([
  298. `${prefixCls}-border`,
  299. `${prefixCls}-border-${size}`,
  300. {
  301. [`${prefixCls}-${shape}`]: shape,
  302. [`${prefixCls}-border-animated`]: this.props.borderMotion,
  303. },
  304. ])}/>
  305. }
  306. </>;
  307. }
  308. if (bottomSlot || topSlot || border) {
  309. return <span className={cls([`${prefixCls}-wrapper`])}>
  310. {avatar}
  311. {topSlot && ["small", "default", "medium", "large","extra-large"].includes(size) && shape === "circle" && this.renderTopSlot()}
  312. {bottomSlot && ["small", "default", "medium", "large","extra-large"].includes(size) && this.renderBottomSlot()}
  313. </span>;
  314. } else {
  315. return avatar;
  316. }
  317. }
  318. }
  319. Avatar.elementType = 'Avatar';