diff --git a/docs/markdown/v2-progress-tracking.md b/docs/markdown/v2-progress-tracking.md
index 181a168f1..a6873f327 100644
--- a/docs/markdown/v2-progress-tracking.md
+++ b/docs/markdown/v2-progress-tracking.md
@@ -52,7 +52,8 @@
## 新特性
- 新增`Skeleton`骨架屏组件
-- 新增`DropdownMenu`下拉菜单组件
+- 新增`IndexBar`、`IndexAnchor`索引栏组件
+- 新增`DropdownMenu`、`DropdownItem`下拉菜单组件
### ActionSheet
diff --git a/docs/src/WapApp.vue b/docs/src/WapApp.vue
index 19a08fa30..a0ff5a598 100644
--- a/docs/src/WapApp.vue
+++ b/docs/src/WapApp.vue
@@ -75,6 +75,11 @@ body {
-webkit-font-smoothing: antialiased;
}
+::-webkit-scrollbar {
+ width: 0;
+ background: transparent;
+}
+
.van-doc-nav-bar {
height: 56px;
line-height: 56px;
diff --git a/docs/src/demo-entry.js b/docs/src/demo-entry.js
index 167bc55f8..c0a78f54a 100644
--- a/docs/src/demo-entry.js
+++ b/docs/src/demo-entry.js
@@ -23,6 +23,7 @@ export default {
'goods-action': () => wrapper(import('../../packages/goods-action/demo'), 'goods-action'),
'icon': () => wrapper(import('../../packages/icon/demo'), 'icon'),
'image-preview': () => wrapper(import('../../packages/image-preview/demo'), 'image-preview'),
+ 'index-bar': () => wrapper(import('../../packages/index-bar/demo'), 'index-bar'),
'lazyload': () => wrapper(import('../../packages/lazyload/demo'), 'lazyload'),
'list': () => wrapper(import('../../packages/list/demo'), 'list'),
'loading': () => wrapper(import('../../packages/loading/demo'), 'loading'),
diff --git a/docs/src/doc.config.js b/docs/src/doc.config.js
index f416f57f8..2afcae2e3 100644
--- a/docs/src/doc.config.js
+++ b/docs/src/doc.config.js
@@ -259,6 +259,10 @@ module.exports = {
groupName: '导航组件',
icon: 'https://img.yzcdn.cn/vant/nav-0401.svg',
list: [
+ {
+ path: '/index-bar',
+ title: 'IndexBar 索引栏'
+ },
{
path: '/nav-bar',
title: 'NavBar 导航栏'
@@ -574,6 +578,10 @@ module.exports = {
groupName: 'Navigation Components',
icon: 'https://img.yzcdn.cn/vant/nav-0401.svg',
list: [
+ {
+ path: '/index-bar',
+ title: 'IndexBar'
+ },
{
path: '/nav-bar',
title: 'NavBar'
diff --git a/docs/src/docs-entry.js b/docs/src/docs-entry.js
index d3a8d2fff..0651306f9 100644
--- a/docs/src/docs-entry.js
+++ b/docs/src/docs-entry.js
@@ -53,6 +53,8 @@ export default {
'icon.zh-CN': () => import('../../packages/icon/zh-CN.md'),
'image-preview.en-US': () => import('../../packages/image-preview/en-US.md'),
'image-preview.zh-CN': () => import('../../packages/image-preview/zh-CN.md'),
+ 'index-bar.en-US': () => import('../../packages/index-bar/en-US.md'),
+ 'index-bar.zh-CN': () => import('../../packages/index-bar/zh-CN.md'),
'lazyload.en-US': () => import('../../packages/lazyload/en-US.md'),
'lazyload.zh-CN': () => import('../../packages/lazyload/zh-CN.md'),
'list.en-US': () => import('../../packages/list/en-US.md'),
diff --git a/packages/index-anchor/index.js b/packages/index-anchor/index.js
new file mode 100644
index 000000000..327bacd3d
--- /dev/null
+++ b/packages/index-anchor/index.js
@@ -0,0 +1,26 @@
+import { use } from '../utils';
+import { ChildrenMixin } from '../mixins/relation';
+
+const [sfc, bem] = use('index-anchor');
+
+export default sfc({
+ mixins: [ChildrenMixin('vanIndexBar', { indexKey: 'childrenIndex' })],
+
+ props: {
+ index: [String, Number]
+ },
+
+ methods: {
+ scrollIntoView() {
+ this.$el.scrollIntoView();
+ }
+ },
+
+ render(h) {
+ return (
+
+ {this.slots('default') ? this.slots('default') : this.index}
+
+ );
+ }
+});
diff --git a/packages/index-anchor/index.less b/packages/index-anchor/index.less
new file mode 100644
index 000000000..62f6a03f0
--- /dev/null
+++ b/packages/index-anchor/index.less
@@ -0,0 +1,8 @@
+@import '../style/var';
+
+.van-index-anchor {
+ padding: 0 15px;
+ font-size: 14px;
+ font-weight: 500;
+ line-height: 32px;
+}
diff --git a/packages/index-bar/demo/index.vue b/packages/index-bar/demo/index.vue
new file mode 100644
index 000000000..5862b6024
--- /dev/null
+++ b/packages/index-bar/demo/index.vue
@@ -0,0 +1,64 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ $t('title') + index }}
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/index-bar/en-US.md b/packages/index-bar/en-US.md
new file mode 100644
index 000000000..35838e692
--- /dev/null
+++ b/packages/index-bar/en-US.md
@@ -0,0 +1,75 @@
+## IndexBar
+
+### Install
+
+``` javascript
+import { IndexBar } from 'vant';
+
+Vue.use(IndexBar);
+```
+
+### Usage
+
+#### Basic Usage
+
+```html
+
+
+
+
+
+
+
+
+
+
+
+ ...
+
+```
+
+#### Custom Index List
+
+```html
+
+ Title 1
+
+
+
+
+ Title 2
+
+
+
+
+ ...
+
+```
+
+```js
+export default {
+ data() {
+ return {
+ indexList: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
+ }
+ }
+}
+```
+
+### IndexBar Props
+
+| Attribute | Description | Type | Default |
+|------|------|------|------|
+| index-list | Index List | `Array` | `A-Z` |
+
+### IndexAnchor Props
+
+| Attribute | Description | Type | Default |
+|------|------|------|------|
+| index | Index | `String | Number` | - |
+
+### IndexAnchor Slots
+
+| Name | Description |
+|------|------|
+| default | Anchor content, show index by default |
diff --git a/packages/index-bar/index.js b/packages/index-bar/index.js
new file mode 100644
index 000000000..c2fff1722
--- /dev/null
+++ b/packages/index-bar/index.js
@@ -0,0 +1,92 @@
+import { use } from '../utils';
+import { TouchMixin } from '../mixins/touch';
+import { ParentMixin } from '../mixins/relation';
+
+const [sfc, bem] = use('index-bar');
+
+export default sfc({
+ mixins: [TouchMixin, ParentMixin('vanIndexBar')],
+
+ props: {
+ indexList: {
+ type: Array,
+ default() {
+ const indexList = [];
+ const charCodeOfA = 'A'.charCodeAt(0);
+
+ for (let i = 0; i < 26; i++) {
+ indexList.push(String.fromCharCode(charCodeOfA + i));
+ }
+
+ return indexList;
+ }
+ }
+ },
+
+ methods: {
+ onClick(event) {
+ this.scrollToElement(event.target);
+ },
+
+ onTouchStart(event) {
+ this.touchStart(event);
+ },
+
+ onTouchMove(event) {
+ this.touchMove(event);
+
+ if (this.direction === 'vertical') {
+ /* istanbul ignore else */
+ if (event.cancelable) {
+ event.preventDefault();
+ }
+
+ const { clientX, clientY } = event.touches[0];
+ const target = document.elementFromPoint(clientX, clientY);
+ this.scrollToElement(target);
+ }
+ },
+
+ scrollToElement(element, setActive) {
+ if (!element) {
+ return;
+ }
+
+ const { index } = element.dataset;
+ if (!index) {
+ return;
+ }
+
+ const match = this.children.filter(item => String(item.index) === index);
+ if (match[0]) {
+ match[0].scrollIntoView();
+ }
+ },
+
+ onTouchEnd() {
+ this.active = null;
+ }
+ },
+
+ render(h) {
+ return (
+
+
+ {this.slots('default')}
+
+ );
+ }
+});
diff --git a/packages/index-bar/index.less b/packages/index-bar/index.less
new file mode 100644
index 000000000..8d606d1bb
--- /dev/null
+++ b/packages/index-bar/index.less
@@ -0,0 +1,22 @@
+@import '../style/var';
+
+.van-index-bar {
+ &__sidebar {
+ position: fixed;
+ display: flex;
+ top: 50%;
+ right: 0;
+ z-index: 1;
+ user-select: none;
+ text-align: center;
+ flex-direction: column;
+ transform: translateY(-50%);
+ }
+
+ &__index {
+ font-size: 10px;
+ font-weight: 500;
+ line-height: 14px;
+ padding: 0 3px 0 15px;
+ }
+}
diff --git a/packages/index-bar/test/__snapshots__/demo.spec.js.snap b/packages/index-bar/test/__snapshots__/demo.spec.js.snap
new file mode 100644
index 000000000..5f6173977
--- /dev/null
+++ b/packages/index-bar/test/__snapshots__/demo.spec.js.snap
@@ -0,0 +1,21 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`renders demo correctly 1`] = `
+
+`;
diff --git a/packages/index-bar/test/__snapshots__/index.spec.js.snap b/packages/index-bar/test/__snapshots__/index.spec.js.snap
new file mode 100644
index 000000000..9873e0449
--- /dev/null
+++ b/packages/index-bar/test/__snapshots__/index.spec.js.snap
@@ -0,0 +1,9 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`custom anchor text 1`] = `
+
+`;
diff --git a/packages/index-bar/test/demo.spec.js b/packages/index-bar/test/demo.spec.js
new file mode 100644
index 000000000..d647cfabc
--- /dev/null
+++ b/packages/index-bar/test/demo.spec.js
@@ -0,0 +1,4 @@
+import Demo from '../demo';
+import demoTest from '../../../test/demo-test';
+
+demoTest(Demo);
diff --git a/packages/index-bar/test/index.spec.js b/packages/index-bar/test/index.spec.js
new file mode 100644
index 000000000..7319fa19b
--- /dev/null
+++ b/packages/index-bar/test/index.spec.js
@@ -0,0 +1,85 @@
+import { mount, trigger, triggerDrag } from '../../../test/utils';
+import Vue from 'vue';
+import IndexBar from '..';
+import IndexAnchor from '../../index-anchor';
+
+Vue.use(IndexBar);
+Vue.use(IndexAnchor);
+
+function mockScrollIntoView() {
+ const fn = jest.fn();
+ Element.prototype.scrollIntoView = fn;
+ return fn;
+}
+
+test('custom anchor text', () => {
+ const wrapper = mount({
+ template: `
+
+ Title A
+ Title B
+
+ `
+ });
+
+ expect(wrapper).toMatchSnapshot();
+});
+
+test('click and scroll to anchor', () => {
+ const wrapper = mount({
+ template: `
+
+
+
+
+ `
+ });
+
+ const fn = mockScrollIntoView();
+ const indexes = wrapper.findAll('.van-index-bar__index');
+ indexes.at(0).trigger('click');
+ expect(fn).toHaveBeenCalledTimes(1);
+});
+
+test('touch and scroll to anchor', () => {
+ const wrapper = mount({
+ template: `
+
+
+
+
+
+ `
+ });
+
+ const fn = mockScrollIntoView();
+ const sidebar = wrapper.find('.van-index-bar__sidebar');
+ const indexes = wrapper.findAll('.van-index-bar__index');
+
+ document.elementFromPoint = function (x, y) {
+ const index = y / 100;
+
+ if (index === 1 || index === 2) {
+ return indexes.at(index).element;
+ }
+
+ if (index === 3) {
+ return {
+ dataset: {}
+ };
+ }
+ };
+
+ // horizontal drag
+ triggerDrag(sidebar, 100, 0);
+ expect(fn).toHaveBeenCalledTimes(0);
+
+ // vertiacl drag
+ trigger(sidebar, 'touchstart', 0, 0);
+ trigger(sidebar, 'touchmove', 0, 100);
+ trigger(sidebar, 'touchmove', 0, 200);
+ trigger(sidebar, 'touchmove', 0, 300);
+ trigger(sidebar, 'touchmove', 0, 400);
+ trigger(sidebar, 'touchend', 0, 400);
+ expect(fn).toHaveBeenCalledTimes(1);
+});
diff --git a/packages/index-bar/zh-CN.md b/packages/index-bar/zh-CN.md
new file mode 100644
index 000000000..75df19b0b
--- /dev/null
+++ b/packages/index-bar/zh-CN.md
@@ -0,0 +1,79 @@
+## IndexBar 索引栏
+
+### 使用指南
+
+``` javascript
+import { IndexBar, IndexAnchor } from 'vant';
+
+Vue.use(IndexBar).use(IndexAnchor);
+```
+
+### 代码演示
+
+#### 基础用法
+
+点击索引栏时,会自动跳转到对应的`IndexAnchor`锚点位置
+
+```html
+
+
+
+
+
+
+
+
+
+
+
+ ...
+
+```
+
+#### 自定义索引列表
+
+可以通过`index-list`属性自定义展示的索引字符列表,
+
+```html
+
+ 标题1
+
+
+
+
+ 标题2
+
+
+
+
+ ...
+
+```
+
+```js
+export default {
+ data() {
+ return {
+ indexList: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
+ }
+ }
+}
+```
+
+### IndexBar Props
+
+| 参数 | 说明 | 类型 | 默认值 | 版本 |
+|------|------|------|------|------|
+| index-list | 索引字符列表 | `Array` | `A-Z` | - |
+
+### IndexAnchor Props
+
+| 参数 | 说明 | 类型 | 默认值 | 版本 |
+|------|------|------|------|------|
+| index | 索引字符 | `String | Number` | - | - |
+
+### IndexAnchor Slots
+
+| 名称 | 说明 |
+|------|------|
+| default | 锚点位置显示内容,默认为索引字符 |
diff --git a/packages/index.less b/packages/index.less
index b069e1e0c..c05cab5ea 100644
--- a/packages/index.less
+++ b/packages/index.less
@@ -34,6 +34,8 @@
@import './stepper/index';
@import './swipe/index';
@import './swipe-item/index';
+@import './index-anchor/index';
+@import './index-bar/index';
/* form components */
@import './checkbox/index';
diff --git a/packages/index.ts b/packages/index.ts
index 3694875e7..82ce439d4 100644
--- a/packages/index.ts
+++ b/packages/index.ts
@@ -31,6 +31,8 @@ import GoodsActionButton from './goods-action-button';
import GoodsActionIcon from './goods-action-icon';
import Icon from './icon';
import ImagePreview from './image-preview';
+import IndexAnchor from './index-anchor';
+import IndexBar from './index-bar';
import Info from './info';
import Lazyload from './lazyload';
import List from './list';
@@ -114,6 +116,8 @@ const components = [
GoodsActionIcon,
Icon,
ImagePreview,
+ IndexAnchor,
+ IndexBar,
Info,
List,
Loading,
@@ -202,6 +206,8 @@ export {
GoodsActionIcon,
Icon,
ImagePreview,
+ IndexAnchor,
+ IndexBar,
Info,
Lazyload,
List,
diff --git a/packages/mixins/relation.js b/packages/mixins/relation.js
index 1f100daec..1ca3b3c86 100644
--- a/packages/mixins/relation.js
+++ b/packages/mixins/relation.js
@@ -1,4 +1,6 @@
-export function ChildrenMixin(parent) {
+export function ChildrenMixin(parent, options = {}) {
+ const indexKey = options.indexKey || 'index';
+
return {
inject: {
[parent]: {
@@ -11,7 +13,7 @@ export function ChildrenMixin(parent) {
return this[parent];
},
- index() {
+ [indexKey]() {
this.bindRelation();
return this.parent.children.indexOf(this);
}