diff --git a/packages/affix/__tests__/affix.spec.ts b/packages/affix/__tests__/affix.spec.ts new file mode 100644 index 0000000000..acc92ac0a8 --- /dev/null +++ b/packages/affix/__tests__/affix.spec.ts @@ -0,0 +1,158 @@ +import { mount } from '@vue/test-utils' +import Affix from '../src/index.vue' +import { defineGetter, makeScroll } from '@element-plus/test-utils' + +let clientHeightRestore = null + +const _mount = (template: string) => mount({ + components: { + 'el-affix': Affix, + }, + template, +}, { attachTo: document.body }) + +const AXIOM = 'Rem is the best girl' + +beforeAll(() => { + clientHeightRestore = defineGetter(window.HTMLElement.prototype, 'clientHeight', 1000, 0) +}) + +afterAll(() => { + clientHeightRestore() +}) + +describe('Affix.vue', () => { + test('render test', async () => { + const wrapper = _mount(` + ${AXIOM} + `) + expect(wrapper.text()).toEqual(AXIOM) + const mockAffixRect = jest.spyOn(wrapper.find('.el-affix').element, 'getBoundingClientRect').mockReturnValue({ + height: 40, + width: 1000, + top: -100, + bottom: -80, + } as DOMRect) + const mockDocumentRect = jest.spyOn(document.documentElement, 'getBoundingClientRect').mockReturnValue({ + height: 200, + width: 1000, + top: 0, + bottom: 200, + } as DOMRect) + expect(wrapper.find('.el-affix--fixed').exists()).toBe(false) + await makeScroll(document.documentElement, 'scrollTop', 200) + expect(wrapper.find('.el-affix--fixed').exists()).toBe(true) + mockAffixRect.mockRestore() + mockDocumentRect.mockRestore() + }) + + test('should render offset props', async () => { + const wrapper = _mount(` + ${AXIOM} + `) + const mockAffixRect = jest.spyOn(wrapper.find('.el-affix').element, 'getBoundingClientRect').mockReturnValue({ + height: 40, + width: 1000, + top: -100, + bottom: -80, + } as DOMRect) + const mockDocumentRect = jest.spyOn(document.documentElement, 'getBoundingClientRect').mockReturnValue({ + height: 200, + width: 1000, + top: 0, + bottom: 200, + } as DOMRect) + await makeScroll(document.documentElement, 'scrollTop', 200) + expect(wrapper.find('.el-affix--fixed').exists()).toBe(true) + expect(wrapper.find('.el-affix--fixed').attributes('style')).toContain('top: 30px;') + mockAffixRect.mockRestore() + mockDocumentRect.mockRestore() + }) + + test('should render position props', async () => { + const wrapper = _mount(` + ${AXIOM} + `) + + const mockAffixRect = jest.spyOn(wrapper.find('.el-affix').element, 'getBoundingClientRect').mockReturnValue({ + height: 40, + width: 1000, + top: 2000, + bottom: 2040, + } as DOMRect) + const mockDocumentRect = jest.spyOn(document.documentElement, 'getBoundingClientRect').mockReturnValue({ + height: 200, + width: 1000, + top: 0, + bottom: 200, + } as DOMRect) + await makeScroll(document.documentElement, 'scrollTop', 0) + expect(wrapper.find('.el-affix--fixed').exists()).toBe(true) + expect(wrapper.find('.el-affix--fixed').attributes('style')).toContain('bottom: 20px;') + mockAffixRect.mockRestore() + mockDocumentRect.mockRestore() + }) + + test('should render target props', async () => { + const wrapper = _mount(` +
+ ${AXIOM} +
+
+ `) + + const mockAffixRect = jest.spyOn(wrapper.find('.el-affix').element, 'getBoundingClientRect').mockReturnValue({ + height: 40, + width: 1000, + top: -100, + bottom: -60, + } as DOMRect) + const mockTargetRect = jest.spyOn(wrapper.find('.target').element, 'getBoundingClientRect').mockReturnValue({ + height: 200, + width: 1000, + top: -100, + bottom: 100, + } as DOMRect) + await makeScroll(document.documentElement, 'scrollTop', 100) + expect(wrapper.find('.el-affix--fixed').exists()).toBe(true) + mockAffixRect.mockReturnValue({ + height: 40, + width: 1000, + top: -300, + bottom: -260, + } as DOMRect) + mockTargetRect.mockReturnValue({ + height: 40, + width: 1000, + top: -300, + bottom: -260, + } as DOMRect) + await makeScroll(document.documentElement, 'scrollTop', 300) + expect(wrapper.find('.el-affix--fixed').exists()).toBe(false) + mockAffixRect.mockRestore() + mockTargetRect.mockRestore() + }) + + test('should render z-index props', async () => { + const wrapper = _mount(` + ${AXIOM} + `) + const mockAffixRect = jest.spyOn(wrapper.find('.el-affix').element, 'getBoundingClientRect').mockReturnValue({ + height: 40, + width: 1000, + top: -100, + bottom: -80, + } as DOMRect) + const mockDocumentRect = jest.spyOn(document.documentElement, 'getBoundingClientRect').mockReturnValue({ + height: 200, + width: 1000, + top: 0, + bottom: 200, + } as DOMRect) + await makeScroll(document.documentElement, 'scrollTop', 200) + expect(wrapper.find('.el-affix--fixed').exists()).toBe(true) + expect(wrapper.find('.el-affix--fixed').attributes('style')).toContain('z-index: 1000;') + mockAffixRect.mockRestore() + mockDocumentRect.mockRestore() + }) +}) diff --git a/packages/affix/index.ts b/packages/affix/index.ts new file mode 100644 index 0000000000..b3a8f07506 --- /dev/null +++ b/packages/affix/index.ts @@ -0,0 +1,11 @@ +import { App } from 'vue' +import type { SFCWithInstall } from '@element-plus/utils/types' +import Affix from './src/index.vue' + +Affix.install = (app: App): void => { + app.component(Affix.name, Affix) +} + +const _Affix: SFCWithInstall = Affix + +export default _Affix diff --git a/packages/affix/package.json b/packages/affix/package.json new file mode 100644 index 0000000000..3d0279146b --- /dev/null +++ b/packages/affix/package.json @@ -0,0 +1,12 @@ +{ + "name": "@element-plus/affix", + "version": "0.0.0", + "main": "dist/index.js", + "license": "MIT", + "peerDependencies": { + "vue": "^3.0.5" + }, + "devDependencies": { + "@vue/test-utils": "^2.0.0-beta.3" + } +} diff --git a/packages/affix/src/index.vue b/packages/affix/src/index.vue new file mode 100644 index 0000000000..221f6ace91 --- /dev/null +++ b/packages/affix/src/index.vue @@ -0,0 +1,141 @@ + + diff --git a/packages/element-plus/index.ts b/packages/element-plus/index.ts index 833a4788c0..7aa84ab7e8 100644 --- a/packages/element-plus/index.ts +++ b/packages/element-plus/index.ts @@ -1,4 +1,5 @@ import type { App } from 'vue' +import ElAffix from '@element-plus/affix' import ElAlert from '@element-plus/alert' import ElAside from '@element-plus/aside' import ElAutocomplete from '@element-plus/autocomplete' @@ -92,19 +93,20 @@ import { use } from '@element-plus/locale' // if you encountered problems alike "Can't resolve './version'" // please run `yarn bootstrap` first import { version as version_ } from './version' -import { setConfig } from '@element-plus/utils/config' import type { InstallOptions } from '@element-plus/utils/config' +import { setConfig } from '@element-plus/utils/config' const version = version_ // version_ to fix tsc issue const locale = use -const defaultInstallOpt: InstallOptions = { +const defaultInstallOpt: InstallOptions = { size: '' as ComponentSize, zIndex: 2000, } const components = [ + ElAffix, ElAlert, ElAside, ElAutocomplete, @@ -215,6 +217,7 @@ const install = (app: App, opt: InstallOptions): void => { } export { + ElAffix, ElAlert, ElAside, ElAutocomplete, diff --git a/packages/theme-chalk/src/affix.scss b/packages/theme-chalk/src/affix.scss new file mode 100644 index 0000000000..4d0268eb5e --- /dev/null +++ b/packages/theme-chalk/src/affix.scss @@ -0,0 +1,7 @@ +@import "mixins/mixins"; + +@include b(affix) { + @include m(fixed) { + position: fixed; + } +} diff --git a/packages/theme-chalk/src/index.scss b/packages/theme-chalk/src/index.scss index a54dc7bfdf..64c04fb839 100644 --- a/packages/theme-chalk/src/index.scss +++ b/packages/theme-chalk/src/index.scss @@ -85,3 +85,4 @@ @import "./skeleton.scss"; @import "./skeleton-item.scss"; @import "./empty.scss"; +@import "./affix.scss"; diff --git a/website/demo-styles/affix.scss b/website/demo-styles/affix.scss new file mode 100644 index 0000000000..5037877fad --- /dev/null +++ b/website/demo-styles/affix.scss @@ -0,0 +1,8 @@ +.demo-block.demo-affix { + .affix-container { + text-align: center; + height: 400px; + border-radius: 4px; + background: #ECF5FF; + } +} diff --git a/website/demo-styles/index.scss b/website/demo-styles/index.scss index 4df50dbb69..28b0915e4b 100644 --- a/website/demo-styles/index.scss +++ b/website/demo-styles/index.scss @@ -1,3 +1,4 @@ +@import "./affix.scss"; @import "./alert.scss"; @import "./badge.scss"; @import "./border.scss"; diff --git a/website/docs/en-US/affix.md b/website/docs/en-US/affix.md new file mode 100644 index 0000000000..2820e1b4ae --- /dev/null +++ b/website/docs/en-US/affix.md @@ -0,0 +1,55 @@ +## Affix + +Fix the element to a specific visible area. + +### Basic usage + +Affix is fixed at the top of the page by default. + +:::demo You can set `offset` attribute to change the offset top,the default value is 0。 +```html + + Offset top 120px + +``` +::: + +### Target container + +You can set `target` attribute to keep the affix in the container at all times. It will be hidden if out of range. + +:::demo Please notice that the container avoid having scrollbar. +```html +
+ + Target container + +
+``` +::: + +### Fixed position + +The affix component provides two fixed positions: `top` and `bottom`. + +:::demo You can set `position` attribute to change the fixed position, the default value is `top`. +```html + + Offset bottom 20px + +``` +::: + +### Attributes +| Attribute | Description | Type | Accepted Values | Default | +|---------- |-------------- |---------- |-------------------------------- |-------- | +| offset | offset distance | number | — | 0 | +| position | position of affix | string | top / bottom | top | +| target | target container (CSS selector) | string | — | — | +| z-index | z-index of affix | number | — | 100 | + +### Events +| Event Name | Description | Parameters | +|---------- |-------- |---------- | +| change | triggers when fixed state changed | (value: boolean) | +| scroll | triggers when scrolling | scroll top and fixed state | diff --git a/website/docs/es/affix.md b/website/docs/es/affix.md new file mode 100644 index 0000000000..2820e1b4ae --- /dev/null +++ b/website/docs/es/affix.md @@ -0,0 +1,55 @@ +## Affix + +Fix the element to a specific visible area. + +### Basic usage + +Affix is fixed at the top of the page by default. + +:::demo You can set `offset` attribute to change the offset top,the default value is 0。 +```html + + Offset top 120px + +``` +::: + +### Target container + +You can set `target` attribute to keep the affix in the container at all times. It will be hidden if out of range. + +:::demo Please notice that the container avoid having scrollbar. +```html +
+ + Target container + +
+``` +::: + +### Fixed position + +The affix component provides two fixed positions: `top` and `bottom`. + +:::demo You can set `position` attribute to change the fixed position, the default value is `top`. +```html + + Offset bottom 20px + +``` +::: + +### Attributes +| Attribute | Description | Type | Accepted Values | Default | +|---------- |-------------- |---------- |-------------------------------- |-------- | +| offset | offset distance | number | — | 0 | +| position | position of affix | string | top / bottom | top | +| target | target container (CSS selector) | string | — | — | +| z-index | z-index of affix | number | — | 100 | + +### Events +| Event Name | Description | Parameters | +|---------- |-------- |---------- | +| change | triggers when fixed state changed | (value: boolean) | +| scroll | triggers when scrolling | scroll top and fixed state | diff --git a/website/docs/fr-FR/affix.md b/website/docs/fr-FR/affix.md new file mode 100644 index 0000000000..2820e1b4ae --- /dev/null +++ b/website/docs/fr-FR/affix.md @@ -0,0 +1,55 @@ +## Affix + +Fix the element to a specific visible area. + +### Basic usage + +Affix is fixed at the top of the page by default. + +:::demo You can set `offset` attribute to change the offset top,the default value is 0。 +```html + + Offset top 120px + +``` +::: + +### Target container + +You can set `target` attribute to keep the affix in the container at all times. It will be hidden if out of range. + +:::demo Please notice that the container avoid having scrollbar. +```html +
+ + Target container + +
+``` +::: + +### Fixed position + +The affix component provides two fixed positions: `top` and `bottom`. + +:::demo You can set `position` attribute to change the fixed position, the default value is `top`. +```html + + Offset bottom 20px + +``` +::: + +### Attributes +| Attribute | Description | Type | Accepted Values | Default | +|---------- |-------------- |---------- |-------------------------------- |-------- | +| offset | offset distance | number | — | 0 | +| position | position of affix | string | top / bottom | top | +| target | target container (CSS selector) | string | — | — | +| z-index | z-index of affix | number | — | 100 | + +### Events +| Event Name | Description | Parameters | +|---------- |-------- |---------- | +| change | triggers when fixed state changed | (value: boolean) | +| scroll | triggers when scrolling | scroll top and fixed state | diff --git a/website/docs/jp/affix.md b/website/docs/jp/affix.md new file mode 100644 index 0000000000..2820e1b4ae --- /dev/null +++ b/website/docs/jp/affix.md @@ -0,0 +1,55 @@ +## Affix + +Fix the element to a specific visible area. + +### Basic usage + +Affix is fixed at the top of the page by default. + +:::demo You can set `offset` attribute to change the offset top,the default value is 0。 +```html + + Offset top 120px + +``` +::: + +### Target container + +You can set `target` attribute to keep the affix in the container at all times. It will be hidden if out of range. + +:::demo Please notice that the container avoid having scrollbar. +```html +
+ + Target container + +
+``` +::: + +### Fixed position + +The affix component provides two fixed positions: `top` and `bottom`. + +:::demo You can set `position` attribute to change the fixed position, the default value is `top`. +```html + + Offset bottom 20px + +``` +::: + +### Attributes +| Attribute | Description | Type | Accepted Values | Default | +|---------- |-------------- |---------- |-------------------------------- |-------- | +| offset | offset distance | number | — | 0 | +| position | position of affix | string | top / bottom | top | +| target | target container (CSS selector) | string | — | — | +| z-index | z-index of affix | number | — | 100 | + +### Events +| Event Name | Description | Parameters | +|---------- |-------- |---------- | +| change | triggers when fixed state changed | (value: boolean) | +| scroll | triggers when scrolling | scroll top and fixed state | diff --git a/website/docs/zh-CN/affix.md b/website/docs/zh-CN/affix.md new file mode 100644 index 0000000000..1c9c3043e9 --- /dev/null +++ b/website/docs/zh-CN/affix.md @@ -0,0 +1,55 @@ +## Affix 固钉 + +将页面元素固定在特定可视区域。 + +### 基本用法 + +固钉默认固定在页面顶部。 + +:::demo 通过设置 `offset` 属性来改变吸顶距离,默认值为 0。 +```html + + 距离顶部 120px + +``` +::: + +### 指定容器 + +通过设置 `target` 属性,让固钉始终保持在容器内,超过范围则隐藏。 + +:::demo 请注意容器避免出现滚动条。 +```html +
+ + 指定容器 + +
+``` +::: + +### 固定位置 + +Affix 组件提供了两个固定位置:`top` 和 `bottom`。 + +:::demo 通过设置 `position` 属性来改变固定位置,默认值为 `top` 。 +```html + + 距离底部 20px + +``` +::: + +### Attributes +| 参数 | 说明 | 类型 | 可选值 | 默认值 | +|---------- |-------------- |---------- |-------------------------------- |-------- | +| offset | 偏移距离 | number | — | 0 | +| position | 固钉位置 | string | top / bottom | top | +| target | 指定容器(CSS 选择器) | string | — | — | +| z-index | 固钉层级 | number | — | 100 | + +### Events +| 事件名称 | 说明 | 回调参数 | +|---------- |-------- |---------- | +| change | 固钉状态改变时触发的事件 | (value: boolean) | +| scroll | 滚动时触发的事件 | 滚动距离和固钉状态 | diff --git a/website/nav.config.json b/website/nav.config.json index d615858e83..48e6cd5f82 100644 --- a/website/nav.config.json +++ b/website/nav.config.json @@ -227,6 +227,10 @@ { "groupName": "Navigation", "list": [ + { + "path": "/affix", + "title": "Affix 固钉" + }, { "path": "/menu", "title": "NavMenu 导航菜单" @@ -541,6 +545,10 @@ { "groupName": "Navigation", "list": [ + { + "path": "/affix", + "title": "Affix" + }, { "path": "/menu", "title": "NavMenu" @@ -859,6 +867,10 @@ { "groupName": "Navigation", "list": [ + { + "path": "/affix", + "title": "Affix" + }, { "path": "/menu", "title": "NavMenu" @@ -1177,6 +1189,10 @@ { "groupName": "Navigation", "list": [ + { + "path": "/affix", + "title": "Affix" + }, { "path": "/menu", "title": "NavMenu" @@ -1495,6 +1511,10 @@ { "groupName": "Navigation", "list": [ + { + "path": "/affix", + "title": "Affix" + }, { "path": "/menu", "title": "NavMenu"