Skip to content

Commit

Permalink
[react-ui] Add preventDefault+stopPropagation to Keyboard + update Fo…
Browse files Browse the repository at this point in the history
…cus components (#16833)
  • Loading branch information
trueadm authored Sep 20, 2019
1 parent 08b51aa commit 57a5805
Show file tree
Hide file tree
Showing 6 changed files with 124 additions and 141 deletions.
12 changes: 6 additions & 6 deletions packages/react-ui/accessibility/src/FocusTable.js
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ export function createFocusTable(): Array<React.Component> {
function Cell({children}): FocusCellProps {
const scopeRef = useRef(null);
const keyboard = useKeyboard({
onKeyDown(event: KeyboardEvent): boolean {
onKeyDown(event: KeyboardEvent): void {
const currentCell = scopeRef.current;
switch (event.key) {
case 'UpArrow': {
Expand All @@ -162,7 +162,7 @@ export function createFocusTable(): Array<React.Component> {
}
}
}
return false;
return;
}
case 'DownArrow': {
const [cells, rowIndex] = getRowCells(currentCell);
Expand All @@ -179,7 +179,7 @@ export function createFocusTable(): Array<React.Component> {
}
}
}
return false;
return;
}
case 'LeftArrow': {
const [cells, rowIndex] = getRowCells(currentCell);
Expand All @@ -190,7 +190,7 @@ export function createFocusTable(): Array<React.Component> {
triggerNavigateOut(currentCell, 'left');
}
}
return false;
return;
}
case 'RightArrow': {
const [cells, rowIndex] = getRowCells(currentCell);
Expand All @@ -203,10 +203,10 @@ export function createFocusTable(): Array<React.Component> {
}
}
}
return false;
return;
}
}
return true;
event.continuePropagation();
},
});
return (
Expand Down
63 changes: 43 additions & 20 deletions packages/react-ui/accessibility/src/TabFocus.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,10 +53,11 @@ function focusElem(elem: null | HTMLElement): void {
}
}

