-
Notifications
You must be signed in to change notification settings - Fork 319
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(back-top): add back-top component (#2037)
* 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
Showing
22 changed files
with
4,816 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Submodule _common
updated
36 files
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
169
src/back-top/__tests__/__snapshots__/vitest-back-top.test.jsx.snap
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
`; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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'); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
); | ||
} |
Oops, something went wrong.