Skip to content

Commit 946cbbb

Browse files
committed
Rename registry
1 parent fb4121b commit 946cbbb

3 files changed

Lines changed: 258 additions & 1 deletion

File tree

packages/react-native-reanimated/Common/cpp/reanimated/CSS/events/CSSEventsEmitter.cpp

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,19 @@ namespace reanimated::css {
88

99
namespace {
1010

11+
bool isAnimationEvent(const std::string &type) {
12+
return type.rfind("animation", 0) == 0;
13+
}
14+
1115
jsi::Object eventToJSIObject(jsi::Runtime &rt, const CSSEvent &event) {
1216
auto obj = jsi::Object(rt);
1317
obj.setProperty(rt, "viewTag", event.viewTag);
1418
obj.setProperty(rt, "type", jsi::String::createFromUtf8(rt, event.type));
15-
obj.setProperty(rt, "animationName", jsi::String::createFromUtf8(rt, event.targetName));
19+
obj.setProperty(
20+
rt,
21+
// TODO - add support for transition events
22+
isAnimationEvent(event.type) ? "animationName" : "propertyName",
23+
jsi::String::createFromUtf8(rt, event.targetName));
1624
obj.setProperty(rt, "elapsedTime", event.elapsedTime);
1725
return obj;
1826
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
'use strict';
2+
3+
export type CSSAnimationEventType =
4+
| 'animationstart'
5+
| 'animationend'
6+
| 'animationiteration';
7+
8+
type CSSEventType = CSSAnimationEventType; // TODO - add CSS transition events support
9+
10+
type CSSAnimationEventPayload = {
11+
viewTag: number;
12+
type: CSSAnimationEventType;
13+
animationName: string;
14+
elapsedTime: number;
15+
};
16+
17+
type CSSEventPayload = CSSAnimationEventPayload;
18+
19+
type CSSEventHandler = (event: CSSEventPayload) => void;
20+
21+
type ViewEventHandlers = Map<CSSEventType, CSSEventHandler>;
22+
23+
/**
24+
* Singleton registry that stores CSS event handlers (e.g. onAnimationStart,
25+
* onAnimationEnd) per view and event type. C++ dispatches batched events to the
26+
* `handleEvents` method, which routes each event to the correct handler.
27+
*/
28+
class CSSEventHandlersRegistry {
29+
private readonly handlers_: Map<number, ViewEventHandlers> = new Map();
30+
31+
addListener(
32+
viewTag: number,
33+
eventType: CSSEventType,
34+
handler: CSSEventHandler
35+
): void {
36+
let viewHandlers = this.handlers_.get(viewTag);
37+
if (!viewHandlers) {
38+
viewHandlers = new Map();
39+
this.handlers_.set(viewTag, viewHandlers);
40+
}
41+
viewHandlers.set(eventType, handler);
42+
}
43+
44+
removeListener(viewTag: number, eventType: CSSEventType): void {
45+
const viewHandlers = this.handlers_.get(viewTag);
46+
if (!viewHandlers) {
47+
return;
48+
}
49+
viewHandlers.delete(eventType);
50+
if (viewHandlers.size === 0) {
51+
this.handlers_.delete(viewTag);
52+
}
53+
}
54+
55+
/**
56+
* Called from C++ via jsInvoker with a batch of events. Routes each event to
57+
* the registered handler for its view + type.
58+
*/
59+
handleEvents(events: CSSEventPayload[]): void {
60+
for (const event of events) {
61+
this.handlers_.get(event.viewTag)?.get(event.type)?.(event);
62+
}
63+
}
64+
65+
clear(): void {
66+
this.handlers_.clear();
67+
}
68+
}
69+
70+
const cssEventHandlersRegistry = new CSSEventHandlersRegistry();
71+
72+
export default cssEventHandlersRegistry;
Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
'use strict';
2+
import { ANIMATION_NAME_PREFIX } from '../../../constants';
3+
import type { CSSAnimationEventType } from '../CSSEventHandlersRegistry';
4+
import cssEventHandlersRegistry from '../CSSEventHandlersRegistry';
5+
6+
const animationName = (id: number) => `${ANIMATION_NAME_PREFIX}${id}`;
7+
8+
function animationEvent(
9+
viewTag: number,
10+
type: CSSAnimationEventType,
11+
elapsedTime: number,
12+
nameId = 0
13+
) {
14+
return {
15+
viewTag,
16+
type,
17+
animationName: animationName(nameId),
18+
elapsedTime,
19+
};
20+
}
21+
22+
describe('CSSEventHandlersRegistry', () => {
23+
const viewTag1 = 1;
24+
const viewTag2 = 2;
25+
26+
beforeEach(() => {
27+
cssEventHandlersRegistry.clear();
28+
});
29+
30+
describe('addListener', () => {
31+
test('registers a handler for a view and event type', () => {
32+
const handler = jest.fn();
33+
cssEventHandlersRegistry.addListener(viewTag1, 'animationstart', handler);
34+
35+
const event = animationEvent(viewTag1, 'animationstart', 0);
36+
cssEventHandlersRegistry.handleEvents([event]);
37+
38+
expect(handler).toHaveBeenCalledTimes(1);
39+
expect(handler).toHaveBeenCalledWith(event);
40+
});
41+
42+
test('replaces existing handler for the same view and event type', () => {
43+
const handler1 = jest.fn();
44+
const handler2 = jest.fn();
45+
cssEventHandlersRegistry.addListener(viewTag1, 'animationend', handler1);
46+
cssEventHandlersRegistry.addListener(viewTag1, 'animationend', handler2);
47+
48+
const event = animationEvent(viewTag1, 'animationend', 1);
49+
cssEventHandlersRegistry.handleEvents([event]);
50+
51+
expect(handler1).not.toHaveBeenCalled();
52+
expect(handler2).toHaveBeenCalledTimes(1);
53+
expect(handler2).toHaveBeenCalledWith(event);
54+
});
55+
56+
test('supports multiple event types for the same view', () => {
57+
const startHandler = jest.fn();
58+
const endHandler = jest.fn();
59+
cssEventHandlersRegistry.addListener(
60+
viewTag1,
61+
'animationstart',
62+
startHandler
63+
);
64+
cssEventHandlersRegistry.addListener(
65+
viewTag1,
66+
'animationend',
67+
endHandler
68+
);
69+
70+
const startEvent = animationEvent(viewTag1, 'animationstart', 0);
71+
const endEvent = animationEvent(viewTag1, 'animationend', 1);
72+
cssEventHandlersRegistry.handleEvents([startEvent, endEvent]);
73+
74+
expect(startHandler).toHaveBeenCalledTimes(1);
75+
expect(startHandler).toHaveBeenCalledWith(startEvent);
76+
expect(endHandler).toHaveBeenCalledTimes(1);
77+
expect(endHandler).toHaveBeenCalledWith(endEvent);
78+
});
79+
80+
test('supports handlers for different views', () => {
81+
const handler1 = jest.fn();
82+
const handler2 = jest.fn();
83+
cssEventHandlersRegistry.addListener(
84+
viewTag1,
85+
'animationstart',
86+
handler1
87+
);
88+
cssEventHandlersRegistry.addListener(
89+
viewTag2,
90+
'animationstart',
91+
handler2
92+
);
93+
94+
const event = animationEvent(viewTag1, 'animationstart', 0);
95+
cssEventHandlersRegistry.handleEvents([event]);
96+
97+
expect(handler1).toHaveBeenCalledTimes(1);
98+
expect(handler1).toHaveBeenCalledWith(event);
99+
expect(handler2).not.toHaveBeenCalled();
100+
});
101+
});
102+
103+
describe('removeListener', () => {
104+
test('stops the handler from being called', () => {
105+
const handler = jest.fn();
106+
cssEventHandlersRegistry.addListener(viewTag1, 'animationstart', handler);
107+
cssEventHandlersRegistry.removeListener(viewTag1, 'animationstart');
108+
109+
cssEventHandlersRegistry.handleEvents([
110+
animationEvent(viewTag1, 'animationstart', 0),
111+
]);
112+
113+
expect(handler).not.toHaveBeenCalled();
114+
});
115+
116+
test('does not throw for non-existent view', () => {
117+
expect(() => {
118+
cssEventHandlersRegistry.removeListener(999, 'animationstart');
119+
}).not.toThrow();
120+
});
121+
});
122+
123+
describe('handleEvents', () => {
124+
test('ignores events for views with no handlers', () => {
125+
const handler = jest.fn();
126+
cssEventHandlersRegistry.addListener(viewTag1, 'animationstart', handler);
127+
128+
cssEventHandlersRegistry.handleEvents([
129+
animationEvent(viewTag2, 'animationstart', 0),
130+
]);
131+
132+
expect(handler).not.toHaveBeenCalled();
133+
});
134+
135+
test('ignores events for unregistered event types', () => {
136+
const handler = jest.fn();
137+
cssEventHandlersRegistry.addListener(viewTag1, 'animationstart', handler);
138+
139+
cssEventHandlersRegistry.handleEvents([
140+
animationEvent(viewTag1, 'animationend', 1),
141+
]);
142+
143+
expect(handler).not.toHaveBeenCalled();
144+
});
145+
146+
test('dispatches multiple events in batch order', () => {
147+
const calls: string[] = [];
148+
const startHandler = () => calls.push('start');
149+
const iterationHandler = () => calls.push('iteration');
150+
const endHandler = () => calls.push('end');
151+
152+
cssEventHandlersRegistry.addListener(
153+
viewTag1,
154+
'animationstart',
155+
startHandler
156+
);
157+
cssEventHandlersRegistry.addListener(
158+
viewTag1,
159+
'animationiteration',
160+
iterationHandler
161+
);
162+
cssEventHandlersRegistry.addListener(
163+
viewTag1,
164+
'animationend',
165+
endHandler
166+
);
167+
168+
cssEventHandlersRegistry.handleEvents([
169+
animationEvent(viewTag1, 'animationstart', 0),
170+
animationEvent(viewTag1, 'animationiteration', 1),
171+
animationEvent(viewTag1, 'animationend', 2),
172+
]);
173+
174+
expect(calls).toEqual(['start', 'iteration', 'end']);
175+
});
176+
});
177+
});

0 commit comments

Comments
 (0)