[new feature] add sticky component (#3888)

This commit is contained in:
neverland
2019-07-18 17:48:18 +08:00
committed by GitHub
parent e1021e70ba
commit b273c89b3a
23 changed files with 651 additions and 124 deletions
+66
View File
@@ -0,0 +1,66 @@
# Sticky
### Install
``` javascript
import { Sticky } from 'vant';
Vue.use(Sticky);
```
## Usage
### Basic Usage
```html
<van-sticky>
<van-button type="primary">Basic Usage</van-button>
</van-sticky>
```
### Offset Top
```html
<van-sticky :offset-top="50">
<van-button type="info">Offset Top</van-button>
</van-sticky>
```
### Set Container
```html
<div ref="container" style="height: 150px;">
<van-sticky :container="container">
<van-button type="warning">Set Container</van-button>
</van-sticky>
</div>
```
```js
export default {
data() {
return {
container: null
};
},
mounted() {
this.container = this.$refs.container;
}
};
```
## API
### Props
| Attribute | Description | Type | Default |
|------|------|------|------|
| offset-top | Offset top | `number` | `0` | - |
| z-index | z-index when sticky | `number` | `99` | - |
| container | Container DOM | `HTMLElement` | - | - |
### Events
| Event | Description | Arguments |
|------|------|------|
| scroll | Triggered when scroll | object: { scrollTop, isFixed } |
+76
View File
@@ -0,0 +1,76 @@
# Sticky 粘性布局
### 介绍
Sticky 组件与 CSS 中`position: sticky`属性实现的效果一致,当组件在屏幕范围内时,会按照正常的布局排列,当组件滚出屏幕范围时,始终会固定在屏幕顶部。
### 引入
``` javascript
import { Sticky } from 'vant';
Vue.use(Sticky);
```
## 代码演示
### 基础用法
将内容包裹在`Sticky`组件内即可
```html
<van-sticky>
<van-button type="primary">基础用法</van-button>
</van-sticky>
```
### 吸顶距离
通过`offset-top`属性可以设置组件在吸顶时与顶部的距离
```html
<van-sticky :offset-top="50">
<van-button type="info">吸顶距离</van-button>
</van-sticky>
```
### 指定容器
通过`container`属性可以指定组件的容器,页面滚动时,组件会始终保持在容器范围内,当组件即将超出容器底部时,会固定在容器的底部
```html
<div ref="container" style="height: 150px;">
<van-sticky :container="container">
<van-button type="warning">指定容器</van-button>
</van-sticky>
</div>
```
```js
export default {
data() {
return {
container: null
};
},
mounted() {
this.container = this.$refs.container;
}
};
```
## API
### Props
| 参数 | 说明 | 类型 | 默认值 | 版本 |
|------|------|------|------|------|
| offset-top | 吸顶时与顶部的距离,单位`px` | `number` | `0` | - |
| z-index | 吸顶时的 z-index | `number` | `99` | - |
| container | 容器对应的 HTML 节点 | `HTMLElement` | - | - |
### Events
| 事件名 | 说明 | 回调参数 |
|------|------|------|
| scroll | 滚动时触发 | { scrollTop: 距离顶部位置, isFixed: 是否吸顶 } |
+76
View File
@@ -0,0 +1,76 @@
<template>
<demo-section>
<demo-block :title="$t('basicUsage')">
<van-sticky>
<van-button
type="primary"
style="margin-left: 15px;"
>
{{ $t('basicUsage') }}
</van-button>
</van-sticky>
</demo-block>
<demo-block :title="$t('offsetTop')">
<van-sticky :offset-top="50">
<van-button
type="info"
style="margin-left: 115px;"
>
{{ $t('offsetTop') }}
</van-button>
</van-sticky>
</demo-block>
<demo-block :title="$t('setContainer')">
<div
ref="container"
style="height: 150px; background-color: #fff;"
>
<van-sticky :container="container">
<van-button
type="warning"
style="margin-left: 215px;"
>
{{ $t('setContainer') }}
</van-button>
</van-sticky>
</div>
</demo-block>
</demo-section>
</template>
<script>
export default {
i18n: {
'zh-CN': {
offsetTop: '吸顶距离',
setContainer: '指定容器'
},
'en-US': {
offsetTop: 'Offset Top',
setContainer: 'Set Container'
}
},
data() {
return {
container: null
};
},
mounted() {
this.container = this.$refs.container;
}
};
</script>
<style lang="less">
.demo-sticky {
height: 200vh;
.van-button {
margin-left: 15px;
}
}
</style>
+119
View File
@@ -0,0 +1,119 @@
import { createNamespace, isDef } from '../utils';
import { BindEventMixin } from '../mixins/bind-event';
import { getScrollTop, getElementTop, getScrollEventTarget } from '../utils/dom/scroll';
const [createComponent, bem] = createNamespace('sticky');
export default createComponent({
mixins: [
BindEventMixin(function (bind) {
if (!this.scroller) {
this.scroller = getScrollEventTarget(this.$el);
}
bind(this.scroller, 'scroll', this.onScroll, true);
this.onScroll();
})
],
props: {
zIndex: Number,
container: null,
offsetTop: {
type: Number,
default: 0
}
},
data() {
return {
fixed: false,
height: 0,
transform: 0
};
},
computed: {
style() {
if (!this.fixed) {
return;
}
const style = {};
if (isDef(this.zIndex)) {
style.zIndex = this.zIndex;
}
if (this.offsetTop && this.fixed) {
style.top = `${this.offsetTop}px`;
}
if (this.transform) {
style.transform = `translate3d(0, ${this.transform}px, 0)`;
}
return style;
}
},
methods: {
onScroll() {
this.height = this.$el.offsetHeight;
const { container, offsetTop } = this;
const scrollTop = getScrollTop(this.scroller);
const topToPageTop = getElementTop(this.$el);
const emitScrollEvent = () => {
this.$emit('scroll', {
scrollTop,
isFixed: this.fixed
});
};
// The sticky component should be kept inside the container element
if (container) {
const bottomToPageTop = topToPageTop + container.offsetHeight;
if (scrollTop + offsetTop + this.height > bottomToPageTop) {
const distanceToBottom = this.height + scrollTop - bottomToPageTop;
if (distanceToBottom < this.height) {
this.fixed = true;
this.transform = -(distanceToBottom + offsetTop);
} else {
this.fixed = false;
}
emitScrollEvent();
return;
}
}
if (scrollTop + offsetTop > topToPageTop) {
this.fixed = true;
this.transform = 0;
} else {
this.fixed = false;
}
emitScrollEvent();
}
},
render(h) {
const { fixed } = this;
const style = {
height: fixed ? `${this.height}px` : null
};
return (
<div style={style}>
<div class={bem({ fixed })} style={this.style}>
{this.slots()}
</div>
</div>
);
}
});
+11
View File
@@ -0,0 +1,11 @@
@import '../style/var';
.van-sticky {
&--fixed {
position: fixed;
top: 0;
right: 0;
left: 0;
z-index: @sticky-z-index;
}
}
@@ -0,0 +1,29 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders demo correctly 1`] = `
<div>
<div>
<div>
<div class="van-sticky"><button class="van-button van-button--primary van-button--normal" style="margin-left: 15px;"><span class="van-button__text">
基础用法
</span></button></div>
</div>
</div>
<div>
<div style="height: 0px;">
<div class="van-sticky van-sticky--fixed" style="top: 50px;"><button class="van-button van-button--info van-button--normal" style="margin-left: 115px;"><span class="van-button__text">
吸顶距离
</span></button></div>
</div>
</div>
<div>
<div style="height: 150px; background-color: rgb(255, 255, 255);">
<div>
<div class="van-sticky"><button class="van-button van-button--warning van-button--normal" style="margin-left: 215px;"><span class="van-button__text">
指定容器
</span></button></div>
</div>
</div>
</div>
</div>
`;
@@ -0,0 +1,53 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`container prop 1`] = `
<div style="height: 20px;">
<div style="height: 10px;">
<div class="van-sticky van-sticky--fixed" style="transform: translate3d(0, -5px, 0);">
Content
</div>
</div>
</div>
`;
exports[`container prop 2`] = `
<div style="height: 20px;">
<div style="height: 10px;">
<div class="van-sticky" style="">
Content
</div>
</div>
</div>
`;
exports[`offset-top prop 1`] = `
<div style="height: 10px;">
<div class="van-sticky van-sticky--fixed" style="top: 10px;">
Content
</div>
</div>
`;
exports[`sticky to top 1`] = `
<div style="height: 10px;">
<div class="van-sticky">
Content
</div>
</div>
`;
exports[`sticky to top 2`] = `
<div style="height: 10px;">
<div class="van-sticky van-sticky--fixed">
Content
</div>
</div>
`;
exports[`z-index prop 1`] = `
<div style="height: 10px;">
<div class="van-sticky van-sticky--fixed" style="z-index: 0;">
Content
</div>
</div>
`;
+6
View File
@@ -0,0 +1,6 @@
import Demo from '../demo';
import demoTest from '../../../test/demo-test';
import { mockHTMLElementOffset } from '../../../test/utils';
mockHTMLElementOffset();
demoTest(Demo);
+77
View File
@@ -0,0 +1,77 @@
import { mount, mockScrollTop, mockHTMLElementOffset } from '../../../test/utils';
import Vue from 'vue';
import Sticky from '..';
Vue.use(Sticky);
test('sticky to top', () => {
const wrapper = mount({
template: `
<van-sticky style="height: 10px;">
Content
</van-sticky>
`
});
expect(wrapper).toMatchSnapshot();
mockScrollTop(100);
expect(wrapper).toMatchSnapshot();
mockScrollTop(0);
});
test('z-index prop', () => {
const wrapper = mount({
template: `
<van-sticky style="height: 10px;" :z-index="0">
Content
</van-sticky>
`
});
mockHTMLElementOffset();
mockScrollTop(100);
expect(wrapper).toMatchSnapshot();
mockScrollTop(0);
});
test('offset-top prop', () => {
const wrapper = mount({
template: `
<van-sticky style="height: 10px;" :offset-top="10">
Content
</van-sticky>
`
});
mockHTMLElementOffset();
mockScrollTop(100);
expect(wrapper).toMatchSnapshot();
mockScrollTop(0);
});
test('container prop', () => {
const wrapper = mount({
template: `
<div ref="container" style="height: 20px;">
<van-sticky ref="sticky" style="height: 10px;" :container="container">
Content
</van-sticky>
</div>
`,
data() {
return {
container: null
};
},
mounted() {
this.container = this.$refs.container;
mockHTMLElementOffset();
}
});
mockScrollTop(15);
expect(wrapper).toMatchSnapshot();
mockScrollTop(25);
expect(wrapper).toMatchSnapshot();
mockScrollTop(0);
});