diff --git a/Libraries/Components/ScrollView/ScrollView.js b/Libraries/Components/ScrollView/ScrollView.js index 599c20142af87e..636c73d49dc2d8 100644 --- a/Libraries/Components/ScrollView/ScrollView.js +++ b/Libraries/Components/ScrollView/ScrollView.js @@ -1184,50 +1184,6 @@ class ScrollView extends React.Component { } // [TODO(macOS GH#774) - _handleKeyDown = (event: ScrollEvent) => { - if (this.props.onScrollKeyDown) { - this.props.onScrollKeyDown(event); - } else { - if (Platform.OS === 'macos') { - const nativeEvent = event.nativeEvent; - const key = nativeEvent.key; - const kMinScrollOffset = 10; - if (key === 'PAGE_UP') { - this._handleScrollByKeyDown(event, { - x: nativeEvent.contentOffset.x, - y: - nativeEvent.contentOffset.y + - -nativeEvent.layoutMeasurement.height, - }); - } else if (key === 'PAGE_DOWN') { - this._handleScrollByKeyDown(event, { - x: nativeEvent.contentOffset.x, - y: - nativeEvent.contentOffset.y + - nativeEvent.layoutMeasurement.height, - }); - } else if (key === 'HOME') { - this.scrollTo({x: 0, y: 0}); - } else if (key === 'END') { - this.scrollToEnd({animated: true}); - } - } - } - }; - - _handleScrollByKeyDown = (event: ScrollEvent, newOffset) => { - const maxX = - event.nativeEvent.contentSize.width - - event.nativeEvent.layoutMeasurement.width; - const maxY = - event.nativeEvent.contentSize.height - - event.nativeEvent.layoutMeasurement.height; - this.scrollTo({ - x: Math.max(0, Math.min(maxX, newOffset.x)), - y: Math.max(0, Math.min(maxY, newOffset.y)), - }); - }; - _handlePreferredScrollerStyleDidChange = (event: ScrollEvent) => { this.setState({contentKey: this.state.contentKey + 1}); }; // ]TODO(macOS GH#774) @@ -1783,7 +1739,6 @@ class ScrollView extends React.Component { // Override the onContentSizeChange from props, since this event can // bubble up from TextInputs onContentSizeChange: null, - onScrollKeyDown: this._handleKeyDown, // TODO(macOS GH#774) onPreferredScrollerStyleDidChange: this._handlePreferredScrollerStyleDidChange, // TODO(macOS GH#774) onLayout: this._handleLayout, diff --git a/Libraries/Components/ScrollView/__tests__/__snapshots__/ScrollView-test.js.snap b/Libraries/Components/ScrollView/__tests__/__snapshots__/ScrollView-test.js.snap index 6fb8cfb75c3c1b..6124f0cf96027d 100644 --- a/Libraries/Components/ScrollView/__tests__/__snapshots__/ScrollView-test.js.snap +++ b/Libraries/Components/ScrollView/__tests__/__snapshots__/ScrollView-test.js.snap @@ -27,7 +27,6 @@ exports[` should render as expected: should deep render when not m onScroll={[Function]} onScrollBeginDrag={[Function]} onScrollEndDrag={[Function]} - onScrollKeyDown={[Function]} onScrollShouldSetResponder={[Function]} onStartShouldSetResponder={[Function]} onStartShouldSetResponderCapture={[Function]} diff --git a/Libraries/Components/View/ViewPropTypes.js b/Libraries/Components/View/ViewPropTypes.js index 22ce4f52121996..c162346321f014 100644 --- a/Libraries/Components/View/ViewPropTypes.js +++ b/Libraries/Components/View/ViewPropTypes.js @@ -76,12 +76,6 @@ type DirectEventProps = $ReadOnly<{| */ onPreferredScrollerStyleDidChange?: ?(event: ScrollEvent) => mixed, // TODO(macOS GH#774) - /** - * When `focusable` is true, the system will try to invoke this function - * when the user performs accessibility key down gesture. - */ - onScrollKeyDown?: ?(event: ScrollEvent) => mixed, // TODO(macOS GH#774) - /** * Invoked on mount and layout changes with: * diff --git a/Libraries/Lists/FlatList.js b/Libraries/Lists/FlatList.js index e4efd87ed67a2a..bdcc5a34d136be 100644 --- a/Libraries/Lists/FlatList.js +++ b/Libraries/Lists/FlatList.js @@ -66,6 +66,24 @@ type OptionalProps = {| * Optional custom style for multi-item rows generated when numColumns > 1. */ columnWrapperStyle?: ViewStyleProp, + // [TODO(macOS GH#774) + /** + * Allows you to 'select' a row using arrow keys. The selected row will have the prop `isSelected` + * passed in as true to it's renderItem / ListItemComponent. You can also imperatively select a row + * using the `selectRowAtIndex` method. You can set the initially selected row using the + * `initialSelectedIndex` prop. + * Keyboard Behavior: + * - ArrowUp: Select row above current selected row + * - ArrowDown: Select row below current selected row + * - Option+ArrowUp: Select the first row + * - Opton+ArrowDown: Select the last 'realized' row + * - Home: Scroll to top of list + * - End: Scroll to end of list + * + * @platform macos + */ + enableSelectionOnKeyPress?: ?boolean, + // ]TODO(macOS GH#774) /** * A marker property for telling the list to re-render (since it implements `PureComponent`). If * any of your `renderItem`, Header, Footer, etc. functions depend on anything outside of the @@ -111,6 +129,12 @@ type OptionalProps = {| * `getItemLayout` to be implemented. */ initialScrollIndex?: ?number, + // [TODO(macOS GH#774) + /** + * The initially selected row, if `enableSelectionOnKeyPress` is set. + */ + initialSelectedIndex?: ?number, + // ]TODO(macOS GH#774) /** * Reverses the direction of scroll. Uses scale transforms of -1. */ diff --git a/Libraries/Lists/VirtualizedList.js b/Libraries/Lists/VirtualizedList.js index b123a0e8e5f706..a987b56ccc8647 100644 --- a/Libraries/Lists/VirtualizedList.js +++ b/Libraries/Lists/VirtualizedList.js @@ -35,7 +35,7 @@ import type { ViewToken, ViewabilityConfigCallbackPair, } from './ViewabilityHelper'; -import type {ScrollEvent} from '../Types/CoreEventTypes'; // TODO(macOS GH#774) +import type {KeyEvent} from '../Types/CoreEventTypes'; // TODO(macOS GH#774) import { VirtualizedListCellContextProvider, VirtualizedListContext, @@ -109,12 +109,24 @@ type OptionalProps = {| * this for debugging purposes. Defaults to false. */ disableVirtualization?: ?boolean, + // [TODO(macOS GH#774) /** - * Handles key down events and updates selection based on the key event + * Allows you to 'select' a row using arrow keys. The selected row will have the prop `isSelected` + * passed in as true to it's renderItem / ListItemComponent. You can also imperatively select a row + * using the `selectRowAtIndex` method. You can set the initially selected row using the + * `initialSelectedIndex` prop. + * Keyboard Behavior: + * - ArrowUp: Select row above current selected row + * - ArrowDown: Select row below current selected row + * - Option+ArrowUp: Select the first row + * - Opton+ArrowDown: Select the last 'realized' row + * - Home: Scroll to top of list + * - End: Scroll to end of list * * @platform macos */ - enableSelectionOnKeyPress?: ?boolean, // TODO(macOS GH#774) + enableSelectionOnKeyPress?: ?boolean, + // ]TODO(macOS GH#774) /** * A marker property for telling the list to re-render (since it implements `PureComponent`). If * any of your `renderItem`, Header, Footer, etc. functions depend on anything outside of the @@ -145,6 +157,12 @@ type OptionalProps = {| * `getItemLayout` to be implemented. */ initialScrollIndex?: ?number, + // [TODO(macOS GH#774) + /** + * The initially selected row, if `enableSelectionOnKeyPress` is set. + */ + initialSelectedIndex?: ?number, + // ]TODO(macOS GH#774) /** * Reverses the direction of scroll. Uses scale transforms of -1. */ @@ -782,7 +800,7 @@ class VirtualizedList extends React.PureComponent { (this.props.initialScrollIndex || 0) + initialNumToRenderOrDefault(this.props.initialNumToRender), ) - 1, - selectedRowIndex: 0, // TODO(macOS GH#774) + selectedRowIndex: this.props.initialSelectedIndex || -1, // TODO(macOS GH#774) }; if (this._isNestedWithSameOrientation()) { @@ -845,7 +863,7 @@ class VirtualizedList extends React.PureComponent { ), last: Math.max(0, Math.min(prevState.last, getItemCount(data) - 1)), selectedRowIndex: Math.max( - 0, + -1, // Used to indicate no row is selected Math.min(prevState.selectedRowIndex, getItemCount(data)), ), // TODO(macOS GH#774) }; @@ -1309,14 +1327,16 @@ class VirtualizedList extends React.PureComponent { } _defaultRenderScrollComponent = props => { - let keyEventHandler = this.props.onScrollKeyDown; // [TODO(macOS GH#774) - if (!keyEventHandler) { - keyEventHandler = this.props.enableSelectionOnKeyPress - ? this._handleKeyDown - : null; - } + // [TODO(macOS GH#774) const preferredScrollerStyleDidChangeHandler = - this.props.onPreferredScrollerStyleDidChange; // ]TODO(macOS GH#774) + this.props.onPreferredScrollerStyleDidChange; + + const keyboardNavigationProps = { + focusable: true, + validKeysDown: ['ArrowUp', 'ArrowDown', 'Home', 'End'], + onKeyDown: this._handleKeyDown, + }; + // ]TODO(macOS GH#774) const onRefresh = props.onRefresh; if (this._isNestedWithSameOrientation()) { // $FlowFixMe[prop-missing] - Typing ReactNativeComponent revealed errors @@ -1333,8 +1353,7 @@ class VirtualizedList extends React.PureComponent { { // $FlowFixMe Invalid prop usage { }; // [TODO(macOS GH#774) - _selectRowAboveIndex = rowIndex => { - const rowAbove = rowIndex > 0 ? rowIndex - 1 : rowIndex; - this.setState(state => { - return {selectedRowIndex: rowAbove}; - }); - return rowAbove; - }; - _selectRowAtIndex = rowIndex => { - this.setState(state => { - return {selectedRowIndex: rowIndex}; - }); - return rowIndex; - }; - - _selectRowBelowIndex = rowIndex => { - if (this.props.getItemCount) { - const {data} = this.props; - const itemCount = this.props.getItemCount(data); - const rowBelow = rowIndex < itemCount - 1 ? rowIndex + 1 : rowIndex; - this.setState(state => { - return {selectedRowIndex: rowBelow}; - }); - return rowBelow; - } else { - return rowIndex; - } - }; - - _handleKeyDown = (event: ScrollEvent) => { - if (this.props.onScrollKeyDown) { - this.props.onScrollKeyDown(event); - } else { - if (Platform.OS === 'macos') { - // $FlowFixMe Cannot get e.nativeEvent because property nativeEvent is missing in Event - const nativeEvent = event.nativeEvent; - const key = nativeEvent.key; - - let prevIndex = -1; - let newIndex = -1; - if ('selectedRowIndex' in this.state) { - prevIndex = this.state.selectedRowIndex; - } - - // const {data, getItem} = this.props; - if (key === 'UP_ARROW') { - newIndex = this._selectRowAboveIndex(prevIndex); - this._handleSelectionChange(prevIndex, newIndex); - } else if (key === 'DOWN_ARROW') { - newIndex = this._selectRowBelowIndex(prevIndex); - this._handleSelectionChange(prevIndex, newIndex); - } else if (key === 'ENTER') { - if (this.props.onSelectionEntered) { - const item = this.props.getItem(this.props.data, prevIndex); - if (this.props.onSelectionEntered) { - this.props.onSelectionEntered(item); - } - } - } else if (key === 'OPTION_UP') { - newIndex = this._selectRowAtIndex(0); - this._handleSelectionChange(prevIndex, newIndex); - } else if (key === 'OPTION_DOWN') { - newIndex = this._selectRowAtIndex(this.state.last); - this._handleSelectionChange(prevIndex, newIndex); - } else if (key === 'PAGE_UP') { - const maxY = - event.nativeEvent.contentSize.height - - event.nativeEvent.layoutMeasurement.height; - const newOffset = Math.min( - maxY, - nativeEvent.contentOffset.y + -nativeEvent.layoutMeasurement.height, - ); - this.scrollToOffset({animated: true, offset: newOffset}); - } else if (key === 'PAGE_DOWN') { - const maxY = - event.nativeEvent.contentSize.height - - event.nativeEvent.layoutMeasurement.height; - const newOffset = Math.min( - maxY, - nativeEvent.contentOffset.y + nativeEvent.layoutMeasurement.height, - ); - this.scrollToOffset({animated: true, offset: newOffset}); - } else if (key === 'HOME') { - this.scrollToOffset({animated: true, offset: 0}); - } else if (key === 'END') { - this.scrollToEnd({animated: true}); - } - } - } - }; + const prevIndex = this.state.selectedRowIndex; + const newIndex = rowIndex; + this.setState({selectedRowIndex: newIndex}); - _handleSelectionChange = (prevIndex, newIndex) => { this.ensureItemAtIndexIsVisible(newIndex); if (prevIndex !== newIndex) { const item = this.props.getItem(this.props.data, newIndex); @@ -1613,6 +1545,62 @@ class VirtualizedList extends React.PureComponent { }); } } + + return newIndex; + }; + + _selectRowAboveIndex = rowIndex => { + const rowAbove = rowIndex > 0 ? rowIndex - 1 : rowIndex; + this._selectRowAtIndex(rowAbove); + }; + + _selectRowBelowIndex = rowIndex => { + const rowBelow = rowIndex < this.state.last ? rowIndex + 1 : rowIndex; + this._selectRowAtIndex(rowBelow); + }; + + _handleKeyDown = (event: KeyEvent) => { + if (Platform.OS === 'macos') { + this.props.onKeyDown?.(event); + if (event.defaultPrevented) { + return; + } + + const nativeEvent = event.nativeEvent; + const key = nativeEvent.key; + + let selectedIndex = -1; + if (this.state.selectedRowIndex >= 0) { + selectedIndex = this.state.selectedRowIndex; + } + + if (key === 'ArrowUp') { + if (nativeEvent.altKey) { + // Option+Up selects the first element + this._selectRowAtIndex(0); + } else { + this._selectRowAboveIndex(selectedIndex); + } + } else if (key === 'ArrowDown') { + if (nativeEvent.altKey) { + // Option+Down selects the last element + this._selectRowAtIndex(this.state.last); + } else { + this._selectRowBelowIndex(selectedIndex); + } + } else if (key === 'Enter') { + if (this.props.onSelectionEntered) { + const item = this.props.getItem(this.props.data, selectedIndex); + if (this.props.onSelectionEntered) { + this.props.onSelectionEntered(item); + } + } + } else if (key === 'Home') { + this.scrollToOffset({animated: true, offset: 0}); + } else if (key === 'End') { + this.scrollToEnd({animated: true}); + } + } }; // ]TODO(macOS GH#774) diff --git a/Libraries/Lists/VirtualizedSectionList.js b/Libraries/Lists/VirtualizedSectionList.js index 23651e9dd23514..2d4fe82eeec550 100644 --- a/Libraries/Lists/VirtualizedSectionList.js +++ b/Libraries/Lists/VirtualizedSectionList.js @@ -12,7 +12,7 @@ const Platform = require('../Utilities/Platform'); // TODO(macOS GH#774) import invariant from 'invariant'; import type {ViewToken} from './ViewabilityHelper'; import type {SelectedRowIndexPathType} from './VirtualizedList'; // TODO(macOS GH#774) -import type {ScrollEvent} from '../Types/CoreEventTypes'; // TODO(macOS GH#774) +import type {KeyEvent} from '../Types/CoreEventTypes'; // TODO(macOS GH#774) import {keyExtractor as defaultKeyExtractor} from './VirtualizeUtils'; import {View, VirtualizedList} from 'react-native'; import * as React from 'react'; @@ -311,8 +311,12 @@ class VirtualizedSectionList< } }; - _handleKeyDown = (e: ScrollEvent) => { + _handleKeyDown = (e: KeyEvent) => { if (Platform.OS === 'macos') { + if (e.defaultPrevented) { + return; + } + const event = e.nativeEvent; const key = event.key; let prevIndexPath = this.state.selectedRowIndexPath; @@ -320,7 +324,7 @@ class VirtualizedSectionList< const sectionIndex = this.state.selectedRowIndexPath.sectionIndex; const rowIndex = this.state.selectedRowIndexPath.rowIndex; - if (key === 'DOWN_ARROW') { + if (key === 'ArrowDown') { nextIndexPath = this._selectRowBelowIndexPath(prevIndexPath); this._ensureItemAtIndexPathIsVisible(nextIndexPath); @@ -332,7 +336,7 @@ class VirtualizedSectionList< item: item, }); } - } else if (key === 'UP_ARROW') { + } else if (key === 'ArrowUp') { nextIndexPath = this._selectRowAboveIndexPath(prevIndexPath); this._ensureItemAtIndexPathIsVisible(nextIndexPath); @@ -344,7 +348,7 @@ class VirtualizedSectionList< item: item, }); } - } else if (key === 'ENTER') { + } else if (key === 'Enter') { if (this.props.onSelectionEntered) { const item = this.props.sections[sectionIndex].data[rowIndex]; this.props.onSelectionEntered(item); diff --git a/Libraries/Lists/__tests__/__snapshots__/FlatList-test.js.snap b/Libraries/Lists/__tests__/__snapshots__/FlatList-test.js.snap index 7118348ac47ddc..2cd737428ad928 100644 --- a/Libraries/Lists/__tests__/__snapshots__/FlatList-test.js.snap +++ b/Libraries/Lists/__tests__/__snapshots__/FlatList-test.js.snap @@ -38,7 +38,6 @@ exports[`FlatList renders all the bells and whistles 1`] = ` onScroll={[Function]} onScrollBeginDrag={[Function]} onScrollEndDrag={[Function]} - onScrollKeyDown={null} refreshControl={ @@ -1486,7 +1469,6 @@ exports[`VirtualizedList renders sticky headers in viewport on batched render 1` onScroll={[Function]} onScrollBeginDrag={[Function]} onScrollEndDrag={[Function]} - onScrollKeyDown={null} renderItem={[Function]} scrollEventThrottle={50} stickyHeaderIndices={ @@ -1564,7 +1546,6 @@ exports[`VirtualizedList test getItem functionality where data is not an Array 1 onScroll={[Function]} onScrollBeginDrag={[Function]} onScrollEndDrag={[Function]} - onScrollKeyDown={null} renderItem={[Function]} scrollEventThrottle={50} stickyHeaderIndices={Array []} @@ -1601,7 +1582,6 @@ exports[`VirtualizedList warns if both renderItem or ListItemComponent are speci onScroll={[Function]} onScrollBeginDrag={[Function]} onScrollEndDrag={[Function]} - onScrollKeyDown={null} renderItem={[Function]} scrollEventThrottle={50} stickyHeaderIndices={Array []} @@ -1699,7 +1679,6 @@ exports[`adjusts render area with non-zero initialScrollIndex after scrolled 1`] onScroll={[Function]} onScrollBeginDrag={[Function]} onScrollEndDrag={[Function]} - onScrollKeyDown={null} renderItem={[Function]} scrollEventThrottle={50} stickyHeaderIndices={Array []} @@ -1883,7 +1862,6 @@ exports[`constrains batch render region when an item is removed 1`] = ` onScroll={[Function]} onScrollBeginDrag={[Function]} onScrollEndDrag={[Function]} - onScrollKeyDown={null} renderItem={[Function]} scrollEventThrottle={50} stickyHeaderIndices={Array []} @@ -2006,7 +1984,6 @@ exports[`discards intitial render if initialScrollIndex != 0 1`] = ` onScroll={[Function]} onScrollBeginDrag={[Function]} onScrollEndDrag={[Function]} - onScrollKeyDown={null} renderItem={[Function]} scrollEventThrottle={50} stickyHeaderIndices={Array []} @@ -2178,7 +2155,6 @@ exports[`does not adjust render area until content area layed out 1`] = ` onScroll={[Function]} onScrollBeginDrag={[Function]} onScrollEndDrag={[Function]} - onScrollKeyDown={null} renderItem={[Function]} scrollEventThrottle={50} stickyHeaderIndices={Array []} @@ -2310,7 +2286,6 @@ exports[`does not adjust render area with non-zero initialScrollIndex until scro onScroll={[Function]} onScrollBeginDrag={[Function]} onScrollEndDrag={[Function]} - onScrollKeyDown={null} renderItem={[Function]} scrollEventThrottle={50} stickyHeaderIndices={Array []} @@ -2418,7 +2393,6 @@ exports[`does not over-render when there is less than initialNumToRender cells 1 onScroll={[Function]} onScrollBeginDrag={[Function]} onScrollEndDrag={[Function]} - onScrollKeyDown={null} renderItem={[Function]} scrollEventThrottle={50} stickyHeaderIndices={Array []} @@ -2548,7 +2522,6 @@ exports[`eventually renders all items when virtualization disabled 1`] = ` onScroll={[Function]} onScrollBeginDrag={[Function]} onScrollEndDrag={[Function]} - onScrollKeyDown={null} renderItem={[Function]} scrollEventThrottle={50} stickyHeaderIndices={Array []} @@ -2678,7 +2651,6 @@ exports[`expands first in viewport to render up to maxToRenderPerBatch on initia onScroll={[Function]} onScrollBeginDrag={[Function]} onScrollEndDrag={[Function]} - onScrollKeyDown={null} renderItem={[Function]} scrollEventThrottle={50} stickyHeaderIndices={Array []} @@ -2815,7 +2787,6 @@ exports[`expands render area by maxToRenderPerBatch on tick 1`] = ` onScroll={[Function]} onScrollBeginDrag={[Function]} onScrollEndDrag={[Function]} - onScrollKeyDown={null} renderItem={[Function]} scrollEventThrottle={50} stickyHeaderIndices={Array []} @@ -2927,7 +2898,6 @@ exports[`renders a zero-height tail spacer on initial render if getItemLayout no onScroll={[Function]} onScrollBeginDrag={[Function]} onScrollEndDrag={[Function]} - onScrollKeyDown={null} renderItem={[Function]} scrollEventThrottle={50} stickyHeaderIndices={Array []} @@ -3015,7 +2985,6 @@ exports[`renders full tail spacer if all cells measured 1`] = ` onScroll={[Function]} onScrollBeginDrag={[Function]} onScrollEndDrag={[Function]} - onScrollKeyDown={null} renderItem={[Function]} scrollEventThrottle={50} stickyHeaderIndices={Array []} @@ -3122,7 +3091,6 @@ exports[`renders initialNumToRender cells when virtualization disabled 1`] = ` onScroll={[Function]} onScrollBeginDrag={[Function]} onScrollEndDrag={[Function]} - onScrollKeyDown={null} renderItem={[Function]} scrollEventThrottle={50} stickyHeaderIndices={Array []} @@ -3224,7 +3192,6 @@ exports[`renders items before initialScrollIndex on first batch tick when virtua onScroll={[Function]} onScrollBeginDrag={[Function]} onScrollEndDrag={[Function]} - onScrollKeyDown={null} renderItem={[Function]} scrollEventThrottle={50} stickyHeaderIndices={Array []} @@ -3333,7 +3300,6 @@ exports[`renders no spacers up to initialScrollIndex on first render when virtua onScroll={[Function]} onScrollBeginDrag={[Function]} onScrollEndDrag={[Function]} - onScrollKeyDown={null} renderItem={[Function]} scrollEventThrottle={50} stickyHeaderIndices={Array []} @@ -3405,7 +3371,6 @@ exports[`renders offset cells in initial render when initialScrollIndex set 1`] onScroll={[Function]} onScrollBeginDrag={[Function]} onScrollEndDrag={[Function]} - onScrollKeyDown={null} renderItem={[Function]} scrollEventThrottle={50} stickyHeaderIndices={Array []} @@ -3525,7 +3490,6 @@ exports[`renders tail spacer up to last measured index if getItemLayout not defi onScroll={[Function]} onScrollBeginDrag={[Function]} onScrollEndDrag={[Function]} - onScrollKeyDown={null} renderItem={[Function]} scrollEventThrottle={50} stickyHeaderIndices={Array []} @@ -3630,7 +3594,6 @@ exports[`renders tail spacer up to last measured with irregular layout when getI onScroll={[Function]} onScrollBeginDrag={[Function]} onScrollEndDrag={[Function]} - onScrollKeyDown={null} renderItem={[Function]} scrollEventThrottle={50} stickyHeaderIndices={Array []} @@ -3728,7 +3691,6 @@ exports[`renders windowSize derived region at bottom 1`] = ` onScroll={[Function]} onScrollBeginDrag={[Function]} onScrollEndDrag={[Function]} - onScrollKeyDown={null} renderItem={[Function]} scrollEventThrottle={50} stickyHeaderIndices={Array []} @@ -3836,7 +3798,6 @@ exports[`renders windowSize derived region at top 1`] = ` onScroll={[Function]} onScrollBeginDrag={[Function]} onScrollEndDrag={[Function]} - onScrollKeyDown={null} renderItem={[Function]} scrollEventThrottle={50} stickyHeaderIndices={Array []} @@ -3930,7 +3891,6 @@ exports[`renders windowSize derived region in middle 1`] = ` onScroll={[Function]} onScrollBeginDrag={[Function]} onScrollEndDrag={[Function]} - onScrollKeyDown={null} renderItem={[Function]} scrollEventThrottle={50} stickyHeaderIndices={Array []} @@ -4058,7 +4018,6 @@ exports[`renders zero-height tail spacer on batch render if cells not yet measur onScroll={[Function]} onScrollBeginDrag={[Function]} onScrollEndDrag={[Function]} - onScrollKeyDown={null} renderItem={[Function]} scrollEventThrottle={50} stickyHeaderIndices={Array []} @@ -4151,7 +4110,6 @@ exports[`retains batch render region when an item is appended 1`] = ` onScroll={[Function]} onScrollBeginDrag={[Function]} onScrollEndDrag={[Function]} - onScrollKeyDown={null} renderItem={[Function]} scrollEventThrottle={50} stickyHeaderIndices={Array []} @@ -4288,7 +4246,6 @@ exports[`retains initial render region when an item is appended 1`] = ` onScroll={[Function]} onScrollBeginDrag={[Function]} onScrollEndDrag={[Function]} - onScrollKeyDown={null} renderItem={[Function]} scrollEventThrottle={50} stickyHeaderIndices={Array []} @@ -4404,7 +4361,6 @@ exports[`retains intitial render if initialScrollIndex == 0 1`] = ` onScroll={[Function]} onScrollBeginDrag={[Function]} onScrollEndDrag={[Function]} - onScrollKeyDown={null} renderItem={[Function]} scrollEventThrottle={50} stickyHeaderIndices={Array []} @@ -4618,7 +4574,6 @@ exports[`unmounts sticky headers moved below viewport 1`] = ` onScroll={[Function]} onScrollBeginDrag={[Function]} onScrollEndDrag={[Function]} - onScrollKeyDown={null} renderItem={[Function]} scrollEventThrottle={50} stickyHeaderIndices={ diff --git a/Libraries/Lists/__tests__/__snapshots__/VirtualizedSectionList-test.js.snap b/Libraries/Lists/__tests__/__snapshots__/VirtualizedSectionList-test.js.snap index 68614f20c2db7a..cc0891f27b9563 100644 --- a/Libraries/Lists/__tests__/__snapshots__/VirtualizedSectionList-test.js.snap +++ b/Libraries/Lists/__tests__/__snapshots__/VirtualizedSectionList-test.js.snap @@ -27,7 +27,6 @@ exports[`VirtualizedSectionList handles nested lists 1`] = ` onScroll={[Function]} onScrollBeginDrag={[Function]} onScrollEndDrag={[Function]} - onScrollKeyDown={null} renderItem={[Function]} rowIndex={-1} scrollEventThrottle={50} @@ -133,7 +132,6 @@ exports[`VirtualizedSectionList handles nested lists 1`] = ` onScroll={[Function]} onScrollBeginDrag={[Function]} onScrollEndDrag={[Function]} - onScrollKeyDown={null} renderItem={[Function]} rowIndex={-1} scrollEventThrottle={50} @@ -234,7 +232,6 @@ exports[`VirtualizedSectionList handles separators correctly 1`] = ` onScroll={[Function]} onScrollBeginDrag={[Function]} onScrollEndDrag={[Function]} - onScrollKeyDown={null} renderItem={[Function]} rowIndex={-1} scrollEventThrottle={50} @@ -370,7 +367,6 @@ exports[`VirtualizedSectionList handles separators correctly 2`] = ` onScroll={[Function]} onScrollBeginDrag={[Function]} onScrollEndDrag={[Function]} - onScrollKeyDown={null} renderItem={[Function]} rowIndex={-1} scrollEventThrottle={50} @@ -506,7 +502,6 @@ exports[`VirtualizedSectionList handles separators correctly 3`] = ` onScroll={[Function]} onScrollBeginDrag={[Function]} onScrollEndDrag={[Function]} - onScrollKeyDown={null} renderItem={[Function]} rowIndex={-1} scrollEventThrottle={50} @@ -643,7 +638,6 @@ exports[`VirtualizedSectionList handles separators correctly 4`] = ` onScroll={[Function]} onScrollBeginDrag={[Function]} onScrollEndDrag={[Function]} - onScrollKeyDown={null} renderItem={[Function]} rowIndex={-1} scrollEventThrottle={50} @@ -793,7 +787,6 @@ exports[`VirtualizedSectionList renders all the bells and whistles 1`] = ` onScroll={[Function]} onScrollBeginDrag={[Function]} onScrollEndDrag={[Function]} - onScrollKeyDown={null} refreshControl={ , zoomScale?: number, responderIgnoreScroll?: boolean, - key?: string, // TODO(macOS GH#774) preferredScrollerStyle?: string, // TODO(macOS GH#774) |}>, >; diff --git a/React/Views/RCTView.h b/React/Views/RCTView.h index 76eedd809a1673..3f44c95ae9c882 100644 --- a/React/Views/RCTView.h +++ b/React/Views/RCTView.h @@ -25,6 +25,10 @@ extern const UIAccessibilityTraits SwitchAccessibilityTrait; - (BOOL)becomeFirstResponder; - (BOOL)resignFirstResponder; + +#if TARGET_OS_OSX +- (BOOL)handleKeyboardEvent:(NSEvent *)event; +#endif // ]TODO(OSS Candidate ISS#2710739) /** diff --git a/React/Views/RCTView.m b/React/Views/RCTView.m index a0eb891b5d5d1f..1bd21e13e7f136 100644 --- a/React/Views/RCTView.m +++ b/React/Views/RCTView.m @@ -1619,177 +1619,38 @@ - (BOOL)performDragOperation:(id )sender #pragma mark - Keyboard Events #if TARGET_OS_OSX -NSString* const leftArrowPressKey = @"ArrowLeft"; -NSString* const rightArrowPressKey = @"ArrowRight"; -NSString* const upArrowPressKey = @"ArrowUp"; -NSString* const downArrowPressKey = @"ArrowDown"; - -- (RCTViewKeyboardEvent*)keyboardEvent:(NSEvent*)event downPress:(BOOL)downPress { - // modifiers - BOOL capsLockKey = NO; - BOOL shiftKey = NO; - BOOL controlKey = NO; - BOOL optionKey = NO; - BOOL commandKey = NO; - BOOL numericPadKey = NO; - BOOL helpKey = NO; - BOOL functionKey = NO; - // commonly used key short-cuts - BOOL leftArrowKey = NO; - BOOL rightArrowKey = NO; - BOOL upArrowKey = NO; - BOOL downArrowKey = NO; - BOOL tabKeyPressed = NO; - BOOL escapeKeyPressed = NO; - NSString *key = event.charactersIgnoringModifiers; - if ([key length] == 0) { - return nil; - } - unichar const code = [key characterAtIndex:0]; - - // detect arrow key presses - if (code == NSLeftArrowFunctionKey) { - leftArrowKey = YES; - } else if (code == NSRightArrowFunctionKey) { - rightArrowKey = YES; - } else if (code == NSUpArrowFunctionKey) { - upArrowKey = YES; - } else if (code == NSDownArrowFunctionKey) { - downArrowKey = YES; - } - - // detect special key presses via the key code - switch (event.keyCode) { - case 48: // Tab - tabKeyPressed = YES; - break; - case 53: // Escape - escapeKeyPressed = YES; - break; - default: - break; +- (RCTViewKeyboardEvent*)keyboardEvent:(NSEvent*)event { + BOOL keyDown = event.type == NSEventTypeKeyDown; + NSArray *validKeys = keyDown ? self.validKeysDown : self.validKeysUp; + NSString *key = [RCTViewKeyboardEvent keyFromEvent:event]; + + // Only post events for keys we care about + if (![validKeys containsObject:key]) { + return nil; } - // detect modifier flags - if (event.modifierFlags & NSEventModifierFlagCapsLock) { - capsLockKey = YES; - } else if (event.modifierFlags & NSEventModifierFlagShift) { - shiftKey = YES; - } else if (event.modifierFlags & NSEventModifierFlagControl) { - controlKey = YES; - } else if (event.modifierFlags & NSEventModifierFlagOption) { - optionKey = YES; - } else if (event.modifierFlags & NSEventModifierFlagCommand) { - commandKey = YES; - } else if (event.modifierFlags & NSEventModifierFlagNumericPad) { - numericPadKey = YES; - } else if (event.modifierFlags & NSEventModifierFlagHelp) { - helpKey = YES; - } else if (event.modifierFlags & NSEventModifierFlagFunction) { - functionKey = YES; - } - - RCTViewKeyboardEvent *keyboardEvent = nil; - // only post events for keys we care about - if (downPress) { - NSString *keyToReturn = [self keyIsValid:key left:leftArrowKey right:rightArrowKey up:upArrowKey down:downArrowKey tabKey:tabKeyPressed escapeKey:escapeKeyPressed validKeys:[self validKeysDown]]; - if (keyToReturn != nil) { - keyboardEvent = [RCTViewKeyboardEvent keyDownEventWithReactTag:self.reactTag - capsLockKey:capsLockKey - shiftKey:shiftKey - ctrlKey:controlKey - altKey:optionKey - metaKey:commandKey - numericPadKey:numericPadKey - helpKey:helpKey - functionKey:functionKey - leftArrowKey:leftArrowKey - rightArrowKey:rightArrowKey - upArrowKey:upArrowKey - downArrowKey:downArrowKey - key:keyToReturn]; - } - } else { - NSString *keyToReturn = [self keyIsValid:key left:leftArrowKey right:rightArrowKey up:upArrowKey down:downArrowKey tabKey:tabKeyPressed escapeKey:escapeKeyPressed validKeys:[self validKeysUp]]; - if (keyToReturn != nil) { - keyboardEvent = [RCTViewKeyboardEvent keyUpEventWithReactTag:self.reactTag - capsLockKey:capsLockKey - shiftKey:shiftKey - ctrlKey:controlKey - altKey:optionKey - metaKey:commandKey - numericPadKey:numericPadKey - helpKey:helpKey - functionKey:functionKey - leftArrowKey:leftArrowKey - rightArrowKey:rightArrowKey - upArrowKey:upArrowKey - downArrowKey:downArrowKey - key:keyToReturn]; + return [RCTViewKeyboardEvent keyEventFromEvent:event reactTag:self.reactTag]; +} + +- (BOOL)handleKeyboardEvent:(NSEvent *)event { + if (event.type == NSEventTypeKeyDown ? self.onKeyDown : self.onKeyUp) { + RCTViewKeyboardEvent *keyboardEvent = [self keyboardEvent:event]; + if (keyboardEvent) { + [_eventDispatcher sendEvent:keyboardEvent]; + return YES; } } - return keyboardEvent; -} - -// check if the user typed key matches a key we need to send an event for -// translate key codes over to JS compatible keys -- (NSString*)keyIsValid:(NSString*)key left:(BOOL)leftArrowPressed right:(BOOL)rightArrowPressed up:(BOOL)upArrowPressed down:(BOOL)downArrowPressed tabKey:(BOOL)tabKeyPressed escapeKey:(BOOL)escapeKeyPressed validKeys:(NSArray*)validKeys { - NSString *keyToReturn = key; - - // Allow the flexibility of defining special keys in multiple ways - BOOL enterKeyValidityCheck = [key isEqualToString:@"\r"] && ([validKeys containsObject:@"Enter"] || [validKeys containsObject:@"\r"]); - BOOL tabKeyValidityCheck = tabKeyPressed && ([validKeys containsObject:@"Tab"]); // tab has to be checked via a key code so we can't just use the key itself here - BOOL escapeKeyValidityCheck = escapeKeyPressed && ([validKeys containsObject:@"Esc"] || [validKeys containsObject:@"Escape"]); // escape has to be checked via a key code so we can't just use the key itself here - BOOL leftArrowValidityCheck = [validKeys containsObject:leftArrowPressKey] && leftArrowPressed; - BOOL rightArrowValidityCheck = [validKeys containsObject:rightArrowPressKey] && rightArrowPressed; - BOOL upArrowValidityCheck = [validKeys containsObject:upArrowPressKey] && upArrowPressed; - BOOL downArrowValidityCheck = [validKeys containsObject:downArrowPressKey] && downArrowPressed; - -if (tabKeyValidityCheck) { - keyToReturn = @"Tab"; - } else if (escapeKeyValidityCheck) { - keyToReturn = @"Escape"; - } else if (enterKeyValidityCheck) { - keyToReturn = @"Enter"; - } else if (leftArrowValidityCheck) { - keyToReturn = leftArrowPressKey; - } else if (rightArrowValidityCheck) { - keyToReturn = rightArrowPressKey; - } else if (upArrowValidityCheck) { - keyToReturn = upArrowPressKey; - } else if (downArrowValidityCheck) { - keyToReturn = downArrowPressKey; - } else if (![validKeys containsObject:key]) { - keyToReturn = nil; - } - - return keyToReturn; + return NO; } - (void)keyDown:(NSEvent *)event { - if (self.onKeyDown == nil) { - [super keyDown:event]; - return; - } - - RCTViewKeyboardEvent *keyboardEvent = [self keyboardEvent:event downPress:YES]; - if (keyboardEvent != nil) { - [_eventDispatcher sendEvent:keyboardEvent]; - } else { + if (![self handleKeyboardEvent:event]) { [super keyDown:event]; } } - (void)keyUp:(NSEvent *)event { - if (self.onKeyUp == nil) { - [super keyUp:event]; - return; - } - - RCTViewKeyboardEvent *keyboardEvent = [self keyboardEvent:event downPress:NO]; - if (keyboardEvent != nil) { - [_eventDispatcher sendEvent:keyboardEvent]; - } else { + if (![self handleKeyboardEvent:event]) { [super keyUp:event]; } } diff --git a/React/Views/RCTViewKeyboardEvent.h b/React/Views/RCTViewKeyboardEvent.h index 0a91c72683c0d9..25d35441dcf32b 100644 --- a/React/Views/RCTViewKeyboardEvent.h +++ b/React/Views/RCTViewKeyboardEvent.h @@ -7,33 +7,11 @@ #import @interface RCTViewKeyboardEvent : RCTComponentEvent -+ (instancetype)keyDownEventWithReactTag:(NSNumber *)reactTag - capsLockKey:(BOOL)capsLockKey - shiftKey:(BOOL)shiftKey - ctrlKey:(BOOL)controlKey - altKey:(BOOL)optionKey - metaKey:(BOOL)commandKey - numericPadKey:(BOOL)numericPadKey - helpKey:(BOOL)helpKey - functionKey:(BOOL)functionKey - leftArrowKey:(BOOL)leftArrowKey - rightArrowKey:(BOOL)rightArrowKey - upArrowKey:(BOOL)upArrowKey - downArrowKey:(BOOL)downArrowKey - key:(NSString *)key; -+ (instancetype)keyUpEventWithReactTag:(NSNumber *)reactTag - capsLockKey:(BOOL)capsLockKey - shiftKey:(BOOL)shiftKey - ctrlKey:(BOOL)controlKey - altKey:(BOOL)optionKey - metaKey:(BOOL)commandKey - numericPadKey:(BOOL)numericPadKey - helpKey:(BOOL)helpKey - functionKey:(BOOL)functionKey - leftArrowKey:(BOOL)leftArrowKey - rightArrowKey:(BOOL)rightArrowKey - upArrowKey:(BOOL)upArrowKey - downArrowKey:(BOOL)downArrowKey - key:(NSString *)key; +#if TARGET_OS_OSX // TODO(macOS GH#774) ++ (NSDictionary *)bodyFromEvent:(NSEvent *)event; ++ (NSString *)keyFromEvent:(NSEvent *)event; ++ (instancetype)keyEventFromEvent:(NSEvent *)event reactTag:(NSNumber *)reactTag; +#endif // TODO(macOS GH#774) + @end diff --git a/React/Views/RCTViewKeyboardEvent.m b/React/Views/RCTViewKeyboardEvent.m index e1f44054170698..3ec04a2e350eb3 100644 --- a/React/Views/RCTViewKeyboardEvent.m +++ b/React/Views/RCTViewKeyboardEvent.m @@ -9,69 +9,74 @@ #import @implementation RCTViewKeyboardEvent -// Keyboard mappings are aligned cross-platform as much as possible as per this doc https://github.com/microsoft/react-native-windows/blob/master/vnext/proposals/active/keyboard-reconcile-desktop.md -+ (instancetype)keyDownEventWithReactTag:(NSNumber *)reactTag - capsLockKey:(BOOL)capsLockKey - shiftKey:(BOOL)shiftKey - ctrlKey:(BOOL)controlKey - altKey:(BOOL)optionKey - metaKey:(BOOL)commandKey - numericPadKey:(BOOL)numericPadKey - helpKey:(BOOL)helpKey - functionKey:(BOOL)functionKey - leftArrowKey:(BOOL)leftArrowKey - rightArrowKey:(BOOL)rightArrowKey - upArrowKey:(BOOL)upArrowKey - downArrowKey:(BOOL)downArrowKey - key:(NSString *)key { - RCTViewKeyboardEvent *event = [[self alloc] initWithName:@"keyDown" - viewTag:reactTag - body:@{ @"capsLockKey" : @(capsLockKey), - @"shiftKey" : @(shiftKey), - @"ctrlKey" : @(controlKey), - @"altKey" : @(optionKey), - @"metaKey" : @(commandKey), - @"numericPadKey" : @(numericPadKey), - @"helpKey" : @(helpKey), - @"functionKey" : @(functionKey), - @"ArrowLeft" : @(leftArrowKey), - @"ArrowRight" : @(rightArrowKey), - @"ArrowUp" : @(upArrowKey), - @"ArrowDown" : @(downArrowKey), - @"key" : key }]; - return event; + +#if TARGET_OS_OSX // TODO(macOS GH#774) ++ (NSDictionary *)bodyFromEvent:(NSEvent *)event +{ + NSString *key = [self keyFromEvent:event]; + NSEventModifierFlags modifierFlags = event.modifierFlags; + + return @{ + @"key" : key, + @"capsLockKey" : (modifierFlags & NSEventModifierFlagCapsLock) ? @YES : @NO, + @"shiftKey" : (modifierFlags & NSEventModifierFlagShift) ? @YES : @NO, + @"ctrlKey" : (modifierFlags & NSEventModifierFlagControl) ? @YES : @NO, + @"altKey" : (modifierFlags & NSEventModifierFlagOption) ? @YES : @NO, + @"metaKey" : (modifierFlags & NSEventModifierFlagCommand) ? @YES : @NO, + @"numericPadKey" : (modifierFlags & NSEventModifierFlagNumericPad) ? @YES : @NO, + @"helpKey" : (modifierFlags & NSEventModifierFlagHelp) ? @YES : @NO, + @"functionKey" : (modifierFlags & NSEventModifierFlagFunction) ? @YES : @NO, + }; } -+(instancetype)keyUpEventWithReactTag:(NSNumber *)reactTag - capsLockKey:(BOOL)capsLockKey - shiftKey:(BOOL)shiftKey - ctrlKey:(BOOL)controlKey - altKey:(BOOL)optionKey - metaKey:(BOOL)commandKey - numericPadKey:(BOOL)numericPadKey - helpKey:(BOOL)helpKey - functionKey:(BOOL)functionKey - leftArrowKey:(BOOL)leftArrowKey - rightArrowKey:(BOOL)rightArrowKey - upArrowKey:(BOOL)upArrowKey - downArrowKey:(BOOL)downArrowKey - key:(NSString *)key { - RCTViewKeyboardEvent *event = [[self alloc] initWithName:@"keyUp" - viewTag:reactTag - body:@{ @"capsLockKey" : @(capsLockKey), - @"shiftKey" : @(shiftKey), - @"ctrlKey" : @(controlKey), - @"altKey" : @(optionKey), - @"metaKey" : @(commandKey), - @"numericPadKey" : @(numericPadKey), - @"helpKey" : @(helpKey), - @"functionKey" : @(functionKey), - @"ArrowLeft" : @(leftArrowKey), - @"ArrowRight" : @(rightArrowKey), - @"ArrowUp" : @(upArrowKey), - @"ArrowDown" : @(downArrowKey), - @"key" : key }]; - return event; ++ (NSString *)keyFromEvent:(NSEvent *)event +{ + NSString *key = event.charactersIgnoringModifiers; + unichar const code = key.length > 0 ? [key characterAtIndex:0] : 0; + + if (event.keyCode == 48) { + return @"Tab"; + } else if (event.keyCode == 53) { + return @"Escape"; + } else if (code == NSEnterCharacter || code == NSNewlineCharacter || code == NSCarriageReturnCharacter) { + return @"Enter"; + } else if (code == NSLeftArrowFunctionKey) { + return @"ArrowLeft"; + } else if (code == NSRightArrowFunctionKey) { + return @"ArrowRight"; + } else if (code == NSUpArrowFunctionKey) { + return @"ArrowUp"; + } else if (code == NSDownArrowFunctionKey) { + return @"ArrowDown"; + } else if (code == NSBackspaceCharacter || code == NSDeleteCharacter) { + return @"Backspace"; + } else if (code == NSDeleteFunctionKey) { + return @"Delete"; + } else if (code == NSHomeFunctionKey) { + return @"Home"; + } else if (code == NSEndFunctionKey) { + return @"End"; + } else if (code == NSPageUpFunctionKey) { + return @"PageUp"; + } else if (code == NSPageDownFunctionKey) { + return @"PageDown"; + } + + return key; +} + +// Keyboard mappings are aligned cross-platform as much as possible as per this doc https://github.com/microsoft/react-native-windows/blob/master/vnext/proposals/active/keyboard-reconcile-desktop.md ++ (instancetype)keyEventFromEvent:(NSEvent *)event reactTag:(NSNumber *)reactTag +{ + // Ignore "dead keys" (key press that waits for another key to make a character) + if (!event.charactersIgnoringModifiers.length) { + return nil; + } + + return [[self alloc] initWithName:(event.type == NSEventTypeKeyDown ? @"keyDown" : @"keyUp") + viewTag:reactTag + body:[self bodyFromEvent:event]]; } +#endif // TODO(macOS GH#774) @end diff --git a/React/Views/RCTViewManager.m b/React/Views/RCTViewManager.m index 902d21bd36397a..1bb8ea2e4ef079 100644 --- a/React/Views/RCTViewManager.m +++ b/React/Views/RCTViewManager.m @@ -490,18 +490,8 @@ - (RCTShadowView *)shadowView RCT_EXPORT_VIEW_PROPERTY(onDrop, RCTDirectEventBlock) RCT_EXPORT_VIEW_PROPERTY(onKeyDown, RCTDirectEventBlock) // macOS keyboard events RCT_EXPORT_VIEW_PROPERTY(onKeyUp, RCTDirectEventBlock) // macOS keyboard events -RCT_CUSTOM_VIEW_PROPERTY(validKeysDown, NSArray, RCTView) -{ - if ([view respondsToSelector:@selector(setValidKeysDown:)]) { - view.validKeysDown = [RCTConvert NSArray:json]; - } -} -RCT_CUSTOM_VIEW_PROPERTY(validKeysUp, NSArray, RCTView) -{ - if ([view respondsToSelector:@selector(setValidKeysUp:)]) { - view.validKeysUp = [RCTConvert NSArray:json]; - } -} +RCT_EXPORT_VIEW_PROPERTY(validKeysDown, NSArray) +RCT_EXPORT_VIEW_PROPERTY(validKeysUp, NSArray) #endif // ]TODO(macOS GH#774) #if TARGET_OS_OSX // [TODO(macOS GH#768) RCT_CUSTOM_VIEW_PROPERTY(nextKeyViewTag, NSNumber, RCTView) diff --git a/React/Views/ScrollView/RCTScrollView.h b/React/Views/ScrollView/RCTScrollView.h index 885c4ed1faf662..2874d4e050990b 100644 --- a/React/Views/ScrollView/RCTScrollView.h +++ b/React/Views/ScrollView/RCTScrollView.h @@ -65,7 +65,6 @@ @property (nonatomic, copy) RCTDirectEventBlock onScrollEndDrag; @property (nonatomic, copy) RCTDirectEventBlock onMomentumScrollBegin; @property (nonatomic, copy) RCTDirectEventBlock onMomentumScrollEnd; -@property (nonatomic, copy) RCTDirectEventBlock onScrollKeyDown; // TODO(macOS GH#774) @property (nonatomic, copy) RCTDirectEventBlock onPreferredScrollerStyleDidChange; // TODO(macOS GH#774) - (void)flashScrollIndicators; // TODO(macOS GH#774) diff --git a/React/Views/ScrollView/RCTScrollView.m b/React/Views/ScrollView/RCTScrollView.m index 51b93853b65426..43d9bc3c828827 100644 --- a/React/Views/ScrollView/RCTScrollView.m +++ b/React/Views/ScrollView/RCTScrollView.m @@ -23,6 +23,8 @@ #if !TARGET_OS_OSX // TODO(macOS GH#774) #import "RCTRefreshControl.h" +#else +#import "RCTViewKeyboardEvent.h" #endif // TODO(macOS GH#774) /** @@ -1261,74 +1263,58 @@ - (void)uiManagerWillPerformMounting:(RCTUIManager *)manager }]; } -#if TARGET_OS_OSX // [TODO(macOS GH#774) - -- (NSString*)keyCommandFromKeyCode:(NSInteger)keyCode modifierFlags:(NSEventModifierFlags)modifierFlags -{ - switch (keyCode) - { - case 36: - return @"ENTER"; - - case 115: - return @"HOME"; - - case 116: - return @"PAGE_UP"; - - case 119: - return @"END"; - - case 121: - return @"PAGE_DOWN"; - - case 123: - return @"LEFT_ARROW"; - - case 124: - return @"RIGHT_ARROW"; +// [TODO(macOS GH#774) +#pragma mark - Keyboard Events - case 125: - if (modifierFlags & NSEventModifierFlagOption) { - return @"OPTION_DOWN"; - } else { - return @"DOWN_ARROW"; - } +#if TARGET_OS_OSX +- (RCTViewKeyboardEvent*)keyboardEvent:(NSEvent*)event { + BOOL keyDown = event.type == NSEventTypeKeyDown; + NSArray *validKeys = keyDown ? self.validKeysDown : self.validKeysUp; + NSString *key = [RCTViewKeyboardEvent keyFromEvent:event]; + + // Only post events for keys we care about + if (![validKeys containsObject:key]) { + return nil; + } - case 126: - if (modifierFlags & NSEventModifierFlagOption) { - return @"OPTION_UP"; - } else { - return @"UP_ARROW"; - } - } - return @""; + return [RCTViewKeyboardEvent keyEventFromEvent:event reactTag:self.reactTag]; } -- (void)keyDown:(UIEvent*)theEvent -{ - // Don't emit a scroll event if tab was pressed while the scrollview is first responder - if (!(self == [[self window] firstResponder] && theEvent.keyCode == 48)) { - NSString *keyCommand = [self keyCommandFromKeyCode:theEvent.keyCode modifierFlags:theEvent.modifierFlags]; - if (![keyCommand isEqual: @""]) { - RCT_SEND_SCROLL_EVENT(onScrollKeyDown, (@{ @"key": keyCommand})); - } else { - [super keyDown:theEvent]; +- (BOOL)handleKeyboardEvent:(NSEvent *)event { + if (event.type == NSEventTypeKeyDown ? self.onKeyDown : self.onKeyUp) { + RCTViewKeyboardEvent *keyboardEvent = [self keyboardEvent:event]; + if (keyboardEvent) { + [_eventDispatcher sendEvent:keyboardEvent]; + return YES; } } + return NO; +} + +- (void)keyDown:(NSEvent *)event { + if (![self handleKeyboardEvent:event]) { + [super keyDown:event]; + + // AX: if a tab key was pressed and the first responder is currently clipped by the scroll view, + // automatically scroll to make the view visible to make it navigable via keyboard. + NSString *key = [RCTViewKeyboardEvent keyFromEvent:event]; + if ([key isEqualToString:@"Tab"]) { + id firstResponder = [[self window] firstResponder]; + if ([firstResponder isKindOfClass:[NSView class]] && + [firstResponder isDescendantOf:[_scrollView documentView]]) { + NSView *view = (NSView*)firstResponder; + NSRect visibleRect = ([view superview] == [_scrollView documentView]) ? NSInsetRect(view.frame, -1, -1) : + [view convertRect:view.frame toView:_scrollView.documentView]; + [[_scrollView documentView] scrollRectToVisible:visibleRect]; + } + } + } +} - // AX: if a tab key was pressed and the first responder is currently clipped by the scroll view, - // automatically scroll to make the view visible to make it navigable via keyboard. - if ([theEvent keyCode] == 48) { //tab key - id firstResponder = [[self window] firstResponder]; - if ([firstResponder isKindOfClass:[NSView class]] && - [firstResponder isDescendantOf:[_scrollView documentView]]) { - NSView *view = (NSView*)firstResponder; - NSRect visibleRect = ([view superview] == [_scrollView documentView]) ? NSInsetRect(view.frame, -1, -1) : - [view convertRect:view.frame toView:_scrollView.documentView]; - [[_scrollView documentView] scrollRectToVisible:visibleRect]; - } - } +- (void)keyUp:(NSEvent *)event { + if (![self handleKeyboardEvent:event]) { + [super keyUp:event]; + } } static NSString *RCTStringForScrollerStyle(NSScrollerStyle scrollerStyle) { @@ -1343,7 +1329,8 @@ - (void)keyDown:(UIEvent*)theEvent - (void)preferredScrollerStyleDidChange:(__unused NSNotification *)notification { RCT_SEND_SCROLL_EVENT(onPreferredScrollerStyleDidChange, (@{ @"preferredScrollerStyle": RCTStringForScrollerStyle([NSScroller preferredScrollerStyle])})); } -#endif // ]TODO(macOS GH#774) +#endif +// ]TODO(macOS GH#774) // Note: setting several properties of UIScrollView has the effect of // resetting its contentOffset to {0, 0}. To prevent this, we generate diff --git a/React/Views/ScrollView/RCTScrollViewManager.m b/React/Views/ScrollView/RCTScrollViewManager.m index 619c27ae96ec09..46f42e0dcbfd49 100644 --- a/React/Views/ScrollView/RCTScrollViewManager.m +++ b/React/Views/ScrollView/RCTScrollViewManager.m @@ -99,7 +99,6 @@ - (RCTPlatformView *)view // TODO(macOS GH#774) RCT_EXPORT_VIEW_PROPERTY(onScrollEndDrag, RCTDirectEventBlock) RCT_EXPORT_VIEW_PROPERTY(onMomentumScrollBegin, RCTDirectEventBlock) RCT_EXPORT_VIEW_PROPERTY(onMomentumScrollEnd, RCTDirectEventBlock) -RCT_EXPORT_OSX_VIEW_PROPERTY(onScrollKeyDown, RCTDirectEventBlock) // TODO(macOS GH#774) RCT_EXPORT_OSX_VIEW_PROPERTY(onPreferredScrollerStyleDidChange, RCTDirectEventBlock) // TODO(macOS GH#774) RCT_EXPORT_VIEW_PROPERTY(inverted, BOOL) #if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 130000 /* __IPHONE_13_0 */ diff --git a/packages/rn-tester/js/components/ListExampleShared.js b/packages/rn-tester/js/components/ListExampleShared.js index 96957402bdfdaf..1b616a7417f625 100644 --- a/packages/rn-tester/js/components/ListExampleShared.js +++ b/packages/rn-tester/js/components/ListExampleShared.js @@ -359,12 +359,18 @@ const styles = StyleSheet.create({ }, // [TODO(macOS GH#774) selectedItem: { - backgroundColor: PlatformColor('selectedContentBackgroundColor'), + backgroundColor: Platform.select({ + macos: PlatformColor('selectedContentBackgroundColor'), + default: 'blue', + }), }, selectedItemText: { // This was the closest UI Element color that looked right... // https://developer.apple.com/documentation/appkit/nscolor/ui_element_colors - color: PlatformColor('selectedMenuItemTextColor'), + color: Platform.select({ + macos: PlatformColor('selectedMenuItemTextColor'), + default: 'white', + }), }, // [TODO(macOS GH#774)] }); diff --git a/packages/rn-tester/js/examples/FlatList/FlatList-basic.js b/packages/rn-tester/js/examples/FlatList/FlatList-basic.js index b50756462aa80d..0e45bb10e4539d 100644 --- a/packages/rn-tester/js/examples/FlatList/FlatList-basic.js +++ b/packages/rn-tester/js/examples/FlatList/FlatList-basic.js @@ -185,11 +185,12 @@ class FlatListExample extends React.PureComponent { this._setBooleanValue('useFlatListItemComponent'), )} {/* [TODO(macOS GH#774) */} - {renderSmallSwitchOption( - 'Keyboard Navigation', - this.state.enableSelectionOnKeyPress, - this._setBooleanValue('enableSelectionOnKeyPress'), - )} + {Platform.OS === 'macos' && + renderSmallSwitchOption( + 'Keyboard Navigation', + this.state.enableSelectionOnKeyPress, + this._setBooleanValue('enableSelectionOnKeyPress'), + )} {/* TODO(macOS GH#774)] */} {Platform.OS === 'android' && ( @@ -210,7 +211,10 @@ class FlatListExample extends React.PureComponent { } diff --git a/packages/rn-tester/js/examples/KeyboardEventsExample/KeyboardEventsExample.js b/packages/rn-tester/js/examples/KeyboardEventsExample/KeyboardEventsExample.js index eb9a22b56b0465..18f27318b02458 100644 --- a/packages/rn-tester/js/examples/KeyboardEventsExample/KeyboardEventsExample.js +++ b/packages/rn-tester/js/examples/KeyboardEventsExample/KeyboardEventsExample.js @@ -53,7 +53,7 @@ class KeyEventExample extends React.Component<{}, State> { {Platform.OS === 'macos' ? (