Skip to content

Commit 57a5805

Browse files
authored
[react-ui] Add preventDefault+stopPropagation to Keyboard + update Focus components (#16833)
1 parent 08b51aa commit 57a5805

File tree

6 files changed

+124
-141
lines changed

6 files changed

+124
-141
lines changed

packages/react-ui/accessibility/src/FocusTable.js

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,7 @@ export function createFocusTable(): Array<React.Component> {
146146
function Cell({children}): FocusCellProps {
147147
const scopeRef = useRef(null);
148148
const keyboard = useKeyboard({
149-
onKeyDown(event: KeyboardEvent): boolean {
149+
onKeyDown(event: KeyboardEvent): void {
150150
const currentCell = scopeRef.current;
151151
switch (event.key) {
152152
case 'UpArrow': {
@@ -162,7 +162,7 @@ export function createFocusTable(): Array<React.Component> {
162162
}
163163
}
164164
}
165-
return false;
165+
return;
166166
}
167167
case 'DownArrow': {
168168
const [cells, rowIndex] = getRowCells(currentCell);
@@ -179,7 +179,7 @@ export function createFocusTable(): Array<React.Component> {
179179
}
180180
}
181181
}
182-
return false;
182+
return;
183183
}
184184
case 'LeftArrow': {
185185
const [cells, rowIndex] = getRowCells(currentCell);
@@ -190,7 +190,7 @@ export function createFocusTable(): Array<React.Component> {
190190
triggerNavigateOut(currentCell, 'left');
191191
}
192192
}
193-
return false;
193+
return;
194194
}
195195
case 'RightArrow': {
196196
const [cells, rowIndex] = getRowCells(currentCell);
@@ -203,10 +203,10 @@ export function createFocusTable(): Array<React.Component> {
203203
}
204204
}
205205
}
206-
return false;
206+
return;
207207
}
208208
}
209-
return true;
209+
event.continuePropagation();
210210
},
211211
});
212212
return (

packages/react-ui/accessibility/src/TabFocus.js

Lines changed: 43 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -53,10 +53,11 @@ function focusElem(elem: null | HTMLElement): void {
5353
}
5454
}
5555

