@@ -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 */
@@ -782,7 +800,7 @@ class VirtualizedList extends React.PureComponent<Props, State> {
782800 ( this . props . initialScrollIndex || 0 ) +
783801 initialNumToRenderOrDefault ( this . props . initialNumToRender ) ,
784802 ) - 1 ,
785- selectedRowIndex : 0 , // TODO(macOS GH#774)
803+ selectedRowIndex : this . props . initialSelectedIndex || - 1 , // TODO(macOS GH#774)
786804 } ;
787805
788806 if ( this . _isNestedWithSameOrientation ( ) ) {
@@ -845,7 +863,7 @@ class VirtualizedList extends React.PureComponent<Props, State> {
845863 ) ,
846864 last : Math . max ( 0 , Math . min ( prevState . last , getItemCount ( data ) - 1 ) ) ,
847865 selectedRowIndex : Math . max (
848- 0 ,
866+ - 1 , // Used to indicate no row is selected
849867 Math . min ( prevState . selectedRowIndex , getItemCount ( data ) ) ,
850868 ) , // TODO(macOS GH#774)
851869 } ;
@@ -1309,14 +1327,16 @@ class VirtualizedList extends React.PureComponent<Props, State> {
13091327 }
13101328
13111329 _defaultRenderScrollComponent = props => {
1312- let keyEventHandler = this . props . onScrollKeyDown ; // [TODO(macOS GH#774)
1313- if ( ! keyEventHandler ) {
1314- keyEventHandler = this . props . enableSelectionOnKeyPress
1315- ? this . _handleKeyDown
1316- : null ;
1317- }
1330+ // [TODO(macOS GH#774)
13181331 const preferredScrollerStyleDidChangeHandler =
1319- this . props . onPreferredScrollerStyleDidChange ; // ]TODO(macOS GH#774)
1332+ this . props . onPreferredScrollerStyleDidChange ;
1333+
1334+ const keyboardNavigationProps = {
1335+ focusable : true ,
1336+ validKeysDown : [ 'ArrowUp' , 'ArrowDown' , 'Home' , 'End' ] ,
1337+ onKeyDown : this . _handleKeyDown ,
1338+ } ;
1339+ // ]TODO(macOS GH#774)
13201340 const onRefresh = props . onRefresh ;
13211341 if ( this . _isNestedWithSameOrientation ( ) ) {
13221342 // $FlowFixMe[prop-missing] - Typing ReactNativeComponent revealed errors
@@ -1333,8 +1353,7 @@ class VirtualizedList extends React.PureComponent<Props, State> {
13331353 < ScrollView
13341354 { ...props }
13351355 // [TODO(macOS GH#774)
1336- { ...( props . enableSelectionOnKeyPress && { focusable : true } ) }
1337- onScrollKeyDown = { keyEventHandler }
1356+ { ...( props . enableSelectionOnKeyPress && keyboardNavigationProps ) }
13381357 onPreferredScrollerStyleDidChange = {
13391358 preferredScrollerStyleDidChangeHandler
13401359 } // TODO(macOS GH#774)]
@@ -1356,8 +1375,8 @@ class VirtualizedList extends React.PureComponent<Props, State> {
13561375 // $FlowFixMe Invalid prop usage
13571376 < ScrollView
13581377 { ...props }
1359- { ... ( props . enableSelectionOnKeyPress && { focusable : true } ) } // [TODO(macOS GH#774)
1360- onScrollKeyDown = { keyEventHandler }
1378+ // [TODO(macOS GH#774)
1379+ { ... ( props . enableSelectionOnKeyPress && keyboardNavigationProps ) }
13611380 onPreferredScrollerStyleDidChange = {
13621381 preferredScrollerStyleDidChangeHandler
13631382 } // TODO(macOS GH#774)]
@@ -1510,98 +1529,11 @@ class VirtualizedList extends React.PureComponent<Props, State> {
15101529 } ;
15111530
15121531 // [TODO(macOS GH#774)
1513- _selectRowAboveIndex = rowIndex => {
1514- const rowAbove = rowIndex > 0 ? rowIndex - 1 : rowIndex ;
1515- this . setState ( state => {
1516- return { selectedRowIndex : rowAbove } ;
1517- } ) ;
1518- return rowAbove ;
1519- } ;
1520-
15211532 _selectRowAtIndex = rowIndex => {
1522- this . setState ( state => {
1523- return { selectedRowIndex : rowIndex } ;
1524- } ) ;
1525- return rowIndex ;
1526- } ;
1527-
1528- _selectRowBelowIndex = rowIndex => {
1529- if ( this . props . getItemCount ) {
1530- const { data} = this . props ;
1531- const itemCount = this . props . getItemCount ( data ) ;
1532- const rowBelow = rowIndex < itemCount - 1 ? rowIndex + 1 : rowIndex ;
1533- this . setState ( state => {
1534- return { selectedRowIndex : rowBelow } ;
1535- } ) ;
1536- return rowBelow ;
1537- } else {
1538- return rowIndex ;
1539- }
1540- } ;
1541-
1542- _handleKeyDown = ( event : ScrollEvent ) => {
1543- if ( this . props . onScrollKeyDown ) {
1544- this . props . onScrollKeyDown ( event ) ;
1545- } else {
1546- if ( Platform . OS === 'macos' ) {
1547- // $FlowFixMe Cannot get e.nativeEvent because property nativeEvent is missing in Event
1548- const nativeEvent = event . nativeEvent ;
1549- const key = nativeEvent . key ;
1550-
1551- let prevIndex = - 1 ;
1552- let newIndex = - 1 ;
1553- if ( 'selectedRowIndex' in this . state ) {
1554- prevIndex = this . state . selectedRowIndex ;
1555- }
1556-
1557- // const {data, getItem} = this.props;
1558- if ( key === 'UP_ARROW' ) {
1559- newIndex = this . _selectRowAboveIndex ( prevIndex ) ;
1560- this . _handleSelectionChange ( prevIndex , newIndex ) ;
1561- } else if ( key = = = 'DOWN_ARROW' ) {
1562- newIndex = this . _selectRowBelowIndex ( prevIndex ) ;
1563- this . _handleSelectionChange ( prevIndex , newIndex ) ;
1564- } else if ( key = = = 'ENTER' ) {
1565- if ( this . props . onSelectionEntered ) {
1566- const item = this . props . getItem ( this . props . data , prevIndex ) ;
1567- if ( this . props . onSelectionEntered ) {
1568- this . props . onSelectionEntered ( item ) ;
1569- }
1570- }
1571- } else if ( key = = = 'OPTION_UP' ) {
1572- newIndex = this . _selectRowAtIndex ( 0 ) ;
1573- this . _handleSelectionChange ( prevIndex , newIndex ) ;
1574- } else if ( key = = = 'OPTION_DOWN' ) {
1575- newIndex = this . _selectRowAtIndex ( this . state . last ) ;
1576- this . _handleSelectionChange ( prevIndex , newIndex ) ;
1577- } else if ( key = = = 'PAGE_UP' ) {
1578- const maxY =
1579- event . nativeEvent . contentSize . height -
1580- event . nativeEvent . layoutMeasurement . height ;
1581- const newOffset = Math . min (
1582- maxY ,
1583- nativeEvent . contentOffset . y + - nativeEvent . layoutMeasurement . height ,
1584- ) ;
1585- this . scrollToOffset ( { animated : true , offset : newOffset } ) ;
1586- } else if ( key = = = 'PAGE_DOWN' ) {
1587- const maxY =
1588- event . nativeEvent . contentSize . height -
1589- event . nativeEvent . layoutMeasurement . height ;
1590- const newOffset = Math . min (
1591- maxY ,
1592- nativeEvent . contentOffset . y + nativeEvent . layoutMeasurement . height ,
1593- ) ;
1594- this . scrollToOffset ( { animated : true , offset : newOffset } ) ;
1595- } else if ( key = = = 'HOME' ) {
1596- this . scrollToOffset ( { animated : true , offset : 0 } ) ;
1597- } else if ( key = = = 'END' ) {
1598- this . scrollToEnd ( { animated : true } ) ;
1599- }
1600- }
1601- }
1602- } ;
1533+ const prevIndex = this . state . selectedRowIndex ;
1534+ const newIndex = rowIndex ;
1535+ this . setState ( { selectedRowIndex : newIndex } ) ;
16031536
1604- _handleSelectionChange = ( prevIndex , newIndex ) => {
16051537 this . ensureItemAtIndexIsVisible ( newIndex ) ;
16061538 if ( prevIndex !== newIndex ) {
16071539 const item = this . props . getItem ( this . props . data , newIndex ) ;
@@ -1613,6 +1545,62 @@ class VirtualizedList extends React.PureComponent<Props, State> {
16131545 } ) ;
16141546 }
16151547 }
1548+
1549+ return newIndex ;
1550+ } ;
1551+
1552+ _selectRowAboveIndex = rowIndex => {
1553+ const rowAbove = rowIndex > 0 ? rowIndex - 1 : rowIndex ;
1554+ this . _selectRowAtIndex ( rowAbove ) ;
1555+ } ;
1556+
1557+ _selectRowBelowIndex = rowIndex => {
1558+ const rowBelow = rowIndex < this . state . last ? rowIndex + 1 : rowIndex ;
1559+ this . _selectRowAtIndex ( rowBelow ) ;
1560+ } ;
1561+
1562+ _handleKeyDown = ( event : KeyEvent ) => {
1563+ if ( Platform . OS === 'macos' ) {
1564+ this . props . onKeyDown ?. ( event ) ;
1565+ if ( event . defaultPrevented ) {
1566+ return ;
1567+ }
1568+
1569+ const nativeEvent = event . nativeEvent ;
1570+ const key = nativeEvent . key ;
1571+
1572+ let selectedIndex = - 1 ;
1573+ if ( this . state . selectedRowIndex > = 0) {
1574+ selectedIndex = this . state . selectedRowIndex ;
1575+ }
1576+
1577+ if (key === 'ArrowUp') {
1578+ if ( nativeEvent . altKey ) {
1579+ // Option+Up selects the first element
1580+ this . _selectRowAtIndex ( 0 ) ;
1581+ } else {
1582+ this . _selectRowAboveIndex ( selectedIndex ) ;
1583+ }
1584+ } else if ( key === 'ArrowDown ') {
1585+ if ( nativeEvent . altKey ) {
1586+ // Option+Down selects the last element
1587+ this . _selectRowAtIndex ( this . state . last ) ;
1588+ } else {
1589+ this . _selectRowBelowIndex ( selectedIndex ) ;
1590+ }
1591+ } else if ( key === 'Enter ') {
1592+ if ( this . props . onSelectionEntered ) {
1593+ const item = this . props . getItem ( this . props . data , selectedIndex ) ;
1594+ if ( this . props . onSelectionEntered ) {
1595+ this . props . onSelectionEntered ( item ) ;
1596+ }
1597+ }
1598+ } else if ( key === 'Home ') {
1599+ this . scrollToOffset ( { animated : true , offset : 0 } ) ;
1600+ } else if (key === 'End') {
1601+ this . scrollToEnd ( { animated : true } ) ;
1602+ }
1603+ }
16161604 } ;
16171605 // ]TODO(macOS GH#774)
16181606
0 commit comments