export function focusNext(
function internalFocusNext(
scope: ReactScopeMethods,
event?: KeyboardEvent,
contain?: boolean,
): boolean {
): void {
const [
tabbableNodes,
firstTabbableElem,
Expand All @@ -66,23 +67,31 @@ export function focusNext(
] = getTabbableNodes(scope);

if (focusedElement === null) {
focusElem(firstTabbableElem);
if (event) {
event.continuePropagation();
}
} else if (focusedElement === lastTabbableElem) {
if (contain === true) {
if (contain) {
focusElem(firstTabbableElem);
} else {
return true;
if (event) {
event.preventDefault();
}
} else if (event) {
event.continuePropagation();
}
} else {
focusElem((tabbableNodes: any)[currentIndex + 1]);
if (event) {
event.preventDefault();
}
}
return false;
}

export function focusPrevious(
function internalFocusPrevious(
scope: ReactScopeMethods,
event?: KeyboardEvent,
contain?: boolean,
): boolean {
): void {
const [
tabbableNodes,
firstTabbableElem,
Expand All @@ -92,17 +101,32 @@ export function focusPrevious(
] = getTabbableNodes(scope);

if (focusedElement === null) {
focusElem(firstTabbableElem);
if (event) {
event.continuePropagation();
}
} else if (focusedElement === firstTabbableElem) {
if (contain === true) {
if (contain) {
focusElem(lastTabbableElem);
} else {
return true;
if (event) {
event.preventDefault();
}
} else if (event) {
event.continuePropagation();
}
} else {
focusElem((tabbableNodes: any)[currentIndex - 1]);
if (event) {
event.preventDefault();
}
}
return false;
}

export function focusPrevious(scope: ReactScopeMethods): void {
internalFocusPrevious(scope);
}

export function focusNext(scope: ReactScopeMethods): void {
internalFocusNext(scope);
}

export function getNextController(
Expand Down Expand Up @@ -137,21 +161,20 @@ export const TabFocusController = React.forwardRef(
({children, contain}: TabFocusControllerProps, ref): React.Node => {
const scopeRef = useRef(null);
const keyboard = useKeyboard({
onKeyDown(event: KeyboardEvent): boolean {
onKeyDown(event: KeyboardEvent): void {
if (event.key !== 'Tab') {
return true;
event.continuePropagation();
return;
}
const scope = scopeRef.current;
if (scope !== null) {
if (event.shiftKey) {
return focusPrevious(scope, contain);
internalFocusPrevious(scope, event, contain);
} else {
return focusNext(scope, contain);
internalFocusNext(scope, event, contain);
}
}
return true;
},
preventKeys: ['Tab', ['Tab', {shiftKey: true}]],
});

return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -255,14 +255,14 @@ describe('TabFocusController', () => {
firstFocusController,
);
expect(nextController).toBe(secondFocusController);
ReactTabFocus.focusNext(nextController);
ReactTabFocus.focusFirst(nextController);
expect(document.activeElement).toBe(divRef.current);

const previousController = ReactTabFocus.getPreviousController(
nextController,
);
expect(previousController).toBe(firstFocusController);
ReactTabFocus.focusNext(previousController);
ReactTabFocus.focusFirst(previousController);
expect(document.activeElement).toBe(buttonRef.current);
});
});
Expand Down
109 changes: 24 additions & 85 deletions packages/react-ui/events/src/dom/Keyboard.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,15 +24,12 @@ type KeyboardEventType =

type KeyboardProps = {|
disabled?: boolean,
onClick?: (e: KeyboardEvent) => ?boolean,
onKeyDown?: (e: KeyboardEvent) => ?boolean,
onKeyUp?: (e: KeyboardEvent) => ?boolean,
preventClick?: boolean,
preventKeys?: PreventKeysArray,
onClick?: (e: KeyboardEvent) => void,
onKeyDown?: (e: KeyboardEvent) => void,
onKeyUp?: (e: KeyboardEvent) => void,
|};

type KeyboardState = {|
defaultPrevented: boolean,
isActive: boolean,
|};

Expand All @@ -48,20 +45,11 @@ export type KeyboardEvent = {|
target: Element | Document,
type: KeyboardEventType,
timeStamp: number,
continuePropagation: () => void,
preventDefault: () => void,
|};

type ModifiersObject = {|
altKey?: boolean,
ctrlKey?: boolean,
metaKey?: boolean,
shiftKey?: boolean,
|};

type PreventKeysArray = Array<string | Array<string | ModifiersObject>>;

const isArray = Array.isArray;
const targetEventTypes = ['click_active', 'keydown_active', 'keyup'];
const modifiers = ['altKey', 'ctrlKey', 'metaKey', 'shiftKey'];

/**
* Normalization of deprecated HTML5 `key` values
Expand Down Expand Up @@ -146,20 +134,31 @@ function createKeyboardEvent(
event: ReactDOMResponderEvent,
context: ReactDOMResponderContext,
type: KeyboardEventType,
defaultPrevented: boolean,
): KeyboardEvent {
const nativeEvent = (event: any).nativeEvent;
const {altKey, ctrlKey, metaKey, shiftKey} = nativeEvent;
let keyboardEvent = {
altKey,
ctrlKey,
defaultPrevented,
defaultPrevented: nativeEvent.defaultPrevented === true,
metaKey,
pointerType: 'keyboard',
shiftKey,
target: event.target,
timeStamp: context.getTimeStamp(),
type,
// We don't use stopPropagation, as the default behavior
// is to not propagate. Plus, there might be confusion
// using stopPropagation as we don't actually stop
// native propagation from working, but instead only
// allow propagation to the others keyboard responders.
continuePropagation() {
context.continuePropagation();
},
preventDefault() {
keyboardEvent.defaultPrevented = true;
nativeEvent.preventDefault();
},
};
if (type !== 'keyboard:click') {
const key = getEventKey(nativeEvent);
Expand All @@ -171,32 +170,18 @@ function createKeyboardEvent(

function dispatchKeyboardEvent(
event: ReactDOMResponderEvent,
listener: KeyboardEvent => ?boolean,
listener: KeyboardEvent => void,
context: ReactDOMResponderContext,
type: KeyboardEventType,
defaultPrevented: boolean,
): void {
const syntheticEvent = createKeyboardEvent(
event,
context,
type,
defaultPrevented,
);
let shouldPropagate;
const listenerWithReturnValue = e => {
shouldPropagate = listener(e);
};
context.dispatchEvent(syntheticEvent, listenerWithReturnValue, DiscreteEvent);
if (shouldPropagate) {
context.continuePropagation();
}
const syntheticEvent = createKeyboardEvent(event, context, type);
context.dispatchEvent(syntheticEvent, listener, DiscreteEvent);
}

const keyboardResponderImpl = {
targetEventTypes,
getInitialState(): KeyboardState {
return {
defaultPrevented: false,
isActive: false,
};
},
Expand All @@ -207,82 +192,36 @@ const keyboardResponderImpl = {
state: KeyboardState,
): void {
const {type} = event;
const nativeEvent: any = event.nativeEvent;

if (props.disabled) {
return;
}

if (type === 'keydown') {
state.defaultPrevented = nativeEvent.defaultPrevented === true;

const preventKeys = ((props.preventKeys: any): PreventKeysArray);
if (!state.defaultPrevented && isArray(preventKeys)) {
preventKeyLoop: for (let i = 0; i < preventKeys.length; i++) {
const preventKey = preventKeys[i];
let key = preventKey;

if (isArray(preventKey)) {
key = preventKey[0];
const config = ((preventKey[1]: any): Object);
for (let s = 0; s < modifiers.length; s++) {
const modifier = modifiers[s];
const configModifier = config[modifier];
const eventModifier = nativeEvent[modifier];
if (
(configModifier && !eventModifier) ||
(!configModifier && eventModifier)
) {
continue preventKeyLoop;
}
}
}

if (key === getEventKey(nativeEvent)) {
state.defaultPrevented = true;
nativeEvent.preventDefault();
break;
}
}
}
state.isActive = true;
const onKeyDown = props.onKeyDown;
if (onKeyDown != null) {
dispatchKeyboardEvent(
event,
((onKeyDown: any): (e: KeyboardEvent) => ?boolean),
((onKeyDown: any): (e: KeyboardEvent) => void),
context,
'keyboard:keydown',
state.defaultPrevented,
);
}
} else if (type === 'click' && isVirtualClick(event)) {
if (props.preventClick !== false) {
// 'click' occurs before or after 'keyup', and may need native
// behavior prevented
nativeEvent.preventDefault();
state.defaultPrevented = true;
}
const onClick = props.onClick;
if (onClick != null) {
dispatchKeyboardEvent(
event,
onClick,
context,
'keyboard:click',
state.defaultPrevented,
);
dispatchKeyboardEvent(event, onClick, context, 'keyboard:click');
}
} else if (type === 'keyup') {
state.isActive = false;
const onKeyUp = props.onKeyUp;
if (onKeyUp != null) {
dispatchKeyboardEvent(
event,
((onKeyUp: any): (e: KeyboardEvent) => ?boolean),
((onKeyUp: any): (e: KeyboardEvent) => void),
context,
'keyboard:keyup',
state.defaultPrevented,
);
}
}
Expand Down
Loading

0 comments on commit 57a5805

Please sign in to comment.