Skip to content

Commit af19e2e

Browse files
authored
Event API: adds pointerType to Focus events (#15645)
1 parent cc24d0e commit af19e2e

2 files changed

Lines changed: 161 additions & 31 deletions

File tree

packages/react-events/src/Focus.js

Lines changed: 88 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -26,13 +26,16 @@ type FocusState = {
2626
focusTarget: null | Element | Document,
2727
isFocused: boolean,
2828
isLocalFocusVisible: boolean,
29+
pointerType: PointerType,
2930
};
3031

32+
type PointerType = '' | 'mouse' | 'keyboard' | 'pen' | 'touch';
3133
type FocusEventType = 'focus' | 'blur' | 'focuschange' | 'focusvisiblechange';
3234

3335
type FocusEvent = {|
3436
target: Element | Document,
3537
type: FocusEventType,
38+
pointerType: PointerType,
3639
timeStamp: number,
3740
|};
3841

@@ -45,25 +48,33 @@ const rootEventTypes = [
4548
'keydown',
4649
'keypress',
4750
'keyup',
48-
'mousemove',
49-
'mousedown',
50-
'mouseup',
5151
'pointermove',
5252
'pointerdown',
5353
'pointerup',
54-
'touchmove',
55-
'touchstart',
56-
'touchend',
5754
];
5855

56+
// If PointerEvents is not supported (e.g., Safari), also listen to touch and mouse events.
57+
if (typeof window !== 'undefined' && window.PointerEvent === undefined) {
58+
rootEventTypes.push(
59+
'mousemove',
60+
'mousedown',
61+
'mouseup',
62+
'touchmove',
63+
'touchstart',
64+
'touchend',
65+
);
66+
}
67+
5968
function createFocusEvent(
6069
context: ReactResponderContext,
6170
type: FocusEventType,
6271
target: Element | Document,
72+
pointerType: PointerType,
6373
): FocusEvent {
6474
return {
6575
target,
6676
type,
77+
pointerType,
6778
timeStamp: context.getTimeStamp(),
6879
};
6980
}
@@ -73,16 +84,27 @@ function dispatchFocusInEvents(
7384
props: FocusProps,
7485
state: FocusState,
7586
) {
87+
const pointerType = state.pointerType;
7688
const target = ((state.focusTarget: any): Element | Document);
7789
if (props.onFocus) {
78-
const syntheticEvent = createFocusEvent(context, 'focus', target);
90+
const syntheticEvent = createFocusEvent(
91+
context,
92+
'focus',
93+
target,
94+
pointerType,
95+
);
7996
context.dispatchEvent(syntheticEvent, props.onFocus, {discrete: true});
8097
}
8198
if (props.onFocusChange) {
8299
const listener = () => {
83100
props.onFocusChange(true);
84101
};
85-
const syntheticEvent = createFocusEvent(context, 'focuschange', target);
102+
const syntheticEvent = createFocusEvent(
103+
context,
104+
'focuschange',
105+
target,
106+
pointerType,
107+
);
86108
context.dispatchEvent(syntheticEvent, listener, {discrete: true});
87109
}
88110
if (props.onFocusVisibleChange && state.isLocalFocusVisible) {
@@ -93,6 +115,7 @@ function dispatchFocusInEvents(
93115
context,
94116
'focusvisiblechange',
95117
target,
118+
pointerType,
96119
);
97120
context.dispatchEvent(syntheticEvent, listener, {discrete: true});
98121
}
@@ -103,16 +126,27 @@ function dispatchFocusOutEvents(
103126
props: FocusProps,
104127
state: FocusState,
105128
) {
129+
const pointerType = state.pointerType;
106130
const target = ((state.focusTarget: any): Element | Document);
107131
if (props.onBlur) {
108-
const syntheticEvent = createFocusEvent(context, 'blur', target);
132+
const syntheticEvent = createFocusEvent(
133+
context,
134+
'blur',
135+
target,
136+
pointerType,
137+
);
109138
context.dispatchEvent(syntheticEvent, props.onBlur, {discrete: true});
110139
}
111140
if (props.onFocusChange) {
112141
const listener = () => {
113142
props.onFocusChange(false);
114143
};
115-
const syntheticEvent = createFocusEvent(context, 'focuschange', target);
144+
const syntheticEvent = createFocusEvent(
145+
context,
146+
'focuschange',
147+
target,
148+
pointerType,
149+
);
116150
context.dispatchEvent(syntheticEvent, listener, {discrete: true});
117151
}
118152
dispatchFocusVisibleOutEvent(context, props, state);
@@ -123,6 +157,7 @@ function dispatchFocusVisibleOutEvent(
123157
props: FocusProps,
124158
state: FocusState,
125159
) {
160+
const pointerType = state.pointerType;
126161
const target = ((state.focusTarget: any): Element | Document);
127162
if (props.onFocusVisibleChange && state.isLocalFocusVisible) {
128163
const listener = () => {
@@ -132,6 +167,7 @@ function dispatchFocusVisibleOutEvent(
132167
context,
133168
'focusvisiblechange',
134169
target,
170+
pointerType,
135171
);
136172
context.dispatchEvent(syntheticEvent, listener, {discrete: true});
137173
state.isLocalFocusVisible = false;
@@ -148,6 +184,31 @@ function unmountResponder(
148184
}
149185
}
150186

187+
function handleRootPointerEvent(
188+
event: ReactResponderEvent,
189+
context: ReactResponderContext,
190+
props: FocusProps,
191+
state: FocusState,
192+
): void {
193+
const {type, target} = event;
194+
// Ignore a Safari quirks where 'mousemove' is dispatched on the 'html'
195+
// element when the window blurs.
196+
if (type === 'mousemove' && target.nodeName === 'HTML') {
197+
return;
198+
}
199+
200+
isGlobalFocusVisible = false;
201+
202+
// Focus should stop being visible if a pointer is used on the element
203+
// after it was focused using a keyboard.
204+
if (
205+
state.focusTarget === context.getEventCurrentTarget(event) &&
206+
(type === 'mousedown' || type === 'touchstart' || type === 'pointerdown')
207+
) {
208+
dispatchFocusVisibleOutEvent(context, props, state);
209+
}
210+
}
211+
151212
let isGlobalFocusVisible = true;
152213

153214
const FocusResponder = {
@@ -158,6 +219,7 @@ const FocusResponder = {
158219
focusTarget: null,
159220
isFocused: false,
160221
isLocalFocusVisible: false,
222+
pointerType: '',
161223
};
162224
},
163225
stopLocalPropagation: true,
@@ -208,36 +270,30 @@ const FocusResponder = {
208270
props: FocusProps,
209271
state: FocusState,
210272
): void {
211-
const {type, target} = event;
273+
const {type} = event;
212274

213275
switch (type) {
214276
case 'mousemove':
215277
case 'mousedown':
216-
case 'mouseup':
278+
case 'mouseup': {
279+
state.pointerType = 'mouse';
280+
handleRootPointerEvent(event, context, props, state);
281+
break;
282+
}
217283
case 'pointermove':
218284
case 'pointerdown':
219-
case 'pointerup':
285+
case 'pointerup': {
286+
// $FlowFixMe: Flow doesn't know about PointerEvents
287+
const nativeEvent = ((event.nativeEvent: any): PointerEvent);
288+
state.pointerType = nativeEvent.pointerType;
289+
handleRootPointerEvent(event, context, props, state);
290+
break;
291+
}
220292
case 'touchmove':
221293
case 'touchstart':
222294
case 'touchend': {
223-
// Ignore a Safari quirks where 'mousemove' is dispatched on the 'html'
224-
// element when the window blurs.
225-
if (type === 'mousemove' && target.nodeName === 'HTML') {
226-
return;
227-
}
228-
229-
isGlobalFocusVisible = false;
230-
231-
// Focus should stop being visible if a pointer is used on the element
232-
// after it was focused using a keyboard.
233-
if (
234-
state.focusTarget === context.getEventCurrentTarget(event) &&
235-
(type === 'mousedown' ||
236-
type === 'touchstart' ||
237-
type === 'pointerdown')
238-
) {
239-
dispatchFocusVisibleOutEvent(context, props, state);
240-
}
295+
state.pointerType = 'touch';
296+
handleRootPointerEvent(event, context, props, state);
241297
break;
242298
}
243299

@@ -249,6 +305,7 @@ const FocusResponder = {
249305
nativeEvent.key === 'Tab' &&
250306
!(nativeEvent.metaKey || nativeEvent.altKey || nativeEvent.ctrlKey)
251307
) {
308+
state.pointerType = 'keyboard';
252309
isGlobalFocusVisible = true;
253310
}
254311
break;

packages/react-events/src/__tests__/Focus-test.internal.js

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,79 @@ describe('Focus event responder', () => {
131131
target.dispatchEvent(createFocusEvent('focus'));
132132
expect(onFocus).not.toBeCalled();
133133
});
134+
135+
it('is called with the correct pointerType using pointer events', () => {
136+
// Pointer mouse
137+
ref.current.dispatchEvent(
138+
createPointerEvent('pointerdown', {
139+
pointerType: 'mouse',
140+
}),
141+
);
142+
ref.current.dispatchEvent(createFocusEvent('focus'));
143+
expect(onFocus).toHaveBeenCalledTimes(1);
144+
expect(onFocus).toHaveBeenCalledWith(
145+
expect.objectContaining({pointerType: 'mouse'}),
146+
);
147+
ref.current.dispatchEvent(createFocusEvent('blur'));
148+
149+
// Pointer touch
150+
ref.current.dispatchEvent(
151+
createPointerEvent('pointerdown', {
152+
pointerType: 'touch',
153+
}),
154+
);
155+
ref.current.dispatchEvent(createFocusEvent('focus'));
156+
expect(onFocus).toHaveBeenCalledTimes(2);
157+
expect(onFocus).toHaveBeenCalledWith(
158+
expect.objectContaining({pointerType: 'touch'}),
159+
);
160+
ref.current.dispatchEvent(createFocusEvent('blur'));
161+
162+
// Pointer pen
163+
ref.current.dispatchEvent(
164+
createPointerEvent('pointerdown', {
165+
pointerType: 'pen',
166+
}),
167+
);
168+
ref.current.dispatchEvent(createFocusEvent('focus'));
169+
expect(onFocus).toHaveBeenCalledTimes(3);
170+
expect(onFocus).toHaveBeenCalledWith(
171+
expect.objectContaining({pointerType: 'pen'}),
172+
);
173+
});
174+
175+
it('is called with the correct pointerType without pointer events', () => {
176+
// Mouse
177+
ref.current.dispatchEvent(createPointerEvent('mousedown'));
178+
ref.current.dispatchEvent(createFocusEvent('focus'));
179+
expect(onFocus).toHaveBeenCalledTimes(1);
180+
expect(onFocus).toHaveBeenCalledWith(
181+
expect.objectContaining({pointerType: 'mouse'}),
182+
);
183+
ref.current.dispatchEvent(createFocusEvent('blur'));
184+
185+
// Touch
186+
ref.current.dispatchEvent(createPointerEvent('touchstart'));
187+
ref.current.dispatchEvent(createFocusEvent('focus'));
188+
expect(onFocus).toHaveBeenCalledTimes(2);
189+
expect(onFocus).toHaveBeenCalledWith(
190+
expect.objectContaining({pointerType: 'touch'}),
191+
);
192+
});
193+
194+
it('is called with the correct pointerType using a keyboard', () => {
195+
// Keyboard tab
196+
ref.current.dispatchEvent(
197+
createPointerEvent('keypress', {
198+
key: 'Tab',
199+
}),
200+
);
201+
ref.current.dispatchEvent(createFocusEvent('focus'));
202+
expect(onFocus).toHaveBeenCalledTimes(1);
203+
expect(onFocus).toHaveBeenCalledWith(
204+
expect.objectContaining({pointerType: 'keyboard'}),
205+
);
206+
});
134207
});
135208

136209
describe('onFocusChange', () => {

0 commit comments

Comments
 (0)