mirror of
https://gitee.com/element-plus/element-plus.git
synced 2024-12-12 12:25:22 +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 type { App } from 'vue'
|
||||||
import ElAvatar from '@element-plus/avatar'
|
import ElAvatar from '@element-plus/avatar'
|
||||||
|
import ElBacktop from '@element-plus/backtop'
|
||||||
import ElButton from '@element-plus/button'
|
import ElButton from '@element-plus/button'
|
||||||
import ElBadge from '@element-plus/badge'
|
import ElBadge from '@element-plus/badge'
|
||||||
import ElCard from '@element-plus/card'
|
import ElCard from '@element-plus/card'
|
||||||
@ -18,6 +19,7 @@ import ElNotification from '@element-plus/notification'
|
|||||||
|
|
||||||
export {
|
export {
|
||||||
ElAvatar,
|
ElAvatar,
|
||||||
|
ElBacktop,
|
||||||
ElLayout,
|
ElLayout,
|
||||||
ElButton,
|
ElButton,
|
||||||
ElBadge,
|
ElBadge,
|
||||||
@ -37,6 +39,7 @@ export {
|
|||||||
|
|
||||||
export default function install(app: App): void {
|
export default function install(app: App): void {
|
||||||
ElAvatar(app)
|
ElAvatar(app)
|
||||||
|
ElBacktop(app)
|
||||||
ElButton(app)
|
ElButton(app)
|
||||||
ElBadge(app)
|
ElBadge(app)
|
||||||
ElCard(app)
|
ElCard(app)
|
||||||
|
@ -16,6 +16,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@element-plus/avatar": "^0.0.0",
|
"@element-plus/avatar": "^0.0.0",
|
||||||
"@element-plus/badge": "^0.0.0",
|
"@element-plus/badge": "^0.0.0",
|
||||||
|
"@element-plus/backtop": "^0.0.0",
|
||||||
"@element-plus/button": "^0.0.0",
|
"@element-plus/button": "^0.0.0",
|
||||||
"@element-plus/breadcrumb": "^0.0.0",
|
"@element-plus/breadcrumb": "^0.0.0",
|
||||||
"@element-plus/card": "^0.0.0",
|
"@element-plus/card": "^0.0.0",
|
||||||
|
Loading…
Reference in New Issue
Block a user