Skip to content

Commit

Permalink
feat: ✨ implement partail index-bar
Browse files Browse the repository at this point in the history
  • Loading branch information
huang-xiao-jian committed Jun 2, 2020
1 parent 0127c15 commit 26bbc76
Show file tree
Hide file tree
Showing 9 changed files with 351 additions and 0 deletions.
22 changes: 22 additions & 0 deletions packages/IndexAnchor/IndexAnchor.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
.van-index-anchor {
padding: 0 16px;
padding: var(--index-anchor-padding, 0 16px);
color: #323233;
color: var(--index-anchor-text-color, #323233);
font-weight: 500;
font-weight: var(--index-anchor-font-weight, 500);
font-size: 14px;
font-size: var(--index-anchor-font-size, 14px);
line-height: 32px;
line-height: var(--index-anchor-line-height, 32px);
background-color: transparent;
background-color: var(--index-anchor-background-color, transparent);
}
.van-index-anchor--active {
right: 0;
left: 0;
color: #07c160;
color: var(--index-anchor-active-text-color, #07c160);
background-color: #fff;
background-color: var(--index-anchor-active-background-color, #fff);
}
62 changes: 62 additions & 0 deletions packages/IndexAnchor/IndexAnchor.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
// packages
import React, { FunctionComponent, useContext, useMemo } from 'react';
import clsx from 'clsx';
import { View } from 'remax/wechat';
import { useNativeEffect } from 'remax';
// internal
import IndexBarContext from '../IndexBar/IndexBarContext';
import './IndexAnchor.css';

// 默认值填充属性
interface IndexAnchorProps {
// 容器类名,用以覆盖内部
className?: string;
// 索引字符
index: string | number;
}

// TODO - adapt stickyOffsetTop
const IndexAnchor: FunctionComponent<IndexAnchorProps> = (props) => {
// index-bar relation
const {
activeAnchorIndex,
scrollTop,
wrapperStyle,
anchorStyle,
} = useContext(IndexBarContext);
const { className, index } = props;
const classnames = {
container: clsx(className, 'van-index-anchor-wrapper'),
anchor: clsx('van-index-anchor', {
'van-index-anchor--active': activeAnchorIndex === index,
'van-hairline--bottom': activeAnchorIndex === index,
}),
};

// 新增属性,便于获取实际节点
const id = useMemo(() => `van-index-anchor-wrapper-${index}`, [index]);

useNativeEffect(() => {
wx.createSelectorQuery()
.select(`#${id}`)
.boundingClientRect()
.exec(([rect]: [WechatMiniprogram.BoundingClientRectResult]) => {
if (activeAnchorIndex === index) {
wx.pageScrollTo({
// duration: 0,
scrollTop: scrollTop + rect.top,
});
}
});
}, [activeAnchorIndex]);

return (
<View id={id} style={wrapperStyle} className={classnames.container}>
<View style={anchorStyle} className={classnames.anchor}>
{index}
</View>
</View>
);
};

export default IndexAnchor;
4 changes: 4 additions & 0 deletions packages/IndexAnchor/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
/**
* @description - nothing but export component
*/
export { default } from './IndexAnchor';
22 changes: 22 additions & 0 deletions packages/IndexBar/IndexBar.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
.van-index-bar {
position: relative;
}
.van-index-bar__sidebar {
position: fixed;
top: 50%;
right: 0;
display: flex;
flex-direction: column;
text-align: center;
transform: translateY(-50%);
user-select: none;
}
.van-index-bar__index {
font-weight: 500;
padding: 0 4px 0 16px;
padding: 0 var(--padding-base, 4px) 0 var(--padding-md, 16px);
font-size: 10px;
font-size: var(--index-bar-index-font-size, 10px);
line-height: 14px;
line-height: var(--index-bar-index-line-height, 14px);
}
149 changes: 149 additions & 0 deletions packages/IndexBar/IndexBar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
// packages
import React, { FunctionComponent, useRef, useState } from 'react';
import clsx from 'clsx';
import { useNativeEffect } from 'remax';
import { View } from 'remax/wechat';
import { usePageEvent } from 'remax/macro';
// internal
import IndexBarContext, { IndexBarContextPayload } from './IndexBarContext';
import withDefaultProps from '../tools/with-default-props-advance';
import pickStyle from '../tools/pick-style';
import './IndexBar.css';

// 默认值填充属性
interface NeutralIndexBarProps {
indexList: string[];
zIndex: number;
sticky: boolean;
stickyOffsetTop: number;
highlightColor: string;
}

interface ExogenousIndexBarProps {
// 容器类名,用以覆盖内部
className?: string;
// 选择字符回调
onSelect?: (event: { detail: string }) => void;
}

type IndexBarProps = NeutralIndexBarProps & ExogenousIndexBarProps;

const DefaultIndexBarProps: NeutralIndexBarProps = {
indexList: 'A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P,Q,R,S,T,U,V,W,X,Y,Z'.split(','),
zIndex: 1,
sticky: true,
stickyOffsetTop: 0,
highlightColor: '#07c160',
};

// TODO - support sticky anchor
// TODO - support sync index from page scroll to sidebar
const IndexBar: FunctionComponent<IndexBarProps> = (props) => {
const {
className,
indexList,
zIndex,
highlightColor,
children,
onSelect,
} = props;
const classnames = {
container: clsx(className, 'van-index-bar'),
};

// continuous track scrollTop, weired, but better way not found
const scrollTop = useRef(0);

usePageEvent('onPageScroll', (event: { scrollTop: number }) => {
scrollTop.current = event.scrollTop;
});

const [activeAnchorIndex, setActiveAnchorIndex] = useState('');
// context 穿透
const payload: IndexBarContextPayload = {
activeAnchorIndex,
scrollTop: scrollTop.current,
};
// 事件冒泡
const onSelectWrap = (event: any) => {
const { index } = event.target.dataset;

// 冒泡
if (typeof onSelect === 'function') {
onSelect({ detail: index });
}

// 滚动
setActiveAnchorIndex(index);
};

// 计算滚动阶段 active anchor index
const sidebar = useRef({
height: 0,
top: 0,
});

// TODO - support multiple sidebar within page, selector unique
useNativeEffect(() => {
wx.createSelectorQuery()
.select('.van-index-bar__sidebar')
.boundingClientRect()
.exec(([rect]: [WechatMiniprogram.BoundingClientRectResult]) => {
sidebar.current.height = rect.height;
sidebar.current.top = rect.top;
});
}, [indexList]);

const onTouchMove = ({ touches: [touch] }: any) => {
const { length } = indexList;
const space = sidebar.current.height / length;
const theory = {
index: Math.floor((touch.clientY - sidebar.current.top) / space),
};

if (theory.index < 0) {
theory.index = 0;
} else if (theory.index > length - 1) {
theory.index = length - 1;
}

// convert serial number into actual index
setActiveAnchorIndex(indexList[theory.index]);
};

return (
<IndexBarContext.Provider value={payload}>
<View className={classnames.container}>
{children}
<View
className="van-index-bar__sidebar"
onClick={onSelectWrap}
onTouchMove={onTouchMove}
>
{indexList
.map((index) => ({
index,
style: pickStyle({
zIndex: zIndex + 1,
color: activeAnchorIndex === index ? highlightColor : '',
}),
}))
.map((item) => (
<View
className="van-index-bar__index"
key={item.index}
style={item.style}
data-index={item.index}
>
{item.index}
</View>
))}
</View>
</View>
</IndexBarContext.Provider>
);
};

export default withDefaultProps<ExogenousIndexBarProps, NeutralIndexBarProps>(
DefaultIndexBarProps
)(IndexBar);
17 changes: 17 additions & 0 deletions packages/IndexBar/IndexBarContext.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// packages
import { CSSProperties, createContext } from 'react';

export interface IndexBarContextPayload {
scrollTop: number;
activeAnchorIndex: string | number;
wrapperStyle?: CSSProperties;
anchorStyle?: CSSProperties;
}

const IndexBarContext = createContext<IndexBarContextPayload>({
// none anchor selected
scrollTop: 0,
activeAnchorIndex: '',
});

export default IndexBarContext;
4 changes: 4 additions & 0 deletions packages/IndexBar/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
/**
* @description - nothing but export component
*/
export { default } from './IndexBar';
1 change: 1 addition & 0 deletions public/app.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { AppConfig } from 'remax/wechat';

const config: AppConfig = {
pages: [
'pages/index-bar/index',
'pages/nav-bar/index',
'pages/tabbar/index',
'pages/tree-select/index',
Expand Down
70 changes: 70 additions & 0 deletions public/pages/index-bar/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
// packages
import * as React from 'react';
import { View, Text } from 'remax/wechat';

// internal
import IndexBar from '../../../packages/IndexBar';
import IndexAnchor from '../../../packages/IndexAnchor';
import Cell from '../../../packages/Cell';

export default () => {
return (
<View className="demo-block">
<Text className="demo-block__title">基础用法</Text>
<IndexBar>
<View>
<IndexAnchor index="A">标题 1</IndexAnchor>
<Cell title="文本" />
<Cell title="文本" />
<Cell title="文本" />
</View>
<View>
<IndexAnchor index="B">标题 1</IndexAnchor>
<Cell title="文本" />
<Cell title="文本" />
<Cell title="文本" />
</View>
<View>
<IndexAnchor index="C">标题 1</IndexAnchor>
<Cell title="文本" />
<Cell title="文本" />
<Cell title="文本" />
</View>
<View>
<IndexAnchor index="D">标题 1</IndexAnchor>
<Cell title="文本" />
<Cell title="文本" />
<Cell title="文本" />
<Cell title="文本" />
<Cell title="文本" />
<Cell title="文本" />
</View>
<View>
<IndexAnchor index="F">标题 1</IndexAnchor>
<Cell title="文本" />
<Cell title="文本" />
<Cell title="文本" />
</View>
<View>
<IndexAnchor index="H">标题 1</IndexAnchor>
<Cell title="文本" />
<Cell title="文本" />
<Cell title="文本" />
</View>
<View>
<IndexAnchor index="W">标题 1</IndexAnchor>
<Cell title="文本" />
<Cell title="文本" />
<Cell title="文本" />
</View>
<View>
<IndexAnchor index="Z">标题 1</IndexAnchor>
<Cell title="文本" />
<Cell title="文本" />
<Cell title="文本" />
<Cell title="文本" />
</View>
</IndexBar>
</View>
);
};

0 comments on commit 26bbc76

Please sign in to comment.