Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(comp: backtop): add backtop component (#79) #185

Merged
merged 1 commit into from
Feb 5, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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