Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[react-ui] Add preventDefault+ to Keyboard + update a11y components #16833

Merged
merged 1 commit into from
Sep 20, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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