Skip to content

Commit 33c559f

Browse files
committed
Deprecate onScrollKeyDown, refactor Flatlist selection logic (microsoft#1365)
* Deprecate onScrollKeyDown remove pressable diff Remove JS handling for PageUp/Down, fix flow errors Add back "autoscroll to focused view" behavior remove commented code remove change to pressable Update documentation fix flow error fix lint issue Fix 'selectRowAtIndex' More simplification lock * Make method public again * Add initialSelectedIndex * macOS tags * fix lint
1 parent 70ffa83 commit 33c559f

File tree

16 files changed

+194
-311
lines changed

16 files changed

+194
-311
lines changed

Libraries/Components/ScrollView/ScrollView.js

Lines changed: 2 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1179,50 +1179,6 @@ class ScrollView extends React.Component<Props, State> {
11791179
}
11801180

11811181
// [TODO(macOS GH#774)
1182-
_handleKeyDown = (event: ScrollEvent) => {
1183-
if (this.props.onScrollKeyDown) {
1184-
this.props.onScrollKeyDown(event);
1185-
} else {
1186-
if (Platform.OS === 'macos') {
1187-
const nativeEvent = event.nativeEvent;
1188-
const key = nativeEvent.key;
1189-
const kMinScrollOffset = 10;
1190-
if (key === 'PAGE_UP') {
1191-
this._handleScrollByKeyDown(event, {
1192-
x: nativeEvent.contentOffset.x,
1193-
y:
1194-
nativeEvent.contentOffset.y +
1195-
-nativeEvent.layoutMeasurement.height,
1196-
});
1197-
} else if (key === 'PAGE_DOWN') {
1198-
this._handleScrollByKeyDown(event, {
1199-
x: nativeEvent.contentOffset.x,
1200-
y:
1201-
nativeEvent.contentOffset.y +
1202-
nativeEvent.layoutMeasurement.height,
1203-
});
1204-
} else if (key === 'HOME') {
1205-
this.scrollTo({x: 0, y: 0});
1206-
} else if (key === 'END') {
1207-
this.scrollToEnd({animated: true});
1208-
}
1209-
}
1210-
}
1211-
};
1212-
1213-
_handleScrollByKeyDown = (event: ScrollEvent, newOffset) => {
1214-
const maxX =
1215-
event.nativeEvent.contentSize.width -
1216-
event.nativeEvent.layoutMeasurement.width;
1217-
const maxY =
1218-
event.nativeEvent.contentSize.height -
1219-
event.nativeEvent.layoutMeasurement.height;
1220-
this.scrollTo({
1221-
x: Math.max(0, Math.min(maxX, newOffset.x)),
1222-
y: Math.max(0, Math.min(maxY, newOffset.y)),
1223-
});
1224-
};
1225-
12261182
_handlePreferredScrollerStyleDidChange = (event: ScrollEvent) => {
12271183
this.setState({contentKey: this.state.contentKey + 1});
12281184
}; // ]TODO(macOS GH#774)
@@ -1776,8 +1732,8 @@ class ScrollView extends React.Component<Props, State> {
17761732
// bubble up from TextInputs
17771733
onContentSizeChange: null,
17781734
onScrollKeyDown: this._handleKeyDown, // TODO(macOS GH#774)
1779-
onPreferredScrollerStyleDidChange: this
1780-
._handlePreferredScrollerStyleDidChange, // TODO(macOS GH#774)
1735+
onPreferredScrollerStyleDidChange:
1736+
this._handlePreferredScrollerStyleDidChange, // TODO(macOS GH#774)
17811737
onLayout: this._handleLayout,
17821738
onMomentumScrollBegin: this._handleMomentumScrollBegin,
17831739
onMomentumScrollEnd: this._handleMomentumScrollEnd,

Libraries/Components/View/ViewPropTypes.js

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -76,12 +76,6 @@ type DirectEventProps = $ReadOnly<{|
7676
*/
7777
onPreferredScrollerStyleDidChange?: ?(event: ScrollEvent) => mixed, // TODO(macOS GH#774)
7878

79-
/**
80-
* When `acceptsKeyboardFocus` is true, the system will try to invoke this function
81-
* when the user performs accessibility key down gesture.
82-
*/
83-
onScrollKeyDown?: ?(event: ScrollEvent) => mixed, // TODO(macOS GH#774)
84-
8579
/**
8680
* Invoked on mount and layout changes with:
8781
*

Libraries/Lists/FlatList.js

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,24 @@ type OptionalProps<ItemT> = {|
6666
* Optional custom style for multi-item rows generated when numColumns > 1.
6767
*/
6868
columnWrapperStyle?: ViewStyleProp,
69+
// [TODO(macOS GH#774)
70+
/**
71+
* Allows you to 'select' a row using arrow keys. The selected row will have the prop `isSelected`
72+
* passed in as true to it's renderItem / ListItemComponent. You can also imperatively select a row
73+
* using the `selectRowAtIndex` method. You can set the initially selected row using the
74+
* `initialSelectedIndex` prop.
75+
* Keyboard Behavior:
76+
* - ArrowUp: Select row above current selected row
77+
* - ArrowDown: Select row below current selected row
78+
* - Option+ArrowUp: Select the first row
79+
* - Opton+ArrowDown: Select the last 'realized' row
80+
* - Home: Scroll to top of list
81+
* - End: Scroll to end of list
82+
*
83+
* @platform macos
84+
*/
85+
enableSelectionOnKeyPress?: ?boolean,
86+
// ]TODO(macOS GH#774)
6987
/**
7088
* A marker property for telling the list to re-render (since it implements `PureComponent`). If
7189
* any of your `renderItem`, Header, Footer, etc. functions depend on anything outside of the
@@ -111,6 +129,12 @@ type OptionalProps<ItemT> = {|
111129
* `getItemLayout` to be implemented.
112130
*/
113131
initialScrollIndex?: ?number,
132+
// [TODO(macOS GH#774)
133+
/**
134+
* The initially selected row, if `enableSelectionOnKeyPress` is set.
135+
*/
136+
initialSelectedIndex?: ?number,
137+
// ]TODO(macOS GH#774)
114138
/**
115139
* Reverses the direction of scroll. Uses scale transforms of -1.
116140
*/

Libraries/Lists/VirtualizedList.js

Lines changed: 95 additions & 107 deletions
Original file line numberDiff line numberDiff line change
@@ -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)
3939
import {
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

Comments
 (0)