Skip to content

Commit

Permalink
feat(back-top): add back-top component (#2037)
Browse files Browse the repository at this point in the history
* feat(back-top): add back-top component

add back-top component

* chore: add snapshot

* chore: add snapshot

* chore: add snapshot

* docs(back-top): update back-top position in menu

update back-top position in menu

* feat(backtop): update backTop example

update backTop example

* feat(back-top): add back-top component

add back-top component

* chore: add snapshot

* chore: add snapshot

* chore: add snapshot

* docs(back-top): update back-top position in menu

update back-top position in menu

* feat(backtop): update backTop example

update backTop example

* chore: update snapshot

* chore: update common

* feat(backtop): 校验document是否存在

* feat(backtop): 校验document是否存在

校验document是否存在

* chore: update snapshot

* chore: update docs

---------

Co-authored-by: Uyarn <uyarnchen@gmail.com>
  • Loading branch information
meiqi502 and uyarn authored Mar 28, 2023
1 parent 336ddfa commit 9a76bd1
Show file tree
Hide file tree
Showing 22 changed files with 4,816 additions and 1 deletion.
6 changes: 6 additions & 0 deletions site/site.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,12 @@ export default {
path: '/react/components/anchor',
component: () => import('tdesign-react/anchor/anchor.md'),
},
{
title: 'BackTop 返回顶部',
name: 'back-top',
path: '/react/components/back-top',
component: () => import('tdesign-react/back-top/back-top.md'),
},
{
title: 'Breadcrumb 面包屑',
name: 'breadcrumb',
Expand Down
2 changes: 1 addition & 1 deletion src/_common
Submodule _common updated 36 files
+2 −1 .stylelintrc
+26 −14 docs/mobile/api_v2/checkbox.md
+6 −16 docs/mobile/api_v2/loading.md
+23 −5 docs/mobile/api_v2/radio.md
+7 −3 docs/mobile/api_v2/switch.md
+19 −13 docs/mobile/api_v2/textarea.md
+15 −8 docs/web/api/back-top.md
+38 −0 docs/web/api/statistic.md
+20 −0 docs/web/api/sticky-tool.md
+2 −2 docs/web/api/table.md
+0 −217 js/table/recalculate-column-width.ts
+0 −121 js/table/set-column-width-by-drag.ts
+1 −1 js/tree/tree-node.ts
+8 −0 style/mobile/base.less
+34 −0 style/mobile/components/cell/v2/_index.less
+8 −0 style/mobile/components/cell/v2/_var.less
+6 −1 style/mobile/components/checkbox/v2/_index.less
+1 −1 style/mobile/components/empty/_index.less
+1 −1 style/mobile/components/footer/_index.less
+39 −84 style/mobile/components/loading/_index.less
+8 −36 style/mobile/components/loading/_var.less
+2 −1 style/mobile/components/progress/v2/_index.less
+174 −0 style/mobile/components/radio/v2/_index.less
+15 −0 style/mobile/components/radio/v2/_var.less
+31 −35 style/mobile/components/switch/v2/_index.less
+1 −1 style/mobile/components/textarea/v2/_index.less
+2 −1 style/mobile/components/toast/v2/_index.less
+0 −8 style/mobile/index.less
+7 −0 style/mobile/mixins/_index.less
+4 −0 style/web/_variables.less
+7 −1 style/web/components/back-top/_index.less
+6 −6 style/web/components/back-top/_var.less
+50 −0 style/web/components/statistic/_index.less
+30 −0 style/web/components/statistic/_var.less
+4 −0 style/web/theme/_dark.less
+4 −0 style/web/theme/_light.less
108 changes: 108 additions & 0 deletions src/back-top/BackTop.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import classNames from 'classnames';
import React, { useCallback, useMemo } from 'react';
import { BacktopIcon } from 'tdesign-icons-react';
import useConfig from '../hooks/useConfig';
import { TdBackTopProps } from './type';
import { backTopDefaultProps } from './defaultProps';
import useScroll from './useScroll';
import { scrollTo } from '../_util/dom';

export type BackTopProps = TdBackTopProps;

const getContainer = (container: string | Function) => {
if (typeof container === 'string') {
if (typeof document !== 'undefined') {
return document.querySelector(container);
}
}
if (typeof container === 'function') {
return container();
}
return null;
};

const BackTop = (props: BackTopProps) => {
const {
theme,
size,
shape,
target,
visibleHeight,
container,
duration,
content,
offset,
children,
default: cusContent,
className,
style,
onClick,
} = props;
const { classPrefix } = useConfig();
const scrollContainer = useMemo(() => getContainer(container), [container]);
const { scrollTop } = useScroll({ target: scrollContainer });
const defaultContent = (
<>
<BacktopIcon className={`${classPrefix}-back-top__icon`} size={24} />
<span className={`${classPrefix}-back-top__text`}>TOP</span>
</>
);
const renderChildren = children || content || cusContent || defaultContent;

const visible = useMemo(() => {
if (typeof visibleHeight === 'string') {
return scrollTop >= Number(visibleHeight.replace('px', ''));
}
return scrollTop >= visibleHeight;
}, [scrollTop, visibleHeight]);

const backTopStyle = useMemo(
() => ({
insetInlineEnd: offset[0],
insetBlockEnd: offset[1],
...style,
}),
[offset, style],
);

const cls = classNames(
className,
`${classPrefix}-back-top`,
`${classPrefix}-back-top--theme-${theme}`,
`${classPrefix}-back-top--${shape}`,
{
[`${classPrefix}-back-top--show`]: visible,
[`${classPrefix}-size-s`]: size === 'small',
[`${classPrefix}-size-m`]: size === 'medium',
},
);

const getBackTo = useCallback(() => {
if (!target) return 0;
const targetNode = getContainer(target);
if (!targetNode) return 0;
const rect = targetNode.getBoundingClientRect();
const { y } = rect;
return y;
}, [target]);

const handleClick = useCallback(
(e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
e.stopPropagation();
onClick?.({ e });
const backTo = getBackTo();
scrollTo(backTo, { container: scrollContainer, duration });
},
[duration, getBackTo, onClick, scrollContainer],
);

return (
<button type="button" className={cls} style={backTopStyle} onClick={handleClick}>
{renderChildren}
</button>
);
};

BackTop.displayName = 'BackTop';
BackTop.defaultProps = backTopDefaultProps;
export default BackTop;
169 changes: 169 additions & 0 deletions src/back-top/__tests__/__snapshots__/vitest-back-top.test.jsx.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
// Vitest Snapshot v1

exports[`BackTop Component > props.children works fine 1`] = `
<div>
<button
class="t-back-top t-back-top--theme-light t-back-top--square t-size-m"
style="inset-inline-end: 24px; inset-block-end: 80px;"
type="button"
>
<span
class="custom-node"
>
TNode
</span>
</button>
</div>
`;

exports[`BackTop Component > props.content works fine 1`] = `
<div>
<button
class="t-back-top t-back-top--theme-light t-back-top--square t-size-m"
style="inset-inline-end: 24px; inset-block-end: 80px;"
type="button"
>
<span
class="custom-node"
>
TNode
</span>
</button>
</div>
`;

exports[`BackTop Component > props.default works fine 1`] = `
<div>
<button
class="t-back-top t-back-top--theme-light t-back-top--square t-size-m"
style="inset-inline-end: 24px; inset-block-end: 80px;"
type="button"
>
<span
class="custom-node"
>
TNode
</span>
</button>
</div>
`;

exports[`BackTop Component > props.shape is equal to circle 1`] = `
<div>
<button
class="t-back-top t-back-top--theme-light t-back-top--circle t-size-m"
style="inset-inline-end: 24px; inset-block-end: 80px;"
type="button"
>
<svg
class="t-icon t-icon-backtop t-back-top__icon"
fill="none"
height="1em"
style="font-size: 24px;"
viewBox="0 0 16 16"
width="1em"
>
<path
d="M2 3h12V2H2v1zM3.38 10.23l4.1-4.03v8.64H8.5V6.2l4.18 4.08.71-.7-5.05-4.93a.5.5 0 00-.7 0l-4.98 4.9.72.69z"
fill="currentColor"
fill-opacity="0.9"
/>
</svg>
<span
class="t-back-top__text"
>
TOP
</span>
</button>
</div>
`;

exports[`BackTop Component > props.shape is equal to square 1`] = `
<div>
<button
class="t-back-top t-back-top--theme-light t-back-top--square t-size-m"
style="inset-inline-end: 24px; inset-block-end: 80px;"
type="button"
>
<svg
class="t-icon t-icon-backtop t-back-top__icon"
fill="none"
height="1em"
style="font-size: 24px;"
viewBox="0 0 16 16"
width="1em"
>
<path
d="M2 3h12V2H2v1zM3.38 10.23l4.1-4.03v8.64H8.5V6.2l4.18 4.08.71-.7-5.05-4.93a.5.5 0 00-.7 0l-4.98 4.9.72.69z"
fill="currentColor"
fill-opacity="0.9"
/>
</svg>
<span
class="t-back-top__text"
>
TOP
</span>
</button>
</div>
`;

exports[`BackTop Component > props.size is equal to medium 1`] = `
<div>
<button
class="t-back-top t-back-top--theme-light t-back-top--square t-size-m"
style="inset-inline-end: 24px; inset-block-end: 80px;"
type="button"
>
BackTop
</button>
</div>
`;

exports[`BackTop Component > props.size is equal to small 1`] = `
<div>
<button
class="t-back-top t-back-top--theme-light t-back-top--square t-size-s"
style="inset-inline-end: 24px; inset-block-end: 80px;"
type="button"
>
BackTop
</button>
</div>
`;

exports[`BackTop Component > props.theme is equal to dark 1`] = `
<div>
<button
class="t-back-top t-back-top--theme-dark t-back-top--square t-size-m"
style="inset-inline-end: 24px; inset-block-end: 80px;"
type="button"
>
Text
</button>
</div>
`;

exports[`BackTop Component > props.theme is equal to light 1`] = `
<div>
<button
class="t-back-top t-back-top--theme-light t-back-top--square t-size-m"
style="inset-inline-end: 24px; inset-block-end: 80px;"
type="button"
>
Text
</button>
</div>
`;

exports[`BackTop Component > props.theme is equal to primary 1`] = `
<div>
<button
class="t-back-top t-back-top--theme-primary t-back-top--square t-size-m"
style="inset-inline-end: 24px; inset-block-end: 80px;"
type="button"
>
Text
</button>
</div>
`;
67 changes: 67 additions & 0 deletions src/back-top/__tests__/vitest-back-top.test.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
/**
* 该文件由脚本自动生成,如需修改请联系 PMC
* This file generated by scripts of tdesign-api. `npm run api:docs BackTop React(PC) vitest,finalProject`
* If you need to modify this file, contact PMC first please.
*/
import React from 'react';
import { fireEvent, vi, render } from '@test/utils';
import { BackTop } from '..';

describe('BackTop Component', () => {
it('props.children works fine', () => {
const { container } = render(
<BackTop>
<span className="custom-node">TNode</span>
</BackTop>,
);
expect(container.querySelector('.custom-node')).toBeTruthy();
expect(container).toMatchSnapshot();
});

it('props.content works fine', () => {
const { container } = render(<BackTop content={<span className="custom-node">TNode</span>}></BackTop>);
expect(container.querySelector('.custom-node')).toBeTruthy();
expect(container).toMatchSnapshot();
});

it('props.default works fine', () => {
const { container } = render(<BackTop default={<span className="custom-node">TNode</span>}></BackTop>);
expect(container.querySelector('.custom-node')).toBeTruthy();
expect(container).toMatchSnapshot();
});

['circle', 'square'].forEach((item) => {
it(`props.shape is equal to ${item}`, () => {
const { container } = render(<BackTop shape={item}></BackTop>);
expect(container.firstChild).toHaveClass(`t-back-top--${item}`);
expect(container).toMatchSnapshot();
});
});

const sizeClassNameList = ['t-size-m', 't-size-s'];
['medium', 'small'].forEach((item, index) => {
it(`props.size is equal to ${item}`, () => {
const { container } = render(<BackTop size={item}>BackTop</BackTop>);
expect(container.firstChild).toHaveClass(sizeClassNameList[index]);
expect(container).toMatchSnapshot();
});
});

['light', 'primary', 'dark'].forEach((item) => {
it(`props.theme is equal to ${item}`, () => {
const { container } = render(<BackTop theme={item}>Text</BackTop>);
expect(container.firstChild).toHaveClass(`t-back-top--theme-${item}`);
expect(container).toMatchSnapshot();
});
});

it('events.click works fine', () => {
const fn = vi.fn();
const { container } = render(<BackTop onClick={fn}></BackTop>);
fireEvent.click(container.firstChild);
expect(fn).toHaveBeenCalled();
expect(fn.mock.calls[0][0].e.stopPropagation).toBeTruthy();
expect(fn.mock.calls[0][0].e.type).toBe('click');
});
});
33 changes: 33 additions & 0 deletions src/back-top/_example/baseList.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// @ts-nocheck
import React, { useState } from 'react';
import { BackTop, List } from 'tdesign-react';

export default function BasicBackTop() {
const [container, setContainer] = useState(null);

const style = {
position: 'absolute',
insetInlineEnd: 24,
insetBlockEnd: 80,
};

const listWrapStyle = {
width: '100%',
height: '280px',
position: 'relative',
overflowY: 'scroll',
overflowX: 'hidden',
border: '1px solid #dcdcdcff',
};

return (
<div style={{ position: 'relative' }}>
<div id='demo_1' style={listWrapStyle} ref={setContainer}>
<List>
{Array.from(Array(50), () => '列表内容').map((item, index) => <List.ListItem key={index}>{item}</List.ListItem>) }
</List>
</div>
<BackTop container={() => container} visibleHeight={46} style={style}></BackTop>
</div>
);
}
Loading

0 comments on commit 9a76bd1

Please sign in to comment.