@@ -35,7 +35,7 @@ import type {
35
35
ViewToken ,
36
36
ViewabilityConfigCallbackPair ,
37
37
} from './ViewabilityHelper' ;
38
- import type { ScrollEvent } from '../Types/CoreEventTypes' ; // TODO(macOS GH#774)
38
+ import type { KeyEvent } from '../Types/CoreEventTypes' ; // TODO(macOS GH#774)
39
39
import {
40
40
VirtualizedListCellContextProvider ,
41
41
VirtualizedListContext ,
@@ -109,12 +109,24 @@ type OptionalProps = {|
109
109
* this for debugging purposes. Defaults to false.
110
110
*/
111
111
disableVirtualization ?: ?boolean ,
112
+ // [TODO(macOS GH#774)
112
113
/**
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
114
125
*
115
126
* @platform macos
116
127
*/
117
- enableSelectionOnKeyPress ?: ?boolean , // TODO(macOS GH#774)
128
+ enableSelectionOnKeyPress ?: ?boolean ,
129
+ // ]TODO(macOS GH#774)
118
130
/**
119
131
* A marker property for telling the list to re-render (since it implements `PureComponent`). If
120
132
* any of your `renderItem`, Header, Footer, etc. functions depend on anything outside of the
@@ -145,6 +157,12 @@ type OptionalProps = {|
145
157
* `getItemLayout` to be implemented.
146
158
*/
147
159
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)
148
166
/**
149
167
* Reverses the direction of scroll. Uses scale transforms of -1.
150
168
*/
@@ -780,7 +798,7 @@ class VirtualizedList extends React.PureComponent<Props, State> {
780
798
( this . props . initialScrollIndex || 0 ) +
781
799
initialNumToRenderOrDefault ( this . props . initialNumToRender ) ,
782
800
) - 1 ,
783
- selectedRowIndex : 0 , // TODO(macOS GH#774)
801
+ selectedRowIndex : this . props . initialSelectedIndex || - 1 , // TODO(macOS GH#774)
784
802
} ;
785
803
786
804
if ( this . _isNestedWithSameOrientation ( ) ) {
@@ -843,7 +861,7 @@ class VirtualizedList extends React.PureComponent<Props, State> {
843
861
) ,
844
862
last : Math . max ( 0 , Math . min ( prevState . last , getItemCount ( data ) - 1 ) ) ,
845
863
selectedRowIndex : Math . max (
846
- 0 ,
864
+ - 1 , // Used to indicate no row is selected
847
865
Math . min ( prevState . selectedRowIndex , getItemCount ( data ) ) ,
848
866
) , // TODO(macOS GH#774)
849
867
} ;
@@ -1310,14 +1328,16 @@ class VirtualizedList extends React.PureComponent<Props, State> {
1310
1328
}
1311
1329
1312
1330
_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)
1321
1341
const onRefresh = props . onRefresh ;
1322
1342
if ( this . _isNestedWithSameOrientation ( ) ) {
1323
1343
// $FlowFixMe[prop-missing] - Typing ReactNativeComponent revealed errors
@@ -1334,8 +1354,7 @@ class VirtualizedList extends React.PureComponent<Props, State> {
1334
1354
< ScrollView
1335
1355
{ ...props }
1336
1356
// [TODO(macOS GH#774)
1337
- { ...( props . enableSelectionOnKeyPress && { focusable : true } ) }
1338
- onScrollKeyDown = { keyEventHandler }
1357
+ { ...( props . enableSelectionOnKeyPress && keyboardNavigationProps ) }
1339
1358
onPreferredScrollerStyleDidChange = {
1340
1359
preferredScrollerStyleDidChangeHandler
1341
1360
} // TODO(macOS GH#774)]
@@ -1357,8 +1376,8 @@ class VirtualizedList extends React.PureComponent<Props, State> {
1357
1376
// $FlowFixMe Invalid prop usage
1358
1377
< ScrollView
1359
1378
{ ...props }
1360
- { ... ( props . enableSelectionOnKeyPress && { focusable : true } ) } // [TODO(macOS GH#774)
1361
- onScrollKeyDown = { keyEventHandler }
1379
+ // [TODO(macOS GH#774)
1380
+ { ... ( props . enableSelectionOnKeyPress && keyboardNavigationProps ) }
1362
1381
onPreferredScrollerStyleDidChange = {
1363
1382
preferredScrollerStyleDidChangeHandler
1364
1383
} // TODO(macOS GH#774)]
@@ -1511,98 +1530,11 @@ class VirtualizedList extends React.PureComponent<Props, State> {
1511
1530
} ;
1512
1531
1513
1532
// [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
-
1522
1533
_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 } ) ;
1528
1537
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 ) => {
1606
1538
this . ensureItemAtIndexIsVisible ( newIndex ) ;
1607
1539
if ( prevIndex !== newIndex ) {
1608
1540
const item = this . props . getItem ( this . props . data , newIndex ) ;
@@ -1614,6 +1546,62 @@ class VirtualizedList extends React.PureComponent<Props, State> {
1614
1546
} ) ;
1615
1547
}
1616
1548
}
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
+ }
1617
1605
} ;
1618
1606
// ]TODO(macOS GH#774)
1619
1607
0 commit comments