diff --git a/packages/IndexAnchor/IndexAnchor.css b/packages/IndexAnchor/IndexAnchor.css new file mode 100644 index 0000000..e695fca --- /dev/null +++ b/packages/IndexAnchor/IndexAnchor.css @@ -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); +} diff --git a/packages/IndexAnchor/IndexAnchor.tsx b/packages/IndexAnchor/IndexAnchor.tsx new file mode 100644 index 0000000..c1e12fb --- /dev/null +++ b/packages/IndexAnchor/IndexAnchor.tsx @@ -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 = (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 ( + + + {index} + + + ); +}; + +export default IndexAnchor; diff --git a/packages/IndexAnchor/index.ts b/packages/IndexAnchor/index.ts new file mode 100644 index 0000000..9490498 --- /dev/null +++ b/packages/IndexAnchor/index.ts @@ -0,0 +1,4 @@ +/** + * @description - nothing but export component + */ +export { default } from './IndexAnchor'; diff --git a/packages/IndexBar/IndexBar.css b/packages/IndexBar/IndexBar.css new file mode 100644 index 0000000..4bb140a --- /dev/null +++ b/packages/IndexBar/IndexBar.css @@ -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); +} diff --git a/packages/IndexBar/IndexBar.tsx b/packages/IndexBar/IndexBar.tsx new file mode 100644 index 0000000..fcc0814 --- /dev/null +++ b/packages/IndexBar/IndexBar.tsx @@ -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 = (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 ( + + + {children} + + {indexList + .map((index) => ({ + index, + style: pickStyle({ + zIndex: zIndex + 1, + color: activeAnchorIndex === index ? highlightColor : '', + }), + })) + .map((item) => ( + + {item.index} + + ))} + + + + ); +}; + +export default withDefaultProps( + DefaultIndexBarProps +)(IndexBar); diff --git a/packages/IndexBar/IndexBarContext.ts b/packages/IndexBar/IndexBarContext.ts new file mode 100644 index 0000000..4514fbc --- /dev/null +++ b/packages/IndexBar/IndexBarContext.ts @@ -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({ + // none anchor selected + scrollTop: 0, + activeAnchorIndex: '', +}); + +export default IndexBarContext; diff --git a/packages/IndexBar/index.ts b/packages/IndexBar/index.ts new file mode 100644 index 0000000..4299a94 --- /dev/null +++ b/packages/IndexBar/index.ts @@ -0,0 +1,4 @@ +/** + * @description - nothing but export component + */ +export { default } from './IndexBar'; diff --git a/public/app.config.ts b/public/app.config.ts index 486ece4..6f1a265 100644 --- a/public/app.config.ts +++ b/public/app.config.ts @@ -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', diff --git a/public/pages/index-bar/index.tsx b/public/pages/index-bar/index.tsx new file mode 100644 index 0000000..7cfb168 --- /dev/null +++ b/public/pages/index-bar/index.tsx @@ -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 ( + + 基础用法 + + + 标题 1 + + + + + + 标题 1 + + + + + + 标题 1 + + + + + + 标题 1 + + + + + + + + + 标题 1 + + + + + + 标题 1 + + + + + + 标题 1 + + + + + + 标题 1 + + + + + + + + ); +};