feat(BackTop): new component (#11236)

* feat(BackTop): new component

* perf(BackTop): perf BackTop component

* perf(BackTop): improve code

* perf: improve type
This commit is contained in:
Gavin 2022-11-19 10:14:39 +08:00 committed by GitHub
parent e19e982292
commit 5cf4322143
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 932 additions and 0 deletions

View File

@ -0,0 +1,124 @@
import {
ref,
computed,
Teleport,
nextTick,
onMounted,
defineComponent,
type PropType,
type TeleportProps,
type ExtractPropTypes,
} from 'vue';
// Utils
import {
isObject,
inBrowser,
getScrollTop,
createNamespace,
makeNumericProp,
} from '../utils';
import { throttle } from '../lazyload/vue-lazyload/util';
// Composables
import { useEventListener, getScrollParent } from '@vant/use';
// Components
import { Icon } from '../icon';
const [name, bem] = createNamespace('back-top');
export const backTopProps = {
right: makeNumericProp(30),
bottom: makeNumericProp(40),
target: [String, Object] as PropType<TeleportProps['to']>,
visibilityHeight: makeNumericProp(200),
teleport: {
type: [String, Object] as PropType<TeleportProps['to']>,
default: 'body',
},
};
export type BackTopProps = ExtractPropTypes<typeof backTopProps>;
export default defineComponent({
name,
props: backTopProps,
emits: ['click'],
setup(props, { emit, slots }) {
const show = ref(false);
const scrollParent = ref<Window | HTMLElement>();
const backTopEl = ref<HTMLElement | null>(null);
let target: Window | HTMLElement;
const backTopStyle = computed(() => ({
right: `${props.right}px`,
bottom: `${props.bottom}px`,
}));
const onClick = (event: MouseEvent) => {
emit('click', event);
target.scrollTo({
top: 0,
behavior: 'smooth',
});
};
const scroll = () => {
show.value = getScrollTop(target) >= props.visibilityHeight;
};
const throttleScroll = throttle(scroll, 300);
const getTarget = () => {
const { target } = props;
if (typeof target === 'string') {
const el = document.querySelector(props.target as string);
if (!el) {
throw Error('[Vant] BackTop: target element is not found.');
}
return el as HTMLElement;
}
if (isObject(target)) return target;
throw Error(
'[Vant] BackTop: type of prop "target" should be a selector or an element object'
);
};
useEventListener('scroll', throttleScroll, { target: scrollParent });
onMounted(() => {
nextTick(() => {
if (inBrowser) {
scrollParent.value = document.documentElement;
target = props.target
? (getTarget() as typeof target)
: (getScrollParent(backTopEl.value!) as typeof target);
scrollParent.value = target as typeof target;
}
});
});
return () => {
const Content = (
<div
ref="backTopEl"
class={bem({ active: show.value })}
style={backTopStyle.value}
onClick={onClick}
>
{slots.default ? slots.default() : <Icon name="back-top" />}
</div>
);
if (props.teleport) {
return <Teleport to={props.teleport}>{Content}</Teleport>;
}
return Content;
};
},
});

View File

@ -0,0 +1,115 @@
# BackTop
### Intro
A button to back to top.
### Install
Register component globally via `app.use`, refer to [Component Registration](#/en-US/advanced-usage#zu-jian-zhu-ce) for more registration ways.
```js
import { createApp } from 'vue';
import { BackTop } from 'vant';
const app = createApp();
app.use(BackTop);
```
## Usage
### Basic Usage
```html
<van-cell v-for="item in list" :key="item" :title="item" />
<van-back-top />
```
```js
export default {
setup() {
const list = [...Array(50).keys()];
},
};
```
### Customizations
```html
<van-cell v-for="item in list" :key="item" :title="item" />
<van-back-top>
<div class="custom" style="">Customizations</div>
</van-back-top>
```
```js
export default {
setup() {
const list = [...Array(50).keys()];
},
};
```
```css
.custom {
width: 200px;
line-height: 40px;
text-align: center;
}
```
### Target to be listened to.
```html
<div class="container">
<van-cell v-for="item in list" :key="item" :title="item" />
<van-back-top target=".container" bottom="100" right="30" />
</div>
```
```js
export default {
setup() {
const list = [...Array(50).keys()];
},
};
```
```css
.container {
height: 300px;
overflow: auto;
}
```
## API
### Props
| 参数 | 说明 | 类型 | 默认值 |
| --- | --- | --- | --- |
| target | Can be a `selector` or `HTMLElement` | _string \| HTMLElement_ | - |
| right | Right distance of the page | _number \| string_ | `30` |
| bottom | Bottom distance of the page | _number \| string_ | `40` |
| visibility-height | The button will not show until the scroll height reaches this value | _number_ | `200` |
| teleport | Specifies a target element where BackTop will be mounted | _string \| Element_ | `body` |
### Slots
| 名称 | 说明 |
| ------- | ------------------------- |
| default | customize default content |
## 主题定制
### 样式变量
组件提供了下列 CSS 变量,可用于自定义样式,使用方法请参考 [ConfigProvider 组件](#/zh-CN/config-provider)。
| 名称 | 默认值 | 描述 |
| ------------------------- | ----------------- | ---- |
| --van-back-top-size | _40px_ | - |
| --van-back-top-icon-size | _20px_ | - |
| --van-back-top-text-color | _#fff_ | - |
| --van-back-top-background | _var(--van-blue)_ | - |

View File

@ -0,0 +1,119 @@
# BackTop 回到顶部
### 介绍
返回页面顶部的操作按钮。
### 引入
通过以下方式来全局注册组件,更多注册方式请参考[组件注册](#/zh-CN/advanced-usage#zu-jian-zhu-ce)。
```js
import { createApp } from 'vue';
import { BackTop } from 'vant';
const app = createApp();
app.use(BackTop);
```
## 代码演示
### 基础用法
通过滚动 Demo 页面查看右下角按钮。
```html
<van-cell v-for="item in list" :key="item" :title="item" />
<van-back-top />
```
```js
export default {
setup() {
const list = [...Array(50).keys()];
},
};
```
### 自定义内容
```html
<van-cell v-for="item in list" :key="item" :title="item" />
<van-back-top>
<div class="custom" style="">自定义内容</div>
</van-back-top>
```
```js
export default {
setup() {
const list = [...Array(50).keys()];
},
};
```
```css
.custom {
width: 200px;
line-height: 40px;
text-align: center;
}
```
### 设置监听目标
可以通过设置 `target` 控制监听哪个元素触发 Back Top。
```html
<div class="container">
<van-cell v-for="item in list" :key="item" :title="item" />
<van-back-top target=".container" bottom="100" right="30" />
</div>
```
```js
export default {
setup() {
const list = [...Array(50).keys()];
},
};
```
```css
.container {
height: 300px;
overflow: auto;
}
```
## API
### Props
| 参数 | 说明 | 类型 | 默认值 |
| --- | --- | --- | --- |
| target | 触发滚动的目标对象,支持`selector``HTMLElement` | _string \| HTMLElement_ | - |
| right | 距离页面右侧的距离 | _number \| string_ | `30` |
| bottom | 距离页面底部的距离 | _number \| string_ | `40` |
| visibility-height | 滚动高度达到此参数值才显示 | _number_ | `200` |
| teleport | 指定挂载的节点,等同于 Teleport 组件的 [to 属性](https://v3.cn.vuejs.org/api/built-in-components.html#teleport) | _string \| Element_ | `body` |
### Slots
| 名称 | 说明 |
| ------- | ------------------ |
| default | 自定义按钮显示内容 |
## 主题定制
### 样式变量
组件提供了下列 CSS 变量,可用于自定义样式,使用方法请参考 [ConfigProvider 组件](#/zh-CN/config-provider)。
| 名称 | 默认值 | 描述 |
| ------------------------- | ----------------- | ---- |
| --van-back-top-size | _40px_ | - |
| --van-back-top-icon-size | _20px_ | - |
| --van-back-top-text-color | _#fff_ | - |
| --van-back-top-background | _var(--van-blue)_ | - |

View File

@ -0,0 +1,59 @@
<script setup lang="ts">
import { ref } from 'vue';
import VanBackTop from '..';
import VanTabs from '../../tabs';
import VanTab from '../../tab';
import VanCell from '../../cell';
import { useTranslate } from '../../../docs/site';
const t = useTranslate({
'zh-CN': {
type1: '基础用法',
type2: '自定义内容',
type3: '设置监听目标',
},
'en-US': {
type1: 'Basic Usage',
type2: 'Customizations',
type3: 'Target',
},
});
const list = [...Array(50).keys()];
const targetEl = ref<HTMLElement>();
</script>
<template>
<van-tabs>
<van-tab :title="t('type1')">
<van-cell v-for="item in list" :key="item" :title="item" />
<van-back-top />
</van-tab>
<van-tab :title="t('type2')">
<van-cell v-for="item in list" :key="item" :title="item" />
<van-back-top bottom="100" right="30">
<div class="custom" style="">{{ t('type2') }}</div>
</van-back-top>
</van-tab>
<van-tab :title="t('type3')">
<div class="back-top--test" ref="targetEl">
<van-cell v-for="item in list" :key="item" :title="item" />
<van-back-top :target="targetEl" bottom="150" right="30" />
</div>
</van-tab>
</van-tabs>
</template>
<style lang="less">
.back-top--test {
height: 400px;
overflow: auto;
}
.custom {
width: 200px;
line-height: 40px;
text-align: center;
}
</style>

View File

@ -0,0 +1,35 @@
:root {
--van-back-top-size: 40px;
--van-back-top-icon-size: 20px;
--van-back-top-text-color: #fff;
--van-back-top-background: var(--van-blue);
}
.van-back-top {
position: fixed;
display: flex;
align-items: center;
justify-content: center;
min-width: var(--van-back-top-size);
min-height: var(--van-back-top-size);
cursor: pointer;
color: var(--van-back-top-text-color);
border-radius: var(--van-radius-max);
box-shadow: 0 2px 8px 0px rgba(0, 0, 0, 0.12);
transform: scale(0);
transition: 0.3s cubic-bezier(0.25, 0.8, 0.5, 1);
background-color: var(--van-back-top-background);
&:active {
opacity: 0.6;
}
&--active {
transform: scale(1);
}
.van-icon {
font-size: var(--van-back-top-icon-size);
font-weight: var(--van-font-bold);
}
}

View File

@ -0,0 +1,15 @@
import { withInstall } from '../utils';
import _BackTop from './BackTop';
export const BackTop = withInstall(_BackTop);
export default BackTop;
export { backTopProps } from './BackTop';
export type { BackTopProps } from './BackTop';
export type { BackTopThemeVars } from './types';
declare module 'vue' {
export interface GlobalComponents {
VanBackTop: typeof BackTop;
}
}

View File

@ -0,0 +1,426 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`should render demo and match snapshot 1`] = `
<div class="van-tabs van-tabs--line">
<div class="van-tabs__wrap">
<div role="tablist"
class="van-tabs__nav van-tabs__nav--line"
aria-orientation="horizontal"
>
<div id="van-tabs-0"
role="tab"
class="van-tab van-tab--line van-tab--active"
tabindex="0"
aria-selected="true"
aria-controls="van-tab"
>
<span class="van-tab__text van-tab__text--ellipsis">
Basic Usage
</span>
</div>
<div id="van-tabs-1"
role="tab"
class="van-tab van-tab--line"
tabindex="-1"
aria-selected="false"
aria-controls="van-tab"
>
<span class="van-tab__text van-tab__text--ellipsis">
Customizations
</span>
</div>
<div id="van-tabs-2"
role="tab"
class="van-tab van-tab--line"
tabindex="-1"
aria-selected="false"
aria-controls="van-tab"
>
<span class="van-tab__text van-tab__text--ellipsis">
Target
</span>
</div>
<div class="van-tabs__line"
style="transform: translateX(50px) translateX(-50%);"
>
</div>
</div>
</div>
<div class="van-tabs__content">
<div id="van-tab"
role="tabpanel"
class="van-tab__panel"
tabindex="0"
aria-labelledby="van-tabs-0"
style
>
<div class="van-cell">
<div class="van-cell__title">
<span>
0
</span>
</div>
</div>
<div class="van-cell">
<div class="van-cell__title">
<span>
1
</span>
</div>
</div>
<div class="van-cell">
<div class="van-cell__title">
<span>
2
</span>
</div>
</div>
<div class="van-cell">
<div class="van-cell__title">
<span>
3
</span>
</div>
</div>
<div class="van-cell">
<div class="van-cell__title">
<span>
4
</span>
</div>
</div>
<div class="van-cell">
<div class="van-cell__title">
<span>
5
</span>
</div>
</div>
<div class="van-cell">
<div class="van-cell__title">
<span>
6
</span>
</div>
</div>
<div class="van-cell">
<div class="van-cell__title">
<span>
7
</span>
</div>
</div>
<div class="van-cell">
<div class="van-cell__title">
<span>
8
</span>
</div>
</div>
<div class="van-cell">
<div class="van-cell__title">
<span>
9
</span>
</div>
</div>
<div class="van-cell">
<div class="van-cell__title">
<span>
10
</span>
</div>
</div>
<div class="van-cell">
<div class="van-cell__title">
<span>
11
</span>
</div>
</div>
<div class="van-cell">
<div class="van-cell__title">
<span>
12
</span>
</div>
</div>
<div class="van-cell">
<div class="van-cell__title">
<span>
13
</span>
</div>
</div>
<div class="van-cell">
<div class="van-cell__title">
<span>
14
</span>
</div>
</div>
<div class="van-cell">
<div class="van-cell__title">
<span>
15
</span>
</div>
</div>
<div class="van-cell">
<div class="van-cell__title">
<span>
16
</span>
</div>
</div>
<div class="van-cell">
<div class="van-cell__title">
<span>
17
</span>
</div>
</div>
<div class="van-cell">
<div class="van-cell__title">
<span>
18
</span>
</div>
</div>
<div class="van-cell">
<div class="van-cell__title">
<span>
19
</span>
</div>
</div>
<div class="van-cell">
<div class="van-cell__title">
<span>
20
</span>
</div>
</div>
<div class="van-cell">
<div class="van-cell__title">
<span>
21
</span>
</div>
</div>
<div class="van-cell">
<div class="van-cell__title">
<span>
22
</span>
</div>
</div>
<div class="van-cell">
<div class="van-cell__title">
<span>
23
</span>
</div>
</div>
<div class="van-cell">
<div class="van-cell__title">
<span>
24
</span>
</div>
</div>
<div class="van-cell">
<div class="van-cell__title">
<span>
25
</span>
</div>
</div>
<div class="van-cell">
<div class="van-cell__title">
<span>
26
</span>
</div>
</div>
<div class="van-cell">
<div class="van-cell__title">
<span>
27
</span>
</div>
</div>
<div class="van-cell">
<div class="van-cell__title">
<span>
28
</span>
</div>
</div>
<div class="van-cell">
<div class="van-cell__title">
<span>
29
</span>
</div>
</div>
<div class="van-cell">
<div class="van-cell__title">
<span>
30
</span>
</div>
</div>
<div class="van-cell">
<div class="van-cell__title">
<span>
31
</span>
</div>
</div>
<div class="van-cell">
<div class="van-cell__title">
<span>
32
</span>
</div>
</div>
<div class="van-cell">
<div class="van-cell__title">
<span>
33
</span>
</div>
</div>
<div class="van-cell">
<div class="van-cell__title">
<span>
34
</span>
</div>
</div>
<div class="van-cell">
<div class="van-cell__title">
<span>
35
</span>
</div>
</div>
<div class="van-cell">
<div class="van-cell__title">
<span>
36
</span>
</div>
</div>
<div class="van-cell">
<div class="van-cell__title">
<span>
37
</span>
</div>
</div>
<div class="van-cell">
<div class="van-cell__title">
<span>
38
</span>
</div>
</div>
<div class="van-cell">
<div class="van-cell__title">
<span>
39
</span>
</div>
</div>
<div class="van-cell">
<div class="van-cell__title">
<span>
40
</span>
</div>
</div>
<div class="van-cell">
<div class="van-cell__title">
<span>
41
</span>
</div>
</div>
<div class="van-cell">
<div class="van-cell__title">
<span>
42
</span>
</div>
</div>
<div class="van-cell">
<div class="van-cell__title">
<span>
43
</span>
</div>
</div>
<div class="van-cell">
<div class="van-cell__title">
<span>
44
</span>
</div>
</div>
<div class="van-cell">
<div class="van-cell__title">
<span>
45
</span>
</div>
</div>
<div class="van-cell">
<div class="van-cell__title">
<span>
46
</span>
</div>
</div>
<div class="van-cell">
<div class="van-cell__title">
<span>
47
</span>
</div>
</div>
<div class="van-cell">
<div class="van-cell__title">
<span>
48
</span>
</div>
</div>
<div class="van-cell">
<div class="van-cell__title">
<span>
49
</span>
</div>
</div>
</div>
<div id="van-tab"
role="tabpanel"
class="van-tab__panel"
tabindex="-1"
aria-labelledby="van-tabs-1"
style="display: none;"
>
</div>
<div id="van-tab"
role="tabpanel"
class="van-tab__panel"
tabindex="-1"
aria-labelledby="van-tabs-2"
style="display: none;"
>
</div>
</div>
</div>
`;

View File

@ -0,0 +1,4 @@
import Demo from '../demo/index.vue';
import { snapshotDemo } from '../../../test/demo';
snapshotDemo(Demo);

View File

@ -0,0 +1,21 @@
import BackTop from '..';
import { mount } from '../../../test';
test('test position prop', async () => {
mount(BackTop, {
props: {
right: 30,
bottom: 100,
},
});
const backTopEl = document.querySelector('.van-back-top') as HTMLDivElement;
expect(backTopEl.style.right).toBe('30px');
expect(backTopEl.style.bottom).toBe('100px');
});
test('test backTop event', async () => {
const wrapper = mount(BackTop);
await wrapper.trigger('click');
expect(wrapper.emitted()).toBeDefined();
});

View File

@ -0,0 +1,6 @@
export type BackTopThemeVars = {
backTopSize?: string;
backTopIconSize?: string;
backTopTextColor?: string;
backTopBackground?: string;
};

View File

@ -384,6 +384,10 @@ location.href = location.href.replace('youzan.github.io', 'vant-ui.github.io');
path: 'tree-select',
title: 'TreeSelect 分类选择',
},
{
path: 'back-top',
title: 'BackTop 回到顶部',
},
],
},
{
@ -795,6 +799,10 @@ location.href = location.href.replace('youzan.github.io', 'vant-ui.github.io');
path: 'tree-select',
title: 'TreeSelect',
},
{
path: 'back-top',
title: 'BackTop',
},
],
},
{