diff --git a/docs/markdown/changelog.zh-CN.md b/docs/markdown/changelog.zh-CN.md index cdb578042..eec9ad1c1 100644 --- a/docs/markdown/changelog.zh-CN.md +++ b/docs/markdown/changelog.zh-CN.md @@ -2,6 +2,10 @@ ### [v2.0.0-beta.3](https://github.com/youzan/vant/tree/v2.0.0-beta.3) +##### IndexBar + +- 新增`sticky`参数 + ##### Rate - 新增`gutter`属性 diff --git a/packages/index-anchor/index.js b/packages/index-anchor/index.js index 327bacd3d..8524cfa61 100644 --- a/packages/index-anchor/index.js +++ b/packages/index-anchor/index.js @@ -10,6 +10,32 @@ export default sfc({ index: [String, Number] }, + data() { + return { + top: 0, + active: false + }; + }, + + computed: { + sticky() { + return this.active && this.parent.sticky; + }, + + anchorStyle() { + if (this.sticky) { + return { + top: `${this.top}px`, + zIndex: `${this.parent.zIndex}` + }; + } + } + }, + + mounted() { + this.height = this.$el.offsetHeight; + }, + methods: { scrollIntoView() { this.$el.scrollIntoView(); @@ -17,9 +43,16 @@ export default sfc({ }, render(h) { + const { sticky } = this; + return ( -
- {this.slots('default') ? this.slots('default') : this.index} +
+
+ {this.slots('default') || this.index} +
); } diff --git a/packages/index-anchor/index.less b/packages/index-anchor/index.less index 6a48362ed..d563ea06d 100644 --- a/packages/index-anchor/index.less +++ b/packages/index-anchor/index.less @@ -5,4 +5,15 @@ font-weight: @index-anchor-font-weight; font-size: @index-anchor-font-size; line-height: @index-anchor-line-height; + background-color: transparent; + transition: background-color .2s; + + &--sticky { + position: fixed; + top: 0; + right: 0; + left: 0; + z-index: 1; + background-color: #fff; + } } diff --git a/packages/index-bar/en-US.md b/packages/index-bar/en-US.md index 6a5b53651..cc348f679 100644 --- a/packages/index-bar/en-US.md +++ b/packages/index-bar/en-US.md @@ -64,6 +64,7 @@ export default { |------|------|------|------| | index-list | Index List | `Array` | `A-Z` | | z-index | z-index | `Number` | `1` | +| sticky | Whether to enable anchor sticky top | `Boolean` | `true` | ### IndexAnchor Props diff --git a/packages/index-bar/index.js b/packages/index-bar/index.js index 450e1b98f..723c8b78c 100644 --- a/packages/index-bar/index.js +++ b/packages/index-bar/index.js @@ -1,6 +1,8 @@ import { use } from '../utils'; import { TouchMixin } from '../mixins/touch'; import { ParentMixin } from '../mixins/relation'; +import { on, off } from '../utils/event'; +import { getScrollTop, getElementTop, getScrollEventTarget } from '../utils/scroll'; const [sfc, bem] = use('index-bar'); @@ -8,6 +10,10 @@ export default sfc({ mixins: [TouchMixin, ParentMixin('vanIndexBar')], props: { + sticky: { + type: Boolean, + default: true + }, zIndex: { type: Number, default: 1 @@ -27,13 +33,72 @@ export default sfc({ } }, + mounted() { + this.scroller = getScrollEventTarget(this.$el); + this.handler(true); + }, + + destroyed() { + this.handler(false); + }, + + activated() { + this.handler(true); + }, + + deactivated() { + this.handler(false); + }, + methods: { - onClick(event) { - this.scrollToElement(event.target); + handler(bind) { + /* istanbul ignore else */ + if (this.binded !== bind) { + this.binded = bind; + (bind ? on : off)(this.scroller, 'scroll', this.onScroll); + } }, - onTouchStart(event) { - this.touchStart(event); + onScroll() { + if (!this.sticky) { + return; + } + + const scrollTop = getScrollTop(this.scroller); + const rects = this.children.map(item => ({ + height: item.height, + top: getElementTop(item.$el) + })); + + const active = this.getActiveAnchorIndex(scrollTop, rects); + + this.children.forEach((item, index) => { + if (index === active) { + item.active = true; + item.top = Math.max(0, rects[index].top - scrollTop); + } else if (index === active - 1) { + const activeItemTop = rects[active].top - scrollTop; + item.active = activeItemTop > 0; + item.top = activeItemTop - rects[active].height; + } else { + item.active = false; + } + }); + }, + + getActiveAnchorIndex(scrollTop, rects) { + for (let i = this.children.length - 1; i >= 0; i--) { + const prevHeight = i > 0 ? rects[i - 1].height : 0; + + if (scrollTop + prevHeight >= rects[i].top) { + return i; + } + } + return -1; + }, + + onClick(event) { + this.scrollToElement(event.target); }, onTouchMove(event) { @@ -80,7 +145,7 @@ export default sfc({ class={bem('sidebar')} style={{ zIndex: this.zIndex }} onClick={this.onClick} - onTouchstart={this.onTouchStart} + onTouchstart={this.touchStart} onTouchmove={this.onTouchMove} onTouchend={this.onTouchEnd} onTouchcancel={this.onTouchEnd} diff --git a/packages/index-bar/test/__snapshots__/demo.spec.js.snap b/packages/index-bar/test/__snapshots__/demo.spec.js.snap index 578dae5bd..7254b82c2 100644 --- a/packages/index-bar/test/__snapshots__/demo.spec.js.snap +++ b/packages/index-bar/test/__snapshots__/demo.spec.js.snap @@ -15,7 +15,9 @@ exports[`renders demo correctly 1`] = `
ABCDEFGHIJKLMNOPQRSTUVWXYZ
-
A
+
+
A
+
文本
@@ -27,7 +29,9 @@ exports[`renders demo correctly 1`] = `
-
B
+
+
B
+
文本
@@ -39,7 +43,9 @@ exports[`renders demo correctly 1`] = `
-
C
+
+
C
+
文本
@@ -51,7 +57,9 @@ exports[`renders demo correctly 1`] = `
-
D
+
+
D
+
文本
@@ -63,7 +71,9 @@ exports[`renders demo correctly 1`] = `
-
E
+
+
E
+
文本
@@ -75,7 +85,9 @@ exports[`renders demo correctly 1`] = `
-
F
+
+
F
+
文本
@@ -87,7 +99,9 @@ exports[`renders demo correctly 1`] = `
-
G
+
+
G
+
文本
@@ -99,7 +113,9 @@ exports[`renders demo correctly 1`] = `
-
H
+
+
H
+
文本
@@ -111,7 +127,9 @@ exports[`renders demo correctly 1`] = `
-
I
+
+
I
+
文本
@@ -123,7 +141,9 @@ exports[`renders demo correctly 1`] = `
-
J
+
+
J
+
文本
@@ -135,7 +155,9 @@ exports[`renders demo correctly 1`] = `
-
K
+
+
K
+
文本
@@ -147,7 +169,9 @@ exports[`renders demo correctly 1`] = `
-
L
+
+
L
+
文本
@@ -159,7 +183,9 @@ exports[`renders demo correctly 1`] = `
-
M
+
+
M
+
文本
@@ -171,7 +197,9 @@ exports[`renders demo correctly 1`] = `
-
N
+
+
N
+
文本
@@ -183,7 +211,9 @@ exports[`renders demo correctly 1`] = `
-
O
+
+
O
+
文本
@@ -195,7 +225,9 @@ exports[`renders demo correctly 1`] = `
-
P
+
+
P
+
文本
@@ -207,7 +239,9 @@ exports[`renders demo correctly 1`] = `
-
Q
+
+
Q
+
文本
@@ -219,7 +253,9 @@ exports[`renders demo correctly 1`] = `
-
R
+
+
R
+
文本
@@ -231,7 +267,9 @@ exports[`renders demo correctly 1`] = `
-
S
+
+
S
+
文本
@@ -243,7 +281,9 @@ exports[`renders demo correctly 1`] = `
-
T
+
+
T
+
文本
@@ -255,7 +295,9 @@ exports[`renders demo correctly 1`] = `
-
U
+
+
U
+
文本
@@ -267,7 +309,9 @@ exports[`renders demo correctly 1`] = `
-
V
+
+
V
+
文本
@@ -279,7 +323,9 @@ exports[`renders demo correctly 1`] = `
-
W
+
+
W
+
文本
@@ -291,7 +337,9 @@ exports[`renders demo correctly 1`] = `
-
X
+
+
X
+
文本
@@ -303,7 +351,9 @@ exports[`renders demo correctly 1`] = `
-
Y
+
+
Y
+
文本
@@ -315,7 +365,9 @@ exports[`renders demo correctly 1`] = `
-
Z
+
+
Z
+
文本
diff --git a/packages/index-bar/test/__snapshots__/index.spec.js.snap b/packages/index-bar/test/__snapshots__/index.spec.js.snap index 13b9a695d..875e11e66 100644 --- a/packages/index-bar/test/__snapshots__/index.spec.js.snap +++ b/packages/index-bar/test/__snapshots__/index.spec.js.snap @@ -3,7 +3,11 @@ exports[`custom anchor text 1`] = `
ABCDEFGHIJKLMNOPQRSTUVWXYZ
-
Title A
-
Title B
+
+
Title A
+
+
+
Title B
+
`; diff --git a/packages/index-bar/zh-CN.md b/packages/index-bar/zh-CN.md index b946d3f44..de4466313 100644 --- a/packages/index-bar/zh-CN.md +++ b/packages/index-bar/zh-CN.md @@ -68,6 +68,7 @@ export default { |------|------|------|------|------| | index-list | 索引字符列表 | `Array` | `A-Z` | - | | z-index | z-index 层级 | `Number` | `1` | - | +| sticky | 是否开启锚点自动吸顶 | `Boolean` | `true` | - | ### IndexAnchor Props diff --git a/packages/slider/test/index.spec.js b/packages/slider/test/index.spec.js index 9e99e2053..7e34177b5 100644 --- a/packages/slider/test/index.spec.js +++ b/packages/slider/test/index.spec.js @@ -1,17 +1,17 @@ import Slider from '..'; -import { mount, trigger, triggerDrag } from '../../../test/utils'; +import { mount, trigger, triggerDrag, mockGetBoundingClientRect } from '../../../test/utils'; -function mockGetBoundingClientRect(vertical) { - Element.prototype.getBoundingClientRect = jest.fn(() => ({ +function mockRect(vertical) { + return mockGetBoundingClientRect({ width: vertical ? 0 : 100, height: vertical ? 100 : 0, top: vertical ? 0 : 100, left: vertical ? 100 : 0 - })); + }); } test('drag button', () => { - mockGetBoundingClientRect(); + const restoreMock = mockRect(); const wrapper = mount(Slider, { propsData: { @@ -37,10 +37,12 @@ test('drag button', () => { expect(wrapper).toMatchSnapshot(); expect(wrapper.emitted('drag-start')).toBeTruthy(); expect(wrapper.emitted('drag-end')).toBeTruthy(); + + restoreMock(); }); it('click bar', () => { - mockGetBoundingClientRect(); + const restoreMock = mockRect(); const wrapper = mount(Slider, { propsData: { @@ -59,10 +61,12 @@ it('click bar', () => { wrapper.setData({ disabled: false }); trigger(wrapper, 'click', 100, 0); expect(wrapper).toMatchSnapshot(); + + restoreMock(); }); test('drag button vertical', () => { - mockGetBoundingClientRect(true); + const restoreMock = mockRect(true); const wrapper = mount(Slider, { propsData: { @@ -78,10 +82,12 @@ test('drag button vertical', () => { const button = wrapper.find('.van-slider__button'); triggerDrag(button, 0, 50); expect(wrapper).toMatchSnapshot(); + + restoreMock(); }); it('click vertical', () => { - mockGetBoundingClientRect(true); + const restoreMock = mockRect(true); const wrapper = mount(Slider, { propsData: { @@ -96,4 +102,6 @@ it('click vertical', () => { trigger(wrapper, 'click', 0, 100); expect(wrapper).toMatchSnapshot(); + + restoreMock(); }); diff --git a/packages/swipe-cell/test/index.spec.js b/packages/swipe-cell/test/index.spec.js index 5b6d956db..43b317333 100644 --- a/packages/swipe-cell/test/index.spec.js +++ b/packages/swipe-cell/test/index.spec.js @@ -1,5 +1,5 @@ import SwipeCell from '..'; -import { mount, triggerDrag, later } from '../../../test/utils'; +import { mount, triggerDrag, later, mockGetBoundingClientRect } from '../../../test/utils'; const THRESHOLD = 0.15; const defaultProps = { @@ -13,12 +13,6 @@ const defaultProps = { } }; -function mockGetBoundingClientRect(vertical) { - Element.prototype.getBoundingClientRect = jest.fn(() => ({ - width: 50 - })); -} - it('drag and show left part', () => { const wrapper = mount(SwipeCell, defaultProps); @@ -98,7 +92,9 @@ it('disabled prop', () => { }); it('auto calc width', async () => { - mockGetBoundingClientRect(); + const restoreMock = mockGetBoundingClientRect({ + width: 50 + }); const wrapper = mount(SwipeCell, { scopedSlots: defaultProps.scopedSlots @@ -107,4 +103,6 @@ it('auto calc width', async () => { await later(); triggerDrag(wrapper, 100, 0); expect(wrapper).toMatchSnapshot(); + + restoreMock(); }); diff --git a/packages/swipe/test/demo.spec.js b/packages/swipe/test/demo.spec.js index e3c3b120a..b9b0acf0f 100644 --- a/packages/swipe/test/demo.spec.js +++ b/packages/swipe/test/demo.spec.js @@ -1,12 +1,17 @@ import Demo from '../demo'; import demoTest from '../../../test/demo-test'; +import { mockGetBoundingClientRect } from '../../../test/utils'; -function mockGetBoundingClientRect(vertical) { - Element.prototype.getBoundingClientRect = jest.fn(() => ({ - width: 100, - height: 100 - })); -} +let restore; -mockGetBoundingClientRect(); -demoTest(Demo); +demoTest(Demo, { + hookBeforeTest: () => { + restore = mockGetBoundingClientRect({ + width: 100, + height: 100 + }); + }, + hookAfterTest: () => { + restore(); + } +}); diff --git a/packages/tabs/index.js b/packages/tabs/index.js index 3493b330b..ef58e7af8 100644 --- a/packages/tabs/index.js +++ b/packages/tabs/index.js @@ -239,6 +239,7 @@ export default sfc({ const elTopToPageTop = getElementTop(this.$el); const elBottomToPageTop = elTopToPageTop + this.$el.offsetHeight - this.$refs.wrap.offsetHeight; + if (scrollTop > elBottomToPageTop) { this.position = 'bottom'; } else if (scrollTop > elTopToPageTop) { @@ -246,10 +247,12 @@ export default sfc({ } else { this.position = ''; } + const scrollParams = { scrollTop, isFixed: this.position === 'top' }; + this.$emit('scroll', scrollParams); }, diff --git a/test/demo-test.ts b/test/demo-test.ts index 15ef2d3d0..bc4de3552 100644 --- a/test/demo-test.ts +++ b/test/demo-test.ts @@ -13,8 +13,12 @@ const empty = { Vue.component('demo-block', empty); Vue.component('demo-section', empty); -export default function (Demo: any) { +export default function (Demo: any, option: any = {}) { test('renders demo correctly', async () => { + if (option.hookBeforeTest) { + option.hookBeforeTest(); + } + if (Demo.i18n) { Locale.add(Demo.i18n); } @@ -24,5 +28,9 @@ export default function (Demo: any) { await later(); expect(wrapper).toMatchSnapshot(); + + if (option.hookAfterTest) { + option.hookAfterTest(); + } }); } diff --git a/test/utils.ts b/test/utils.ts index a779403ab..7a1ce140f 100644 --- a/test/utils.ts +++ b/test/utils.ts @@ -70,3 +70,13 @@ export function later(delay: number = 0): Promise { export function transitionStub(): void { Vue.component('transition', TransitionStub as any); } + +export function mockGetBoundingClientRect(rect: ClientRect | DOMRect): Function { + const originMethod = Element.prototype.getBoundingClientRect; + + Element.prototype.getBoundingClientRect = jest.fn(() => rect); + + return function () { + Element.prototype.getBoundingClientRect = originMethod; + }; +}