Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(SideBar): 创建SideBar组件 #491

Merged
merged 25 commits into from
Aug 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
38ec9de
feat(SideBar): 升级SideBar样式
tobytovi Aug 23, 2024
525afc5
feat(SideBar): 创建SideBar组件
tobytovi Aug 23, 2024
1ee5730
feat(SideBar): 创建SideBar文档
tobytovi Aug 23, 2024
f4e95fd
fix(Grid): 补充Grid遗漏的type
tobytovi Aug 23, 2024
4b0ef86
Merge branch 'develop' into feat/sidebar
tobytovi Aug 26, 2024
83e64e8
chore: update snap
tobytovi Aug 26, 2024
f712d8a
test(SideBar): 测试增加React路由
tobytovi Aug 26, 2024
c04b192
test(SideBar): update snap
tobytovi Aug 26, 2024
65a8d90
docs(SideBar): 解决演示用例样式冲突问题
tobytovi Aug 26, 2024
f6ef414
test(SideBar): update snap
tobytovi Aug 26, 2024
2106943
docs(SideBar): 修复示例内容体高度样式
tobytovi Aug 26, 2024
db9666b
refactor(SideBar): 按规范改用useDefaultProps、usePrefixClass
tobytovi Aug 26, 2024
fb2dd82
docs(SideBar): 移除多余API文档
tobytovi Aug 26, 2024
91b259f
chore(SideBar): update tdesign-api
tobytovi Aug 26, 2024
00ebe91
Merge branch 'develop' into feat/sidebar
tobytovi Aug 26, 2024
9a0c07d
test: update snap
tobytovi Aug 26, 2024
7139ba0
docs(SideBar): 删除多余示例参数
tobytovi Aug 26, 2024
16e1535
chore(SideBar): update tdesign-api
tobytovi Aug 26, 2024
19810a4
chore: 变更common分支
tobytovi Aug 26, 2024
880cc01
chore(SideBar): update tdesign-api
tobytovi Aug 26, 2024
2db35b4
chore(SideBar): update tdesign-api
tobytovi Aug 26, 2024
fa8a482
chore(SideBar): update api
tobytovi Aug 26, 2024
1371b24
perf(Demo): 示例头部的返回按钮优化为按需引入
tobytovi Aug 26, 2024
575a259
chore: merge develop
github-actions[bot] Aug 28, 2024
21e495c
chore: update snapshot
github-actions[bot] Aug 28, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading