Skip to content

Commit

Permalink
feat(SideBar): 创建SideBar组件 (#491)
Browse files Browse the repository at this point in the history
* feat(SideBar): 升级SideBar样式

* feat(SideBar): 创建SideBar组件

* feat(SideBar): 创建SideBar文档

* fix(Grid): 补充Grid遗漏的type

* chore: update snap

* test(SideBar): 测试增加React路由

* test(SideBar): update snap

* docs(SideBar): 解决演示用例样式冲突问题

* test(SideBar): update snap

* docs(SideBar): 修复示例内容体高度样式

* refactor(SideBar): 按规范改用useDefaultProps、usePrefixClass

* docs(SideBar): 移除多余API文档

* chore(SideBar):  update tdesign-api

* test: update snap

* docs(SideBar): 删除多余示例参数

* chore(SideBar):  update tdesign-api

* chore: 变更common分支

* chore(SideBar):  update tdesign-api

* chore(SideBar):  update tdesign-api

* chore(SideBar): update api

* perf(Demo): 示例头部的返回按钮优化为按需引入

* chore: update snapshot

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
  • Loading branch information
tobytovi and github-actions[bot] authored Aug 28, 2024
1 parent 8a52e88 commit 2cb1212
Show file tree
Hide file tree
Showing 29 changed files with 6,581 additions and 253 deletions.
11 changes: 10 additions & 1 deletion site/mobile/components/Header.jsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,22 @@
import React from 'react';
import { ChevronLeftIcon } from 'tdesign-icons-react';
import { useSearchParams, useNavigate } from 'react-router-dom';

const THeader = (prop) => {
const { title } = prop;
const [searchParams] = useSearchParams();
const navigate = useNavigate();

const showNavBack = !!searchParams.get('showNavBack');

const navBack = () => navigate(-1);

return (
<>
{title ? (
<div className="tdesign-demo-topnav">
<div className="tdesign-demo-topnav-title">{title}</div>
{/* <chevron-left-icon className="tdesign-demo-topnav__back" name="chevron-left"/> */}
{showNavBack && <ChevronLeftIcon className="tdesign-demo-topnav__back" onClick={navBack} />}
</div>
) : null}
</>
Expand Down
25 changes: 25 additions & 0 deletions site/mobile/mobile.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,31 @@ export default {
name: 'navbar',
component: () => import('tdesign-mobile-react/navbar/_example/index.tsx'),
},
{
title: 'SideBar 侧边栏',
name: 'side-bar',
component: () => import('tdesign-mobile-react/side-bar/_example/index.tsx'),
},
{
title: 'SideBar 侧边栏',
name: 'side-bar-base',
component: () => import('tdesign-mobile-react/side-bar/_example/base.tsx'),
},
{
title: 'SideBar 侧边栏',
name: 'side-bar-switch',
component: () => import('tdesign-mobile-react/side-bar/_example/switch.tsx'),
},
{
title: 'SideBar 侧边栏',
name: 'side-bar-with-icon',
component: () => import('tdesign-mobile-react/side-bar/_example/with-icon.tsx'),
},
{
title: 'SideBar 侧边栏',
name: 'side-bar-custom',
component: () => import('tdesign-mobile-react/side-bar/_example/custom.tsx'),
},
{
title: 'SwipeCell 滑动单元格',
name: 'swipe-cell',
Expand Down
6 changes: 6 additions & 0 deletions site/web/site.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,12 @@ export default {
path: '/mobile-react/components/navbar',
component: () => import('tdesign-mobile-react/navbar/navbar.md'),
},
{
title: 'SideBar 侧边栏',
name: 'side-bar',
path: '/mobile-react/components/side-bar',
component: () => import('tdesign-mobile-react/side-bar/side-bar.md'),
},
{
title: 'Tabs 选项卡',
name: 'tabs',
Expand Down
4 changes: 4 additions & 0 deletions src/grid/type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ export interface TdGridProps {
* @default default
*/
theme?: 'default' | 'card';
/**
* 标签栏内容
*/
children?: TNode;
}

export interface TdGridItemProps {
Expand Down
88 changes: 88 additions & 0 deletions src/side-bar/SideBar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import React, { forwardRef, memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import type { StyledProps } from '../common';
import type { TdSideBarProps } from './type';
import { sideBarDefaultProps } from './defaultProps';
import { SideBarProvider } from './SideBarContext';
import useDefault from '../_util/useDefault';
import parseTNode from '../_util/parseTNode';
import useDefaultProps from '../hooks/useDefaultProps';
import { usePrefixClass } from '../hooks/useClass';

export interface SideBarProps extends TdSideBarProps, StyledProps {}

/**
* SideBar is a sidebar component that can be used to display a list of items in a collapsible menu.
*
* @param {Object} props - The properties of the SideBar component.
* @returns The rendered SideBar component.
*/
const SideBar = forwardRef<HTMLDivElement, SideBarProps>((originProps, ref) => {
const props = useDefaultProps(originProps, sideBarDefaultProps);
const sideBarClass = usePrefixClass('side-bar');

const { onClick, onChange, children, defaultValue, value } = props;
const [activeValue, onToggleActiveValue] = useDefault(value, defaultValue, onChange);

const defaultIndex = useRef(-1);
const updateChild = onToggleActiveValue;

const [childrenList, setChildrenList] = useState([]);

useEffect(() => {
onChange?.(activeValue);
}, [activeValue, onChange]);

/**
* Adds a child component to the children array.
*
* @param {React.Element} child - The child component to add.
*/
const relation = useCallback((child) => {
setChildrenList((prevChildrenList) => [...prevChildrenList, child]);
}, []);

/**
* Removes a child component from the children array.
*
* @param {React.Element} child - The child component to remove.
*/
const removeRelation = useCallback((child) => {
setChildrenList((prevChildrenList) => prevChildrenList.filter((item) => item !== child));
}, []);

/**
* Handles the click event on a SideBar item.
*
* @param {string | number} cur - The value of the clicked item.
* @param {string} label - The label of the clicked item.
*/
const onClickItem = useCallback(
(cur, label) => {
onToggleActiveValue(cur);
onClick?.(cur, label);
},
[onToggleActiveValue, onClick],
);

const memoProviderValues = useMemo(
() => ({
defaultIndex,
activeValue,
updateChild,
childrenList,
relation,
removeRelation,
onClickItem,
}),
[activeValue, updateChild, childrenList, relation, removeRelation, onClickItem],
);

return (
<div ref={ref} className={sideBarClass}>
<SideBarProvider value={memoProviderValues}>{parseTNode(children)}</SideBarProvider>
<div className={`${sideBarClass}__padding`}></div>
</div>
);
});

export default memo(SideBar);
22 changes: 22 additions & 0 deletions src/side-bar/SideBarContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import React, { createContext, MutableRefObject, useMemo } from 'react';
import { ChangeHandler } from '../_util/useDefault';
import { TdSideBarProps } from './type';

/**
* SideBarContext is a React context that stores the state and actions related to the SideBar component.
*/
export const SideBarContext = createContext<
{
defaultIndex: MutableRefObject<number>;
activeValue: number | string | (number | string)[];
updateChild: ChangeHandler<number | string | (number | string)[], any[]>;
relation: (child: any) => void;
removeRelation: (child: any) => void;
onClickItem: (cur: any, label: any) => void;
} & Pick<TdSideBarProps, 'onChange' | 'onClick'>
>(null);

export function SideBarProvider({ children, value }) {
const memoValue = useMemo(() => value, [value]);
return <SideBarContext.Provider value={memoValue}>{children}</SideBarContext.Provider>;
}
58 changes: 58 additions & 0 deletions src/side-bar/SideBarItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import React, { forwardRef, memo, useContext, useEffect } from 'react';
import cls from 'classnames';
import type { StyledProps } from '../common';
import type { TdSideBarItemProps } from './type';
import { SideBarContext } from './SideBarContext';
import Badge from '../badge';
import parseTNode from '../_util/parseTNode';
import { sideBarItemDefaultProps } from './defaultProps';
import useDefaultProps from '../hooks/useDefaultProps';
import { usePrefixClass } from '../hooks/useClass';

export interface SideBarItemProps extends TdSideBarItemProps, StyledProps {}

const SideBarItem = forwardRef<HTMLDivElement, SideBarItemProps>((originProps, ref) => {
const props = useDefaultProps(originProps, sideBarItemDefaultProps);
const { badgeProps, disabled, icon, label, value } = props;
const { relation, removeRelation, activeValue, onClickItem } = useContext(SideBarContext);

const sideBarItemClass = usePrefixClass('side-bar-item');

useEffect(() => {
relation(props);
return () => {
removeRelation(props);
};
}, [props, relation, removeRelation]);

const isActive = activeValue === value;

const onClick = () => {
if (!disabled) onClickItem(value, label);
};

const isShowBadge = badgeProps?.count || badgeProps?.dot;

return (
<div
ref={ref}
className={cls(sideBarItemClass, {
[`${sideBarItemClass}--active`]: isActive,
[`${sideBarItemClass}--disabled`]: disabled,
})}
onClick={onClick}
>
{isActive && (
<div>
<div className={`${sideBarItemClass}__line`}></div>
<div className={`${sideBarItemClass}__prefix`}></div>
<div className={`${sideBarItemClass}__suffix`}></div>
</div>
)}
{icon && <div className={`${sideBarItemClass}__icon`}>{parseTNode(icon)}</div>}
{isShowBadge ? <Badge content={label} {...badgeProps} /> : <div>{label}</div>}
</div>
);
});

export default memo(SideBarItem);
3 changes: 3 additions & 0 deletions src/side-bar/__tests__/__snapshots__/side-bar.test.tsx.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html

exports[`SideBar 组件测试 > content 1`] = `null`;
12 changes: 12 additions & 0 deletions src/side-bar/__tests__/side-bar.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import React from 'react';
import { describe, it, expect, render } from '@test/utils';

import SideBar from '../SideBar';

describe('SideBar 组件测试', () => {
const SideBarText = 'SideBar组件';
it('content', async () => {
const { queryByText } = render(<SideBar />);
expect(queryByText(SideBarText)).toMatchSnapshot();
});
});
93 changes: 93 additions & 0 deletions src/side-bar/_example/base.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import React, { useState, useEffect, useRef, useCallback } from 'react';
import { Grid, GridItem } from '../../grid';
import { SideBarProps, SideBarItemProps, SideBar, SideBarItem } from '..';
import './style/base.less';

const image = 'https://tdesign.gtimg.com/mobile/demos/example2.png';
const items = new Array(12).fill({ label: '标题文字', image });

const categories = [
{ label: '选项一', title: '标题一', badgeProps: {}, items },
{ label: '选项二', title: '标题二', badgeProps: { dot: true }, items: items.slice(0, 9) },
{ label: '选项三', title: '标题三', badgeProps: {}, items: items.slice(0, 9) },
{ label: '选项四', title: '标题四', badgeProps: { count: 6 }, items: items.slice(0, 6) },
{ label: '选项五', title: '标题五', badgeProps: {}, items: items.slice(0, 3) },
];

function SideBarWrapper() {
const [sideBarIndex, setSideBarIndex] = useState<SideBarProps['value']>(1);
const [offsetTopList, setOffsetTopList] = useState([]);
const wrapperRef = useRef(null);

const moveToActiveSideBar = useCallback(
(index) => {
if (wrapperRef.current) {
wrapperRef.current.scrollTop = offsetTopList[index] - offsetTopList[0];
}
},
[offsetTopList],
);

const onSideBarChange = (value) => {
setSideBarIndex(value);
moveToActiveSideBar(value);
};

const onScroll = (e) => {
const threshold = offsetTopList[0];
const { scrollTop } = e.target;
if (scrollTop < threshold) {
setSideBarIndex(0);
return;
}
const index = offsetTopList.findIndex((top) => top > scrollTop && top - scrollTop <= threshold);
if (index > -1) {
setSideBarIndex(index);
}
};

const onSideBarClick = (value: SideBarProps['value'], label: SideBarItemProps['label']) => {
console.log('=onSideBarClick===', value, label);
};

useEffect(() => {
const newOffsetTopList = [];
const titles = wrapperRef.current.querySelectorAll('.title');
titles.forEach((title) => {
newOffsetTopList.push(title.offsetTop);
});
setOffsetTopList(newOffsetTopList);
moveToActiveSideBar(sideBarIndex);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [sideBarIndex]);

return (
<>
<div className="side-bar-wrapper section-base">
<SideBar value={sideBarIndex} onChange={onSideBarChange} onClick={onSideBarClick}>
{categories.map((item, index) => (
<SideBarItem key={index} value={index} label={item.label} badgeProps={item.badgeProps} />
))}
</SideBar>
<div ref={wrapperRef} className="content" onScroll={onScroll}>
{categories.map((item, index) => (
<div key={index} className="section">
<div className="title">{item.title || item.label}</div>
<Grid column={3} border={false}>
{item.items.map((cargo, cargoIndex) => (
<GridItem
key={cargoIndex}
text={cargo.label}
image={{ src: cargo.image, shape: 'round', lazy: true }}
></GridItem>
))}
</Grid>
</div>
))}
</div>
</div>
</>
);
}

export default SideBarWrapper;
Loading

0 comments on commit 2cb1212

Please sign in to comment.