@@ -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
@@ -576,23 +588,6 @@ class VirtualizedList extends React.PureComponent<Props, State> {
576
588
}
577
589
578
590
// [TODO(macOS GH#774)
579
- ensureItemAtIndexIsVisible ( rowIndex : number ) {
580
- const frame = this . _getFrameMetricsApprox ( rowIndex ) ;
581
- const visTop = this . _scrollMetrics . offset ;
582
- const visLen = this . _scrollMetrics . visibleLength ;
583
- const visEnd = visTop + visLen ;
584
- const contentLength = this . _scrollMetrics . contentLength ;
585
- const frameEnd = frame . offset + frame . length ;
586
-
587
- if ( frameEnd > visEnd ) {
588
- const newOffset = Math . min ( contentLength , visTop + ( frameEnd - visEnd ) ) ;
589
- this . scrollToOffset ( { offset : newOffset } ) ;
590
- } else if ( frame . offset < visTop ) {
591
- const newOffset = Math . min ( frame . offset , visTop - frame . length ) ;
592
- this . scrollToOffset ( { offset : newOffset } ) ;
593
- }
594
- }
595
-
596
591
selectRowAtIndex ( rowIndex : number ) {
597
592
this . _selectRowAtIndex ( rowIndex ) ;
598
593
}
@@ -1309,14 +1304,16 @@ class VirtualizedList extends React.PureComponent<Props, State> {
1309
1304
}
1310
1305
1311
1306
_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
- }
1307
+ // [TODO(macOS GH#774)
1318
1308
const preferredScrollerStyleDidChangeHandler =
1319
- this . props . onPreferredScrollerStyleDidChange ; // ]TODO(macOS GH#774)
1309
+ this . props . onPreferredScrollerStyleDidChange ;
1310
+
1311
+ const keyboardNavigationProps = {
1312
+ focusable : true ,
1313
+ validKeysDown : [ 'ArrowUp' , 'ArrowDown' , 'Home' , 'End' ] ,
1314
+ onKeyDown : this . _handleKeyDown ,
1315
+ } ;
1316
+ // ]TODO(macOS GH#774)
1320
1317
const onRefresh = props . onRefresh ;
1321
1318
if ( this . _isNestedWithSameOrientation ( ) ) {
1322
1319
// $FlowFixMe[prop-missing] - Typing ReactNativeComponent revealed errors
@@ -1333,8 +1330,7 @@ class VirtualizedList extends React.PureComponent<Props, State> {
1333
1330
< ScrollView
1334
1331
{ ...props }
1335
1332
// [TODO(macOS GH#774)
1336
- { ...( props . enableSelectionOnKeyPress && { focusable : true } ) }
1337
- onScrollKeyDown = { keyEventHandler }
1333
+ { ...( props . enableSelectionOnKeyPress && keyboardNavigationProps ) }
1338
1334
onPreferredScrollerStyleDidChange = {
1339
1335
preferredScrollerStyleDidChangeHandler
1340
1336
} // TODO(macOS GH#774)]
@@ -1356,8 +1352,8 @@ class VirtualizedList extends React.PureComponent<Props, State> {
1356
1352
// $FlowFixMe Invalid prop usage
1357
1353
< ScrollView
1358
1354
{ ...props }
1359
- { ... ( props . enableSelectionOnKeyPress && { focusable : true } ) } // [TODO(macOS GH#774)
1360
- onScrollKeyDown = { keyEventHandler }
1355
+ // [TODO(macOS GH#774)
1356
+ { ... ( props . enableSelectionOnKeyPress && keyboardNavigationProps ) }
1361
1357
onPreferredScrollerStyleDidChange = {
1362
1358
preferredScrollerStyleDidChangeHandler
1363
1359
} // TODO(macOS GH#774)]
@@ -1510,99 +1506,32 @@ class VirtualizedList extends React.PureComponent<Props, State> {
1510
1506
} ;
1511
1507
1512
1508
// [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
-
1521
- _selectRowAtIndex = rowIndex => {
1522
- this . setState ( state => {
1523
- return { selectedRowIndex : rowIndex } ;
1524
- } ) ;
1525
- return rowIndex ;
1526
- } ;
1509
+ _ensureItemAtIndexIsVisible ( rowIndex : number ) {
1510
+ const frame = this . _getFrameMetricsApprox ( rowIndex ) ;
1511
+ const visTop = this . _scrollMetrics . offset ;
1512
+ const visLen = this . _scrollMetrics . visibleLength ;
1513
+ const visEnd = visTop + visLen ;
1514
+ const contentLength = this . _scrollMetrics . contentLength ;
1515
+ const frameEnd = frame . offset + frame . length ;
1527
1516
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 ;
1517
+ if ( frameEnd > visEnd ) {
1518
+ const newOffset = Math . min ( contentLength , visTop + ( frameEnd - visEnd ) ) ;
1519
+ this . scrollToOffset ( { offset : newOffset } ) ;
1520
+ } else if ( frame . offset < visTop ) {
1521
+ const newOffset = Math . min ( frame . offset , visTop - frame . length ) ;
1522
+ this . scrollToOffset ( { offset : newOffset } ) ;
1539
1523
}
1540
- } ;
1524
+ }
1541
1525
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
- }
1526
+ _selectRowAtIndex = rowIndex => {
1527
+ const prevIndex = this . state . selectedRowIndex ;
1528
+ const newIndex = rowIndex ;
1556
1529
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
- } ;
1530
+ this . setState ( state => {
1531
+ return { selectedRowIndex : newIndex } ;
1532
+ } ) ;
1603
1533
1604
- _handleSelectionChange = ( prevIndex , newIndex ) => {
1605
- this . ensureItemAtIndexIsVisible ( newIndex ) ;
1534
+ this . _ensureItemAtIndexIsVisible ( newIndex ) ;
1606
1535
if ( prevIndex !== newIndex ) {
1607
1536
const item = this . props . getItem ( this . props . data , newIndex ) ;
1608
1537
if ( this . props . onSelectionChanged ) {
@@ -1613,6 +1542,62 @@ class VirtualizedList extends React.PureComponent<Props, State> {
1613
1542
} ) ;
1614
1543
}
1615
1544
}
1545
+
1546
+ return newIndex ;
1547
+ } ;
1548
+
1549
+ _selectRowAboveIndex = rowIndex => {
1550
+ const rowAbove = rowIndex > 0 ? rowIndex - 1 : rowIndex ;
1551
+ this . _selectRowAtIndex ( rowAbove ) ;
1552
+ } ;
1553
+
1554
+ _selectRowBelowIndex = rowIndex => {
1555
+ const rowBelow = rowIndex < this . state . last ? rowIndex + 1 : rowIndex ;
1556
+ this . _selectRowAtIndex ( rowBelow ) ;
1557
+ } ;
1558
+
1559
+ _handleKeyDown = ( event : KeyEvent ) => {
1560
+ if ( Platform . OS === 'macos' ) {
1561
+ this . props . onKeyDown ?. ( event ) ;
1562
+ if ( event . defaultPrevented ) {
1563
+ return ;
1564
+ }
1565
+
1566
+ const nativeEvent = event . nativeEvent ;
1567
+ const key = nativeEvent . key ;
1568
+
1569
+ let selectedIndex = - 1 ;
1570
+ if ( 'selectedRowIndex' in this . state ) {
1571
+ selectedIndex = this . state . selectedRowIndex ;
1572
+ }
1573
+
1574
+ if ( key === 'ArrowUp' ) {
1575
+ if ( nativeEvent . altKey ) {
1576
+ // Option+Up selects the first element
1577
+ this . _selectRowAtIndex ( 0 ) ;
1578
+ } else {
1579
+ this . _selectRowAboveIndex ( selectedIndex ) ;
1580
+ }
1581
+ } else if ( key === 'ArrowDown' ) {
1582
+ if ( nativeEvent . altKey ) {
1583
+ // Option+Down selects the last element
1584
+ this . _selectRowAtIndex ( this . state . last ) ;
1585
+ } else {
1586
+ this . _selectRowBelowIndex ( selectedIndex ) ;
1587
+ }
1588
+ } else if ( key === 'Enter' ) {
1589
+ if ( this . props . onSelectionEntered ) {
1590
+ const item = this . props . getItem ( this . props . data , selectedIndex ) ;
1591
+ if ( this . props . onSelectionEntered ) {
1592
+ this . props . onSelectionEntered ( item ) ;
1593
+ }
1594
+ }
1595
+ } else if ( key === 'Home' ) {
1596
+ this . scrollToOffset ( { animated : true , offset : 0 } ) ;
1597
+ } else if ( key === 'End' ) {
1598
+ this . scrollToEnd ( { animated : true } ) ;
1599
+ }
1600
+ }
1616
1601
} ;
1617
1602
// ]TODO(macOS GH#774)
1618
1603
0 commit comments