Skip to content

Commit 05b9dc0

Browse files
committed
feat: remove stale gestures when diff
1 parent 3059ad3 commit 05b9dc0

File tree

9 files changed

+183
-10
lines changed

9 files changed

+183
-10
lines changed

.changeset/all-crabs-smell.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@lynx-js/testing-environment': patch
3+
---
4+
5+
Add `__RemoveGestureDetector` PAPI binding

.changeset/fair-horses-worry.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@lynx-js/react': patch
3+
---
4+
5+
Remove stale gestures when gestures are removed

packages/react/runtime/__test__/snapshot/gesture.test.jsx

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -357,6 +357,8 @@ describe('Gesture', () => {
357357
globalEnvManager.switchToMainThread();
358358
const rLynxChange = lynx.getNativeApp().callLepusMethod.mock.calls[0];
359359
globalThis[rLynxChange[0]](rLynxChange[1]);
360+
const textElement = __root.__element_root.children[0].children[0];
361+
expect(elementTree.__GetGestureDetectorIds(textElement)).toEqual([3]);
360362

361363
expect(__root.__element_root).toMatchInlineSnapshot(`
362364
<page
@@ -631,6 +633,7 @@ describe('Gesture', () => {
631633

632634
describe('Gesture in spread', () => {
633635
it('normal gesture', async function() {
636+
const spySetGesture = vi.spyOn(globalThis, '__SetGestureDetector');
634637
function Comp() {
635638
const gesture = {
636639
id: 1,
@@ -696,6 +699,29 @@ describe('Gesture in spread', () => {
696699

697700
// Main Thread Render
698701
{
702+
const textElement = __root.__element_root.children[0].children[0];
703+
expect(spySetGesture).toHaveBeenCalledTimes(1);
704+
expect(spySetGesture).toHaveBeenCalledWith(
705+
textElement,
706+
1,
707+
0,
708+
{
709+
callbacks: [
710+
{
711+
name: 'onUpdate',
712+
callback: expect.objectContaining({
713+
_wkltId: 'bdd4:dd564:2',
714+
}),
715+
},
716+
],
717+
},
718+
{
719+
waitFor: [],
720+
simultaneous: [],
721+
continueWith: [],
722+
},
723+
);
724+
699725
expect(__root.__element_root).toMatchInlineSnapshot(`
700726
<page
701727
cssId="default-entry-from-native:0"
@@ -737,6 +763,8 @@ describe('Gesture in spread', () => {
737763
}
738764
});
739765
it('update gesture', async function() {
766+
const spySetGesture = vi.spyOn(globalThis, '__SetGestureDetector');
767+
const spyRemoveGesture = vi.spyOn(globalThis, '__RemoveGestureDetector');
740768
let _gesture = {
741769
id: 1,
742770
type: 0,
@@ -804,6 +832,8 @@ describe('Gesture in spread', () => {
804832
{
805833
globalEnvManager.switchToBackground();
806834
lynx.getNativeApp().callLepusMethod.mockClear();
835+
spySetGesture.mockClear();
836+
spyRemoveGesture.mockClear();
807837

808838
_gesture = {
809839
..._gesture,
@@ -815,6 +845,32 @@ describe('Gesture in spread', () => {
815845
globalEnvManager.switchToMainThread();
816846
const rLynxChange = lynx.getNativeApp().callLepusMethod.mock.calls[0];
817847
globalThis[rLynxChange[0]](rLynxChange[1]);
848+
const textElement = __root.__element_root.children[0].children[0];
849+
850+
expect(spySetGesture).toHaveBeenCalledTimes(1);
851+
expect(spySetGesture).toHaveBeenCalledWith(
852+
textElement,
853+
2,
854+
0,
855+
{
856+
callbacks: [
857+
{
858+
name: 'onUpdate',
859+
callback: expect.objectContaining({
860+
_wkltId: 'bdd4:dd564:2',
861+
}),
862+
},
863+
],
864+
},
865+
{
866+
waitFor: [],
867+
simultaneous: [],
868+
continueWith: [],
869+
},
870+
);
871+
expect(spyRemoveGesture).toHaveBeenCalledTimes(1);
872+
expect(spyRemoveGesture).toHaveBeenCalledWith(textElement, 1);
873+
expect(elementTree.__GetGestureDetectorIds(textElement)).toEqual([2]);
818874

819875
expect(__root.__element_root).toMatchInlineSnapshot(`
820876
<page
@@ -981,6 +1037,8 @@ describe('Gesture in spread', () => {
9811037
}
9821038
});
9831039
it('remove gesture', async function() {
1040+
const spySetGesture = vi.spyOn(globalThis, '__SetGestureDetector');
1041+
const spyRemoveGesture = vi.spyOn(globalThis, '__RemoveGestureDetector');
9841042
let _gesture = {
9851043
id: 1,
9861044
type: 0,
@@ -1048,6 +1106,8 @@ describe('Gesture in spread', () => {
10481106
{
10491107
globalEnvManager.switchToBackground();
10501108
lynx.getNativeApp().callLepusMethod.mockClear();
1109+
spySetGesture.mockClear();
1110+
spyRemoveGesture.mockClear();
10511111

10521112
_gesture = undefined;
10531113

@@ -1056,6 +1116,12 @@ describe('Gesture in spread', () => {
10561116
globalEnvManager.switchToMainThread();
10571117
const rLynxChange = lynx.getNativeApp().callLepusMethod.mock.calls[0];
10581118
globalThis[rLynxChange[0]](rLynxChange[1]);
1119+
const textElement = __root.__element_root.children[0].children[0];
1120+
expect(spySetGesture).not.toHaveBeenCalled();
1121+
expect(spyRemoveGesture).toHaveBeenCalledTimes(1);
1122+
expect(spyRemoveGesture).toHaveBeenCalledWith(textElement, 1);
1123+
expect(elementTree.__GetGestureDetectorIds(textElement)).toEqual([]);
1124+
expect(textElement.props['has-react-gesture']).toBeUndefined();
10591125

10601126
expect(__root.__element_root).toMatchInlineSnapshot(`
10611127
<page

packages/react/runtime/__test__/utils/nativeMethod.ts

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,15 @@ interface ElementOptions {
1919

2020
export let uiSignNext = 0;
2121
export const parentMap = new WeakMap<Element, Element>();
22+
let gestureDetectorMap = new WeakMap<
23+
Element,
24+
Map<number, {
25+
id: number;
26+
type: number;
27+
config: any;
28+
relationMap: Record<string, number[]>;
29+
}>
30+
>();
2231
// export const elementPrototype = Object.create(null);
2332
export const options: ElementOptions = {};
2433

@@ -185,12 +194,40 @@ export const elementTree = new (class {
185194
}
186195

187196
__SetGestureDetector(e: Element, id: number, type: number, config: any, relationMap: Record<string, number[]>) {
188-
e.props.gesture = {
197+
const detector = {
189198
id,
190199
type,
191200
config,
192201
relationMap,
193202
};
203+
let detectors = gestureDetectorMap.get(e);
204+
if (!detectors) {
205+
detectors = new Map();
206+
gestureDetectorMap.set(e, detectors);
207+
}
208+
detectors.set(id, detector);
209+
e.props.gesture = detector;
210+
}
211+
212+
__RemoveGestureDetector(e: Element, id: number) {
213+
const detectors = gestureDetectorMap.get(e);
214+
detectors?.delete(id);
215+
if (e.props.gesture?.id === id) {
216+
const latestDetector = detectors ? [...detectors.values()].at(-1) : undefined;
217+
if (latestDetector) {
218+
e.props.gesture = latestDetector;
219+
} else {
220+
delete e.props.gesture;
221+
}
222+
}
223+
}
224+
225+
__GetGestureDetector(e: Element, id: number) {
226+
return gestureDetectorMap.get(e)?.get(id);
227+
}
228+
229+
__GetGestureDetectorIds(e: Element) {
230+
return [...(gestureDetectorMap.get(e)?.keys() ?? [])];
194231
}
195232

196233
__GetDataset(e: Element) {
@@ -317,6 +354,7 @@ export const elementTree = new (class {
317354
clear() {
318355
this.root = undefined;
319356
uiSignNext = 0;
357+
gestureDetectorMap = new WeakMap();
320358
}
321359

322360
toTree() {

packages/react/runtime/src/alog/elementPAPICall.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ const fiberElementPAPINameList = [
4242
'__OnLifecycleEvent',
4343
'__QueryComponent',
4444
'__SetGestureDetector',
45+
'__RemoveGestureDetector',
4546
];
4647

4748
export function initElementPAPICallAlog(globalWithIndex: Record<string, unknown> = globalThis): void {

packages/react/runtime/src/gesture/processGesture.ts

Lines changed: 59 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,25 @@ function isSerializedGesture(gesture: GestureKind): boolean {
1010
return gesture.__isSerialized ?? false;
1111
}
1212

13+
function collectSerializedBaseGestures(gesture: GestureKind | undefined): BaseGesture[] {
14+
if (!gesture || !isSerializedGesture(gesture)) {
15+
return [];
16+
}
17+
18+
if (gesture.type === GestureTypeInner.COMPOSED) {
19+
return (gesture as ComposedGesture).gestures.flatMap((subGesture) => collectSerializedBaseGestures(subGesture));
20+
}
21+
22+
return [gesture as BaseGesture];
23+
}
24+
25+
function removeGestureDetector(dom: FiberElement, id: number): void {
26+
// Keep compatibility with old runtimes where remove API is not exposed.
27+
if (typeof __RemoveGestureDetector === 'function') {
28+
__RemoveGestureDetector(dom, id);
29+
}
30+
}
31+
1332
function getGestureInfo(
1433
gesture: BaseGesture,
1534
oldGesture: BaseGesture | undefined,
@@ -58,7 +77,39 @@ export function processGesture(
5877
domSet: boolean;
5978
},
6079
): void {
61-
if (!gesture || !isSerializedGesture(gesture)) {
80+
const baseGestures = collectSerializedBaseGestures(gesture);
81+
const oldBaseGestures = collectSerializedBaseGestures(oldGesture);
82+
83+
const uniqBaseGestures: BaseGesture[] = [];
84+
const uniqBaseGestureIds = new Set<number>();
85+
for (const baseGesture of baseGestures) {
86+
if (!uniqBaseGestureIds.has(baseGesture.id)) {
87+
uniqBaseGestureIds.add(baseGesture.id);
88+
uniqBaseGestures.push(baseGesture);
89+
}
90+
}
91+
92+
const uniqOldBaseGestures: BaseGesture[] = [];
93+
const oldBaseGesturesById = new Map<number, BaseGesture>();
94+
for (const oldBaseGesture of oldBaseGestures) {
95+
if (!oldBaseGesturesById.has(oldBaseGesture.id)) {
96+
oldBaseGesturesById.set(oldBaseGesture.id, oldBaseGesture);
97+
uniqOldBaseGestures.push(oldBaseGesture);
98+
}
99+
}
100+
101+
if (uniqBaseGestures.length === 0) {
102+
if (oldBaseGesturesById.size === 0) {
103+
return;
104+
}
105+
106+
for (const oldBaseGesture of oldBaseGesturesById.values()) {
107+
removeGestureDetector(dom, oldBaseGesture.id);
108+
}
109+
110+
if (!(gestureOptions && gestureOptions.domSet)) {
111+
__SetAttribute(dom, 'has-react-gesture', null);
112+
}
62113
return;
63114
}
64115

@@ -67,16 +118,15 @@ export function processGesture(
67118
__SetAttribute(dom, 'flatten', false);
68119
}
69120

70-
if (gesture.type === GestureTypeInner.COMPOSED) {
71-
for (const [index, subGesture] of (gesture as ComposedGesture).gestures.entries()) {
72-
processGesture(dom, subGesture, (oldGesture as ComposedGesture)?.gestures[index], isFirstScreen, {
73-
domSet: true,
74-
});
121+
for (const oldBaseGesture of oldBaseGesturesById.values()) {
122+
if (!uniqBaseGestureIds.has(oldBaseGesture.id)) {
123+
removeGestureDetector(dom, oldBaseGesture.id);
75124
}
76-
} else {
77-
const baseGesture = gesture as BaseGesture;
78-
const oldBaseGesture = oldGesture as BaseGesture | undefined;
125+
}
79126

127+
for (const [index, baseGesture] of uniqBaseGestures.entries()) {
128+
const oldBaseGesture = oldBaseGesturesById.get(baseGesture.id)
129+
?? uniqOldBaseGestures[index];
80130
const { config, relationMap } = getGestureInfo(baseGesture, oldBaseGesture, isFirstScreen, dom);
81131
__SetGestureDetector(
82132
dom,

packages/react/runtime/types/types.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@ declare global {
116116
config: any,
117117
relationMap: Record<string, number[]>,
118118
): void;
119+
declare function __RemoveGestureDetector(node: FiberElement, id: number): void;
119120

120121
declare interface FiberElement {}
121122

packages/testing-library/testing-environment/etc/testing-environment.api.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ export const initElementTree: () => {
4848
__AddDataset(e: LynxElement, key: string, value: string): void;
4949
__SetDataset(e: LynxElement, dataset: any): void;
5050
__SetGestureDetector(e: LynxElement, id: number, type: number, config: any, relationMap: Record<string, number[]>): void;
51+
__RemoveGestureDetector(e: LynxElement, id: number): void;
5152
__GetDataset(e: LynxElement): DOMStringMap;
5253
__RemoveElement(parent: LynxElement, child: LynxElement): void;
5354
__InsertElementBefore(parent: LynxElement, child: LynxElement, ref?: LynxElement | undefined): void;

packages/testing-library/testing-environment/src/lynx/ElementPAPI.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -315,6 +315,12 @@ export const initElementTree = () => {
315315
};
316316
}
317317

318+
__RemoveGestureDetector(e: LynxElement, id: number) {
319+
if (e.gesture?.id === id) {
320+
e.gesture = undefined;
321+
}
322+
}
323+
318324
__GetDataset(e: LynxElement) {
319325
return e.dataset;
320326
}

0 commit comments

Comments
 (0)