@@ -35,7 +35,7 @@ import type {
3535 ViewToken ,
3636 ViewabilityConfigCallbackPair ,
3737} from './ViewabilityHelper' ;
38- import type { ScrollEvent } from '../Types/CoreEventTypes' ; // TODO(macOS GH#774)
38+ import type { KeyEvent } from '../Types/CoreEventTypes' ; // TODO(macOS GH#774)
3939import {
4040 VirtualizedListCellContextProvider ,
4141 VirtualizedListContext ,
@@ -109,12 +109,24 @@ type OptionalProps = {|
109109 * this for debugging purposes. Defaults to false.
110110 */
111111 disableVirtualization ?: ?boolean ,
112+ // [TODO(macOS GH#774)
112113 /**
113- * Handles key down events and updates selection based on the key event
114+ * Allows you to 'select' a row using arrow keys. The selected row will have the prop `isSelected`
115+ * passed in as true to it's renderItem / ListItemComponent. You can also imperatively select a row
116+ * using the `selectRowAtIndex` method. You can set the initially selected row using the
117+ * `initialSelectedIndex` prop.
118+ * Keyboard Behavior:
119+ * - ArrowUp: Select row above current selected row
120+ * - ArrowDown: Select row below current selected row
121+ * - Option+ArrowUp: Select the first row
122+ * - Opton+ArrowDown: Select the last 'realized' row
123+ * - Home: Scroll to top of list
124+ * - End: Scroll to end of list
114125 *
115126 * @platform macos
116127 */
117- enableSelectionOnKeyPress ?: ?boolean , // TODO(macOS GH#774)
128+ enableSelectionOnKeyPress ?: ?boolean ,
129+ // ]TODO(macOS GH#774)
118130 /**
119131 * A marker property for telling the list to re-render (since it implements `PureComponent`). If
120132 * any of your `renderItem`, Header, Footer, etc. functions depend on anything outside of the
@@ -145,6 +157,12 @@ type OptionalProps = {|
145157 * `getItemLayout` to be implemented.
146158 */
147159 initialScrollIndex ?: ?number ,
160+ // [TODO(macOS GH#774)
161+ /**
162+ * The initially selected row, if `enableSelectionOnKeyPress` is set.
163+ */
164+ initialSelectedIndex ?: ?number ,
165+ // ]TODO(macOS GH#774)
148166 /**
149167 * Reverses the direction of scroll. Uses scale transforms of -1.
150168 */
@@ -780,7 +798,7 @@ class VirtualizedList extends React.PureComponent<Props, State> {
780798 ( this . props . initialScrollIndex || 0 ) +
781799 initialNumToRenderOrDefault ( this . props . initialNumToRender ) ,
782800 ) - 1 ,
783- selectedRowIndex : 0 , // TODO(macOS GH#774)
801+ selectedRowIndex : this . props . initialSelectedIndex || - 1 , // TODO(macOS GH#774)
784802 } ;
785803
786804 if ( this . _isNestedWithSameOrientation ( ) ) {
@@ -843,7 +861,7 @@ class VirtualizedList extends React.PureComponent<Props, State> {
843861 ) ,
844862 last : Math . max ( 0 , Math . min ( prevState . last , getItemCount ( data ) - 1 ) ) ,
845863 selectedRowIndex : Math . max (
846- 0 ,
864+ - 1 , // Used to indicate no row is selected
847865 Math . min ( prevState . selectedRowIndex , getItemCount ( data ) ) ,
848866 ) , // TODO(macOS GH#774)
849867 } ;
@@ -1310,14 +1328,16 @@ class VirtualizedList extends React.PureComponent<Props, State> {
13101328 }
13111329
13121330 _defaultRenderScrollComponent = props => {
1313- let keyEventHandler = this . props . onScrollKeyDown ; // [TODO(macOS GH#774)
1314- if ( ! keyEventHandler ) {
1315- keyEventHandler = this . props . enableSelectionOnKeyPress
1316- ? this . _handleKeyDown
1317- : null ;
1318- }
1319- const preferredScrollerStyleDidChangeHandler = this . props
1320- . onPreferredScrollerStyleDidChange ; // ]TODO(macOS GH#774)
1331+ // [TODO(macOS GH#774)
1332+ const preferredScrollerStyleDidChangeHandler =
1333+ this . props . onPreferredScrollerStyleDidChange ;
1334+
1335+ const keyboardNavigationProps = {
1336+ focusable : true ,
1337+ validKeysDown : [ 'ArrowUp' , 'ArrowDown' , 'Home' , 'End' ] ,
1338+ onKeyDown : this . _handleKeyDown ,
1339+ } ;
1340+ // ]TODO(macOS GH#774)
13211341 const onRefresh = props . onRefresh ;
13221342 if ( this . _isNestedWithSameOrientation ( ) ) {
13231343 // $FlowFixMe[prop-missing] - Typing ReactNativeComponent revealed errors
@@ -1334,8 +1354,7 @@ class VirtualizedList extends React.PureComponent<Props, State> {
13341354 < ScrollView
13351355 { ...props }
13361356 // [TODO(macOS GH#774)
1337- { ...( props . enableSelectionOnKeyPress && { focusable : true } ) }
1338- onScrollKeyDown = { keyEventHandler }
1357+ { ...( props . enableSelectionOnKeyPress && keyboardNavigationProps ) }
13391358 onPreferredScrollerStyleDidChange = {
13401359 preferredScrollerStyleDidChangeHandler
13411360 } // TODO(macOS GH#774)]
@@ -1357,8 +1376,8 @@ class VirtualizedList extends React.PureComponent<Props, State> {
13571376 // $FlowFixMe Invalid prop usage
13581377 < ScrollView
13591378 { ...props }
1360- { ... ( props . enableSelectionOnKeyPress && { focusable : true } ) } // [TODO(macOS GH#774)
1361- onScrollKeyDown = { keyEventHandler }
1379+ // [TODO(macOS GH#774)
1380+ { ... ( props . enableSelectionOnKeyPress && keyboardNavigationProps ) }
13621381 onPreferredScrollerStyleDidChange = {
13631382 preferredScrollerStyleDidChangeHandler
13641383 } // TODO(macOS GH#774)]
@@ -1511,98 +1530,11 @@ class VirtualizedList extends React.PureComponent<Props, State> {
15111530 } ;
15121531
15131532 // [TODO(macOS GH#774)
1514- _selectRowAboveIndex = rowIndex => {
1515- const rowAbove = rowIndex > 0 ? rowIndex - 1 : rowIndex ;
1516- this . setState ( state => {
1517- return { selectedRowIndex : rowAbove } ;
1518- } ) ;
1519- return rowAbove ;
1520- } ;
1521-
15221533 _selectRowAtIndex = rowIndex => {
1523- this . setState ( state => {
1524- return { selectedRowIndex : rowIndex } ;
1525- } ) ;
1526- return rowIndex ;
1527- } ;
1534+ const prevIndex = this . state . selectedRowIndex ;
1535+ const newIndex = rowIndex ;
1536+ this . setState ( { selectedRowIndex : newIndex } ) ;
15281537
1529- _selectRowBelowIndex = rowIndex => {
1530- if ( this . props . getItemCount ) {
1531- const { data} = this . props ;
1532- const itemCount = this . props . getItemCount ( data ) ;
1533- const rowBelow = rowIndex < itemCount - 1 ? rowIndex + 1 : rowIndex ;
1534- this . setState ( state => {
1535- return { selectedRowIndex : rowBelow } ;
1536- } ) ;
1537- return rowBelow ;
1538- } else {
1539- return rowIndex ;
1540- }
1541- } ;
1542-
1543- _handleKeyDown = ( event : ScrollEvent ) => {
1544- if ( this . props . onScrollKeyDown ) {
1545- this . props . onScrollKeyDown ( event ) ;
1546- } else {
1547- if ( Platform . OS === 'macos' ) {
1548- // $FlowFixMe Cannot get e.nativeEvent because property nativeEvent is missing in Event
1549- const nativeEvent = event . nativeEvent ;
1550- const key = nativeEvent . key ;
1551-
1552- let prevIndex = - 1 ;
1553- let newIndex = - 1 ;
1554- if ( 'selectedRowIndex' in this . state ) {
1555- prevIndex = this . state . selectedRowIndex ;
1556- }
1557-
1558- // const {data, getItem} = this.props;
1559- if ( key === 'UP_ARROW' ) {
1560- newIndex = this . _selectRowAboveIndex ( prevIndex ) ;
1561- this . _handleSelectionChange ( prevIndex , newIndex ) ;
1562- } else if ( key = = = 'DOWN_ARROW' ) {
1563- newIndex = this . _selectRowBelowIndex ( prevIndex ) ;
1564- this . _handleSelectionChange ( prevIndex , newIndex ) ;
1565- } else if ( key = = = 'ENTER' ) {
1566- if ( this . props . onSelectionEntered ) {
1567- const item = this . props . getItem ( this . props . data , prevIndex ) ;
1568- if ( this . props . onSelectionEntered ) {
1569- this . props . onSelectionEntered ( item ) ;
1570- }
1571- }
1572- } else if ( key = = = 'OPTION_UP' ) {
1573- newIndex = this . _selectRowAtIndex ( 0 ) ;
1574- this . _handleSelectionChange ( prevIndex , newIndex ) ;
1575- } else if ( key = = = 'OPTION_DOWN' ) {
1576- newIndex = this . _selectRowAtIndex ( this . state . last ) ;
1577- this . _handleSelectionChange ( prevIndex , newIndex ) ;
1578- } else if ( key = = = 'PAGE_UP' ) {
1579- const maxY =
1580- event . nativeEvent . contentSize . height -
1581- event . nativeEvent . layoutMeasurement . height ;
1582- const newOffset = Math . min (
1583- maxY ,
1584- nativeEvent . contentOffset . y + - nativeEvent . layoutMeasurement . height ,
1585- ) ;
1586- this . scrollToOffset ( { animated : true , offset : newOffset } ) ;
1587- } else if ( key = = = 'PAGE_DOWN' ) {
1588- const maxY =
1589- event . nativeEvent . contentSize . height -
1590- event . nativeEvent . layoutMeasurement . height ;
1591- const newOffset = Math . min (
1592- maxY ,
1593- nativeEvent . contentOffset . y + nativeEvent . layoutMeasurement . height ,
1594- ) ;
1595- this . scrollToOffset ( { animated : true , offset : newOffset } ) ;
1596- } else if ( key = = = 'HOME' ) {
1597- this . scrollToOffset ( { animated : true , offset : 0 } ) ;
1598- } else if ( key = = = 'END' ) {
1599- this . scrollToEnd ( { animated : true } ) ;
1600- }
1601- }
1602- }
1603- } ;
1604-
1605- _handleSelectionChange = ( prevIndex , newIndex ) => {
16061538 this . ensureItemAtIndexIsVisible ( newIndex ) ;
16071539 if ( prevIndex !== newIndex ) {
16081540 const item = this . props . getItem ( this . props . data , newIndex ) ;
@@ -1614,6 +1546,62 @@ class VirtualizedList extends React.PureComponent<Props, State> {
16141546 } ) ;
16151547 }
16161548 }
1549+
1550+ return newIndex ;
1551+ } ;
1552+
1553+ _selectRowAboveIndex = rowIndex => {
1554+ const rowAbove = rowIndex > 0 ? rowIndex - 1 : rowIndex ;
1555+ this . _selectRowAtIndex ( rowAbove ) ;
1556+ } ;
1557+
1558+ _selectRowBelowIndex = rowIndex => {
1559+ const rowBelow = rowIndex < this . state . last ? rowIndex + 1 : rowIndex ;
1560+ this . _selectRowAtIndex ( rowBelow ) ;
1561+ } ;
1562+
1563+ _handleKeyDown = ( event : KeyEvent ) => {
1564+ if ( Platform . OS === 'macos' ) {
1565+ this . props . onKeyDown ?. ( event ) ;
1566+ if ( event . defaultPrevented ) {
1567+ return ;
1568+ }
1569+
1570+ const nativeEvent = event . nativeEvent ;
1571+ const key = nativeEvent . key ;
1572+
1573+ let selectedIndex = - 1 ;
1574+ if ( this . state . selectedRowIndex > = 0) {
1575+ selectedIndex = this . state . selectedRowIndex ;
1576+ }
1577+
1578+ if (key === 'ArrowUp') {
1579+ if ( nativeEvent . altKey ) {
1580+ // Option+Up selects the first element
1581+ this . _selectRowAtIndex ( 0 ) ;
1582+ } else {
1583+ this . _selectRowAboveIndex ( selectedIndex ) ;
1584+ }
1585+ } else if ( key === 'ArrowDown ') {
1586+ if ( nativeEvent . altKey ) {
1587+ // Option+Down selects the last element
1588+ this . _selectRowAtIndex ( this . state . last ) ;
1589+ } else {
1590+ this . _selectRowBelowIndex ( selectedIndex ) ;
1591+ }
1592+ } else if ( key === 'Enter ') {
1593+ if ( this . props . onSelectionEntered ) {
1594+ const item = this . props . getItem ( this . props . data , selectedIndex ) ;
1595+ if ( this . props . onSelectionEntered ) {
1596+ this . props . onSelectionEntered ( item ) ;
1597+ }
1598+ }
1599+ } else if ( key === 'Home ') {
1600+ this . scrollToOffset ( { animated : true , offset : 0 } ) ;
1601+ } else if (key === 'End') {
1602+ this . scrollToEnd ( { animated : true } ) ;
1603+ }
1604+ }
16171605 } ;
16181606 // ]TODO(macOS GH#774)
16191607
0 commit comments