import { Component, createRef } from 'react'; import cx from 'classnames'; import Icon, { IconType } from '../icon'; const NO_STYLE = {}; const HIDDEN_STYLE = { opacity: 0, }; export interface IAvatarProps { shape: 'circle' | 'square'; size: 'small' | 'default' | 'large' | number; icon?: IconType; src?: string; children?: string; bordered: boolean; style?: React.CSSProperties; className?: string; } export interface IAvatarState { textScale: number; textReady: boolean; prevChildren?: string; } export class Avatar extends Component { static defaultProps = { shape: 'circle', size: 'default', bordered: false, }; textNodeRef = createRef(); avatarNodeRef = createRef(); state = { textScale: 1, textReady: false, }; render() { const { size, shape, src, icon, children, bordered, style, className } = this.props; const useImage = !!src; const useString = !!children; const cls = cx('zent-avatar', className, { 'zent-avatar--size-large': size === 'large', 'zent-avatar--size-default': size === 'default', 'zent-avatar--size-small': size === 'small', 'zent-avatar--shape-circle': shape === 'circle', 'zent-avatar--shape-square': shape === 'square', 'zent-avatar--type-icon': !!icon, 'zent-avatar--type-image': useImage, 'zent-avatar--type-string': useString, 'zent-avatar--bordered': bordered, }); if (useImage) { return ( avatar ); } if (icon) { return ( ); } const { textScale, textReady } = this.state; const textNode = this.textNodeRef.current; let textStyle = NO_STYLE; if (!textReady || !textNode) { textStyle = HIDDEN_STYLE; } else if (textScale === 1) { textStyle = NO_STYLE; } else { const textTransformString = `scale(${textScale})`; textStyle = { msTransform: textTransformString, WebkitTransform: textTransformString, MozTransform: textTransformString, transform: textTransformString, position: 'absolute', display: 'inline-block', left: `calc(50% - ${Math.floor(textNode.offsetWidth / 2)}px)`, }; } const avatarStyle = typeof size === 'number' ? { width: `${size}px`, height: `${size}px`, lineHeight: `${size}px`, ...style, } : style; return ( {children} ); } componentDidMount() { this.updateTextScale(); } static getDerivedStateFromProps( { children }: IAvatarProps, { prevChildren }: IAvatarState ): Partial | null { if (children !== prevChildren) { return { textReady: false, prevChildren: children, }; } return null; } componentDidUpdate(prevProps: IAvatarProps) { if (prevProps.children !== this.props.children) { this.updateTextScale(); } } updateTextScale() { const { children } = this.props; if (children) { const scale = fitText( this.avatarNodeRef.current, this.textNodeRef.current ); this.setState({ textScale: scale, textReady: true, }); } } } function fitText( containerNode: HTMLSpanElement | null, textNode: HTMLSpanElement | null ) { if (!containerNode || !textNode) { return 1; } // When using with transforms, getBoundingClientRect().width and offsetWidth // returns different results. // https://developer.mozilla.org/en-US/docs/Web/API/CSS_Object_Model/Determining_the_dimensions_of_elements const containerWidth = containerNode.getBoundingClientRect().width; const textWidth = textNode.offsetWidth; // Leave some space const visualWidth = containerWidth - 6; if (visualWidth > textWidth) { return 1; } return visualWidth / textWidth; } export default Avatar;