[change] Text and View onClick handling

Makes `onClick` part of the stable props API. In the future this will be used
to implement `onPress` in the Touchables/Pressables. Special handling of click
for keyboards is performed in `createElement`. At the moment, `Text` still
includes the `onPress` prop, which will only be called if `onClick` is not also
being used. In the future `Text` (in React Native) should remove the Touchable
props from its API.
This commit is contained in:
Nicolas Gallagher
2020-03-31 14:06:01 -07:00
parent 66751502a3
commit 204c432f66
7 changed files with 58 additions and 70 deletions
+9 -14
View File
@@ -35,6 +35,7 @@ const Text = forwardRef<TextProps, *>((props, ref) => {
nativeID,
numberOfLines,
onBlur,
onClick,
onContextMenu,
onFocus,
onLayout,
@@ -128,19 +129,14 @@ const Text = forwardRef<TextProps, *>((props, ref) => {
onStartShouldSetResponderCapture
});
function createEnterHandler(fn) {
return e => {
if (e.keyCode === 13) {
fn && fn(e);
}
};
}
function createPressHandler(fn) {
return e => {
function handleClick(e) {
if (onClick != null) {
onClick(e);
}
if (onClick == null && onPress != null) {
e.stopPropagation();
fn && fn(e);
};
onPress(e);
}
}
const component = hasTextAncestor ? 'span' : 'div';
@@ -159,14 +155,13 @@ const Text = forwardRef<TextProps, *>((props, ref) => {
lang,
nativeID,
onBlur,
onClick: handleClick,
onContextMenu,
onFocus,
ref: setRef,
style,
testID,
// unstable
onClick: onPress != null ? createPressHandler(onPress) : null,
onKeyDown: onPress != null ? createEnterHandler(onPress) : null,
onMouseDown,
onMouseEnter,
onMouseLeave,
+2 -4
View File
@@ -103,6 +103,8 @@ export type TextProps = {
nativeID?: ?string,
numberOfLines?: ?number,
onBlur?: (e: any) => void,
onClick?: (e: any) => void,
onContextMenu?: (e: any) => void,
onFocus?: (e: any) => void,
onLayout?: (e: LayoutEvent) => void,
onPress?: (e: any) => void,
@@ -126,10 +128,6 @@ export type TextProps = {
style?: GenericStyleProp<TextStyle>,
testID?: ?string,
// unstable
onContextMenu?: (e: any) => void,
onKeyDown?: (e: any) => void,
onKeyPress?: (e: any) => void,
onKeyUp?: (e: any) => void,
onMouseDown?: (e: any) => void,
onMouseEnter?: (e: any) => void,
onMouseLeave?: (e: any) => void,
+8 -10
View File
@@ -44,11 +44,13 @@ const View = forwardRef<ViewProps, *>((props, ref) => {
accessibilityRole,
accessibilityState,
accessibilityValue,
accessible,
forwardedRef,
hitSlop,
importantForAccessibility,
nativeID,
onBlur,
onClick,
onContextMenu,
onFocus,
onLayout,
@@ -71,12 +73,7 @@ const View = forwardRef<ViewProps, *>((props, ref) => {
pointerEvents,
testID,
// unstable
onClick,
onClickCapture,
onScroll,
onWheel,
onKeyDown,
onKeyPress,
onKeyUp,
onMouseDown,
onMouseEnter,
@@ -85,6 +82,7 @@ const View = forwardRef<ViewProps, *>((props, ref) => {
onMouseOver,
onMouseOut,
onMouseUp,
onScroll,
onTouchCancel,
onTouchCancelCapture,
onTouchEnd,
@@ -93,6 +91,7 @@ const View = forwardRef<ViewProps, *>((props, ref) => {
onTouchMoveCapture,
onTouchStart,
onTouchStartCapture,
onWheel,
href,
itemID,
itemRef,
@@ -159,11 +158,13 @@ const View = forwardRef<ViewProps, *>((props, ref) => {
accessibilityRole,
accessibilityState,
accessibilityValue,
accessible,
children,
classList,
importantForAccessibility,
nativeID,
onBlur,
onClick,
onContextMenu,
onFocus,
pointerEvents,
@@ -171,12 +172,7 @@ const View = forwardRef<ViewProps, *>((props, ref) => {
style,
testID,
// unstable
onClick,
onClickCapture,
onScroll,
onWheel,
onKeyDown,
onKeyPress,
onKeyUp,
onMouseDown,
onMouseEnter,
@@ -185,6 +181,7 @@ const View = forwardRef<ViewProps, *>((props, ref) => {
onMouseOver,
onMouseOut,
onMouseUp,
onScroll,
onTouchCancel,
onTouchCancelCapture,
onTouchEnd,
@@ -193,6 +190,7 @@ const View = forwardRef<ViewProps, *>((props, ref) => {
onTouchMoveCapture,
onTouchStart,
onTouchStartCapture,
onWheel,
href,
itemID,
itemRef,
+4 -6
View File
@@ -98,6 +98,8 @@ export type ViewProps = {
importantForAccessibility?: 'auto' | 'yes' | 'no' | 'no-hide-descendants',
nativeID?: ?string,
onBlur?: (e: any) => void,
onClick?: (e: any) => void,
onContextMenu?: (e: any) => void,
onFocus?: (e: any) => void,
onLayout?: (e: LayoutEvent) => void,
onMoveShouldSetResponder?: (e: any) => boolean,
@@ -120,13 +122,7 @@ export type ViewProps = {
style?: GenericStyleProp<ViewStyle>,
testID?: ?string,
// unstable
onClick?: (e: any) => void,
onClickCapture?: (e: any) => void,
onContextMenu?: (e: any) => void,
onScroll?: (e: any) => void,
onWheel?: (e: any) => void,
onKeyDown?: (e: any) => void,
onKeyPress?: (e: any) => void,
onKeyUp?: (e: any) => void,
onMouseDown?: (e: any) => void,
onMouseEnter?: (e: any) => void,
@@ -135,6 +131,7 @@ export type ViewProps = {
onMouseOver?: (e: any) => void,
onMouseOut?: (e: any) => void,
onMouseUp?: (e: any) => void,
onScroll?: (e: any) => void,
onTouchCancel?: (e: any) => void,
onTouchCancelCapture?: (e: any) => void,
onTouchEnd?: (e: any) => void,
@@ -143,6 +140,7 @@ export type ViewProps = {
onTouchMoveCapture?: (e: any) => void,
onTouchStart?: (e: any) => void,
onTouchStartCapture?: (e: any) => void,
onWheel?: (e: any) => void,
href?: ?string,
itemID?: ?string,
itemRef?: ?string,
@@ -25,7 +25,7 @@ describe('modules/createElement', () => {
});
const testRole = ({ accessibilityRole, disabled }) => {
[{ key: 'Enter', which: 13 }, { key: 'Space', which: 32 }].forEach(({ key, which }) => {
[{ key: 'Enter' }, { key: ' ' }].forEach(({ key }) => {
test(`"onClick" is ${disabled ? 'not ' : ''}called when "${key}" key is pressed`, () => {
const onClick = jest.fn();
const component = shallow(
@@ -35,7 +35,7 @@ describe('modules/createElement', () => {
isDefaultPrevented() {},
nativeEvent: {},
preventDefault() {},
which
key
});
expect(onClick).toHaveBeenCalledTimes(disabled ? 0 : 1);
});
@@ -11,39 +11,15 @@ import AccessibilityUtil from '../../modules/AccessibilityUtil';
import createDOMProps from '../../modules/createDOMProps';
import React from 'react';
const adjustProps = domProps => {
const { onClick, role } = domProps;
const isButtonLikeRole = AccessibilityUtil.buttonLikeRoles[role];
const isDisabled = AccessibilityUtil.isDisabled(domProps);
// Button-like roles should not trigger 'onClick' if they are disabled.
if (isButtonLikeRole && isDisabled && domProps.onClick != null) {
domProps.onClick = undefined;
}
// Button-like roles should trigger 'onClick' if SPACE or ENTER keys are pressed.
if (isButtonLikeRole && !isDisabled) {
domProps.onKeyPress = function(e) {
if (!e.isDefaultPrevented() && (e.which === 13 || e.which === 32)) {
e.preventDefault();
if (onClick) {
onClick(e);
}
}
};
}
};
const createElement = (component, props, ...children) => {
// use equivalent platform elements where possible
// Use equivalent platform elements where possible.
let accessibilityComponent;
if (component && component.constructor === String) {
accessibilityComponent = AccessibilityUtil.propsToAccessibilityComponent(props);
}
const Component = accessibilityComponent || component;
const domProps = createDOMProps(Component, props);
adjustProps(domProps);
return React.createElement(Component, domProps, ...children);
};
@@ -67,6 +67,7 @@ const createDOMProps = (component, props, styleResolver) => {
accessibilityRelationship,
accessibilityState,
accessibilityValue,
accessible,
classList,
disabled: providedDisabled,
importantForAccessibility,
@@ -75,7 +76,6 @@ const createDOMProps = (component, props, styleResolver) => {
style: providedStyle,
testID,
/* eslint-disable */
accessible,
accessibilityRole,
/* eslint-enable */
unstable_ariaSet,
@@ -176,18 +176,18 @@ const createDOMProps = (component, props, styleResolver) => {
// FOCUS
// Assume that 'link' is focusable by default (uses <a>).
// Assume that 'button' is not (uses <div role='button'>) but must be treated as such.
const focusable =
!disabled &&
importantForAccessibility !== 'no' &&
importantForAccessibility !== 'no-hide-descendants';
if (
const isInteractiveElement =
role === 'link' ||
component === 'a' ||
component === 'button' ||
component === 'input' ||
component === 'select' ||
component === 'textarea'
) {
component === 'textarea';
const focusable =
!disabled &&
importantForAccessibility !== 'no' &&
importantForAccessibility !== 'no-hide-descendants';
if (isInteractiveElement) {
if (accessible === false || !focusable) {
domProps.tabIndex = '-1';
} else {
@@ -251,6 +251,29 @@ const createDOMProps = (component, props, styleResolver) => {
domProps['data-testid'] = testID;
}
// Keyboard accessibility
// Button-like roles should trigger 'onClick' if SPACE or ENTER keys are pressed.
// Button-like roles should not trigger 'onClick' if they are disabled.
if (domProps['data-focusable']) {
const onClick = domProps.onClick;
if (onClick != null) {
if (disabled) {
domProps.onClick = undefined;
} else if (!isInteractiveElement) {
const onKeyDown = domProps.onKeyDown;
domProps.onKeyDown = function(e) {
if (!e.isDefaultPrevented() && (e.key === 'Enter' || e.key === ' ')) {
e.preventDefault();
if (onKeyDown != null) {
onKeyDown(e);
}
onClick(e);
}
};
}
}
}
return domProps;
};