Skip to content

Commit

Permalink
feat(comp: backtop): add backtop component (#79)
Browse files Browse the repository at this point in the history
fix #79
  • Loading branch information
imguolao committed Feb 5, 2021
1 parent b6d178c commit deabe66
Show file tree
Hide file tree
Showing 24 changed files with 453 additions and 2 deletions.
15 changes: 15 additions & 0 deletions packages/cdk/utils/dom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

const trim = (s: string) => (s || '').replace(/^[\s\uFEFF]+|[\s\uFEFF]+$/g, '')

export const raf = window.requestAnimationFrame || (cb => window.setTimeout(cb, 1000 / 60))

export function on(
el: HTMLElement | Document | Window,
type: string,
Expand Down Expand Up @@ -85,3 +87,16 @@ export function removeClass(el: HTMLElement, cls: string): void {
el.className = trim(curClass)
}
}

/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
export function isHTMLElement(el: any): boolean {
const div = document.createElement('div')

try {
div.appendChild(el.cloneNode(true))
return el.nodeType == 1 ? true : false
} catch {
return false
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`BackTop.vue render work 1`] = `"<transition-stub><div class=\\"ix-back-top\\" style=\\"display: none;\\"><i class=\\"ix-icon ix-icon-vertical-align-top\\" role=\\"img\\" arialabel=\\"vertical-align-top\\"></i></div></transition-stub>"`;
108 changes: 108 additions & 0 deletions packages/components/back-top/__test__/backTop.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import { mount } from '@vue/test-utils'
import { nextTick } from 'vue'
import BackTop from '../src/BackTop.vue'
import { renderWork, waitRAF, isShow, wait } from '@tests'

const mockFn = jest.fn()
const warn = jest.spyOn(console, 'warn').mockImplementation()

const backTopMount = (template: string, options = {}) =>
mount(
{
components: {
BackTop,
},
template,
...options,
},
{ attachTo: document.body },
)

describe('BackTop.vue', () => {
renderWork(BackTop)

test('scroll work', async () => {
const wrapper = backTopMount(
`
<div class="ix-back-top-test" style="height: 300px; overflow: auto;">
<div style="height: 1000px;">
<back-top :duration="100" :visibilityHeight="200" target=".ix-back-top-test"></back-top>
</div>
</div>
`,
{
beforeUnmount() {
mockFn()
},
},
)

expect(isShow(wrapper.find('.ix-back-top'))).toBe(false)

wrapper.element.scrollTop = 600
await wrapper.trigger('scroll')
await wait(1000)
expect(isShow(wrapper.find('.ix-back-top'))).toBe(true)

await wrapper.find('.ix-back-top').trigger('click')
await wait(1000)
expect(wrapper.element.scrollTop).toBe(0)

wrapper.unmount()
expect(mockFn).toBeCalled()
})

test('props work: target is a HTMLElement', async () => {
const wrapper = backTopMount(
`
<div style="height: 1000px;">
<back-top :target="target"></back-top>
</div>
`,
{
data() {
return {
target: document.documentElement,
}
},
},
)

expect(isShow(wrapper.find('.ix-back-top'))).toBe(false)

document.documentElement.scrollTop = 600
window.dispatchEvent(new Event('scroll'))

await wait(1000)

expect(isShow(wrapper.find('.ix-back-top'))).toBe(true)
})

test('props work: target does not exist', async () => {
mount(BackTop, {
props: {
target: '#ix-back-top-test',
},
})

await nextTick()
expect(warn).toBeCalled()
})

test('props work: target is the default value', async () => {
window.scrollTo = mockFn
const wrapper = backTopMount(`
<div style="height: 1000px; overflow: auto;">
<back-top></back-top>
</div>
`)

document.documentElement.scrollTop = 600
window.dispatchEvent(new Event('scroll'))
await wait(1000)
await wrapper.find('.ix-back-top').trigger('click')

await waitRAF()
expect(mockFn).toBeCalled()
})
})
4 changes: 4 additions & 0 deletions packages/components/back-top/demo/Basic.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<template>
Scroll down to see the bottom-right button.
<ix-back-top />
</template>
23 changes: 23 additions & 0 deletions packages/components/back-top/demo/Custom.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<template>
<div ref="target" class="ix-back-top-demo-custom" style="height: 300px; overflow: auto">
<div style="height: 1000px">
<div>Scroll down to see the bottom-right button.</div>
<div>Scroll down to see the bottom-right button.</div>
<div>Scroll down to see the bottom-right button.</div>
<div>Scroll down to see the bottom-right button.</div>
<div>Scroll down to see the bottom-right button.</div>
<ix-back-top style="bottom: 100px; border-radius: 4px" :duration="200" :target="target"> UP </ix-back-top>
</div>
</div>
</template>

<script lang="ts">
import { defineComponent, ref } from 'vue'
export default defineComponent({
setup() {
const target = ref<HTMLElement | null>(null)
return { target }
},
})
</script>
9 changes: 9 additions & 0 deletions packages/components/back-top/demo/basic.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
order: 0
title:
zh: 基本
---

## zh

基础用法,滑动页面即可看到右下方的按钮。
9 changes: 9 additions & 0 deletions packages/components/back-top/demo/custom.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
order: 1
title:
zh: 自定义
---

## zh

自定义显示内容或样式。
38 changes: 38 additions & 0 deletions packages/components/back-top/docs/index.zh.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
---
category: components
type: 其他
title: BackTop
subtitle: 回到顶部
cover:
---

返回页面顶部的操作按钮

## 何时使用

- 当页面内容区域比较长时。
- 当用户需要频繁返回顶部查看相关内容时。

## API

### `ix-back-top`

#### Props

| 参数 | 说明 | 类型 | 默认值 | 全局配置 | 备注 |
| -- | -- | -- | -- | -- | -- |
| `target` | 需要监听其滚动事件的元素 | `string \| HTMLElement` | `window` | - | 传入 string 类型,会在 mounted 的时候使用 querySelector 来获取元素 |
| `duration` | 回到顶部所需时间(ms) | `number` | `450` || - |
| `visibility-height` | 滚动高度达到此参数值才出现 | `number` | `400` || - |

#### Slots

| 名称 | 说明 | 参数类型 | 备注 |
| -- | -- | -- | -- |
| `defalut` | 自定义显示内容 | - | - |

#### Emits

| 名称 | 说明 | 参数类型 | 备注 |
| -- | -- | -- | -- |
| `click` | 点击按钮触发的事件 | `event` | - |
7 changes: 7 additions & 0 deletions packages/components/back-top/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { installComponent } from '@idux/components/core/utils'
import IxBackTop from './src/BackTop.vue'

IxBackTop.install = installComponent(IxBackTop)

export { IxBackTop }
export * from './src/types'
116 changes: 116 additions & 0 deletions packages/components/back-top/src/BackTop.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
<template>
<transition name="ix-fade">
<div v-show="visible" class="ix-back-top" @click.stop="handleClick">
<slot>
<ix-icon name="vertical-align-top" />
</slot>
</div>
</transition>
</template>

<script lang="ts">
import type { SetupContext } from 'vue'
import type { BackTopProps } from './types'
import { defineComponent, ref, onUnmounted, onMounted, nextTick } from 'vue'
import { IxIcon } from '@idux/components/icon'
import { PropTypes, withUndefined, isString, isHTMLElement, on, off, raf } from '@idux/cdk/utils'
import { easeInOutQuad } from '@idux/components/core/utils'
import { useGlobalConfig } from '@idux/components/core/config'
import throttle from 'lodash/throttle'
import { Logger } from '@idux/components/core/logger'
const getTarget = (target: HTMLElement | string | undefined) => {
const defaultTarget = window
if (isString(target)) {
const contaniner = document.querySelector<HTMLElement>(target)
if (contaniner) {
return contaniner
}
Logger.warn(`target does not exist: ${target}, default value are already used: window.`)
return defaultTarget
}
if (isHTMLElement(target)) {
return target as HTMLElement
}
return defaultTarget
}
const getCurrentScrollTop = (currTarget: Window | HTMLElement) => {
if (currTarget === window) {
return window.pageYOffset || document.body.scrollTop || document.documentElement.scrollTop
}
return (currTarget as HTMLElement).scrollTop
}
const scrollTo = (y: number, duration: number, container: HTMLElement | Window) => {
const scrollTop = getCurrentScrollTop(container)
const startTime = Date.now()
const change = y - scrollTop
const frameFunc = () => {
const time = Date.now() - startTime
const nextScrollTop = easeInOutQuad(time > duration ? duration : time, scrollTop, change, duration)
if (container === window) {
container.scrollTo(window.pageXOffset, nextScrollTop)
} else {
;(container as HTMLElement).scrollTop = nextScrollTop
}
if (time < duration) {
raf(frameFunc)
}
}
raf(frameFunc)
}
export default defineComponent({
name: 'IxBackTop',
components: { IxIcon },
props: {
target: withUndefined(PropTypes.oneOfType([PropTypes.string, HTMLElement])),
duration: PropTypes.number,
visibilityHeight: PropTypes.number,
},
emits: ['click'],
setup(props: BackTopProps, { emit }: SetupContext) {
const backTopConfig = useGlobalConfig('backTop')
const eventType = 'scroll'
const visible = ref(false)
const container = ref<Window | HTMLElement>((null as unknown) as HTMLElement)
const handleScroll = () => {
visible.value = getCurrentScrollTop(container.value) >= (props.visibilityHeight ?? backTopConfig.visibilityHeight)
}
const handleClick = (event: MouseEvent) => {
scrollTo(0, props.duration ?? backTopConfig.duration, container.value)
emit('click', event)
}
const throttledScrollHandler = throttle(handleScroll, 300)
onMounted(async () => {
await nextTick()
container.value = getTarget(props.target)
on(container.value, eventType, throttledScrollHandler)
handleScroll()
})
onUnmounted(() => {
off(container.value, eventType, throttledScrollHandler)
})
return {
visible,
handleClick,
}
},
})
</script>
11 changes: 11 additions & 0 deletions packages/components/back-top/src/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import type { DefineComponent } from 'vue'

interface BackTopOriginalProps {
target?: string | HTMLElement
duration?: number
visibilityHeight?: number
}

export type BackTopProps = Readonly<BackTopOriginalProps>

export type BackTopComponent = InstanceType<DefineComponent<BackTopProps>>
Loading

0 comments on commit deabe66

Please sign in to comment.