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