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; }