From 8952eccf86eb07d19fd32f07959db0889b0cca4b Mon Sep 17 00:00:00 2001 From: Nicolas Gallagher Date: Thu, 6 Feb 2020 11:23:20 -0800 Subject: [PATCH] [change] Explicitly forward props and introduce unstable_{ariaSet,dataSet} Rather than filtering props, they are explicitly forwarded in each component. This makes it easier to see exactly which props are being forwarded to host components by each React Native component. Two new props - `unstable_ariaSet` and `unstable_dataSet` - are introduced to avoid iterating over props to find `aria` and `data` props. The `accessibilityValue` prop is also implemented. --- .../docs/src/components/View/View.stories.js | 2 +- .../docs/src/guides/accessibility.stories.mdx | 4 + .../src/exports/ActivityIndicator/index.js | 5 +- .../src/exports/ProgressBar/index.js | 8 +- .../src/exports/Text/index.js | 145 +++++++++++++++-- .../src/exports/Text/types.js | 43 ++++- .../src/exports/TextInput/index.js | 116 ++++++++++---- .../src/exports/TouchableHighlight/index.js | 5 +- .../src/exports/TouchableOpacity/index.js | 5 +- .../exports/TouchableWithoutFeedback/index.js | 5 + .../src/exports/View/filterSupportedProps.js | 92 ----------- .../src/exports/View/index.js | 149 ++++++++++++++++-- .../src/exports/View/types.js | 49 ++++-- .../src/hooks/usePlatformMethods.js | 116 ++++++++++---- ... => propsToAccessibilityComponent-test.js} | 5 +- .../propsToAccessibilityComponent.js | 8 +- .../src/modules/createDOMProps/index.js | 51 ++++-- 17 files changed, 591 insertions(+), 217 deletions(-) delete mode 100644 packages/react-native-web/src/exports/View/filterSupportedProps.js rename packages/react-native-web/src/modules/AccessibilityUtil/__tests__/{propsToAccessibilityComponent.js => propsToAccessibilityComponent-test.js} (87%) diff --git a/packages/docs/src/components/View/View.stories.js b/packages/docs/src/components/View/View.stories.js index a4958f6b..74b97692 100644 --- a/packages/docs/src/components/View/View.stories.js +++ b/packages/docs/src/components/View/View.stories.js @@ -3,12 +3,12 @@ import PropTypes, { any, bool, func, object, oneOf, string } from 'prop-types'; const ofProps = () => {}; ofProps.propTypes = { - /* test */ accessibilityLabel: PropTypes.string, accessibilityLiveRegion: oneOf(['assertive', 'none', 'polite']), accessibilityRelationship: object, accessibilityRole: string, accessibilityState: object, + accessibilityValue: object, accessible: bool, children: any, forwardedRef: any, diff --git a/packages/docs/src/guides/accessibility.stories.mdx b/packages/docs/src/guides/accessibility.stories.mdx index b767aad4..db32c027 100644 --- a/packages/docs/src/guides/accessibility.stories.mdx +++ b/packages/docs/src/guides/accessibility.stories.mdx @@ -89,6 +89,10 @@ assistive technologies of a `role` value change. ... +### accessibilityValue: ?object + +... + ### accessible When `true`, indicates that the view is an accessibility element. When a view diff --git a/packages/react-native-web/src/exports/ActivityIndicator/index.js b/packages/react-native-web/src/exports/ActivityIndicator/index.js index bc8bfe59..74485145 100644 --- a/packages/react-native-web/src/exports/ActivityIndicator/index.js +++ b/packages/react-native-web/src/exports/ActivityIndicator/index.js @@ -14,6 +14,8 @@ import StyleSheet from '../StyleSheet'; import View from '../View'; import React, { forwardRef } from 'react'; +const accessibilityValue = { max: 1, min: 0 }; + const createSvgCircle = style => ( ); @@ -54,8 +56,7 @@ const ActivityIndicator = forwardRef((props, ref) => diff --git a/packages/react-native-web/src/exports/ProgressBar/index.js b/packages/react-native-web/src/exports/ProgressBar/index.js index 96f3f4f0..9af90211 100644 --- a/packages/react-native-web/src/exports/ProgressBar/index.js +++ b/packages/react-native-web/src/exports/ProgressBar/index.js @@ -48,9 +48,11 @@ const ProgressBar = forwardRef((props, ref) => { diff --git a/packages/react-native-web/src/exports/Text/index.js b/packages/react-native-web/src/exports/Text/index.js index 6de8f66d..69f3059e 100644 --- a/packages/react-native-web/src/exports/Text/index.js +++ b/packages/react-native-web/src/exports/Text/index.js @@ -12,7 +12,6 @@ import type { TextProps } from './types'; import createElement from '../createElement'; import css from '../StyleSheet/css'; -import filterSupportedProps from '../View/filterSupportedProps'; import setAndForwardRef from '../../modules/setAndForwardRef'; import useElementLayout from '../../hooks/useElementLayout'; import usePlatformMethods from '../../hooks/usePlatformMethods'; @@ -21,7 +20,69 @@ import StyleSheet from '../StyleSheet'; import TextAncestorContext from './TextAncestorContext'; const Text = forwardRef((props, ref) => { - const { dir, forwardedRef, numberOfLines, onLayout, onPress, selectable } = props; + const { + accessibilityLabel, + accessibilityLiveRegion, + accessibilityRelationship, + accessibilityRole, + accessibilityState, + children, + dir, + forwardedRef, + importantForAccessibility, + lang, + nativeID, + numberOfLines, + onBlur, + onContextMenu, + onFocus, + onLayout, + onPress, + onMoveShouldSetResponder, + onMoveShouldSetResponderCapture, + onResponderEnd, + onResponderGrant, + onResponderMove, + onResponderReject, + onResponderRelease, + onResponderStart, + onResponderTerminate, + onResponderTerminationRequest, + onScrollShouldSetResponder, + onScrollShouldSetResponderCapture, + onSelectionChangeShouldSetResponder, + onSelectionChangeShouldSetResponderCapture, + onStartShouldSetResponder, + onStartShouldSetResponderCapture, + selectable, + testID, + // unstable + onMouseDown, + onMouseEnter, + onMouseLeave, + onMouseMove, + onMouseOver, + onMouseOut, + onMouseUp, + onTouchCancel, + onTouchCancelCapture, + onTouchEnd, + onTouchEndCapture, + onTouchMove, + onTouchMoveCapture, + onTouchStart, + onTouchStartCapture, + href, + itemID, + itemRef, + itemProp, + itemScope, + itemType, + rel, + target, + unstable_ariaSet, + unstable_dataSet + } = props; const hasTextAncestor = useContext(TextAncestorContext); const hostRef = useRef(null); @@ -63,22 +124,72 @@ const Text = forwardRef((props, ref) => { }; } - const supportedProps = filterSupportedProps(props); - - if (onPress) { - supportedProps.accessible = true; - supportedProps.onClick = createPressHandler(onPress); - supportedProps.onKeyDown = createEnterHandler(onPress); - } - - supportedProps.classList = classList; - // allow browsers to automatically infer the language writing direction - supportedProps.dir = dir !== undefined ? dir : 'auto'; - supportedProps.ref = setRef; - supportedProps.style = style; - const component = hasTextAncestor ? 'span' : 'div'; - const element = createElement(component, supportedProps); + const element = createElement(component, { + accessibilityLabel, + accessibilityLiveRegion, + accessibilityRelationship, + accessibilityRole, + accessibilityState, + accessible: onPress != null ? true : null, + children, + classList, + // allow browsers to automatically infer the language writing direction + dir: dir !== undefined ? dir : 'auto', + importantForAccessibility, + lang, + nativeID, + onBlur, + onFocus, + onMoveShouldSetResponder, + onMoveShouldSetResponderCapture, + onResponderEnd, + onResponderGrant, + onResponderMove, + onResponderReject, + onResponderRelease, + onResponderStart, + onResponderTerminate, + onResponderTerminationRequest, + onScrollShouldSetResponder, + onScrollShouldSetResponderCapture, + onSelectionChangeShouldSetResponder, + onSelectionChangeShouldSetResponderCapture, + onStartShouldSetResponder, + onStartShouldSetResponderCapture, + ref: setRef, + style, + testID, + // unstable + onClick: onPress != null ? createPressHandler(onPress) : null, + onContextMenu, + onKeyDown: onPress != null ? createEnterHandler(onPress) : null, + onMouseDown, + onMouseEnter, + onMouseLeave, + onMouseMove, + onMouseOver, + onMouseOut, + onMouseUp, + onTouchCancel, + onTouchCancelCapture, + onTouchEnd, + onTouchEndCapture, + onTouchMove, + onTouchMoveCapture, + onTouchStart, + onTouchStartCapture, + href, + itemID, + itemRef, + itemProp, + itemScope, + itemType, + rel, + target, + unstable_ariaSet, + unstable_dataSet + }); return hasTextAncestor ? ( element diff --git a/packages/react-native-web/src/exports/Text/types.js b/packages/react-native-web/src/exports/Text/types.js index 0a028484..681fbd55 100644 --- a/packages/react-native-web/src/exports/Text/types.js +++ b/packages/react-native-web/src/exports/Text/types.js @@ -104,14 +104,53 @@ export type TextProps = { onFocus?: (e: any) => void, onLayout?: (e: LayoutEvent) => void, onPress?: (e: any) => void, + onMoveShouldSetResponder?: (e: any) => boolean, + onMoveShouldSetResponderCapture?: (e: any) => boolean, + onResponderEnd?: (e: any) => void, + onResponderGrant?: (e: any) => void, + onResponderMove?: (e: any) => void, + onResponderReject?: (e: any) => void, + onResponderRelease?: (e: any) => void, + onResponderStart?: (e: any) => void, + onResponderTerminate?: (e: any) => void, + onResponderTerminationRequest?: (e: any) => void, + onScrollShouldSetResponder?: (e: any) => boolean, + onScrollShouldSetResponderCapture?: (e: any) => boolean, + onSelectionChangeShouldSetResponder?: (e: any) => boolean, + onSelectionChangeShouldSetResponderCapture?: (e: any) => boolean, + onStartShouldSetResponder?: (e: any) => boolean, + onStartShouldSetResponderCapture?: (e: any) => boolean, selectable?: boolean, style?: GenericStyleProp, testID?: string, - // web extensions + // 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, + onMouseMove?: (e: any) => void, + onMouseOver?: (e: any) => void, + onMouseOut?: (e: any) => void, + onMouseUp?: (e: any) => void, + onTouchCancel?: (e: any) => void, + onTouchCancelCapture?: (e: any) => void, + onTouchEnd?: (e: any) => void, + onTouchEndCapture?: (e: any) => void, + onTouchMove?: (e: any) => void, + onTouchMoveCapture?: (e: any) => void, + onTouchStart?: (e: any) => void, + onTouchStartCapture?: (e: any) => void, + href?: string, itemID?: string, itemRef?: string, itemProp?: string, itemScope?: string, - itemType?: string + itemType?: string, + rel?: string, + target?: string, + unstable_ariaSet?: Object, + unstable_dataSet?: Object }; diff --git a/packages/react-native-web/src/exports/TextInput/index.js b/packages/react-native-web/src/exports/TextInput/index.js index 22dd6c76..71023db6 100644 --- a/packages/react-native-web/src/exports/TextInput/index.js +++ b/packages/react-native-web/src/exports/TextInput/index.js @@ -12,11 +12,11 @@ import type { TextInputProps } from './types'; import createElement from '../createElement'; import css from '../StyleSheet/css'; -import filterSupportedProps from '../View/filterSupportedProps'; import setAndForwardRef from '../../modules/setAndForwardRef'; import useElementLayout from '../../hooks/useElementLayout'; -import usePlatformMethods from '../../hooks/usePlatformMethods'; -import { forwardRef, useEffect, useRef } from 'react'; +import useLayoutEffect from '../../hooks/useLayoutEffect'; +import { usePlatformInputMethods } from '../../hooks/usePlatformMethods'; +import { forwardRef, useRef } from 'react'; import StyleSheet from '../StyleSheet'; import TextInputState from '../../modules/TextInputState'; @@ -50,6 +50,9 @@ const setSelection = (node, selection) => { const TextInput = forwardRef((props, ref) => { const { + accessibilityLabel, + accessibilityRelationship, + accessibilityState, autoCapitalize = 'sentences', autoComplete, autoCompleteType, @@ -60,27 +63,58 @@ const TextInput = forwardRef((props, ref) => { defaultValue, disabled, editable = true, + forwardedRef, + importantForAccessibility, keyboardType = 'default', maxLength, multiline = false, + nativeID, numberOfLines = 1, onBlur, onChange, onChangeText, onContentSizeChange, + onContextMenu, onFocus, onKeyPress, onLayout, + onScroll, onSelectionChange, onSubmitEditing, + onMoveShouldSetResponder, + onMoveShouldSetResponderCapture, + onResponderEnd, + onResponderGrant, + onResponderMove, + onResponderReject, + onResponderRelease, + onResponderStart, + onResponderTerminate, + onResponderTerminationRequest, + onScrollShouldSetResponder, + onScrollShouldSetResponderCapture, + onSelectionChangeShouldSetResponder, + onSelectionChangeShouldSetResponderCapture, + onStartShouldSetResponder, + onStartShouldSetResponderCapture, placeholder, placeholderTextColor, + pointerEvents, returnKeyType, secureTextEntry = false, selection = emptyObject, selectTextOnFocus, spellCheck, - value + testID, + value, + // unstable + itemID, + itemRef, + itemProp, + itemScope, + itemType, + unstable_ariaSet, + unstable_dataSet } = props; let type; @@ -114,7 +148,7 @@ const TextInput = forwardRef((props, ref) => { const hostRef = useRef(null); const dimensions = useRef({ height: null, width: null }); const setRef = setAndForwardRef({ - getForwardedRef: () => ref, + getForwardedRef: () => forwardedRef, setLocalRef: c => { hostRef.current = c; if (hostRef.current != null) { @@ -124,7 +158,6 @@ const TextInput = forwardRef((props, ref) => { }); const component = multiline ? 'textarea' : 'input'; - const supportedProps = filterSupportedProps(props); const classList = [classes.textinput]; const style = StyleSheet.compose( props.style, @@ -160,7 +193,7 @@ const TextInput = forwardRef((props, ref) => { } function handleChange(e) { - const { text } = e.nativeEvent; + const text = e.target.value; e.nativeEvent.text = text; handleContentSizeChange(); if (onChange) { @@ -219,7 +252,7 @@ const TextInput = forwardRef((props, ref) => { e.nativeEvent = { target: e.target, text: e.target.value }; onSubmitEditing(e); } - if (shouldBlurOnSubmit) { + if (shouldBlurOnSubmit && hostRef.current != null) { // $FlowFixMe hostRef.current.blur(); } @@ -243,26 +276,21 @@ const TextInput = forwardRef((props, ref) => { } } - useEffect(() => { - setSelection(hostRef.current, selection); - if (document.activeElement === hostRef.current) { - TextInputState._currentlyFocusedNode = hostRef.current; + useLayoutEffect(() => { + const node = hostRef.current; + setSelection(node, selection); + if (document.activeElement === node) { + TextInputState._currentlyFocusedNode = node; } }, [hostRef, selection]); useElementLayout(hostRef, onLayout); - usePlatformMethods(hostRef, ref, classList, style, { - clear() { - if (hostRef.current != null) { - hostRef.current.value = ''; - } - }, - isFocused() { - return hostRef.current != null && TextInputState.currentlyFocusedField() === hostRef.current; - } - }); + usePlatformInputMethods(hostRef, ref, classList, style); - Object.assign(supportedProps, { + return createElement(component, { + accessibilityLabel, + accessibilityRelationship, + accessibilityState, autoCapitalize, autoComplete: autoComplete || autoCompleteType || 'on', autoCorrect: autoCorrect ? 'on' : 'off', @@ -272,27 +300,51 @@ const TextInput = forwardRef((props, ref) => { dir: 'auto', disabled, enterkeyhint: returnKeyType, + importantForAccessibility, maxLength, + nativeID, onBlur: handleBlur, onChange: handleChange, + onContextMenu, onFocus: handleFocus, onKeyDown: handleKeyDown, + onScroll, onSelect: handleSelectionChange, + onMoveShouldSetResponder, + onMoveShouldSetResponderCapture, + onResponderEnd, + onResponderGrant, + onResponderMove, + onResponderReject, + onResponderRelease, + onResponderStart, + onResponderTerminate, + onResponderTerminationRequest, + onScrollShouldSetResponder, + onScrollShouldSetResponderCapture, + onSelectionChangeShouldSetResponder, + onSelectionChangeShouldSetResponderCapture, + onStartShouldSetResponder, + onStartShouldSetResponderCapture, placeholder, + pointerEvents, + testID, readOnly: !editable, ref: setRef, + rows: multiline ? numberOfLines : undefined, spellCheck: spellCheck != null ? spellCheck : autoCorrect, style, - value + type: multiline ? undefined : type, + value, + // unstable + itemID, + itemRef, + itemProp, + itemScope, + itemType, + unstable_ariaSet, + unstable_dataSet }); - - if (multiline) { - supportedProps.rows = numberOfLines; - } else { - supportedProps.type = type; - } - - return createElement(component, supportedProps); }); TextInput.displayName = 'TextInput'; diff --git a/packages/react-native-web/src/exports/TouchableHighlight/index.js b/packages/react-native-web/src/exports/TouchableHighlight/index.js index 18c7c0f9..db3db8aa 100644 --- a/packages/react-native-web/src/exports/TouchableHighlight/index.js +++ b/packages/react-native-web/src/exports/TouchableHighlight/index.js @@ -291,7 +291,10 @@ const TouchableHighlight = ((createReactClass({ accessibilityHint={this.props.accessibilityHint} accessibilityLabel={this.props.accessibilityLabel} accessibilityRole={this.props.accessibilityRole} - accessibilityState={this.props.accessibilityState} + accessibilityState={{ + disabled: this.props.disabled, + ...this.props.accessibilityState + }} accessible={this.props.accessible !== false} hitSlop={this.props.hitSlop} nativeID={this.props.nativeID} diff --git a/packages/react-native-web/src/exports/TouchableOpacity/index.js b/packages/react-native-web/src/exports/TouchableOpacity/index.js index 3222a303..e1833db9 100644 --- a/packages/react-native-web/src/exports/TouchableOpacity/index.js +++ b/packages/react-native-web/src/exports/TouchableOpacity/index.js @@ -246,7 +246,10 @@ const TouchableOpacity = ((createReactClass({ accessibilityHint={this.props.accessibilityHint} accessibilityLabel={this.props.accessibilityLabel} accessibilityRole={this.props.accessibilityRole} - accessibilityState={this.props.accessibilityState} + accessibilityState={{ + disabled: this.props.disabled, + ...this.props.accessibilityState + }} accessible={this.props.accessible !== false} hitSlop={this.props.hitSlop} nativeID={this.props.nativeID} diff --git a/packages/react-native-web/src/exports/TouchableWithoutFeedback/index.js b/packages/react-native-web/src/exports/TouchableWithoutFeedback/index.js index 2091672b..e8c915cb 100644 --- a/packages/react-native-web/src/exports/TouchableWithoutFeedback/index.js +++ b/packages/react-native-web/src/exports/TouchableWithoutFeedback/index.js @@ -148,6 +148,11 @@ const TouchableWithoutFeedback = ((createReactClass({ } } + overrides.accessibilityState = { + disabled: this.props.disabled, + ...this.props.accessibilityState + }; + return (React: any).cloneElement(child, { ...overrides, accessible: this.props.accessible !== false, diff --git a/packages/react-native-web/src/exports/View/filterSupportedProps.js b/packages/react-native-web/src/exports/View/filterSupportedProps.js deleted file mode 100644 index 5c23416a..00000000 --- a/packages/react-native-web/src/exports/View/filterSupportedProps.js +++ /dev/null @@ -1,92 +0,0 @@ -/** - * Copyright (c) Nicolas Gallagher. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - * - * @noflow - */ - -const supportedProps = { - accessibilityLabel: true, - accessibilityLiveRegion: true, - accessibilityRelationship: true, - accessibilityRole: true, - accessibilityState: true, - accessible: true, - children: true, - disabled: true, - importantForAccessibility: true, - nativeID: true, - onBlur: true, - onContextMenu: true, - onFocus: true, - onMoveShouldSetResponder: true, - onMoveShouldSetResponderCapture: true, - onResponderEnd: true, - onResponderGrant: true, - onResponderMove: true, - onResponderReject: true, - onResponderRelease: true, - onResponderStart: true, - onResponderTerminate: true, - onResponderTerminationRequest: true, - onScrollShouldSetResponder: true, - onScrollShouldSetResponderCapture: true, - onSelectionChangeShouldSetResponder: true, - onSelectionChangeShouldSetResponderCapture: true, - onStartShouldSetResponder: true, - onStartShouldSetResponderCapture: true, - onTouchCancel: true, - onTouchCancelCapture: true, - onTouchEnd: true, - onTouchEndCapture: true, - onTouchMove: true, - onTouchMoveCapture: true, - onTouchStart: true, - onTouchStartCapture: true, - pointerEvents: true, - style: true, - testID: true, - /* @platform web */ - lang: true, - onScroll: true, - onWheel: true, - // keyboard events - onKeyDown: true, - onKeyPress: true, - onKeyUp: true, - // mouse events (e.g, hover effects) - onMouseDown: true, - onMouseEnter: true, - onMouseLeave: true, - onMouseMove: true, - onMouseOver: true, - onMouseOut: true, - onMouseUp: true, - // unstable escape-hatches for web - href: true, - itemID: true, - itemRef: true, - itemProp: true, - itemScope: true, - itemType: true, - onClick: true, - onClickCapture: true, - rel: true, - target: true -}; - -const filterSupportedProps = props => { - const safeProps = {}; - for (const prop in props) { - if (props.hasOwnProperty(prop)) { - if (supportedProps[prop] || prop.indexOf('aria-') === 0 || prop.indexOf('data-') === 0) { - safeProps[prop] = props[prop]; - } - } - } - return safeProps; -}; - -export default filterSupportedProps; diff --git a/packages/react-native-web/src/exports/View/index.js b/packages/react-native-web/src/exports/View/index.js index f015e2e8..7abad2aa 100644 --- a/packages/react-native-web/src/exports/View/index.js +++ b/packages/react-native-web/src/exports/View/index.js @@ -12,7 +12,6 @@ import type { ViewProps } from './types'; import createElement from '../createElement'; import css from '../StyleSheet/css'; -import filterSupportedProps from './filterSupportedProps'; import setAndForwardRef from '../../modules/setAndForwardRef'; import useElementLayout from '../../hooks/useElementLayout'; import usePlatformMethods from '../../hooks/usePlatformMethods'; @@ -37,7 +36,73 @@ function createHitSlopElement(hitSlop) { } const View = forwardRef((props, ref) => { - const { children, forwardedRef, hitSlop, onLayout } = props; + const { + accessibilityLabel, + accessibilityLiveRegion, + accessibilityRelationship, + accessibilityRole, + accessibilityState, + accessibilityValue, + forwardedRef, + hitSlop, + importantForAccessibility, + nativeID, + onBlur, + onContextMenu, + onFocus, + onLayout, + onMoveShouldSetResponder, + onMoveShouldSetResponderCapture, + onResponderEnd, + onResponderGrant, + onResponderMove, + onResponderReject, + onResponderRelease, + onResponderStart, + onResponderTerminate, + onResponderTerminationRequest, + onScrollShouldSetResponder, + onScrollShouldSetResponderCapture, + onSelectionChangeShouldSetResponder, + onSelectionChangeShouldSetResponderCapture, + onStartShouldSetResponder, + onStartShouldSetResponderCapture, + pointerEvents, + testID, + // unstable + onClick, + onClickCapture, + onScroll, + onWheel, + onKeyDown, + onKeyPress, + onKeyUp, + onMouseDown, + onMouseEnter, + onMouseLeave, + onMouseMove, + onMouseOver, + onMouseOut, + onMouseUp, + onTouchCancel, + onTouchCancelCapture, + onTouchEnd, + onTouchEndCapture, + onTouchMove, + onTouchMoveCapture, + onTouchStart, + onTouchStartCapture, + href, + itemID, + itemRef, + itemProp, + itemScope, + itemType, + rel, + target, + unstable_ariaSet, + unstable_dataSet + } = props; if (process.env.NODE_ENV !== 'production') { React.Children.toArray(props.children).forEach(item => { @@ -56,6 +121,9 @@ const View = forwardRef((props, ref) => { } }); + const children = hitSlop + ? React.Children.toArray([createHitSlopElement(hitSlop), props.children]) + : props.children; const classList = [classes.view]; const style = StyleSheet.compose( hasTextAncestor && styles.inline, @@ -65,15 +133,74 @@ const View = forwardRef((props, ref) => { useElementLayout(hostRef, onLayout); usePlatformMethods(hostRef, ref, classList, style); - const supportedProps = filterSupportedProps(props); - supportedProps.children = hitSlop - ? React.Children.toArray([createHitSlopElement(hitSlop), children]) - : children; - supportedProps.classList = classList; - supportedProps.ref = setRef; - supportedProps.style = style; - - return createElement('div', supportedProps); + return createElement('div', { + accessibilityLabel, + accessibilityLiveRegion, + accessibilityRelationship, + accessibilityRole, + accessibilityState, + accessibilityValue, + children, + classList, + importantForAccessibility, + nativeID, + onBlur, + onContextMenu, + onFocus, + onMoveShouldSetResponder, + onMoveShouldSetResponderCapture, + onResponderEnd, + onResponderGrant, + onResponderMove, + onResponderReject, + onResponderRelease, + onResponderStart, + onResponderTerminate, + onResponderTerminationRequest, + onScrollShouldSetResponder, + onScrollShouldSetResponderCapture, + onSelectionChangeShouldSetResponder, + onSelectionChangeShouldSetResponderCapture, + onStartShouldSetResponder, + onStartShouldSetResponderCapture, + pointerEvents, + ref: setRef, + style, + testID, + // unstable + onClick, + onClickCapture, + onScroll, + onWheel, + onKeyDown, + onKeyPress, + onKeyUp, + onMouseDown, + onMouseEnter, + onMouseLeave, + onMouseMove, + onMouseOver, + onMouseOut, + onMouseUp, + onTouchCancel, + onTouchCancelCapture, + onTouchEnd, + onTouchEndCapture, + onTouchMove, + onTouchMoveCapture, + onTouchStart, + onTouchStartCapture, + href, + itemID, + itemRef, + itemProp, + itemScope, + itemType, + rel, + target, + unstable_ariaSet, + unstable_dataSet + }); }); View.displayName = 'View'; diff --git a/packages/react-native-web/src/exports/View/types.js b/packages/react-native-web/src/exports/View/types.js index 853c2520..552fbaed 100644 --- a/packages/react-native-web/src/exports/View/types.js +++ b/packages/react-native-web/src/exports/View/types.js @@ -83,6 +83,12 @@ export type ViewProps = { required?: ?boolean, selected?: ?boolean }, + accessibilityValue?: { + max?: ?number, + min?: ?number, + now?: ?number, + text?: ?string + }, accessible?: boolean, children?: any, forwardedRef?: any, @@ -90,20 +96,43 @@ export type ViewProps = { importantForAccessibility?: 'auto' | 'yes' | 'no' | 'no-hide-descendants', nativeID?: string, onBlur?: (e: any) => void, - onClick?: (e: any) => void, - onClickCapture?: (e: any) => void, onFocus?: (e: any) => void, onLayout?: (e: LayoutEvent) => void, + onMoveShouldSetResponder?: (e: any) => boolean, + onMoveShouldSetResponderCapture?: (e: any) => boolean, + onResponderEnd?: (e: any) => void, onResponderGrant?: (e: any) => void, onResponderMove?: (e: any) => void, onResponderReject?: (e: any) => void, onResponderRelease?: (e: any) => void, + onResponderStart?: (e: any) => void, onResponderTerminate?: (e: any) => void, onResponderTerminationRequest?: (e: any) => void, + onScrollShouldSetResponder?: (e: any) => boolean, + onScrollShouldSetResponderCapture?: (e: any) => boolean, + onSelectionChangeShouldSetResponder?: (e: any) => boolean, + onSelectionChangeShouldSetResponderCapture?: (e: any) => boolean, onStartShouldSetResponder?: (e: any) => boolean, onStartShouldSetResponderCapture?: (e: any) => boolean, - onMoveShouldSetResponder?: (e: any) => boolean, - onMoveShouldSetResponderCapture?: (e: any) => boolean, + pointerEvents?: 'box-none' | 'none' | 'box-only' | 'auto', + style?: GenericStyleProp, + 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, + onMouseLeave?: (e: any) => void, + onMouseMove?: (e: any) => void, + onMouseOver?: (e: any) => void, + onMouseOut?: (e: any) => void, + onMouseUp?: (e: any) => void, onTouchCancel?: (e: any) => void, onTouchCancelCapture?: (e: any) => void, onTouchEnd?: (e: any) => void, @@ -112,14 +141,14 @@ export type ViewProps = { onTouchMoveCapture?: (e: any) => void, onTouchStart?: (e: any) => void, onTouchStartCapture?: (e: any) => void, - pointerEvents?: 'box-none' | 'none' | 'box-only' | 'auto', - style?: GenericStyleProp, - testID?: string, - // web extensions - onContextMenu?: (e: any) => void, + href?: string, itemID?: string, itemRef?: string, itemProp?: string, itemScope?: string, - itemType?: string + itemType?: string, + rel?: string, + target?: string, + unstable_ariaSet?: Object, + unstable_dataSet?: Object }; diff --git a/packages/react-native-web/src/hooks/usePlatformMethods.js b/packages/react-native-web/src/hooks/usePlatformMethods.js index 13c85eda..f5564bc4 100644 --- a/packages/react-native-web/src/hooks/usePlatformMethods.js +++ b/packages/react-native-web/src/hooks/usePlatformMethods.js @@ -13,6 +13,34 @@ import type { ElementRef } from 'react'; import UIManager from '../exports/UIManager'; import createDOMProps from '../modules/createDOMProps'; import { useImperativeHandle, useRef } from 'react'; +import TextInputState from '../modules/TextInputState'; + +function setNativeProps(node, nativeProps, classList, style, previousStyle) { + if (node != null && nativeProps) { + const domProps = createDOMProps(null, { + ...nativeProps, + classList: [nativeProps.className, classList], + style: [style, nativeProps.style] + }); + + const nextDomStyle = domProps.style; + + if (previousStyle.current != null) { + if (domProps.style == null) { + domProps.style = {}; + } + for (const styleName in previousStyle.current) { + if (domProps.style[styleName] == null) { + domProps.style[styleName] = ''; + } + } + } + + previousStyle.current = nextDomStyle; + + UIManager.updateView(node, domProps); + } +} export default function usePlatformMethods( hostRef: ElementRef, @@ -26,52 +54,74 @@ export default function usePlatformMethods( useImperativeHandle( ref, () => { + const hostNode = hostRef.current; return { blur() { - UIManager.blur(hostRef.current); + UIManager.blur(hostNode); }, focus() { - UIManager.focus(hostRef.current); + UIManager.focus(hostNode); }, measure(callback) { - UIManager.measure(hostRef.current, callback); + UIManager.measure(hostNode, callback); }, measureLayout(relativeToNativeNode, onFail, onSuccess) { - UIManager.measureLayout(hostRef.current, relativeToNativeNode, onFail, onSuccess); + UIManager.measureLayout(hostNode, relativeToNativeNode, onFail, onSuccess); }, measureInWindow(callback) { - UIManager.measureInWindow(hostRef.current, callback); + UIManager.measureInWindow(hostNode, callback); }, setNativeProps(nativeProps) { - const node = hostRef.current; - if (node && nativeProps) { - const domProps = createDOMProps(null, { - ...nativeProps, - classList: [nativeProps.className, classList], - style: [style, nativeProps.style] - }); - - const nextDomStyle = domProps.style; - - if (previousStyle.current != null) { - if (domProps.style == null) { - domProps.style = {}; - } - for (const styleName in previousStyle.current) { - if (domProps.style[styleName] == null) { - domProps.style[styleName] = ''; - } - } - } - - previousStyle.current = nextDomStyle; - - UIManager.updateView(node, domProps); - } - }, - ...extras + setNativeProps(hostNode, nativeProps, classList, style, previousStyle); + } }; }, - [classList, hostRef, style, extras] + [classList, hostRef, style] + ); +} + +export function usePlatformInputMethods( + hostRef: ElementRef, + ref: ElementRef, + classList: Array, + style: GenericStyleProp, + extras: any +) { + const previousStyle = useRef(null); + + useImperativeHandle( + ref, + () => { + const hostNode = hostRef.current; + return { + blur() { + UIManager.blur(hostNode); + }, + clear() { + if (hostNode != null) { + hostNode.value = ''; + } + }, + focus() { + UIManager.focus(hostNode); + }, + isFocused() { + return hostNode != null && TextInputState.currentlyFocusedField() === hostNode; + }, + measure(callback) { + UIManager.measure(hostNode, callback); + }, + measureLayout(relativeToNativeNode, onFail, onSuccess) { + UIManager.measureLayout(hostNode, relativeToNativeNode, onFail, onSuccess); + }, + measureInWindow(callback) { + UIManager.measureInWindow(hostNode, callback); + }, + setNativeProps(nativeProps) { + setNativeProps(hostNode, nativeProps, classList, style, previousStyle); + } + }; + }, + [classList, hostRef, style] ); } diff --git a/packages/react-native-web/src/modules/AccessibilityUtil/__tests__/propsToAccessibilityComponent.js b/packages/react-native-web/src/modules/AccessibilityUtil/__tests__/propsToAccessibilityComponent-test.js similarity index 87% rename from packages/react-native-web/src/modules/AccessibilityUtil/__tests__/propsToAccessibilityComponent.js rename to packages/react-native-web/src/modules/AccessibilityUtil/__tests__/propsToAccessibilityComponent-test.js index f7386ce9..8ac3cd4e 100644 --- a/packages/react-native-web/src/modules/AccessibilityUtil/__tests__/propsToAccessibilityComponent.js +++ b/packages/react-native-web/src/modules/AccessibilityUtil/__tests__/propsToAccessibilityComponent-test.js @@ -17,7 +17,10 @@ describe('modules/AccessibilityUtil/propsToAccessibilityComponent', () => { test('when "accessibilityRole" is "heading" and "aria-level" is set', () => { expect( - propsToAccessibilityComponent({ accessibilityRole: 'heading', 'aria-level': 3 }) + propsToAccessibilityComponent({ + accessibilityRole: 'heading', + unstable_ariaSet: { level: 3 } + }) ).toEqual('h3'); }); diff --git a/packages/react-native-web/src/modules/AccessibilityUtil/propsToAccessibilityComponent.js b/packages/react-native-web/src/modules/AccessibilityUtil/propsToAccessibilityComponent.js index 85a068f5..5144a039 100644 --- a/packages/react-native-web/src/modules/AccessibilityUtil/propsToAccessibilityComponent.js +++ b/packages/react-native-web/src/modules/AccessibilityUtil/propsToAccessibilityComponent.js @@ -34,8 +34,12 @@ const propsToAccessibilityComponent = (props: Object = emptyObject) => { const role = propsToAriaRole(props); if (role) { if (role === 'heading') { - const level = props['aria-level'] || 1; - return `h${level}`; + const ariaSet = props.unstable_ariaSet; + if (ariaSet != null && ariaSet.level) { + const level = ariaSet.level; + return `h${level}`; + } + return 'h1'; } return roleComponents[role]; } diff --git a/packages/react-native-web/src/modules/createDOMProps/index.js b/packages/react-native-web/src/modules/createDOMProps/index.js index 2e0980dd..4d2486f4 100644 --- a/packages/react-native-web/src/modules/createDOMProps/index.js +++ b/packages/react-native-web/src/modules/createDOMProps/index.js @@ -14,6 +14,7 @@ import styleResolver from '../../exports/StyleSheet/styleResolver'; import { STYLE_GROUPS } from '../../exports/StyleSheet/constants'; const emptyObject = {}; +const hasOwnProperty = Object.prototype.hasOwnProperty; // Reset styles for heading, link, and list DOM elements const classes = css.create( @@ -65,8 +66,8 @@ const createDOMProps = (component, props, styleResolver) => { accessibilityLiveRegion, accessibilityRelationship, accessibilityState, + accessibilityValue, classList, - className: deprecatedClassName, disabled: providedDisabled, importantForAccessibility, nativeID, @@ -77,6 +78,8 @@ const createDOMProps = (component, props, styleResolver) => { accessible, accessibilityRole, /* eslint-enable */ + unstable_ariaSet, + unstable_dataSet, ...domProps } = props; @@ -84,6 +87,30 @@ const createDOMProps = (component, props, styleResolver) => { (accessibilityState != null && accessibilityState.disabled === true) || providedDisabled; const role = AccessibilityUtil.propsToAriaRole(props); + // unstable_ariaSet + if (unstable_ariaSet != null) { + for (const prop in unstable_ariaSet) { + if (hasOwnProperty.call(unstable_ariaSet, prop)) { + const value = unstable_ariaSet[prop]; + if (value != null) { + domProps[`aria-${prop}`] = value; + } + } + } + } + + // unstable_dataSet + if (unstable_dataSet != null) { + for (const prop in unstable_dataSet) { + if (hasOwnProperty.call(unstable_dataSet, prop)) { + const value = unstable_dataSet[prop]; + if (value != null) { + domProps[`data-${prop}`] = value; + } + } + } + } + // accessibilityLabel if (accessibilityLabel != null) { domProps['aria-label'] = accessibilityLabel; @@ -126,6 +153,17 @@ const createDOMProps = (component, props, styleResolver) => { } } } + + // accessibilityValue + if (accessibilityValue != null) { + for (const prop in accessibilityValue) { + const value = accessibilityValue[prop]; + if (value != null) { + domProps[`aria-value${prop}`] = value; + } + } + } + // legacy fallbacks if (importantForAccessibility === 'no-hide-descendants') { domProps['aria-hidden'] = true; @@ -182,12 +220,7 @@ const createDOMProps = (component, props, styleResolver) => { component === 'ul' || role === 'heading'; // Classic CSS styles - const finalClassList = [ - deprecatedClassName, - needsReset && classes.reset, - needsCursor && classes.cursor, - classList - ]; + const finalClassList = [needsReset && classes.reset, needsCursor && classes.cursor, classList]; // Resolve styles const { className, style } = styleResolver(reactNativeStyle, finalClassList); @@ -202,7 +235,7 @@ const createDOMProps = (component, props, styleResolver) => { // OTHER // Native element ID - if (nativeID && nativeID.constructor === String) { + if (nativeID != null) { domProps.id = nativeID; } @@ -214,7 +247,7 @@ const createDOMProps = (component, props, styleResolver) => { domProps.rel = `${domProps.rel || ''} noopener noreferrer`; } // Automated test IDs - if (testID && testID.constructor === String) { + if (testID != null) { domProps['data-testid'] = testID; }