diff --git a/packages/vant/src/back-top/BackTop.tsx b/packages/vant/src/back-top/BackTop.tsx new file mode 100644 index 000000000..ceaae592d --- /dev/null +++ b/packages/vant/src/back-top/BackTop.tsx @@ -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, + visibilityHeight: makeNumericProp(200), + teleport: { + type: [String, Object] as PropType, + default: 'body', + }, +}; + +export type BackTopProps = ExtractPropTypes; + +export default defineComponent({ + name, + + props: backTopProps, + + emits: ['click'], + + setup(props, { emit, slots }) { + const show = ref(false); + const scrollParent = ref(); + const backTopEl = ref(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 = ( +
+ {slots.default ? slots.default() : } +
+ ); + if (props.teleport) { + return {Content}; + } + return Content; + }; + }, +}); diff --git a/packages/vant/src/back-top/README.md b/packages/vant/src/back-top/README.md new file mode 100644 index 000000000..df1800e06 --- /dev/null +++ b/packages/vant/src/back-top/README.md @@ -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 + + + +``` + +```js +export default { + setup() { + const list = [...Array(50).keys()]; + }, +}; +``` + +### Customizations + +```html + + +
Customizations
+
+``` + +```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 +
+ + +
+``` + +```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)_ | - | diff --git a/packages/vant/src/back-top/README.zh-CN.md b/packages/vant/src/back-top/README.zh-CN.md new file mode 100644 index 000000000..64a2c1e20 --- /dev/null +++ b/packages/vant/src/back-top/README.zh-CN.md @@ -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 + + + +``` + +```js +export default { + setup() { + const list = [...Array(50).keys()]; + }, +}; +``` + +### 自定义内容 + +```html + + +
自定义内容
+
+``` + +```js +export default { + setup() { + const list = [...Array(50).keys()]; + }, +}; +``` + +```css +.custom { + width: 200px; + line-height: 40px; + text-align: center; +} +``` + +### 设置监听目标 + +可以通过设置 `target` 控制监听哪个元素触发 Back Top。 + +```html +
+ + +
+``` + +```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)_ | - | diff --git a/packages/vant/src/back-top/demo/index.vue b/packages/vant/src/back-top/demo/index.vue new file mode 100644 index 000000000..30a0237c0 --- /dev/null +++ b/packages/vant/src/back-top/demo/index.vue @@ -0,0 +1,59 @@ + + + + + diff --git a/packages/vant/src/back-top/index.less b/packages/vant/src/back-top/index.less new file mode 100644 index 000000000..3518f5484 --- /dev/null +++ b/packages/vant/src/back-top/index.less @@ -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); + } +} diff --git a/packages/vant/src/back-top/index.ts b/packages/vant/src/back-top/index.ts new file mode 100644 index 000000000..79d5066ea --- /dev/null +++ b/packages/vant/src/back-top/index.ts @@ -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; + } +} diff --git a/packages/vant/src/back-top/test/__snapshots__/demo.spec.ts.snap b/packages/vant/src/back-top/test/__snapshots__/demo.spec.ts.snap new file mode 100644 index 000000000..bdb41fa7c --- /dev/null +++ b/packages/vant/src/back-top/test/__snapshots__/demo.spec.ts.snap @@ -0,0 +1,426 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should render demo and match snapshot 1`] = ` +
+
+
+ + + +
+
+
+
+
+
+
+
+ + 0 + +
+
+
+
+ + 1 + +
+
+
+
+ + 2 + +
+
+
+
+ + 3 + +
+
+
+
+ + 4 + +
+
+
+
+ + 5 + +
+
+
+
+ + 6 + +
+
+
+
+ + 7 + +
+
+
+
+ + 8 + +
+
+
+
+ + 9 + +
+
+
+
+ + 10 + +
+
+
+
+ + 11 + +
+
+
+
+ + 12 + +
+
+
+
+ + 13 + +
+
+
+
+ + 14 + +
+
+
+
+ + 15 + +
+
+
+
+ + 16 + +
+
+
+
+ + 17 + +
+
+
+
+ + 18 + +
+
+
+
+ + 19 + +
+
+
+
+ + 20 + +
+
+
+
+ + 21 + +
+
+
+
+ + 22 + +
+
+
+
+ + 23 + +
+
+
+
+ + 24 + +
+
+
+
+ + 25 + +
+
+
+
+ + 26 + +
+
+
+
+ + 27 + +
+
+
+
+ + 28 + +
+
+
+
+ + 29 + +
+
+
+
+ + 30 + +
+
+
+
+ + 31 + +
+
+
+
+ + 32 + +
+
+
+
+ + 33 + +
+
+
+
+ + 34 + +
+
+
+
+ + 35 + +
+
+
+
+ + 36 + +
+
+
+
+ + 37 + +
+
+
+
+ + 38 + +
+
+
+
+ + 39 + +
+
+
+
+ + 40 + +
+
+
+
+ + 41 + +
+
+
+
+ + 42 + +
+
+
+
+ + 43 + +
+
+
+
+ + 44 + +
+
+
+
+ + 45 + +
+
+
+
+ + 46 + +
+
+
+
+ + 47 + +
+
+
+
+ + 48 + +
+
+
+
+ + 49 + +
+
+
+ + +
+
+`; diff --git a/packages/vant/src/back-top/test/demo.spec.ts b/packages/vant/src/back-top/test/demo.spec.ts new file mode 100644 index 000000000..c0e0c95b9 --- /dev/null +++ b/packages/vant/src/back-top/test/demo.spec.ts @@ -0,0 +1,4 @@ +import Demo from '../demo/index.vue'; +import { snapshotDemo } from '../../../test/demo'; + +snapshotDemo(Demo); diff --git a/packages/vant/src/back-top/test/index.spec.ts b/packages/vant/src/back-top/test/index.spec.ts new file mode 100644 index 000000000..72b950ee5 --- /dev/null +++ b/packages/vant/src/back-top/test/index.spec.ts @@ -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(); +}); diff --git a/packages/vant/src/back-top/types.ts b/packages/vant/src/back-top/types.ts new file mode 100644 index 000000000..f8fd46147 --- /dev/null +++ b/packages/vant/src/back-top/types.ts @@ -0,0 +1,6 @@ +export type BackTopThemeVars = { + backTopSize?: string; + backTopIconSize?: string; + backTopTextColor?: string; + backTopBackground?: string; +}; diff --git a/packages/vant/vant.config.mjs b/packages/vant/vant.config.mjs index 1a8bb395a..7fdea3267 100644 --- a/packages/vant/vant.config.mjs +++ b/packages/vant/vant.config.mjs @@ -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', + }, ], }, {