From deabe66f639e95870790f170768b700124b8bfd8 Mon Sep 17 00:00:00 2001 From: imguolao Date: Mon, 25 Jan 2021 15:33:16 +0800 Subject: [PATCH] feat(comp: backtop): add backtop component (#79) fix #79 --- packages/cdk/utils/dom.ts | 15 +++ .../__snapshots__/backTop.spec.ts.snap | 3 + .../back-top/__test__/backTop.spec.ts | 108 ++++++++++++++++ packages/components/back-top/demo/Basic.vue | 4 + packages/components/back-top/demo/Custom.vue | 23 ++++ packages/components/back-top/demo/basic.md | 9 ++ packages/components/back-top/demo/custom.md | 9 ++ packages/components/back-top/docs/index.zh.md | 38 ++++++ packages/components/back-top/index.ts | 7 ++ packages/components/back-top/src/BackTop.vue | 116 ++++++++++++++++++ packages/components/back-top/src/types.ts | 11 ++ packages/components/back-top/style/index.less | 44 +++++++ packages/components/components.less | 1 + .../components/core/config/defaultConfig.ts | 7 ++ packages/components/core/config/types.ts | 6 + .../components/core/utils/easingFunctions.ts | 1 - packages/components/core/utils/index.ts | 1 + packages/components/icon/src/icons.ts | 2 + packages/components/index.ts | 3 + packages/components/style/default.less | 20 +++ packages/components/style/motion/index.less | 4 + packages/components/style/variable/index.less | 1 + .../components/style/variable/screen.less | 16 +++ tests/utils.ts | 6 +- 24 files changed, 453 insertions(+), 2 deletions(-) create mode 100644 packages/components/back-top/__test__/__snapshots__/backTop.spec.ts.snap create mode 100644 packages/components/back-top/__test__/backTop.spec.ts create mode 100644 packages/components/back-top/demo/Basic.vue create mode 100644 packages/components/back-top/demo/Custom.vue create mode 100644 packages/components/back-top/demo/basic.md create mode 100644 packages/components/back-top/demo/custom.md create mode 100644 packages/components/back-top/docs/index.zh.md create mode 100644 packages/components/back-top/index.ts create mode 100644 packages/components/back-top/src/BackTop.vue create mode 100644 packages/components/back-top/src/types.ts create mode 100644 packages/components/back-top/style/index.less create mode 100644 packages/components/style/motion/index.less create mode 100644 packages/components/style/variable/screen.less diff --git a/packages/cdk/utils/dom.ts b/packages/cdk/utils/dom.ts index bba0a9a7f..8e48d6183 100644 --- a/packages/cdk/utils/dom.ts +++ b/packages/cdk/utils/dom.ts @@ -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, @@ -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 + } +} diff --git a/packages/components/back-top/__test__/__snapshots__/backTop.spec.ts.snap b/packages/components/back-top/__test__/__snapshots__/backTop.spec.ts.snap new file mode 100644 index 000000000..b91616387 --- /dev/null +++ b/packages/components/back-top/__test__/__snapshots__/backTop.spec.ts.snap @@ -0,0 +1,3 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`BackTop.vue render work 1`] = `"
"`; diff --git a/packages/components/back-top/__test__/backTop.spec.ts b/packages/components/back-top/__test__/backTop.spec.ts new file mode 100644 index 000000000..d833a06d8 --- /dev/null +++ b/packages/components/back-top/__test__/backTop.spec.ts @@ -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( + ` +
+
+ +
+
+ `, + { + 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( + ` +
+ +
+ `, + { + 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(` +
+ +
+ `) + + 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() + }) +}) diff --git a/packages/components/back-top/demo/Basic.vue b/packages/components/back-top/demo/Basic.vue new file mode 100644 index 000000000..49fd1375b --- /dev/null +++ b/packages/components/back-top/demo/Basic.vue @@ -0,0 +1,4 @@ + diff --git a/packages/components/back-top/demo/Custom.vue b/packages/components/back-top/demo/Custom.vue new file mode 100644 index 000000000..f6725c10d --- /dev/null +++ b/packages/components/back-top/demo/Custom.vue @@ -0,0 +1,23 @@ + + + diff --git a/packages/components/back-top/demo/basic.md b/packages/components/back-top/demo/basic.md new file mode 100644 index 000000000..a60d1226c --- /dev/null +++ b/packages/components/back-top/demo/basic.md @@ -0,0 +1,9 @@ +--- +order: 0 +title: + zh: 基本 +--- + +## zh + +基础用法,滑动页面即可看到右下方的按钮。 diff --git a/packages/components/back-top/demo/custom.md b/packages/components/back-top/demo/custom.md new file mode 100644 index 000000000..8ff151be9 --- /dev/null +++ b/packages/components/back-top/demo/custom.md @@ -0,0 +1,9 @@ +--- +order: 1 +title: + zh: 自定义 +--- + +## zh + +自定义显示内容或样式。 diff --git a/packages/components/back-top/docs/index.zh.md b/packages/components/back-top/docs/index.zh.md new file mode 100644 index 000000000..24beb74ea --- /dev/null +++ b/packages/components/back-top/docs/index.zh.md @@ -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` | - | diff --git a/packages/components/back-top/index.ts b/packages/components/back-top/index.ts new file mode 100644 index 000000000..bc4636213 --- /dev/null +++ b/packages/components/back-top/index.ts @@ -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' diff --git a/packages/components/back-top/src/BackTop.vue b/packages/components/back-top/src/BackTop.vue new file mode 100644 index 000000000..8d46e3e4e --- /dev/null +++ b/packages/components/back-top/src/BackTop.vue @@ -0,0 +1,116 @@ + + + diff --git a/packages/components/back-top/src/types.ts b/packages/components/back-top/src/types.ts new file mode 100644 index 000000000..38494e834 --- /dev/null +++ b/packages/components/back-top/src/types.ts @@ -0,0 +1,11 @@ +import type { DefineComponent } from 'vue' + +interface BackTopOriginalProps { + target?: string | HTMLElement + duration?: number + visibilityHeight?: number +} + +export type BackTopProps = Readonly + +export type BackTopComponent = InstanceType> diff --git a/packages/components/back-top/style/index.less b/packages/components/back-top/style/index.less new file mode 100644 index 000000000..02f588423 --- /dev/null +++ b/packages/components/back-top/style/index.less @@ -0,0 +1,44 @@ +@import '../../style/default.less'; + +@back-top-prefix: ~'@{idux-prefix}-back-top'; + +.@{back-top-prefix} { + position: fixed; + right: @back-top-right-xl; + bottom: @back-top-bottom; + z-index: 99; + background-color: @back-top-background-color; + width: @back-top-width; + height: @back-top-height; + color: @white; + display: flex; + align-items: center; + justify-content: center; + font-size: @back-top-font-size; + border-radius: @back-top-border-radius; + cursor: pointer; +} + +@media screen and (min-width: @screen-lg) and (max-width: @screen-lg-max) { + .@{back-top-prefix} { + right: @back-top-right-lg; + } +} + +@media screen and (min-width: @screen-md) and (max-width: @screen-md-max) { + .@{back-top-prefix} { + right: @back-top-right-md; + } +} + +@media screen and (min-width: @screen-sm) and (max-width: @screen-sm-max) { + .@{back-top-prefix} { + right: @back-top-right-sm; + } +} + +@media screen and (max-width: @screen-xs-max) { + .@{back-top-prefix} { + right: @back-top-right-xs; + } +} diff --git a/packages/components/components.less b/packages/components/components.less index 3464f510c..5c322f742 100644 --- a/packages/components/components.less +++ b/packages/components/components.less @@ -11,3 +11,4 @@ @import './rate/style/index.less'; @import './checkbox/style/index.less'; @import './input/style/index.less'; +@import './back-top/style/index.less'; diff --git a/packages/components/core/config/defaultConfig.ts b/packages/components/core/config/defaultConfig.ts index fa0800ebf..76b92a32a 100644 --- a/packages/components/core/config/defaultConfig.ts +++ b/packages/components/core/config/defaultConfig.ts @@ -11,6 +11,7 @@ import type { RateConfig, InputConfig, TextareaConfig, + BackTopConfig, } from './types' import { shallowReactive } from 'vue' @@ -67,6 +68,11 @@ const textarea = shallowReactive({ clearable: false, }) +const backTop = shallowReactive({ + duration: 450, + visibilityHeight: 400, +}) + export const defaultConfig: GlobalConfig = { button, icon, @@ -79,4 +85,5 @@ export const defaultConfig: GlobalConfig = { rate, input, textarea, + backTop, } diff --git a/packages/components/core/config/types.ts b/packages/components/core/config/types.ts index 43a83d2db..5ce3ea61e 100644 --- a/packages/components/core/config/types.ts +++ b/packages/components/core/config/types.ts @@ -12,6 +12,7 @@ export interface GlobalConfig { rate: RateConfig input: InputConfig textarea: TextareaConfig + backTop: BackTopConfig } export type ButtonMode = 'primary' | 'default' | 'dashed' | 'text' | 'link' @@ -90,3 +91,8 @@ export interface TextareaConfig { size: InputSize clearable: boolean } + +export interface BackTopConfig { + duration: number + visibilityHeight: number +} diff --git a/packages/components/core/utils/easingFunctions.ts b/packages/components/core/utils/easingFunctions.ts index df2f1d17b..fd05aaa0b 100644 --- a/packages/components/core/utils/easingFunctions.ts +++ b/packages/components/core/utils/easingFunctions.ts @@ -15,6 +15,5 @@ export function easeInOutQuad(elapsed: number, initialValue: number, amountOfCha if ((elapsed /= duration / 2) < 1) { return (amountOfChange / 2) * elapsed * elapsed + initialValue } - return (-amountOfChange / 2) * (--elapsed * (elapsed - 2) - 1) + initialValue } diff --git a/packages/components/core/utils/index.ts b/packages/components/core/utils/index.ts index b6a32d542..b49d86c17 100644 --- a/packages/components/core/utils/index.ts +++ b/packages/components/core/utils/index.ts @@ -1,3 +1,4 @@ export * from './installComponent' export * from './isDevMode' export * from './useAttrs' +export * from './easingFunctions' diff --git a/packages/components/icon/src/icons.ts b/packages/components/icon/src/icons.ts index 362d9778f..d52c64266 100644 --- a/packages/components/icon/src/icons.ts +++ b/packages/components/icon/src/icons.ts @@ -15,6 +15,7 @@ import { CloseCircle } from '../definitions/closeCircle' import { InfoCircle } from '../definitions/infoCircle' import { ExclamationCircle } from '../definitions/exclamationCircle' import { Star } from '../definitions/star' +import { VerticalAlignTop } from '../definitions/verticalAlignTop' export const innerStaticIcons: IconDefinition[] = [ Up, @@ -33,4 +34,5 @@ export const innerStaticIcons: IconDefinition[] = [ InfoCircle, ExclamationCircle, Star, + VerticalAlignTop, ] diff --git a/packages/components/index.ts b/packages/components/index.ts index 140bea821..f5a1e44d5 100644 --- a/packages/components/index.ts +++ b/packages/components/index.ts @@ -12,6 +12,7 @@ import { IxTypography } from './typography' import { IxRate } from './rate' import { IxCheckbox, IxCheckboxGroup } from './checkbox' import { IxInput, IxTextarea } from './input' +import { IxBackTop } from './back-top' const components = [ IxButton, @@ -29,6 +30,7 @@ const components = [ IxCheckboxGroup, IxInput, IxTextarea, + IxBackTop, ] const directives: Record = { @@ -67,3 +69,4 @@ export { IxTypography } export { IxRate } export { IxCheckbox, IxCheckboxGroup } export { IxInput, IxTextarea } +export { IxBackTop } diff --git a/packages/components/style/default.less b/packages/components/style/default.less index 2bb41921e..1cba9cd6f 100644 --- a/packages/components/style/default.less +++ b/packages/components/style/default.less @@ -1,5 +1,6 @@ @import './mixins/index.less'; @import './variable/index.less'; +@import './motion/index.less'; @theme: default; @@ -241,3 +242,22 @@ @input-border-radius: @border-radius-md; @input-transition-duration: @transition-duration-base; + +/* back-top -------------------------- */ +@back-top-height: @height-lg; +@back-top-width: @height-lg; + +@back-top-font-size: @font-size-2xl; + +@back-top-border-radius: 50%; + +@back-top-right-gutter: 32px; +@back-top-right-xs: (@back-top-right-gutter / 2); +@back-top-right-sm: @back-top-right-gutter; +@back-top-right-md: @back-top-right-gutter * 1.5; +@back-top-right-lg: @back-top-right-gutter * 2; +@back-top-right-xl: @back-top-right-gutter * 3; + +@back-top-bottom: 50px; + +@back-top-background-color: @primary-color; diff --git a/packages/components/style/motion/index.less b/packages/components/style/motion/index.less new file mode 100644 index 000000000..a8450581a --- /dev/null +++ b/packages/components/style/motion/index.less @@ -0,0 +1,4 @@ +@import './fade.less'; +@import './move.less'; +@import './slide.less'; +@import './zoom.less'; diff --git a/packages/components/style/variable/index.less b/packages/components/style/variable/index.less index db5c8c7cf..c729fc68f 100644 --- a/packages/components/style/variable/index.less +++ b/packages/components/style/variable/index.less @@ -4,3 +4,4 @@ @import './font.less'; @import './shadow.less'; @import './spacing.less'; +@import './screen.less'; diff --git a/packages/components/style/variable/screen.less b/packages/components/style/variable/screen.less new file mode 100644 index 000000000..faa08d0b2 --- /dev/null +++ b/packages/components/style/variable/screen.less @@ -0,0 +1,16 @@ +@screen-sm: 768px; +@screen-sm-min: @screen-sm; + +@screen-md: 1024px; +@screen-md-min: @screen-md; + +@screen-lg: 1280px; +@screen-lg-min: @screen-lg; + +@screen-xl: 1720px; +@screen-xl-min: @screen-xl; + +@screen-xs-max: (@screen-sm-min - 0.01px); +@screen-sm-max: (@screen-md-min - 0.01px); +@screen-md-max: (@screen-lg-min - 0.01px); +@screen-lg-max: (@screen-xl-min - 0.01px); diff --git a/tests/utils.ts b/tests/utils.ts index 2cb2f2565..b028659b1 100644 --- a/tests/utils.ts +++ b/tests/utils.ts @@ -1,7 +1,7 @@ /* eslint-disable @typescript-eslint/explicit-module-boundary-types */ /* eslint-disable @typescript-eslint/no-explicit-any */ -import { mount } from '@vue/test-utils' +import { mount, DOMWrapper } from '@vue/test-utils' export const wait = (timeout?: number): Promise => { return new Promise(resolve => setTimeout(resolve, timeout)) @@ -35,3 +35,7 @@ export const renderWork = (component: any, options = {}): void => { }).not.toThrow() }) } + +export const isShow = (wrapper: DOMWrapper): boolean => { + return window.getComputedStyle(wrapper.element).display !== 'none' +}