From cb82a0dba6b85ce28a1921c35ce70a0d32dee83e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8C=E8=B4=A7=E6=9C=BA=E5=99=A8=E4=BA=BA?= Date: Wed, 14 May 2025 19:12:28 +0800 Subject: [PATCH 1/4] chore: fix demo --- docs/examples/simple.tsx | 45 +++++++++++++++++++--------------------- 1 file changed, 21 insertions(+), 24 deletions(-) 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, }} > - + From bbcd5f4f25ce6f2b7a03eddf1a20a187e3d724b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8C=E8=B4=A7=E6=9C=BA=E5=99=A8=E4=BA=BA?= Date: Wed, 14 May 2025 19:22:43 +0800 Subject: [PATCH 2/4] chore: perf before render --- src/index.tsx | 104 ++++++++++++++++++++++++++------------------------ 1 file changed, 55 insertions(+), 49 deletions(-) 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 && ( + + + + )} ); }); From 7f8d5784e74e564ee4b6bd38b7b9f42010b1a403 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8C=E8=B4=A7=E6=9C=BA=E5=99=A8=E4=BA=BA?= Date: Wed, 14 May 2025 19:34:26 +0800 Subject: [PATCH 3/4] chore: perf before render --- tests/perf.test.jsx | 60 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 tests/perf.test.jsx diff --git a/tests/perf.test.jsx b/tests/perf.test.jsx new file mode 100644 index 00000000..47e77a09 --- /dev/null +++ b/tests/perf.test.jsx @@ -0,0 +1,60 @@ +/* eslint-disable max-classes-per-file */ +import { act, cleanup, fireEvent, render } from '@testing-library/react'; +import { spyElementPrototypes } from '@rc-component/util/lib/test/domHook'; +import React from 'react'; +import Trigger from '../src'; +import { awaitFakeTimer, placementAlignMap } from './util'; + +jest.mock('../src/Popup', () => { + const OriPopup = jest.requireActual('../src/Popup').default; + + return React.forwardRef((props, ref) => { + console.log(2333); + return ; + }); +}); + +describe('Trigger.Basic', () => { + beforeAll(() => { + spyElementPrototypes(HTMLElement, { + offsetParent: { + get: () => document.body, + }, + }); + }); + + beforeEach(() => { + jest.useFakeTimers(); + }); + + afterEach(() => { + cleanup(); + jest.useRealTimers(); + }); + + function trigger(dom, selector, method = 'click') { + fireEvent[method](dom.querySelector(selector)); + act(() => jest.runAllTimers()); + } + + describe('Performance', () => { + it('not create Popup when !open', () => { + const { container } = render( + tooltip2} + > +
click
+
, + ); + + // trigger(container, '.target'); + + // const popupDomNode = document.querySelector('.rc-trigger-popup'); + // expect(popupDomNode.parentNode.parentNode).toBeInstanceOf( + // HTMLBodyElement, + // ); + }); + }); +}); From 868a6ade85040965eb8fbb37a9a57398c353186c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8C=E8=B4=A7=E6=9C=BA=E5=99=A8=E4=BA=BA?= Date: Wed, 14 May 2025 19:43:25 +0800 Subject: [PATCH 4/4] test: add test case --- tests/perf.test.jsx | 60 -------------------------- tests/perf.test.tsx | 102 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 102 insertions(+), 60 deletions(-) delete mode 100644 tests/perf.test.jsx create mode 100644 tests/perf.test.tsx diff --git a/tests/perf.test.jsx b/tests/perf.test.jsx deleted file mode 100644 index 47e77a09..00000000 --- a/tests/perf.test.jsx +++ /dev/null @@ -1,60 +0,0 @@ -/* eslint-disable max-classes-per-file */ -import { act, cleanup, fireEvent, render } from '@testing-library/react'; -import { spyElementPrototypes } from '@rc-component/util/lib/test/domHook'; -import React from 'react'; -import Trigger from '../src'; -import { awaitFakeTimer, placementAlignMap } from './util'; - -jest.mock('../src/Popup', () => { - const OriPopup = jest.requireActual('../src/Popup').default; - - return React.forwardRef((props, ref) => { - console.log(2333); - return ; - }); -}); - -describe('Trigger.Basic', () => { - beforeAll(() => { - spyElementPrototypes(HTMLElement, { - offsetParent: { - get: () => document.body, - }, - }); - }); - - beforeEach(() => { - jest.useFakeTimers(); - }); - - afterEach(() => { - cleanup(); - jest.useRealTimers(); - }); - - function trigger(dom, selector, method = 'click') { - fireEvent[method](dom.querySelector(selector)); - act(() => jest.runAllTimers()); - } - - describe('Performance', () => { - it('not create Popup when !open', () => { - const { container } = render( - tooltip2} - > -
click
-
, - ); - - // trigger(container, '.target'); - - // const popupDomNode = document.querySelector('.rc-trigger-popup'); - // expect(popupDomNode.parentNode.parentNode).toBeInstanceOf( - // HTMLBodyElement, - // ); - }); - }); -}); 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); + }); + }); +});