diff --git a/Libraries/Components/ScrollView/ScrollView.js b/Libraries/Components/ScrollView/ScrollView.js index 3b92b3ec4a05fc..784386fcbfa23d 100644 --- a/Libraries/Components/ScrollView/ScrollView.js +++ b/Libraries/Components/ScrollView/ScrollView.js @@ -1188,50 +1188,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) @@ -1787,7 +1743,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/RCTViewKeyboardEvent.m b/React/Views/RCTViewKeyboardEvent.m index 4d051b8cd9f319..3ec04a2e350eb3 100644 --- a/React/Views/RCTViewKeyboardEvent.m +++ b/React/Views/RCTViewKeyboardEvent.m @@ -52,7 +52,15 @@ + (NSString *)keyFromEvent:(NSEvent *)event 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; } diff --git a/React/Views/RCTViewManager.m b/React/Views/RCTViewManager.m index 395bb9b7eb9cee..adee6d64f63e93 100644 --- a/React/Views/RCTViewManager.m +++ b/React/Views/RCTViewManager.m @@ -495,18 +495,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/Podfile.lock b/packages/rn-tester/Podfile.lock index 41ebd33863c817..b9ab0305cd5657 100644 --- a/packages/rn-tester/Podfile.lock +++ b/packages/rn-tester/Podfile.lock @@ -568,8 +568,8 @@ SPEC CHECKSUMS: boost-for-react-native: 8f7c9ecfe357664c072ffbe2432569667cbf1f1b CocoaAsyncSocket: 065fd1e645c7abab64f7a6a2007a48038fdc6a99 DoubleConversion: ed15e075aa758ac0e4c1f8b830bd4e4d40d669e8 - FBLazyVector: 7b94bf4fe8fa8fc37e754af90ca67d25e48bc5f2 - FBReactNativeSpec: 609b6508be3aa0a4c04494bf45b8649ee04871fa + FBLazyVector: a1f4b28e8f4b8f16633d3d0c087c350582fde111 + FBReactNativeSpec: 38a0a40bdde2e82bff67545c7f3747cde3389fae Flipper: 26fc4b7382499f1281eb8cb921e5c3ad6de91fe0 Flipper-Boost-iOSX: fd1e2b8cbef7e662a122412d7ac5f5bea715403c Flipper-DoubleConversion: 57ffbe81ef95306cc9e69c4aa3aeeeeb58a6a28c @@ -580,44 +580,44 @@ SPEC CHECKSUMS: Flipper-RSocket: d9d9ade67cbecf6ac10730304bf5607266dd2541 FlipperKit: cbdee19bdd4e7f05472a66ce290f1b729ba3cb86 fmt: ff9d55029c625d3757ed641535fd4a75fedc7ce9 - glog: 20113a0d46931b6f096cf8302c68691d75a456ff + glog: 42c4bf47024808486e90b25ea9e5ac3959047641 libevent: 4049cae6c81cdb3654a443be001fb9bdceff7913 OCMock: 9491e4bec59e0b267d52a9184ff5605995e74be8 OpenSSL-Universal: ebc357f1e6bc71fa463ccb2fe676756aff50e88c - RCT-Folly: 24c6da766832002a4a2aac5f79ee0ca50fbe8507 - RCTRequired: 4706407fae87532509c680b65e330090a1299798 - RCTTypeSafety: 17ff1805826c902e4e91381952f4900a28a6fee7 - React: 3134ceca68fa8e90213c09a96bfcabe02bb12a1b - React-callinvoker: 8ece25302fd8701ee5d41d51fba4e1943ef0b4e0 + RCT-Folly: 43adc9ce880eb76792f88c011773cb5c664c1419 + RCTRequired: e10ca0d2175068628e108176a5a6a0fb9ec4d10a + RCTTypeSafety: 22fb005f9f0dd33e64b366070763d51cfd6aea97 + React: a9d66e76a86fd524cc70e3da091eb6364d15d448 + React-callinvoker: 5abe650b70d876a167904a5d8a867d2a52d5e9e1 React-Codegen: b3fbef96f960cb15fc61250078b0700cfd4cd8a1 - React-Core: e00c609b15999cd20fc1204d7cb8703a487f1791 - React-CoreModules: c398ee4410de61088e20fae5a62a75c9f315d0be - React-cxxreact: c8d2074107930610c962dfa89b99ed563b606c0e - React-jsi: 1afba7c112a54a0d20d809c29dfa51c92fa11ac7 - React-jsiexecutor: 802a25020d0fe67e4920f089bdb0d392467034f0 - React-jsinspector: 51e58c52aa1fe8a778816a26df3a6689e73f489e - React-logger: 6fbf43fafe8e25b53c195305088a1f8ffe8e6b4d - React-perflogger: 43b596dde60bf15cf8b4053008a2fa10d82926a1 - React-RCTActionSheet: de9dd1793c1b660ed55c8514900a5a87f79d1f90 - React-RCTAnimation: d11b97d2e9bc46fff3c0794b9379a9e16e566243 - React-RCTBlob: d6294e3a84b3c4e48be6c9b4663061103f34f31f - React-RCTImage: c789b0ff95fef0d4d69525acde2a247bb46d4ec1 - React-RCTLinking: 5ad63a2805334e36ea183b2b2aeb76a014c9ad83 - React-RCTNetwork: 4c531d5bd78ca34f845503f52cbd91341e6b3d2b - React-RCTPushNotification: 532147a2cbe281bda07579d5fc6610aff6e7ec79 - React-RCTSettings: b7191d404980767bf068351df0999ca220873983 - React-RCTTest: 99eb16b30306238a15bdc3e10edfd262f7169582 - React-RCTText: a377ec0145c1ce89b15cb9b598f850e3373cd5b1 - React-RCTVibration: 317549dff97581b507ee8fc01b1b1975e2e71945 - React-runtimeexecutor: fc502c0b0f1aa8ff30e013fdb4e8c47dd39aeea1 + React-Core: 569ae2fadd6e8e23fd3b8cc3942045e9aebcfbab + React-CoreModules: 04fe14f29ebd2860be836b597a11a77c98fa5a69 + React-cxxreact: 42baf7f4c02c6e78ace77bc9ec65ee31e242562b + React-jsi: 3e6d4ce9d83859fa7c3b8792473b8c2c7614c732 + React-jsiexecutor: 5e906d791d9a901323ec89756e0a4d559d85ef3a + React-jsinspector: 84fcb302ed400956c3b7219f22f1a58e20013973 + React-logger: 0d5abc0a9254b52dc47364035338df05c5ed3111 + React-perflogger: 89c189f867e8078292d8b6a38c912bee2bbd810b + React-RCTActionSheet: 50a4ba480c9bfc828ac9e60b6d47b55b631f0972 + React-RCTAnimation: 3b74aae2f8a2ad12c85aa4426216e581ebbd77dc + React-RCTBlob: e8853f74b4cb2ebdfc3fd805b1264babcaf5695a + React-RCTImage: e06f49c300ad78ac0cb045e8cb27c872968143f0 + React-RCTLinking: fa8203168234abd116f6356e94e4354572f11f45 + React-RCTNetwork: 9be8fb04c34dfda28596b8381ca926115d14c635 + React-RCTPushNotification: 7db0e5dbdbc30cbb363f59b47bf0bd13aaba9400 + React-RCTSettings: c07f636338d1b1615cf098a2b500c0a6d70d863b + React-RCTTest: 97eb70490841c11cf6e2fb0ac1e04dcd977e1783 + React-RCTText: 66e3fb3d4320d6cd9440b595b03896a86bf2e458 + React-RCTVibration: 62b69e7569bfc273fa84680bb041a4a5db0fb3bf + React-runtimeexecutor: a0db67f5641258c58c3aeb67cafe48e011308b48 React-TurboModuleCxx-RNW: f2e32cbfced49190a61d66c993a8975de79a158a - React-TurboModuleCxx-WinRTPort: 2cc7a4a1411069b755b937d9808cf35475f519da - ReactCommon: 3c7eb594c2363463eadb28689689246400f420a9 - ScreenshotManager: cfaf988fbf133b1f2dc9226d6f530e5a97ad46c1 + React-TurboModuleCxx-WinRTPort: 628aa2c8a14194ec9fe817f18719dead4004d56c + ReactCommon: 2c1ae88c7f3198abc56d3fbc7645bd76cc33cf49 + ScreenshotManager: 1704bd762dccfafbdb1efa1fc0dab28d4f53e0c2 SocketRocket: fccef3f9c5cedea1353a9ef6ada904fde10d6608 - Yoga: 0eb02dfb0fe67d34c2743c24f51ee8c2c91ebf3b + Yoga: 5637343932eb96b5fb627e055f8fe4c26c0375a9 YogaKit: f782866e155069a2cca2517aafea43200b01fd5a PODFILE CHECKSUM: 269190aa122418d1013ce622d5ca1fdbe3fdf45e -COCOAPODS: 1.11.2 +COCOAPODS: 1.11.3 diff --git a/packages/rn-tester/js/examples/FlatList/FlatList-basic.js b/packages/rn-tester/js/examples/FlatList/FlatList-basic.js index b50756462aa80d..970e2088688091 100644 --- a/packages/rn-tester/js/examples/FlatList/FlatList-basic.js +++ b/packages/rn-tester/js/examples/FlatList/FlatList-basic.js @@ -210,7 +210,10 @@ class FlatListExample extends React.PureComponent { }