56-
export function focusNext(
56+
function internalFocusNext(
5757
scope: ReactScopeMethods,
58+
event?: KeyboardEvent,
5859
contain?: boolean,
59-
): boolean {
60+
): void {
6061
const [
6162
tabbableNodes,
6263
firstTabbableElem,
@@ -66,23 +67,31 @@ export function focusNext(
6667
] = getTabbableNodes(scope);
6768

6869
if (focusedElement === null) {
69-
focusElem(firstTabbableElem);
70+
if (event) {
71+
event.continuePropagation();
72+
}
7073
} else if (focusedElement === lastTabbableElem) {
71-
if (contain === true) {
74+
if (contain) {
7275
focusElem(firstTabbableElem);
73-
} else {
74-
return true;
76+
if (event) {
77+
event.preventDefault();
78+
}
79+
} else if (event) {
80+
event.continuePropagation();
7581
}
7682
} else {
7783
focusElem((tabbableNodes: any)[currentIndex + 1]);
84+
if (event) {
85+
event.preventDefault();
86+
}
7887
}
79-
return false;
8088
}
8189

82-
export function focusPrevious(
90+
function internalFocusPrevious(
8391
scope: ReactScopeMethods,
92+
event?: KeyboardEvent,
8493
contain?: boolean,
85-
): boolean {
94+
): void {
8695
const [
8796
tabbableNodes,
8897
firstTabbableElem,
@@ -92,17 +101,32 @@ export function focusPrevious(
92101
] = getTabbableNodes(scope);
93102

94103
if (focusedElement === null) {
95-
focusElem(firstTabbableElem);
104+
if (event) {
105+
event.continuePropagation();
106+
}
96107
} else if (focusedElement === firstTabbableElem) {
97-
if (contain === true) {
108+
if (contain) {
98109
focusElem(lastTabbableElem);
99-
} else {
100-
return true;
110+
if (event) {
111+
event.preventDefault();
112+
}
113+
} else if (event) {
114+
event.continuePropagation();
101115
}
102116
} else {
103117
focusElem((tabbableNodes: any)[currentIndex - 1]);
118+
if (event) {
119+
event.preventDefault();
120+
}
104121
}
105-
return false;
122+
}
123+
124+
export function focusPrevious(scope: ReactScopeMethods): void {
125+
internalFocusPrevious(scope);
126+
}
127+
128+
export function focusNext(scope: ReactScopeMethods): void {
129+
internalFocusNext(scope);
106130
}
107131

108132
export function getNextController(
@@ -137,21 +161,20 @@ export const TabFocusController = React.forwardRef(
137161
({children, contain}: TabFocusControllerProps, ref): React.Node => {
138162
const scopeRef = useRef(null);
139163
const keyboard = useKeyboard({
140-
onKeyDown(event: KeyboardEvent): boolean {
164+
onKeyDown(event: KeyboardEvent): void {
141165
if (event.key !== 'Tab') {
142-
return true;
166+
event.continuePropagation();
167+
return;
143168
}
144169
const scope = scopeRef.current;
145170
if (scope !== null) {
146171
if (event.shiftKey) {
147-
return focusPrevious(scope, contain);
172+
internalFocusPrevious(scope, event, contain);
148173
} else {
149-
return focusNext(scope, contain);
174+
internalFocusNext(scope, event, contain);
150175
}
151176
}
152-
return true;
153177
},
154-
preventKeys: ['Tab', ['Tab', {shiftKey: true}]],
155178
});
156179

157180
return (

packages/react-ui/accessibility/src/__tests__/TabFocus-test.internal.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -255,14 +255,14 @@ describe('TabFocusController', () => {
255255
firstFocusController,
256256
);
257257
expect(nextController).toBe(secondFocusController);
258-
ReactTabFocus.focusNext(nextController);
258+
ReactTabFocus.focusFirst(nextController);
259259
expect(document.activeElement).toBe(divRef.current);
260260

261261
const previousController = ReactTabFocus.getPreviousController(
262262
nextController,
263263
);
264264
expect(previousController).toBe(firstFocusController);
265-
ReactTabFocus.focusNext(previousController);
265+
ReactTabFocus.focusFirst(previousController);
266266
expect(document.activeElement).toBe(buttonRef.current);
267267
});
268268
});

packages/react-ui/events/src/dom/Keyboard.js

Lines changed: 24 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -24,15 +24,12 @@ type KeyboardEventType =
2424

2525
type KeyboardProps = {|
2626
disabled?: boolean,
27-
onClick?: (e: KeyboardEvent) => ?boolean,
28-
onKeyDown?: (e: KeyboardEvent) => ?boolean,
29-
onKeyUp?: (e: KeyboardEvent) => ?boolean,
30-
preventClick?: boolean,
31-
preventKeys?: PreventKeysArray,
27+
onClick?: (e: KeyboardEvent) => void,
28+
onKeyDown?: (e: KeyboardEvent) => void,
29+
onKeyUp?: (e: KeyboardEvent) => void,
3230
|};
3331

3432
type KeyboardState = {|
35-
defaultPrevented: boolean,
3633
isActive: boolean,
3734
|};
3835

@@ -48,20 +45,11 @@ export type KeyboardEvent = {|
4845
target: Element | Document,
4946
type: KeyboardEventType,
5047
timeStamp: number,
48+
continuePropagation: () => void,
49+
preventDefault: () => void,
5150
|};
5251

53-
type ModifiersObject = {|
54-
altKey?: boolean,
55-
ctrlKey?: boolean,
56-
metaKey?: boolean,
57-
shiftKey?: boolean,
58-
|};
59-
60-
type PreventKeysArray = Array<string | Array<string | ModifiersObject>>;
61-
62-
const isArray = Array.isArray;
6352
const targetEventTypes = ['click_active', 'keydown_active', 'keyup'];
64-
const modifiers = ['altKey', 'ctrlKey', 'metaKey', 'shiftKey'];
6553

6654
/**
6755
* Normalization of deprecated HTML5 `key` values
@@ -146,20 +134,31 @@ function createKeyboardEvent(
146134
event: ReactDOMResponderEvent,
147135
context: ReactDOMResponderContext,
148136
type: KeyboardEventType,
149-
defaultPrevented: boolean,
150137
): KeyboardEvent {
151138
const nativeEvent = (event: any).nativeEvent;
152139
const {altKey, ctrlKey, metaKey, shiftKey} = nativeEvent;
153140
let keyboardEvent = {
154141
altKey,
155142
ctrlKey,
156-
defaultPrevented,
143+
defaultPrevented: nativeEvent.defaultPrevented === true,
157144
metaKey,
158145
pointerType: 'keyboard',
159146
shiftKey,
160147
target: event.target,
161148
timeStamp: context.getTimeStamp(),
162149
type,
150+
// We don't use stopPropagation, as the default behavior
151+
// is to not propagate. Plus, there might be confusion
152+
// using stopPropagation as we don't actually stop
153+
// native propagation from working, but instead only
154+
// allow propagation to the others keyboard responders.
155+
continuePropagation() {
156+
context.continuePropagation();
157+
},
158+
preventDefault() {
159+
keyboardEvent.defaultPrevented = true;
160+
nativeEvent.preventDefault();
161+
},
163162
};
164163
if (type !== 'keyboard:click') {
165164
const key = getEventKey(nativeEvent);
@@ -171,32 +170,18 @@ function createKeyboardEvent(
171170

172171
function dispatchKeyboardEvent(
173172
event: ReactDOMResponderEvent,
174-
listener: KeyboardEvent => ?boolean,
173+
listener: KeyboardEvent => void,
175174
context: ReactDOMResponderContext,
176175
type: KeyboardEventType,
177-
defaultPrevented: boolean,
178176
): void {
179-
const syntheticEvent = createKeyboardEvent(
180-
event,
181-
context,
182-
type,
183-
defaultPrevented,
184-
);
185-
let shouldPropagate;
186-
const listenerWithReturnValue = e => {
187-
shouldPropagate = listener(e);
188-
};
189-
context.dispatchEvent(syntheticEvent, listenerWithReturnValue, DiscreteEvent);
190-
if (shouldPropagate) {
191-
context.continuePropagation();
192-
}
177+
const syntheticEvent = createKeyboardEvent(event, context, type);
178+
context.dispatchEvent(syntheticEvent, listener, DiscreteEvent);
193179
}
194180

195181
const keyboardResponderImpl = {
196182
targetEventTypes,
197183
getInitialState(): KeyboardState {
198184
return {
199-
defaultPrevented: false,
200185
isActive: false,
201186
};
202187
},
@@ -207,82 +192,36 @@ const keyboardResponderImpl = {
207192
state: KeyboardState,
208193
): void {
209194
const {type} = event;
210-
const nativeEvent: any = event.nativeEvent;
211195

212196
if (props.disabled) {
213197
return;
214198
}
215199

216200
if (type === 'keydown') {
217-
state.defaultPrevented = nativeEvent.defaultPrevented === true;
218-
219-
const preventKeys = ((props.preventKeys: any): PreventKeysArray);
220-
if (!state.defaultPrevented && isArray(preventKeys)) {
221-
preventKeyLoop: for (let i = 0; i < preventKeys.length; i++) {
222-
const preventKey = preventKeys[i];
223-
let key = preventKey;
224-
225-
if (isArray(preventKey)) {
226-
key = preventKey[0];
227-
const config = ((preventKey[1]: any): Object);
228-
for (let s = 0; s < modifiers.length; s++) {
229-
const modifier = modifiers[s];
230-
const configModifier = config[modifier];
231-
const eventModifier = nativeEvent[modifier];
232-
if (
233-
(configModifier && !eventModifier) ||
234-
(!configModifier && eventModifier)
235-
) {
236-
continue preventKeyLoop;
237-
}
238-
}
239-
}
240-
241-
if (key === getEventKey(nativeEvent)) {
242-
state.defaultPrevented = true;
243-
nativeEvent.preventDefault();
244-
break;
245-
}
246-
}
247-
}
248201
state.isActive = true;
249202
const onKeyDown = props.onKeyDown;
250203
if (onKeyDown != null) {
251204
dispatchKeyboardEvent(
252205
event,
253-
((onKeyDown: any): (e: KeyboardEvent) => ?boolean),
206+
((onKeyDown: any): (e: KeyboardEvent) => void),
254207
context,
255208
'keyboard:keydown',
256-
state.defaultPrevented,
257209
);
258210
}
259211
} else if (type === 'click' && isVirtualClick(event)) {
260-
if (props.preventClick !== false) {
261-
// 'click' occurs before or after 'keyup', and may need native
262-
// behavior prevented
263-
nativeEvent.preventDefault();
264-
state.defaultPrevented = true;
265-
}
266212
const onClick = props.onClick;
267213
if (onClick != null) {
268-
dispatchKeyboardEvent(
269-
event,
270-
onClick,
271-
context,
272-
'keyboard:click',
273-
state.defaultPrevented,
274-
);
214+
dispatchKeyboardEvent(event, onClick, context, 'keyboard:click');
275215
}
276216
} else if (type === 'keyup') {
277217
state.isActive = false;
278218
const onKeyUp = props.onKeyUp;
279219
if (onKeyUp != null) {
280220
dispatchKeyboardEvent(
281221
event,
282-
((onKeyUp: any): (e: KeyboardEvent) => ?boolean),
222+
((onKeyUp: any): (e: KeyboardEvent) => void),
283223
context,
284224
'keyboard:keyup',
285-
state.defaultPrevented,
286225
);
287226
}
288227
}

0 commit comments

Comments
 (0)