From 0c3ba124b48117cff7c3d5910095a2f4b810faec Mon Sep 17 00:00:00 2001 From: lijianan <574980606@qq.com> Date: Thu, 24 Nov 2022 14:59:17 +0800 Subject: [PATCH] fix[BackTop]: delete visible & use visibilityHeight=0 replace visible (#38763) * type: delete visible & code optimization * add PureBackTop * fix: update snap * fix: cov * fix: add istanbul ignore * fix * feat: use visibilityHeight=0 replace visible=true * snap * cov * test case * test case * fix * fix cov * fix test * simplify code * rename function --- components/back-top/__tests__/index.test.tsx | 35 +++--- components/back-top/index.tsx | 101 +++++++----------- .../__tests__/components.test.tsx | 2 +- components/float-button/BackTop.tsx | 28 ++--- components/float-button/PurePanel.tsx | 11 +- .../float-button/__tests__/back-top.test.tsx | 39 +++---- components/float-button/index.tsx | 2 +- components/float-button/interface.ts | 1 - 8 files changed, 92 insertions(+), 127 deletions(-) diff --git a/components/back-top/__tests__/index.test.tsx b/components/back-top/__tests__/index.test.tsx index 8328029294..66fa73a410 100644 --- a/components/back-top/__tests__/index.test.tsx +++ b/components/back-top/__tests__/index.test.tsx @@ -5,47 +5,42 @@ import rtlTest from '../../../tests/shared/rtlTest'; import { fireEvent, render, waitFakeTimer } from '../../../tests/utils'; describe('BackTop', () => { + beforeEach(() => { + jest.useFakeTimers(); + }); + afterEach(() => { + jest.useRealTimers(); + }); mountTest(BackTop); rtlTest(BackTop); it('should scroll to top after click it', async () => { - jest.useFakeTimers(); - - const { container } = render(); + const { container } = render(); const scrollToSpy = jest.spyOn(window, 'scrollTo').mockImplementation((_, y) => { window.scrollY = y; window.pageYOffset = y; document.documentElement.scrollTop = y; }); window.scrollTo(0, 400); + await waitFakeTimer(); expect(document.documentElement.scrollTop).toBe(400); - fireEvent.click(container.querySelector('.ant-back-top')!); + fireEvent.click(container.querySelector('.ant-back-top')!); await waitFakeTimer(); expect(document.documentElement.scrollTop).toBe(0); scrollToSpy.mockRestore(); - - jest.clearAllTimers(); - jest.useRealTimers(); }); - it('support onClick', async () => { + it('support onClick', () => { const onClick = jest.fn(); - const { container } = render(); - const scrollToSpy = jest.spyOn(window, 'scrollTo').mockImplementation((_, y) => { - window.scrollY = y; - window.pageYOffset = y; - }); - document.dispatchEvent(new Event('scroll')); - window.scrollTo(0, 400); - fireEvent.click(container.querySelector('.ant-back-top')!); + const { container } = render(); + fireEvent.click(container.querySelector('.ant-back-top')!); expect(onClick).toHaveBeenCalled(); - scrollToSpy.mockRestore(); }); - it('invalid target', async () => { + it('invalid target', () => { const onClick = jest.fn(); - const { container } = render(); - fireEvent.click(container.querySelector('.ant-back-top')!); + const { container } = render(); + fireEvent.click(container.querySelector('.ant-back-top')!); expect(onClick).toHaveBeenCalled(); }); it('should console Error', () => { diff --git a/components/back-top/index.tsx b/components/back-top/index.tsx index a7c68be867..827e571631 100644 --- a/components/back-top/index.tsx +++ b/components/back-top/index.tsx @@ -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 = (props) => { - const { prefixCls, rootPrefixCls, children, visible } = props; - const defaultElement = ( -
-
- -
-
- ); - return ( - - {({ className: motionClassName }) => - cloneElement(children || defaultElement, ({ className }) => ({ - className: classNames(motionClassName, className), - })) - } - - ); -}; - const BackTop: React.FC = (props) => { - const [visible, setVisible] = useMergedState(false, { - value: props.visible, - }); - - const ref = React.createRef(); - const scrollEvent = React.useRef>(null); - - const getDefaultTarget = () => + const { + prefixCls: customizePrefixCls, + className = '', + visibilityHeight = 400, + target, + onClick, + duration = 450, + } = props; + const [visible, setVisible] = React.useState(visibilityHeight === 0); + + const ref = React.useRef(null); + const scrollEvent = React.useRef | null>(null); + + const getDefaultTarget = (): HTMLElement | Document | Window => ref.current && ref.current.ownerDocument ? ref.current.ownerDocument : window; const handleScroll = throttleByAnimationFrame( - (e: React.UIEvent | { target: any }) => { - const { visibilityHeight = 400 } = props; + (e: React.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) => { - handleScroll(e); - }); + scrollEvent.current = addEventListener(container, 'scroll', handleScroll); handleScroll({ target: container }); }; @@ -88,26 +62,18 @@ const BackTop: React.FC = (props) => { React.useEffect(() => { bindScrollEvent(); return () => { - if (scrollEvent.current) { - scrollEvent.current.remove(); - } handleScroll.cancel(); + scrollEvent.current?.remove(); }; - }, [props.target]); + }, [target]); const scrollToTop = (e: React.MouseEvent) => { - 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(ConfigContext); + const prefixCls = getPrefixCls('back-top', customizePrefixCls); const rootPrefixCls = getPrefixCls(); const [wrapSSR, hashId] = useStyle(prefixCls); @@ -128,14 +94,25 @@ const BackTop: React.FC = (props) => { 'children', 'visibilityHeight', 'target', - 'visible', ]); + const defaultElement = ( +
+
+ +
+
+ ); + return wrapSSR(
- - {props.children} - + + {({ className: motionClassName }) => + cloneElement(props.children || defaultElement, ({ className: cloneCls }) => ({ + className: classNames(motionClassName, cloneCls), + })) + } +
, ); }; diff --git a/components/config-provider/__tests__/components.test.tsx b/components/config-provider/__tests__/components.test.tsx index f2b95ed8e9..d9c4ebea4f 100644 --- a/components/config-provider/__tests__/components.test.tsx +++ b/components/config-provider/__tests__/components.test.tsx @@ -144,7 +144,7 @@ describe('ConfigProvider', () => { testPair('Avatar', (props) => ); // BackTop - testPair('BackTop', (props) => ); + testPair('BackTop', (props) => ); // Badge testPair('Badge', (props) => { diff --git a/components/float-button/BackTop.tsx b/components/float-button/BackTop.tsx index 7d2431e22b..827086bfd8 100644 --- a/components/float-button/BackTop.tsx +++ b/components/float-button/BackTop.tsx @@ -2,8 +2,7 @@ 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 React, { memo, useContext, useEffect, useRef } from 'react'; +import React, { memo, useContext, useEffect, useRef, useState } from 'react'; import FloatButton, { floatButtonPrefixCls } from './FloatButton'; import type { ConfigConsumerProps } from '../config-provider'; import { ConfigContext } from '../config-provider'; @@ -11,7 +10,7 @@ import getScroll from '../_util/getScroll'; import scrollTo from '../_util/scrollTo'; import throttleByAnimationFrame from '../_util/throttleByAnimationFrame'; import FloatButtonGroupContext from './context'; -import type { BackTopProps, FloatButtonShape } from './interface'; +import type { BackTopProps, FloatButtonProps, FloatButtonShape } from './interface'; import useStyle from './style'; const BackTop: React.FC = (props) => { @@ -28,46 +27,39 @@ const BackTop: React.FC = (props) => { ...restProps } = props; - const [visible, setVisible] = useMergedState(false, { value: props.visible }); + const [visible, setVisible] = useState(visibilityHeight === 0); const ref = useRef(null); - - const scrollEvent = useRef(null); + const scrollEvent = useRef | null>(null); const getDefaultTarget = (): HTMLElement | Document | Window => ref.current && ref.current.ownerDocument ? ref.current.ownerDocument : window; const handleScroll = throttleByAnimationFrame( - (e: React.UIEvent | { target: any }) => { + (e: React.UIEvent | { target: any }) => { const scrollTop = getScroll(e.target, true); - setVisible(scrollTop > visibilityHeight!); + setVisible(scrollTop >= visibilityHeight); }, ); const bindScrollEvent = () => { const getTarget = target || getDefaultTarget; const container = getTarget(); - scrollEvent.current = addEventListener(container, 'scroll', (e: React.UIEvent) => { - handleScroll(e); - }); + scrollEvent.current = addEventListener(container, 'scroll', handleScroll); handleScroll({ target: container }); }; useEffect(() => { bindScrollEvent(); return () => { - if (scrollEvent.current) { - scrollEvent.current.remove(); - } handleScroll.cancel(); + scrollEvent.current?.remove(); }; }, [target]); const scrollToTop: React.MouseEventHandler = (e) => { scrollTo(0, { getContainer: target || getDefaultTarget, duration }); - if (typeof onClick === 'function') { - onClick(e); - } + onClick?.(e); }; const { getPrefixCls } = useContext(ConfigContext); @@ -80,7 +72,7 @@ const BackTop: React.FC = (props) => { const mergeShape = groupShape || shape; - const contentProps = { prefixCls, icon, type, shape: mergeShape, ...restProps }; + const contentProps: FloatButtonProps = { prefixCls, icon, type, shape: mergeShape, ...restProps }; return wrapSSR( diff --git a/components/float-button/PurePanel.tsx b/components/float-button/PurePanel.tsx index ae9a3817e5..2732a06331 100644 --- a/components/float-button/PurePanel.tsx +++ b/components/float-button/PurePanel.tsx @@ -7,7 +7,7 @@ import BackTop from './BackTop'; import type { FloatButtonProps, FloatButtonGroupProps } from './interface'; import { ConfigContext } from '../config-provider'; -export interface PureFloatButtonProps extends FloatButtonProps { +export interface PureFloatButtonProps extends Omit { backTop?: boolean; } @@ -18,11 +18,10 @@ export interface PurePanelProps items?: PureFloatButtonProps[]; } -function PureFloatButton({ backTop, ...props }: PureFloatButtonProps) { - return backTop ? : ; -} +const PureFloatButton: React.FC = ({ backTop, ...props }) => + backTop ? : ; -export default function PurePanel({ className, items, ...props }: PurePanelProps) { +function PurePanel({ className, items, ...props }: PurePanelProps) { const { prefixCls: customizePrefixCls } = props; const { getPrefixCls } = React.useContext(ConfigContext); @@ -41,3 +40,5 @@ export default function PurePanel({ className, items, ...props }: PurePanelProps return ; } + +export default React.memo(PurePanel); diff --git a/components/float-button/__tests__/back-top.test.tsx b/components/float-button/__tests__/back-top.test.tsx index 8695db60aa..251ddd9dec 100644 --- a/components/float-button/__tests__/back-top.test.tsx +++ b/components/float-button/__tests__/back-top.test.tsx @@ -2,52 +2,53 @@ import React from 'react'; import FloatButton from '..'; import mountTest from '../../../tests/shared/mountTest'; import rtlTest from '../../../tests/shared/rtlTest'; -import { fireEvent, render, sleep } from '../../../tests/utils'; +import { fireEvent, render, waitFakeTimer } from '../../../tests/utils'; const { BackTop } = FloatButton; describe('BackTop', () => { + beforeEach(() => { + jest.useFakeTimers(); + }); + afterEach(() => { + jest.useRealTimers(); + }); mountTest(BackTop); rtlTest(BackTop); it('should scroll to top after click it', async () => { - const { container } = render(); + const { container } = render(); const scrollToSpy = jest.spyOn(window, 'scrollTo').mockImplementation((_, y) => { window.scrollY = y; window.pageYOffset = y; document.documentElement.scrollTop = y; }); window.scrollTo(0, 400); + await waitFakeTimer(); expect(document.documentElement.scrollTop).toBe(400); - fireEvent.click(container.querySelector('.ant-float-btn')!); - await sleep(500); + fireEvent.click(container.querySelector('.ant-float-btn')!); + await waitFakeTimer(); expect(document.documentElement.scrollTop).toBe(0); scrollToSpy.mockRestore(); }); it('support onClick', () => { const onClick = jest.fn(); - const { container } = render(); - const scrollToSpy = jest.spyOn(window, 'scrollTo').mockImplementation((_, y) => { - window.scrollY = y; - window.pageYOffset = y; - }); - document.dispatchEvent(new Event('scroll')); - window.scrollTo(0, 400); - fireEvent.click(container.querySelector('.ant-float-btn')!); + const { container } = render(); + fireEvent.click(container.querySelector('.ant-float-btn')!); expect(onClick).toHaveBeenCalled(); - scrollToSpy.mockRestore(); }); - it('invalid target', () => { + it('support invalid target', () => { const onClick = jest.fn(); - const { container } = render(); - fireEvent.click(container.querySelector('.ant-float-btn')!); + const { container } = render( + , + ); + fireEvent.click(container.querySelector('.ant-float-btn')!); expect(onClick).toHaveBeenCalled(); }); it('pass style to float button', () => { - const { container } = render(); - const btn = container.querySelector('.ant-float-btn')!; - expect(btn).toHaveAttribute('style', 'color: red;'); + const { container } = render(); + expect(container.querySelector('.ant-float-btn')?.style.color).toBe('red'); }); }); diff --git a/components/float-button/index.tsx b/components/float-button/index.tsx index 1c7da440bd..d3f6bdccb8 100644 --- a/components/float-button/index.tsx +++ b/components/float-button/index.tsx @@ -3,8 +3,8 @@ import FloatButtonGroup from './FloatButtonGroup'; import BackTop from './BackTop'; import PurePanel from './PurePanel'; -FloatButton.Group = FloatButtonGroup; FloatButton.BackTop = BackTop; +FloatButton.Group = FloatButtonGroup; FloatButton._InternalPanelDoNotUseOrYouWillBeFired = PurePanel; export default FloatButton; diff --git a/components/float-button/interface.ts b/components/float-button/interface.ts index 923f2cab0b..26216be0e3 100644 --- a/components/float-button/interface.ts +++ b/components/float-button/interface.ts @@ -53,7 +53,6 @@ export interface BackTopProps extends Omit { className?: string; style?: React.CSSProperties; duration?: number; - visible?: boolean; // Only for test. Don't use it. } export type CompoundedComponent = React.ForwardRefExoticComponent<