diff --git a/docs/examples/simple.tsx b/docs/examples/simple.tsx index 37131216..fbfefea2 100644 --- a/docs/examples/simple.tsx +++ b/docs/examples/simple.tsx @@ -1,6 +1,6 @@ /* eslint no-console:0 */ -import Trigger, { ActionType } from '@rc-component/trigger'; +import Trigger, { type ActionType } from '@rc-component/trigger'; import React from 'react'; import '../../assets/index.less'; @@ -43,29 +43,26 @@ function getPopupContainer(trigger) { return trigger.parentNode; } -const InnerTarget = (props) => ( -
-

This is a example of trigger usage.

-

You can adjust the value above

-

which will also change the behaviour of popup.

-
+const InnerTarget = React.forwardRef( + (props: any, ref: React.Ref) => ( +
+

This is a example of trigger usage.

+

You can adjust the value above

+

which will also change the behavior of popup.

+
+ ), ); -const RefTarget = React.forwardRef((props, ref) => { - React.useImperativeHandle(ref, () => ({})); - - return ; -}); - interface TestState { mask: boolean; maskClosable: boolean; @@ -88,7 +85,7 @@ interface TestState { class Test extends React.Component { state: TestState = { - mask: true, + mask: false, maskClosable: true, placement: 'bottom', trigger: { @@ -377,7 +374,7 @@ class Test extends React.Component { motionName: state.transitionName, }} > - + diff --git a/src/index.tsx b/src/index.tsx index 83923b72..861a1b37 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -595,6 +595,10 @@ export function generateTrigger( }; } + // ============================ Perf ============================ + const rendedRef = React.useRef(false); + rendedRef.current ||= forceRender || mergedOpen || inMotion; + // =========================== Render =========================== const mergedChildrenProps = { ...originChildProps, @@ -651,55 +655,57 @@ export function generateTrigger( > {triggerNode} - - - + {rendedRef.current && ( + + + + )} ); }); diff --git a/tests/perf.test.tsx b/tests/perf.test.tsx new file mode 100644 index 00000000..a02d0861 --- /dev/null +++ b/tests/perf.test.tsx @@ -0,0 +1,102 @@ +import { cleanup, fireEvent, render } from '@testing-library/react'; +import { spyElementPrototypes } from '@rc-component/util/lib/test/domHook'; +import React from 'react'; +import Trigger, { type TriggerProps } from '../src'; +import { awaitFakeTimer, placementAlignMap } from './util'; + +jest.mock('../src/Popup', () => { + const OriReact = jest.requireActual('react'); + const OriPopup = jest.requireActual('../src/Popup').default; + + return OriReact.forwardRef((props, ref) => { + global.popupCalledTimes = (global.popupCalledTimes || 0) + 1; + return ; + }); +}); + +describe('Trigger.Basic', () => { + beforeAll(() => { + spyElementPrototypes(HTMLElement, { + offsetParent: { + get: () => document.body, + }, + }); + }); + + beforeEach(() => { + global.popupCalledTimes = 0; + jest.useFakeTimers(); + }); + + afterEach(() => { + cleanup(); + jest.useRealTimers(); + }); + + async function trigger(dom: HTMLElement, selector: string, method = 'click') { + fireEvent[method](dom.querySelector(selector)); + await awaitFakeTimer(); + } + + const renderTrigger = (props?: Partial) => ( + tooltip2} + {...props} + > +
click
+
+ ); + + describe('Performance', () => { + it('not create Popup when !open', async () => { + const { container } = render(renderTrigger()); + + // Not render Popup + await awaitFakeTimer(); + expect(global.popupCalledTimes).toBe(0); + + // Now can render Popup + await trigger(container, '.target'); + expect(global.popupCalledTimes).toBeGreaterThan(0); + + expect(document.querySelector('.rc-trigger-popup')).toBeTruthy(); + }); + + it('forceRender should create when !open', async () => { + const { container } = render( + renderTrigger({ + forceRender: true, + }), + ); + + await awaitFakeTimer(); + await trigger(container, '.target'); + expect(global.popupCalledTimes).toBeGreaterThan(0); + + expect(document.querySelector('.rc-trigger-popup')).toBeTruthy(); + }); + + it('hide should keep render Popup', async () => { + const { rerender } = render( + renderTrigger({ + popupVisible: true, + }), + ); + + await awaitFakeTimer(); + expect(global.popupCalledTimes).toBeGreaterThan(0); + + // Hide + global.popupCalledTimes = 0; + rerender( + renderTrigger({ + popupVisible: false, + }), + ); + await awaitFakeTimer(); + expect(global.popupCalledTimes).toBeGreaterThan(0); + }); + }); +});