Files
vant/src/list/List.tsx
T

172 lines
4.0 KiB
TypeScript

import {
ref,
watch,
nextTick,
PropType,
onUpdated,
onMounted,
defineComponent,
} from 'vue';
// Utils
import { isHidden, truthProp, createNamespace } from '../utils';
// Composables
import { useRect, useScrollParent, useEventListener } from '@vant/use';
import { useExpose } from '../composables/use-expose';
import { useTabStatus } from '../composables/use-tab-status';
// Components
import { Loading } from '../loading';
const [name, bem, t] = createNamespace('list');
export type ListDirection = 'up' | 'down';
export default defineComponent({
name,
props: {
error: Boolean,
loading: Boolean,
finished: Boolean,
errorText: String,
loadingText: String,
finishedText: String,
immediateCheck: truthProp,
offset: {
type: [Number, String],
default: 300,
},
direction: {
type: String as PropType<ListDirection>,
default: 'down',
},
},
emits: ['load', 'update:error', 'update:loading'],
setup(props, { emit, slots }) {
// use sync innerLoading state to avoid repeated loading in some edge cases
const loading = ref(false);
const root = ref<HTMLElement>();
const placeholder = ref<HTMLElement>();
const tabStatus = useTabStatus();
const scrollParent = useScrollParent(root);
const check = () => {
nextTick(() => {
if (
loading.value ||
props.finished ||
props.error ||
// skip check when inside an inactive tab
tabStatus?.value === false
) {
return;
}
const { offset, direction } = props;
const scrollParentRect = useRect(scrollParent);
if (!scrollParentRect.height || isHidden(root)) {
return;
}
let isReachEdge = false;
const placeholderRect = useRect(placeholder);
if (direction === 'up') {
isReachEdge = scrollParentRect.top - placeholderRect.top <= offset;
} else {
isReachEdge =
placeholderRect.bottom - scrollParentRect.bottom <= offset;
}
if (isReachEdge) {
loading.value = true;
emit('update:loading', true);
emit('load');
}
});
};
const renderFinishedText = () => {
if (props.finished) {
const text = slots.finished ? slots.finished() : props.finishedText;
if (text) {
return <div class={bem('finished-text')}>{text}</div>;
}
}
};
const clickErrorText = () => {
emit('update:error', false);
check();
};
const renderErrorText = () => {
if (props.error) {
const text = slots.error ? slots.error() : props.errorText;
if (text) {
return (
<div class={bem('error-text')} onClick={clickErrorText}>
{text}
</div>
);
}
}
};
const renderLoading = () => {
if (loading.value && !props.finished) {
return (
<div class={bem('loading')}>
{slots.loading ? (
slots.loading()
) : (
<Loading class={bem('loading-icon')}>
{props.loadingText || t('loading')}
</Loading>
)}
</div>
);
}
};
watch(
[() => props.loading, () => props.finished, () => props.error],
check
);
onUpdated(() => {
loading.value = props.loading!;
});
onMounted(() => {
if (props.immediateCheck) {
check();
}
});
useExpose({ check });
useEventListener('scroll', check, { target: scrollParent });
return () => {
const Content = slots.default?.();
const Placeholder = <div ref={placeholder} class={bem('placeholder')} />;
return (
<div ref={root} role="feed" class={bem()} aria-busy={loading.value}>
{props.direction === 'down' ? Content : Placeholder}
{renderLoading()}
{renderFinishedText()}
{renderErrorText()}
{props.direction === 'up' ? Content : Placeholder}
</div>
);
};
},
});