Skip to content

Commit 57a5805

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

6 files changed

Lines changed: 124 additions & 141 deletions

File tree

‎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
functionCell({children}): FocusCellProps{
147147
constscopeRef=useRef(null);
148148
constkeyboard=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-
returnfalse;
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-
returnfalse;
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-
returnfalse;
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-
returnfalse;
206+
return;
207207
}
208208
}
209-
returntrue;
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-
exportfunctionfocusNext(
56+
functioninternalFocusNext(
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
}elseif(focusedElement===lastTabbableElem){
71-
if(contain===true){
74+
if(contain){
7275
focusElem(firstTabbableElem);
73-
}else{
74-
returntrue;
76+
if(event){
77+
event.preventDefault();
78+
}
79+
}elseif(event){
80+
event.continuePropagation();
7581
}
7682
}else{
7783
focusElem((tabbableNodes: any)[currentIndex+1]);
84+
if(event){
85+
event.preventDefault();
86+
}
7887
}
79-
returnfalse;
8088
}
8189

82-
exportfunctionfocusPrevious(
90+
functioninternalFocusPrevious(
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
}elseif(focusedElement===firstTabbableElem){
97-
if(contain===true){
108+
if(contain){
98109
focusElem(lastTabbableElem);
99-
}else{
100-
returntrue;
110+
if(event){
111+
event.preventDefault();
112+
}
113+
}elseif(event){
114+
event.continuePropagation();
101115
}
102116
}else{
103117
focusElem((tabbableNodes: any)[currentIndex-1]);
118+
if(event){
119+
event.preventDefault();
120+
}
104121
}
105-
returnfalse;
122+
}
123+
124+
exportfunctionfocusPrevious(scope: ReactScopeMethods): void{
125+
internalFocusPrevious(scope);
126+
}
127+
128+
exportfunctionfocusNext(scope: ReactScopeMethods): void{
129+
internalFocusNext(scope);
106130
}
107131

108132
exportfunctiongetNextController(
@@ -137,21 +161,20 @@ export const TabFocusController = React.forwardRef(
137161
({children, contain}: TabFocusControllerProps,ref): React.Node=>{
138162
constscopeRef=useRef(null);
139163
constkeyboard=useKeyboard({
140-
onKeyDown(event: KeyboardEvent): boolean{
164+
onKeyDown(event: KeyboardEvent): void{
141165
if(event.key!=='Tab'){
142-
returntrue;
166+
event.continuePropagation();
167+
return;
143168
}
144169
const scope =scopeRef.current;
145170
if(scope!==null){
146171
if(event.shiftKey){
147-
returnfocusPrevious(scope,contain);
172+
internalFocusPrevious(scope,event,contain);
148173
}else{
149-
returnfocusNext(scope,contain);
174+
internalFocusNext(scope,event,contain);
150175
}
151176
}
152-
returntrue;
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
constpreviousController=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
typeKeyboardProps={|
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
typeKeyboardState={|
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-
typeModifiersObject={|
54-
altKey?: boolean,
55-
ctrlKey?: boolean,
56-
metaKey?: boolean,
57-
shiftKey?: boolean,
58-
|};
59-
60-
typePreventKeysArray=Array<string|Array<string|ModifiersObject>>;
61-
62-
constisArray=Array.isArray;
6352
consttargetEventTypes=['click_active','keydown_active','keyup'];
64-
constmodifiers=['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
constnativeEvent=(event: any).nativeEvent;
152139
const{altKey, ctrlKey, metaKey, shiftKey}=nativeEvent;
153140
letkeyboardEvent={
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
constkey=getEventKey(nativeEvent);
@@ -171,32 +170,18 @@ function createKeyboardEvent(
171170

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

195181
constkeyboardResponderImpl={
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-
constnativeEvent: any=event.nativeEvent;
211195

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

216200
if(type==='keydown'){
217-
state.defaultPrevented=nativeEvent.defaultPrevented===true;
218-
219-
constpreventKeys=((props.preventKeys: any): PreventKeysArray);
220-
if(!state.defaultPrevented&&isArray(preventKeys)){
221-
preventKeyLoop: for(leti=0;i<preventKeys.length;i++){
222-
constpreventKey=preventKeys[i];
223-
letkey=preventKey;
224-
225-
if(isArray(preventKey)){
226-
key=preventKey[0];
227-
constconfig=((preventKey[1]: any): Object);
228-
for(lets=0;s<modifiers.length;s++){
229-
constmodifier=modifiers[s];
230-
constconfigModifier=config[modifier];
231-
consteventModifier=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
constonKeyDown=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
}elseif(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
}elseif(type==='keyup'){
277217
state.isActive=false;
278218
constonKeyUp=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)