|
|
@ -2,9 +2,9 @@ import VerticalAlignTopOutlined from '@ant-design/icons/VerticalAlignTopOutlined |
|
|
|
import classNames from 'classnames'; |
|
|
|
import CSSMotion from 'rc-motion'; |
|
|
|
import addEventListener from 'rc-util/lib/Dom/addEventListener'; |
|
|
|
import useMergedState from 'rc-util/lib/hooks/useMergedState'; |
|
|
|
import omit from 'rc-util/lib/omit'; |
|
|
|
import * as React from 'react'; |
|
|
|
import type { ConfigConsumerProps } from '../config-provider'; |
|
|
|
import { ConfigContext } from '../config-provider'; |
|
|
|
import getScroll from '../_util/getScroll'; |
|
|
|
import { cloneElement } from '../_util/reactNode'; |
|
|
@ -22,62 +22,36 @@ export interface BackTopProps { |
|
|
|
className?: string; |
|
|
|
style?: React.CSSProperties; |
|
|
|
duration?: number; |
|
|
|
visible?: boolean; // Only for test. Don't use it.
|
|
|
|
} |
|
|
|
|
|
|
|
interface ChildrenProps { |
|
|
|
prefixCls: string; |
|
|
|
rootPrefixCls: string; |
|
|
|
children?: React.ReactNode; |
|
|
|
visible?: boolean; // Only for test. Don't use it.
|
|
|
|
} |
|
|
|
|
|
|
|
const BackTopContent: React.FC<ChildrenProps> = (props) => { |
|
|
|
const { prefixCls, rootPrefixCls, children, visible } = props; |
|
|
|
const defaultElement = ( |
|
|
|
<div className={`${prefixCls}-content`}> |
|
|
|
<div className={`${prefixCls}-icon`}> |
|
|
|
<VerticalAlignTopOutlined /> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
); |
|
|
|
return ( |
|
|
|
<CSSMotion visible={visible} motionName={`${rootPrefixCls}-fade`}> |
|
|
|
{({ className: motionClassName }) => |
|
|
|
cloneElement(children || defaultElement, ({ className }) => ({ |
|
|
|
className: classNames(motionClassName, className), |
|
|
|
})) |
|
|
|
} |
|
|
|
</CSSMotion> |
|
|
|
); |
|
|
|
}; |
|
|
|
|
|
|
|
const BackTop: React.FC<BackTopProps> = (props) => { |
|
|
|
const [visible, setVisible] = useMergedState(false, { |
|
|
|
value: props.visible, |
|
|
|
}); |
|
|
|
|
|
|
|
const ref = React.createRef<HTMLDivElement>(); |
|
|
|
const scrollEvent = React.useRef<ReturnType<typeof addEventListener>>(null); |
|
|
|
|
|
|
|
const getDefaultTarget = () => |
|
|
|
const { |
|
|
|
prefixCls: customizePrefixCls, |
|
|
|
className = '', |
|
|
|
visibilityHeight = 400, |
|
|
|
target, |
|
|
|
onClick, |
|
|
|
duration = 450, |
|
|
|
} = props; |
|
|
|
const [visible, setVisible] = React.useState<boolean>(visibilityHeight === 0); |
|
|
|
|
|
|
|
const ref = React.useRef<HTMLDivElement>(null); |
|
|
|
const scrollEvent = React.useRef<ReturnType<typeof addEventListener> | null>(null); |
|
|
|
|
|
|
|
const getDefaultTarget = (): HTMLElement | Document | Window => |
|
|
|
ref.current && ref.current.ownerDocument ? ref.current.ownerDocument : window; |
|
|
|
|
|
|
|
const handleScroll = throttleByAnimationFrame( |
|
|
|
(e: React.UIEvent<HTMLElement> | { target: any }) => { |
|
|
|
const { visibilityHeight = 400 } = props; |
|
|
|
(e: React.UIEvent<HTMLElement, UIEvent> | { target: any }) => { |
|
|
|
const scrollTop = getScroll(e.target, true); |
|
|
|
setVisible(scrollTop > visibilityHeight); |
|
|
|
setVisible(scrollTop >= visibilityHeight); |
|
|
|
}, |
|
|
|
); |
|
|
|
|
|
|
|
const bindScrollEvent = () => { |
|
|
|
const { target } = props; |
|
|
|
const getTarget = target || getDefaultTarget; |
|
|
|
const container = getTarget(); |
|
|
|
scrollEvent.current = addEventListener(container, 'scroll', (e: React.UIEvent<HTMLElement>) => { |
|
|
|
handleScroll(e); |
|
|
|
}); |
|
|
|
scrollEvent.current = addEventListener(container, 'scroll', handleScroll); |
|
|
|
handleScroll({ target: container }); |
|
|
|
}; |
|
|
|
|
|
|
@ -88,26 +62,18 @@ const BackTop: React.FC<BackTopProps> = (props) => { |
|
|
|
React.useEffect(() => { |
|
|
|
bindScrollEvent(); |
|
|
|
return () => { |
|
|
|
if (scrollEvent.current) { |
|
|
|
scrollEvent.current.remove(); |
|
|
|
} |
|
|
|
handleScroll.cancel(); |
|
|
|
scrollEvent.current?.remove(); |
|
|
|
}; |
|
|
|
}, [props.target]); |
|
|
|
}, [target]); |
|
|
|
|
|
|
|
const scrollToTop = (e: React.MouseEvent<HTMLDivElement>) => { |
|
|
|
const { onClick, target, duration = 450 } = props; |
|
|
|
scrollTo(0, { |
|
|
|
getContainer: target || getDefaultTarget, |
|
|
|
duration, |
|
|
|
}); |
|
|
|
if (typeof onClick === 'function') { |
|
|
|
onClick(e); |
|
|
|
} |
|
|
|
scrollTo(0, { getContainer: target || getDefaultTarget, duration }); |
|
|
|
onClick?.(e); |
|
|
|
}; |
|
|
|
|
|
|
|
const { getPrefixCls, direction } = React.useContext(ConfigContext); |
|
|
|
const { prefixCls: customizePrefixCls, className = '' } = props; |
|
|
|
const { getPrefixCls, direction } = React.useContext<ConfigConsumerProps>(ConfigContext); |
|
|
|
|
|
|
|
const prefixCls = getPrefixCls('back-top', customizePrefixCls); |
|
|
|
const rootPrefixCls = getPrefixCls(); |
|
|
|
const [wrapSSR, hashId] = useStyle(prefixCls); |
|
|
@ -128,14 +94,25 @@ const BackTop: React.FC<BackTopProps> = (props) => { |
|
|
|
'children', |
|
|
|
'visibilityHeight', |
|
|
|
'target', |
|
|
|
'visible', |
|
|
|
]); |
|
|
|
|
|
|
|
const defaultElement = ( |
|
|
|
<div className={`${prefixCls}-content`}> |
|
|
|
<div className={`${prefixCls}-icon`}> |
|
|
|
<VerticalAlignTopOutlined /> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
); |
|
|
|
|
|
|
|
return wrapSSR( |
|
|
|
<div {...divProps} className={classString} onClick={scrollToTop} ref={ref}> |
|
|
|
<BackTopContent prefixCls={prefixCls} rootPrefixCls={rootPrefixCls} visible={visible}> |
|
|
|
{props.children} |
|
|
|
</BackTopContent> |
|
|
|
<CSSMotion visible={visible} motionName={`${rootPrefixCls}-fade`}> |
|
|
|
{({ className: motionClassName }) => |
|
|
|
cloneElement(props.children || defaultElement, ({ className: cloneCls }) => ({ |
|
|
|
className: classNames(motionClassName, cloneCls), |
|
|
|
})) |
|
|
|
} |
|
|
|
</CSSMotion> |
|
|
|
</div>, |
|
|
|
); |
|
|
|
}; |
|
|
|