refactor(v3/avatar): refactor using composition api (#4052)
* refactor(avatar): refactor using composition api * refactor: update props definepull/4060/head
							parent
							
								
									184957e3c7
								
							
						
					
					
						commit
						4d178debd7
					
				| 
						 | 
				
			
			@ -1,170 +1,175 @@
 | 
			
		|||
import { tuple, VueNode } from '../_util/type';
 | 
			
		||||
import { CSSProperties, defineComponent, inject, nextTick, PropType } from 'vue';
 | 
			
		||||
import {
 | 
			
		||||
  CSSProperties,
 | 
			
		||||
  defineComponent,
 | 
			
		||||
  ExtractPropTypes,
 | 
			
		||||
  inject,
 | 
			
		||||
  nextTick,
 | 
			
		||||
  onMounted,
 | 
			
		||||
  onUpdated,
 | 
			
		||||
  PropType,
 | 
			
		||||
  ref,
 | 
			
		||||
  watch,
 | 
			
		||||
} from 'vue';
 | 
			
		||||
import { defaultConfigProvider } from '../config-provider';
 | 
			
		||||
import { getComponent } from '../_util/props-util';
 | 
			
		||||
import { getPropsSlot } from '../_util/props-util';
 | 
			
		||||
import PropTypes from '../_util/vue-types';
 | 
			
		||||
 | 
			
		||||
export default defineComponent({
 | 
			
		||||
const avatarProps = {
 | 
			
		||||
  prefixCls: PropTypes.string,
 | 
			
		||||
  shape: PropTypes.oneOf(tuple('circle', 'square')),
 | 
			
		||||
  size: {
 | 
			
		||||
    type: [Number, String] as PropType<'large' | 'small' | 'default' | number>,
 | 
			
		||||
    default: 'default',
 | 
			
		||||
  },
 | 
			
		||||
  src: PropTypes.string,
 | 
			
		||||
  /** Srcset of image avatar */
 | 
			
		||||
  srcset: PropTypes.string,
 | 
			
		||||
  icon: PropTypes.VNodeChild,
 | 
			
		||||
  alt: PropTypes.string,
 | 
			
		||||
  loadError: {
 | 
			
		||||
    type: Function as PropType<() => boolean>,
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export type AvatarProps = Partial<ExtractPropTypes<typeof avatarProps>>;
 | 
			
		||||
 | 
			
		||||
const Avatar = defineComponent({
 | 
			
		||||
  name: 'AAvatar',
 | 
			
		||||
  props: {
 | 
			
		||||
    prefixCls: PropTypes.string,
 | 
			
		||||
    shape: PropTypes.oneOf(tuple('circle', 'square')),
 | 
			
		||||
    size: {
 | 
			
		||||
      type: [Number, String] as PropType<'large' | 'small' | 'default' | number>,
 | 
			
		||||
      default: 'default',
 | 
			
		||||
    },
 | 
			
		||||
    src: PropTypes.string,
 | 
			
		||||
    /** Srcset of image avatar */
 | 
			
		||||
    srcset: PropTypes.string,
 | 
			
		||||
    /** @deprecated please use `srcset` instead `srcSet` */
 | 
			
		||||
    srcSet: PropTypes.string,
 | 
			
		||||
    icon: PropTypes.VNodeChild,
 | 
			
		||||
    alt: PropTypes.string,
 | 
			
		||||
    loadError: {
 | 
			
		||||
      type: Function as PropType<() => boolean>,
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  setup() {
 | 
			
		||||
    return {
 | 
			
		||||
      configProvider: inject('configProvider', defaultConfigProvider),
 | 
			
		||||
    };
 | 
			
		||||
  },
 | 
			
		||||
  data() {
 | 
			
		||||
    return {
 | 
			
		||||
      isImgExist: true,
 | 
			
		||||
      isMounted: false,
 | 
			
		||||
      scale: 1,
 | 
			
		||||
      lastChildrenWidth: undefined,
 | 
			
		||||
      lastNodeWidth: undefined,
 | 
			
		||||
    };
 | 
			
		||||
  },
 | 
			
		||||
  watch: {
 | 
			
		||||
    src() {
 | 
			
		||||
      nextTick(() => {
 | 
			
		||||
        this.isImgExist = true;
 | 
			
		||||
        this.scale = 1;
 | 
			
		||||
        // force uodate for position
 | 
			
		||||
        this.$forceUpdate();
 | 
			
		||||
      });
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  mounted() {
 | 
			
		||||
    nextTick(() => {
 | 
			
		||||
      this.setScale();
 | 
			
		||||
      this.isMounted = true;
 | 
			
		||||
    });
 | 
			
		||||
  },
 | 
			
		||||
  updated() {
 | 
			
		||||
    nextTick(() => {
 | 
			
		||||
      this.setScale();
 | 
			
		||||
    });
 | 
			
		||||
  },
 | 
			
		||||
  methods: {
 | 
			
		||||
    setScale() {
 | 
			
		||||
      if (!this.$refs.avatarChildren || !this.$refs.avatarNode) {
 | 
			
		||||
  props: avatarProps,
 | 
			
		||||
  setup(props, { slots }) {
 | 
			
		||||
    const isImgExist = ref(true);
 | 
			
		||||
    const isMounted = ref(false);
 | 
			
		||||
    const scale = ref(1);
 | 
			
		||||
    const lastChildrenWidth = ref<number>(undefined);
 | 
			
		||||
    const lastNodeWidth = ref<number>(undefined);
 | 
			
		||||
 | 
			
		||||
    const avatarChildrenRef = ref<HTMLElement>(null);
 | 
			
		||||
    const avatarNodeRef = ref<HTMLElement>(null);
 | 
			
		||||
 | 
			
		||||
    const configProvider = inject('configProvider', defaultConfigProvider);
 | 
			
		||||
 | 
			
		||||
    const setScale = () => {
 | 
			
		||||
      if (!avatarChildrenRef.value || !avatarNodeRef.value) {
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
      const childrenWidth = (this.$refs.avatarChildren as HTMLElement).offsetWidth; // offsetWidth avoid affecting be transform scale
 | 
			
		||||
      const nodeWidth = (this.$refs.avatarNode as HTMLElement).offsetWidth;
 | 
			
		||||
      const childrenWidth = avatarChildrenRef.value.offsetWidth; // offsetWidth avoid affecting be transform scale
 | 
			
		||||
      const nodeWidth = avatarNodeRef.value.offsetWidth;
 | 
			
		||||
      // denominator is 0 is no meaning
 | 
			
		||||
      if (
 | 
			
		||||
        childrenWidth === 0 ||
 | 
			
		||||
        nodeWidth === 0 ||
 | 
			
		||||
        (this.lastChildrenWidth === childrenWidth && this.lastNodeWidth === nodeWidth)
 | 
			
		||||
        (lastChildrenWidth.value === childrenWidth && lastNodeWidth.value === nodeWidth)
 | 
			
		||||
      ) {
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
      this.lastChildrenWidth = childrenWidth;
 | 
			
		||||
      this.lastNodeWidth = nodeWidth;
 | 
			
		||||
      lastChildrenWidth.value = childrenWidth;
 | 
			
		||||
      lastNodeWidth.value = nodeWidth;
 | 
			
		||||
      // add 4px gap for each side to get better performance
 | 
			
		||||
      this.scale = nodeWidth - 8 < childrenWidth ? (nodeWidth - 8) / childrenWidth : 1;
 | 
			
		||||
    },
 | 
			
		||||
    handleImgLoadError() {
 | 
			
		||||
      const { loadError } = this.$props;
 | 
			
		||||
      const errorFlag = loadError ? loadError() : undefined;
 | 
			
		||||
      scale.value = nodeWidth - 8 < childrenWidth ? (nodeWidth - 8) / childrenWidth : 1;
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const handleImgLoadError = () => {
 | 
			
		||||
      const { loadError } = props;
 | 
			
		||||
      const errorFlag = loadError?.();
 | 
			
		||||
      if (errorFlag !== false) {
 | 
			
		||||
        this.isImgExist = false;
 | 
			
		||||
        isImgExist.value = false;
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  render() {
 | 
			
		||||
    const { prefixCls: customizePrefixCls, shape, size, src, alt, srcset, srcSet } = this.$props;
 | 
			
		||||
    const icon = getComponent(this, 'icon');
 | 
			
		||||
    const getPrefixCls = this.configProvider.getPrefixCls;
 | 
			
		||||
    const prefixCls = getPrefixCls('avatar', customizePrefixCls);
 | 
			
		||||
 | 
			
		||||
    const { isImgExist, scale, isMounted } = this.$data;
 | 
			
		||||
 | 
			
		||||
    const sizeCls = {
 | 
			
		||||
      [`${prefixCls}-lg`]: size === 'large',
 | 
			
		||||
      [`${prefixCls}-sm`]: size === 'small',
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const classString = {
 | 
			
		||||
      [prefixCls]: true,
 | 
			
		||||
      ...sizeCls,
 | 
			
		||||
      [`${prefixCls}-${shape}`]: shape,
 | 
			
		||||
      [`${prefixCls}-image`]: src && isImgExist,
 | 
			
		||||
      [`${prefixCls}-icon`]: icon,
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const sizeStyle: CSSProperties =
 | 
			
		||||
      typeof size === 'number'
 | 
			
		||||
        ? {
 | 
			
		||||
            width: `${size}px`,
 | 
			
		||||
            height: `${size}px`,
 | 
			
		||||
            lineHeight: `${size}px`,
 | 
			
		||||
            fontSize: icon ? `${size / 2}px` : '18px',
 | 
			
		||||
          }
 | 
			
		||||
        : {};
 | 
			
		||||
 | 
			
		||||
    let children: VueNode = this.$slots.default?.();
 | 
			
		||||
    if (src && isImgExist) {
 | 
			
		||||
      children = (
 | 
			
		||||
        <img src={src} srcset={srcset || srcSet} onError={this.handleImgLoadError} alt={alt} />
 | 
			
		||||
      );
 | 
			
		||||
    } else if (icon) {
 | 
			
		||||
      children = icon;
 | 
			
		||||
    } else {
 | 
			
		||||
      const childrenNode = this.$refs.avatarChildren;
 | 
			
		||||
      if (childrenNode || scale !== 1) {
 | 
			
		||||
        const transformString = `scale(${scale}) translateX(-50%)`;
 | 
			
		||||
        const childrenStyle: CSSProperties = {
 | 
			
		||||
          msTransform: transformString,
 | 
			
		||||
          WebkitTransform: transformString,
 | 
			
		||||
          transform: transformString,
 | 
			
		||||
        };
 | 
			
		||||
        const sizeChildrenStyle =
 | 
			
		||||
          typeof size === 'number'
 | 
			
		||||
            ? {
 | 
			
		||||
                lineHeight: `${size}px`,
 | 
			
		||||
              }
 | 
			
		||||
            : {};
 | 
			
		||||
        children = (
 | 
			
		||||
          <span
 | 
			
		||||
            class={`${prefixCls}-string`}
 | 
			
		||||
            ref="avatarChildren"
 | 
			
		||||
            style={{ ...sizeChildrenStyle, ...childrenStyle }}
 | 
			
		||||
          >
 | 
			
		||||
            {children}
 | 
			
		||||
          </span>
 | 
			
		||||
        );
 | 
			
		||||
      } else {
 | 
			
		||||
        const childrenStyle: CSSProperties = {};
 | 
			
		||||
        if (!isMounted) {
 | 
			
		||||
          childrenStyle.opacity = 0;
 | 
			
		||||
        }
 | 
			
		||||
        children = (
 | 
			
		||||
          <span class={`${prefixCls}-string`} ref="avatarChildren" style={{ opacity: 0 }}>
 | 
			
		||||
            {children}
 | 
			
		||||
          </span>
 | 
			
		||||
        );
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    return (
 | 
			
		||||
      <span ref="avatarNode" class={classString} style={sizeStyle}>
 | 
			
		||||
        {children}
 | 
			
		||||
      </span>
 | 
			
		||||
    watch(
 | 
			
		||||
      () => props.src,
 | 
			
		||||
      () => {
 | 
			
		||||
        nextTick(() => {
 | 
			
		||||
          isImgExist.value = true;
 | 
			
		||||
          scale.value = 1;
 | 
			
		||||
        });
 | 
			
		||||
      },
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    onMounted(() => {
 | 
			
		||||
      nextTick(() => {
 | 
			
		||||
        setScale();
 | 
			
		||||
        isMounted.value = true;
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    onUpdated(() => {
 | 
			
		||||
      nextTick(() => {
 | 
			
		||||
        setScale();
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    return () => {
 | 
			
		||||
      const { prefixCls: customizePrefixCls, shape, size, src, alt, srcset } = props;
 | 
			
		||||
      const icon = getPropsSlot(slots, props, 'icon');
 | 
			
		||||
      const getPrefixCls = configProvider.getPrefixCls;
 | 
			
		||||
      const prefixCls = getPrefixCls('avatar', customizePrefixCls);
 | 
			
		||||
 | 
			
		||||
      const classString = {
 | 
			
		||||
        [prefixCls]: true,
 | 
			
		||||
        [`${prefixCls}-lg`]: size === 'large',
 | 
			
		||||
        [`${prefixCls}-sm`]: size === 'small',
 | 
			
		||||
        [`${prefixCls}-${shape}`]: shape,
 | 
			
		||||
        [`${prefixCls}-image`]: src && isImgExist.value,
 | 
			
		||||
        [`${prefixCls}-icon`]: icon,
 | 
			
		||||
      };
 | 
			
		||||
 | 
			
		||||
      const sizeStyle: CSSProperties =
 | 
			
		||||
        typeof size === 'number'
 | 
			
		||||
          ? {
 | 
			
		||||
              width: `${size}px`,
 | 
			
		||||
              height: `${size}px`,
 | 
			
		||||
              lineHeight: `${size}px`,
 | 
			
		||||
              fontSize: icon ? `${size / 2}px` : '18px',
 | 
			
		||||
            }
 | 
			
		||||
          : {};
 | 
			
		||||
 | 
			
		||||
      let children: VueNode = slots.default?.();
 | 
			
		||||
      if (src && isImgExist.value) {
 | 
			
		||||
        children = <img src={src} srcset={srcset} onError={handleImgLoadError} alt={alt} />;
 | 
			
		||||
      } else if (icon) {
 | 
			
		||||
        children = icon;
 | 
			
		||||
      } else {
 | 
			
		||||
        const childrenNode = avatarChildrenRef.value;
 | 
			
		||||
 | 
			
		||||
        if (childrenNode || scale.value !== 1) {
 | 
			
		||||
          const transformString = `scale(${scale.value}) translateX(-50%)`;
 | 
			
		||||
          const childrenStyle: CSSProperties = {
 | 
			
		||||
            msTransform: transformString,
 | 
			
		||||
            WebkitTransform: transformString,
 | 
			
		||||
            transform: transformString,
 | 
			
		||||
          };
 | 
			
		||||
          const sizeChildrenStyle =
 | 
			
		||||
            typeof size === 'number'
 | 
			
		||||
              ? {
 | 
			
		||||
                  lineHeight: `${size}px`,
 | 
			
		||||
                }
 | 
			
		||||
              : {};
 | 
			
		||||
          children = (
 | 
			
		||||
            <span
 | 
			
		||||
              class={`${prefixCls}-string`}
 | 
			
		||||
              ref={avatarChildrenRef}
 | 
			
		||||
              style={{ ...sizeChildrenStyle, ...childrenStyle }}
 | 
			
		||||
            >
 | 
			
		||||
              {children}
 | 
			
		||||
            </span>
 | 
			
		||||
          );
 | 
			
		||||
        } else {
 | 
			
		||||
          children = (
 | 
			
		||||
            <span class={`${prefixCls}-string`} ref={avatarChildrenRef} style={{ opacity: 0 }}>
 | 
			
		||||
              {children}
 | 
			
		||||
            </span>
 | 
			
		||||
          );
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      return (
 | 
			
		||||
        <span ref={avatarNodeRef} class={classString} style={sizeStyle}>
 | 
			
		||||
          {children}
 | 
			
		||||
        </span>
 | 
			
		||||
      );
 | 
			
		||||
    };
 | 
			
		||||
  },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export default Avatar;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -41,16 +41,8 @@ describe('Avatar Render', () => {
 | 
			
		|||
      props: {
 | 
			
		||||
        src: 'http://error.url',
 | 
			
		||||
      },
 | 
			
		||||
      sync: false,
 | 
			
		||||
      attachTo: 'body',
 | 
			
		||||
    });
 | 
			
		||||
    wrapper.vm.setScale = jest.fn(() => {
 | 
			
		||||
      if (wrapper.vm.scale === 0.5) {
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
      wrapper.vm.scale = 0.5;
 | 
			
		||||
      wrapper.vm.$forceUpdate();
 | 
			
		||||
    });
 | 
			
		||||
    await asyncExpect(() => {
 | 
			
		||||
      wrapper.find('img').trigger('error');
 | 
			
		||||
    }, 0);
 | 
			
		||||
| 
						 | 
				
			
			@ -58,14 +50,7 @@ describe('Avatar Render', () => {
 | 
			
		|||
      const children = wrapper.findAll('.ant-avatar-string');
 | 
			
		||||
      expect(children.length).toBe(1);
 | 
			
		||||
      expect(children[0].text()).toBe('Fallback');
 | 
			
		||||
      expect(wrapper.vm.setScale).toHaveBeenCalled();
 | 
			
		||||
    });
 | 
			
		||||
    await asyncExpect(() => {
 | 
			
		||||
      expect(global.document.body.querySelector('.ant-avatar-string').style.transform).toContain(
 | 
			
		||||
        'scale(0.5)',
 | 
			
		||||
      );
 | 
			
		||||
      global.document.body.innerHTML = '';
 | 
			
		||||
    }, 1000);
 | 
			
		||||
  });
 | 
			
		||||
  it('should handle onError correctly', async () => {
 | 
			
		||||
    global.document.body.innerHTML = '';
 | 
			
		||||
| 
						 | 
				
			
			@ -91,17 +76,17 @@ describe('Avatar Render', () => {
 | 
			
		|||
      },
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const wrapper = mount(Foo, { sync: false, attachTo: 'body' });
 | 
			
		||||
    const wrapper = mount(Foo, { attachTo: 'body' });
 | 
			
		||||
    await asyncExpect(() => {
 | 
			
		||||
      // mock img load Error, since jsdom do not load resource by default
 | 
			
		||||
      // https://github.com/jsdom/jsdom/issues/1816
 | 
			
		||||
      wrapper.find('img').trigger('error');
 | 
			
		||||
    }, 0);
 | 
			
		||||
    await asyncExpect(() => {
 | 
			
		||||
      expect(wrapper.findComponent({ name: 'AAvatar' }).vm.isImgExist).toBe(true);
 | 
			
		||||
      expect(wrapper.find('img')).not.toBeNull();
 | 
			
		||||
    }, 0);
 | 
			
		||||
    await asyncExpect(() => {
 | 
			
		||||
      expect(global.document.body.querySelector('img').getAttribute('src')).toBe(LOAD_SUCCESS_SRC);
 | 
			
		||||
      expect(wrapper.find('img').attributes('src')).toBe(LOAD_SUCCESS_SRC);
 | 
			
		||||
    }, 0);
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -126,9 +111,8 @@ describe('Avatar Render', () => {
 | 
			
		|||
    await asyncExpect(() => {
 | 
			
		||||
      wrapper.find('img').trigger('error');
 | 
			
		||||
    }, 0);
 | 
			
		||||
 | 
			
		||||
    await asyncExpect(() => {
 | 
			
		||||
      expect(wrapper.findComponent({ name: 'AAvatar' }).vm.isImgExist).toBe(false);
 | 
			
		||||
      expect(wrapper.findComponent({ name: 'AAvatar' }).findAll('img').length).toBe(0);
 | 
			
		||||
      expect(wrapper.findAll('.ant-avatar-string').length).toBe(1);
 | 
			
		||||
    }, 0);
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -136,7 +120,7 @@ describe('Avatar Render', () => {
 | 
			
		|||
      wrapper.vm.src = LOAD_SUCCESS_SRC;
 | 
			
		||||
    });
 | 
			
		||||
    await asyncExpect(() => {
 | 
			
		||||
      expect(wrapper.findComponent({ name: 'AAvatar' }).vm.isImgExist).toBe(true);
 | 
			
		||||
      expect(wrapper.findComponent({ name: 'AAvatar' }).findAll('img').length).toBe(1);
 | 
			
		||||
      expect(wrapper.findAll('.ant-avatar-image').length).toBe(1);
 | 
			
		||||
    }, 0);
 | 
			
		||||
  });
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,4 +1,6 @@
 | 
			
		|||
import Avatar from './Avatar';
 | 
			
		||||
import { withInstall } from '../_util/type';
 | 
			
		||||
 | 
			
		||||
export { AvatarProps } from './Avatar';
 | 
			
		||||
 | 
			
		||||
export default withInstall(Avatar);
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue