Skip to content

Commit 8a4cf38

Browse files
rozelemeta-codesync[bot]
authored andcommitted
Read hover callbacks from the live Pressability config at invocation (#59363)
Summary: Pull Request resolved: #59363 `Pressability._createEventHandlers` destructured `onHoverIn`/`onHoverOut` out of `this._config` once, and `getEventHandlers` memoises the handler object for the life of the instance. `configure` replaces the config on every update, but the W3C-pointer hover closures kept calling the callbacks captured when the handlers were first created, so a hover callback that closes over component state observed that state as it was at mount, for the life of the view, however many times the component re-rendered. Every other handler in `_createEventHandlers` already reads `this._config` inside its own body, and so does the legacy mouse branch below. This makes the W3C pointer branch do the same. It also restores `reset`'s documented guarantee for hover: after `reset` freezes `this._config`, a late `onPointerEnter` no longer calls a configured callback. Whether to attach `onPointerEnter`/`onPointerLeave` at all is still decided once, from the config present when the handlers are created, because that presence is what makes the host view listen for pointer enter/leave. A view that mounts without a hover callback and is given one later still does not begin listening. That behaviour is unchanged by this diff. Only the branch behind `shouldPressibilityUseW3CPointerEventsForHover` is touched. With that flag at its default the generated handlers are byte-identical to before. Changelog: [General][Fixed] - `Pressability` now invokes the current `onHoverIn`/`onHoverOut` rather than the ones captured when its event handlers were created Reviewed By: christophpurrer Differential Revision: D126767220 fbshipit-source-id: 6bc1d2f663d503797c9c0762a79b6d35c7311fdf
1 parent e9443ba commit 8a4cf38

2 files changed

Lines changed: 206 additions & 5 deletions

File tree

‎packages/react-native/Libraries/Pressability/Pressability.js‎

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -563,11 +563,16 @@ export default class Pressability {
563563
onPointerEnter: undefined as void | (PointerEvent => void),
564564
onPointerLeave: undefined as void | (PointerEvent => void),
565565
};
566-
const {onHoverIn, onHoverOut} = this._config;
567-
if (onHoverIn != null) {
566+
// Whether to attach these at all is necessarily decided once: their
567+
// presence is what makes the host view listen for pointer enter/leave.
568+
// The callbacks themselves are read from `this._config` at invocation,
569+
// as every other handler here does, so that `configure` is observed.
570+
const configAtCreation = this._config;
571+
if (configAtCreation.onHoverIn != null) {
568572
hoverPointerEvents.onPointerEnter = (event: PointerEvent) => {
569573
this._isHovered = true;
570574
this._cancelHoverOutDelayTimeout();
575+
const {onHoverIn} = this._config;
571576
if (onHoverIn != null) {
572577
const delayHoverIn = normalizeDelay(this._config.delayHoverIn);
573578
if (delayHoverIn > 0) {
@@ -581,11 +586,12 @@ export default class Pressability {
581586
}
582587
};
583588
}
584-
if (onHoverOut != null) {
589+
if (configAtCreation.onHoverOut != null) {
585590
hoverPointerEvents.onPointerLeave = (event: PointerEvent) => {
586591
if (this._isHovered) {
587592
this._isHovered = false;
588593
this._cancelHoverInDelayTimeout();
594+
const {onHoverOut} = this._config;
589595
if (onHoverOut != null) {
590596
const delayHoverOut = normalizeDelay(this._config.delayHoverOut);
591597
if (delayHoverOut > 0) {

‎packages/react-native/Libraries/Pressability/__tests__/Pressability-test.js‎

Lines changed: 197 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,11 @@
88
* @format
99
*/
1010

11-
import type {GestureResponderEvent} from '../../Types/CoreEventTypes';
12-
import type {PressabilityConfig} from '../Pressability';
11+
import type {
12+
GestureResponderEvent,
13+
PointerEvent,
14+
} from '../../Types/CoreEventTypes';
15+
import type {EventHandlers, PressabilityConfig} from '../Pressability';
1316

1417
const UIManager = require('../../ReactNative/UIManager').default;
1518
const Platform = require('../../Utilities/Platform').default;
@@ -164,6 +167,60 @@ const createMockMouseEvent = (registrationName: string) => {
164167
};
165168
};
166169

170+
const createMockPointerEvent = (registrationName: string): PointerEvent => {
171+
const nativeEvent = {
172+
altKey: false,
173+
button: 0,
174+
buttons: 0,
175+
clientX: 0,
176+
clientY: 0,
177+
ctrlKey: false,
178+
detail: 0,
179+
height: 1,
180+
isPrimary: true,
181+
metaKey: false,
182+
offsetX: 0,
183+
offsetY: 0,
184+
pageX: 0,
185+
pageY: 0,
186+
pointerId: 1,
187+
pointerType: 'mouse',
188+
pressure: 0,
189+
relatedTarget: null,
190+
screenX: 0,
191+
screenY: 0,
192+
shiftKey: false,
193+
tangentialPressure: 0,
194+
tiltX: 0,
195+
tiltY: 0,
196+
twist: 0,
197+
width: 1,
198+
x: 0,
199+
y: 0,
200+
};
201+
202+
return {
203+
bubbles: null,
204+
cancelable: null,
205+
currentTarget: 42,
206+
defaultPrevented: null,
207+
dispatchConfig: {
208+
registrationName,
209+
},
210+
eventPhase: null,
211+
preventDefault: jest.fn(() => undefined),
212+
isDefaultPrevented: jest.fn(() => false),
213+
stopPropagation: jest.fn(() => undefined),
214+
isPropagationStopped: jest.fn(() => false),
215+
isTrusted: null,
216+
nativeEvent,
217+
persist: jest.fn(),
218+
target: null,
219+
timeStamp: 1075881600000,
220+
type: null,
221+
};
222+
};
223+
167224
const createMockPressEvent = (
168225
nameOrOverrides:
169226
| string
@@ -983,6 +1040,144 @@ describe('Pressability', () => {
9831040
});
9841041
});
9851042

1043+
// The host view is given the handler object `getEventHandlers` built at
1044+
// mount; `configure` then replaces the config on every subsequent render.
1045+
// A handler that closes over its callback instead of reading `this._config`
1046+
// therefore keeps calling the mount-time closure for the life of the view,
1047+
// which on Android is the only path hover can take.
1048+
describe('hover with W3C pointer events', () => {
1049+
function requirePressabilityWithW3CHover() {
1050+
jest.doMock(
1051+
'../../../src/private/featureflags/specs/NativeReactNativeFeatureFlags',
1052+
() => ({
1053+
__esModule: true,
1054+
default: {
1055+
shouldPressibilityUseW3CPointerEventsForHover: () => true,
1056+
},
1057+
}),
1058+
);
1059+
return require('../Pressability').default;
1060+
}
1061+
1062+
// Mirrors a React render closure: each render produces a new callback
1063+
// that has captured that render's state value.
1064+
function configForRender(
1065+
isReady: boolean,
1066+
observed: Array<boolean>,
1067+
): PressabilityConfig {
1068+
return {
1069+
onHoverIn: () => {
1070+
observed.push(isReady);
1071+
},
1072+
onHoverOut: () => {
1073+
observed.push(isReady);
1074+
},
1075+
};
1076+
}
1077+
1078+
// `onPointerEnter`/`onPointerLeave` are optional on `EventHandlers`.
1079+
function dispatchPointerEnter(handlers: EventHandlers): void {
1080+
const {onPointerEnter} = handlers;
1081+
invariant(
1082+
typeof onPointerEnter === 'function',
1083+
'Expected to find "onPointerEnter" function',
1084+
);
1085+
onPointerEnter(createMockPointerEvent('onPointerEnter'));
1086+
}
1087+
1088+
function dispatchPointerLeave(handlers: EventHandlers): void {
1089+
const {onPointerLeave} = handlers;
1090+
invariant(
1091+
typeof onPointerLeave === 'function',
1092+
'Expected to find "onPointerLeave" function',
1093+
);
1094+
onPointerLeave(createMockPointerEvent('onPointerLeave'));
1095+
}
1096+
1097+
it('gives the host view the same handlers across reconfigures', () => {
1098+
const PressabilityWithW3CHover = requirePressabilityWithW3CHover();
1099+
const observed: Array<boolean> = [];
1100+
const pressability = new PressabilityWithW3CHover(
1101+
configForRender(false, observed),
1102+
);
1103+
const mountHandlers = pressability.getEventHandlers();
1104+
1105+
pressability.configure(configForRender(true, observed));
1106+
1107+
expect(pressability.getEventHandlers().onPointerEnter).toBe(
1108+
mountHandlers.onPointerEnter,
1109+
);
1110+
expect(pressability.getEventHandlers().onPointerLeave).toBe(
1111+
mountHandlers.onPointerLeave,
1112+
);
1113+
});
1114+
1115+
it('calls the current `onHoverIn`, not the one captured at mount', () => {
1116+
const PressabilityWithW3CHover = requirePressabilityWithW3CHover();
1117+
const observed: Array<boolean> = [];
1118+
const pressability = new PressabilityWithW3CHover(
1119+
configForRender(false, observed),
1120+
);
1121+
const mountHandlers = pressability.getEventHandlers();
1122+
1123+
pressability.configure(configForRender(true, observed));
1124+
dispatchPointerEnter(mountHandlers);
1125+
1126+
expect(observed).toEqual([true]);
1127+
});
1128+
1129+
it('calls the current `onHoverOut`, not the one captured at mount', () => {
1130+
const PressabilityWithW3CHover = requirePressabilityWithW3CHover();
1131+
const observed: Array<boolean> = [];
1132+
const pressability = new PressabilityWithW3CHover(
1133+
configForRender(false, observed),
1134+
);
1135+
const mountHandlers = pressability.getEventHandlers();
1136+
1137+
dispatchPointerEnter(mountHandlers);
1138+
observed.length = 0;
1139+
1140+
pressability.configure(configForRender(true, observed));
1141+
dispatchPointerLeave(mountHandlers);
1142+
1143+
expect(observed).toEqual([true]);
1144+
});
1145+
1146+
it('calls the current `onHoverIn` after `delayHoverIn`', () => {
1147+
const PressabilityWithW3CHover = requirePressabilityWithW3CHover();
1148+
const observed: Array<boolean> = [];
1149+
const pressability = new PressabilityWithW3CHover({
1150+
...configForRender(false, observed),
1151+
delayHoverIn: 100,
1152+
});
1153+
const mountHandlers = pressability.getEventHandlers();
1154+
1155+
pressability.configure({
1156+
...configForRender(true, observed),
1157+
delayHoverIn: 100,
1158+
});
1159+
dispatchPointerEnter(mountHandlers);
1160+
jest.advanceTimersByTime(100);
1161+
1162+
expect(observed).toEqual([true]);
1163+
});
1164+
1165+
it('calls nothing after `reset`', () => {
1166+
const PressabilityWithW3CHover = requirePressabilityWithW3CHover();
1167+
const observed: Array<boolean> = [];
1168+
const pressability = new PressabilityWithW3CHover(
1169+
configForRender(false, observed),
1170+
);
1171+
const mountHandlers = pressability.getEventHandlers();
1172+
1173+
pressability.reset();
1174+
dispatchPointerEnter(mountHandlers);
1175+
dispatchPointerLeave(mountHandlers);
1176+
1177+
expect(observed).toEqual([]);
1178+
});
1179+
});
1180+
9861181
afterEach(() => {
9871182
jest.useRealTimers();
9881183
});

0 commit comments

Comments
 (0)