mirror of
https://gitee.com/element-plus/element-plus.git
synced 2024-12-03 11:47:48 +08:00
feat(backtop): add backtop component
This commit is contained in:
parent
df4fbb31b4
commit
548568abb0
29
packages/backtop/__tests__/backtop.spec.ts
Normal file
29
packages/backtop/__tests__/backtop.spec.ts
Normal file
@ -0,0 +1,29 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import Backtop from '../src/index.vue'
|
||||
|
||||
const _mount = (template: string) => mount({
|
||||
components: {
|
||||
'el-backtop': Backtop,
|
||||
},
|
||||
template,
|
||||
}, { attachTo: document.body })
|
||||
|
||||
describe('Backtop.vue', () => {
|
||||
test('render',async () => {
|
||||
const wrapper = _mount(`
|
||||
<div class="target" style="height: 100px; overflow: auto">
|
||||
<div style="height: 10000px; width: 100%">
|
||||
<el-backtop target=".target" :visibilityHeight="2000" :right="100" :bottom="200" />
|
||||
</div>
|
||||
</div>
|
||||
`)
|
||||
expect(wrapper.find('.el-backtop').exists()).toBe(false)
|
||||
wrapper.element.scrollTop = 2000
|
||||
await wrapper.trigger('scroll')
|
||||
expect(wrapper.find('.el-backtop').exists()).toBe(true)
|
||||
expect(wrapper.find('.el-backtop').attributes('style')).toBe('right: 100px; bottom: 200px;')
|
||||
expect(wrapper.find('.el-icon-caret-top').exists()).toBe(true)
|
||||
await wrapper.trigger('click')
|
||||
expect(wrapper.emitted()).toBeDefined()
|
||||
})
|
||||
})
|
40
packages/backtop/doc/basic.vue
Normal file
40
packages/backtop/doc/basic.vue
Normal file
@ -0,0 +1,40 @@
|
||||
<template>
|
||||
<div v-if="visible" class="target" style="height:400px; overflow-y: auto">
|
||||
<div style="height: 1000px; width: 100%">
|
||||
<el-backtop target=".target">
|
||||
<div
|
||||
style="{
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
background-color: #f2f5f6;
|
||||
box-shadow: 0 0 6px rgba(0,0,0, .12);
|
||||
text-align: center;
|
||||
line-height: 40px;
|
||||
color: #1989fa;
|
||||
}"
|
||||
>
|
||||
UP
|
||||
</div>
|
||||
</el-backtop>
|
||||
<el-backtop target=".target" :bottom="100" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { onMounted, defineComponent, ref } from 'vue'
|
||||
|
||||
export default defineComponent({
|
||||
setup() {
|
||||
const visible = ref(false)
|
||||
|
||||
onMounted(() => {
|
||||
visible.value = true
|
||||
})
|
||||
|
||||
return {
|
||||
visible,
|
||||
}
|
||||
},
|
||||
})
|
||||
</script>
|
5
packages/backtop/doc/index.stories.ts
Normal file
5
packages/backtop/doc/index.stories.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export { default as BasicUsage } from './basic.vue'
|
||||
|
||||
export default {
|
||||
title: 'Backtop',
|
||||
}
|
5
packages/backtop/index.ts
Normal file
5
packages/backtop/index.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import { App } from 'vue'
|
||||
import Backtop from './src/index.vue'
|
||||
export default (app: App): void => {
|
||||
app.component(Backtop.name, Backtop)
|
||||
}
|
12
packages/backtop/package.json
Normal file
12
packages/backtop/package.json
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"name": "@element-plus/backtop",
|
||||
"version": "0.0.0",
|
||||
"main": "dist/index.js",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"vue": "^3.0.0-rc.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vue/test-utils": "^2.0.0-beta.0"
|
||||
}
|
||||
}
|
113
packages/backtop/src/index.vue
Normal file
113
packages/backtop/src/index.vue
Normal file
@ -0,0 +1,113 @@
|
||||
<template>
|
||||
<transition name="el-fade-in">
|
||||
<div
|
||||
v-if="visible"
|
||||
:style="{
|
||||
'right': styleRight,
|
||||
'bottom': styleBottom
|
||||
}"
|
||||
class="el-backtop"
|
||||
@click.stop="handleClick"
|
||||
>
|
||||
<slot>
|
||||
<i class="el-icon-caret-top"></i>
|
||||
</slot>
|
||||
</div>
|
||||
</transition>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, ref, computed, onMounted, onBeforeUnmount } from 'vue'
|
||||
import { throttle } from 'lodash-es'
|
||||
|
||||
interface IElBacktopProps {
|
||||
visibilityHeight: number
|
||||
target: string
|
||||
right: number
|
||||
bottom: number
|
||||
}
|
||||
|
||||
const cubic = value => Math.pow(value, 3)
|
||||
const easeInOutCubic = value => value < 0.5 ? cubic(value * 2) / 2 : 1 - cubic((1 - value) * 2) / 2
|
||||
|
||||
export default defineComponent({
|
||||
name: 'ElBacktop',
|
||||
props: {
|
||||
visibilityHeight: {
|
||||
type: Number,
|
||||
default: 200,
|
||||
},
|
||||
target: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
right: {
|
||||
type: Number,
|
||||
default: 40,
|
||||
},
|
||||
bottom: {
|
||||
type: Number,
|
||||
default: 40,
|
||||
},
|
||||
},
|
||||
emits: ['click'],
|
||||
setup(props: IElBacktopProps, ctx) {
|
||||
let el, container
|
||||
const visible = ref(false)
|
||||
const styleBottom = computed(() => `${props.bottom}px`)
|
||||
const styleRight = computed(() => `${props.right}px`)
|
||||
|
||||
const init = () => {
|
||||
container = document
|
||||
el = document.documentElement
|
||||
if (props.target) {
|
||||
el = document.querySelector(props.target)
|
||||
if (!el) {
|
||||
throw new Error(`target is not existed: ${props.target}`)
|
||||
}
|
||||
container = el
|
||||
}
|
||||
}
|
||||
const scrollToTop = () => {
|
||||
const beginTime = Date.now()
|
||||
const beginValue = el.scrollTop
|
||||
const rAF = window.requestAnimationFrame || (func => setTimeout(func, 16))
|
||||
const frameFunc = () => {
|
||||
const progress = (Date.now() - beginTime) / 500
|
||||
if (progress < 1) {
|
||||
el.scrollTop = beginValue * (1 - easeInOutCubic(progress))
|
||||
rAF(frameFunc)
|
||||
} else {
|
||||
el.scrollTop = 0
|
||||
}
|
||||
}
|
||||
rAF(frameFunc)
|
||||
}
|
||||
const onScroll = () => {
|
||||
const scrollTop = el.scrollTop
|
||||
visible.value = scrollTop >= props.visibilityHeight
|
||||
}
|
||||
const handleClick = (event) => {
|
||||
scrollToTop()
|
||||
ctx.emit('click', event)
|
||||
}
|
||||
|
||||
const throttledScrollHandler = throttle(onScroll, 300)
|
||||
|
||||
onMounted(() => {
|
||||
init()
|
||||
container.addEventListener('scroll', throttledScrollHandler)
|
||||
})
|
||||
onBeforeUnmount(() => {
|
||||
container.removeEventListener('scroll', throttledScrollHandler)
|
||||
})
|
||||
|
||||
return {
|
||||
visible,
|
||||
styleBottom,
|
||||
styleRight,
|
||||
handleClick,
|
||||
}
|
||||
},
|
||||
})
|
||||
</script>
|
@ -1,5 +1,6 @@
|
||||
import type { App } from 'vue'
|
||||
import ElAvatar from '@element-plus/avatar'
|
||||
import ElBacktop from '@element-plus/backtop'
|
||||
import ElButton from '@element-plus/button'
|
||||
import ElBadge from '@element-plus/badge'
|
||||
import ElCard from '@element-plus/card'
|
||||
@ -18,6 +19,7 @@ import ElNotification from '@element-plus/notification'
|
||||
|
||||
export {
|
||||
ElAvatar,
|
||||
ElBacktop,
|
||||
ElLayout,
|
||||
ElButton,
|
||||
ElBadge,
|
||||
@ -37,6 +39,7 @@ export {
|
||||
|
||||
export default function install(app: App): void {
|
||||
ElAvatar(app)
|
||||
ElBacktop(app)
|
||||
ElButton(app)
|
||||
ElBadge(app)
|
||||
ElCard(app)
|
||||
|
@ -16,6 +16,7 @@
|
||||
"dependencies": {
|
||||
"@element-plus/avatar": "^0.0.0",
|
||||
"@element-plus/badge": "^0.0.0",
|
||||
"@element-plus/backtop": "^0.0.0",
|
||||
"@element-plus/button": "^0.0.0",
|
||||
"@element-plus/breadcrumb": "^0.0.0",
|
||||
"@element-plus/card": "^0.0.0",
|
||||
|
Loading…
Reference in New Issue
Block a user