Skip to content

Commit 8a4ade2

Browse files
authored
Deprecate onScrollKeyDown, refactor Flatlist selection logic (#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 a4ade0a commit 8a4ade2

File tree

18 files changed

+224
-343
lines changed

18 files changed

+224
-343
lines changed

Libraries/Components/ScrollView/ScrollView.js

Lines changed: 0 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1188,50 +1188,6 @@ class ScrollView extends React.Component<Props, State> {
11881188
}
11891189

11901190
// [TODO(macOS GH#774)
1191-
_handleKeyDown = (event: ScrollEvent) => {
1192-
if (this.props.onScrollKeyDown) {
1193-
this.props.onScrollKeyDown(event);
1194-
} else {
1195-
if (Platform.OS === 'macos') {
1196-
const nativeEvent = event.nativeEvent;
1197-
const key = nativeEvent.key;
1198-
const kMinScrollOffset = 10;
1199-
if (key === 'PAGE_UP') {
1200-
this._handleScrollByKeyDown(event, {
1201-
x: nativeEvent.contentOffset.x,
1202-
y:
1203-
nativeEvent.contentOffset.y +
1204-
-nativeEvent.layoutMeasurement.height,
1205-
});
1206-
} else if (key === 'PAGE_DOWN') {
1207-
this._handleScrollByKeyDown(event, {
1208-
x: nativeEvent.contentOffset.x,
1209-
y:
1210-
nativeEvent.contentOffset.y +
1211-
nativeEvent.layoutMeasurement.height,
1212-
});
1213-
} else if (key === 'HOME') {
1214-
this.scrollTo({x: 0, y: 0});
1215-
} else if (key === 'END') {
1216-
this.scrollToEnd({animated: true});
1217-
}
1218-
}
1219-
}
1220-
};
1221-
1222-
_handleScrollByKeyDown = (event: ScrollEvent, newOffset) => {
1223-
const maxX =
1224-
event.nativeEvent.contentSize.width -
1225-
event.nativeEvent.layoutMeasurement.width;
1226-
const maxY =
1227-
event.nativeEvent.contentSize.height -
1228-
event.nativeEvent.layoutMeasurement.height;
1229-
this.scrollTo({
1230-
x: Math.max(0, Math.min(maxX, newOffset.x)),
1231-
y: Math.max(0, Math.min(maxY, newOffset.y)),
1232-
});
1233-
};
1234-
12351191
_handlePreferredScrollerStyleDidChange = (event: ScrollEvent) => {
12361192
this.setState({contentKey: this.state.contentKey + 1});
12371193
}; // ]TODO(macOS GH#774)
@@ -1787,7 +1743,6 @@ class ScrollView extends React.Component<Props, State> {
17871743
// Override the onContentSizeChange from props, since this event can
17881744
// bubble up from TextInputs
17891745
onContentSizeChange: null,
1790-
onScrollKeyDown: this._handleKeyDown, // TODO(macOS GH#774)
17911746
onPreferredScrollerStyleDidChange:
17921747
this._handlePreferredScrollerStyleDidChange, // TODO(macOS GH#774)
17931748
onLayout: this._handleLayout,

Libraries/Components/ScrollView/__tests__/__snapshots__/ScrollView-test.js.snap

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@ exports[`<ScrollView /> should render as expected: should deep render when not m
2727
onScroll={[Function]}
2828
onScrollBeginDrag={[Function]}
2929
onScrollEndDrag={[Function]}
30-
onScrollKeyDown={[Function]}
3130
onScrollShouldSetResponder={[Function]}
3231
onStartShouldSetResponder={[Function]}
3332
onStartShouldSetResponderCapture={[Function]}

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 `focusable` 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: 94 additions & 106 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
*/
@@ -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

Comments
 (0)