Skip to content

Commit 3942b6f

Browse files
Saadnajmipull[bot]ryanlntnntremganandraj
authored
[0.68] Keyboard navigation in Flatlist (#1258) (#1262)
* Keyboard navigation in Flatlist (#1258) * add pull yml * match handleOpenURLNotification event payload with iOS (#755) (#2) Co-authored-by: Ryan Linton <ryanlntn@gmail.com> * [pull] master from microsoft:master (#11) * Deprecated api (#853) * Remove deprecated/unused context param * Update a few Mac deprecated APIs * Packing RN dependencies, hermes and ignoring javadoc failure, (#852) * Ignore javadoc failure * Bringing few more changes from 0.63-stable * Fixing a patch in engine selection * Fixing a patch in nuget spec * Fixing the output directory of nuget pack * Packaging dependencies in the nuget * Fix onMouseEnter/onMouseLeave callbacks not firing on Pressable (#855) * add pull yml * match handleOpenURLNotification event payload with iOS (#755) (#2) Co-authored-by: Ryan Linton <ryanlntn@gmail.com> * fix mouse evetns on pressable * delete extra yml from this branch * Add macOS tags * reorder props to have onMouseEnter/onMouseLeave always be before onPress Co-authored-by: pull[bot] <39814207+pull[bot]@users.noreply.github.com> Co-authored-by: Ryan Linton <ryanlntn@gmail.com> * Grammar fixes. (#856) Updates simple grammar issues. Co-authored-by: Nick Trescases <42704557+ntre@users.noreply.github.com> Co-authored-by: Anandraj <anandrag@microsoft.com> Co-authored-by: Saad Najmi <saadnajmi2@gmail.com> Co-authored-by: pull[bot] <39814207+pull[bot]@users.noreply.github.com> Co-authored-by: Ryan Linton <ryanlntn@gmail.com> Co-authored-by: Muhammad Hamza Zaman <mh.zaman.4069@gmail.com> * wip * wip * more wip * Home/End/OptionUp/OptionDown work * ensureItemAtIndexIsVisible works * Home/End work * Initial cleanup for PR * More cleanup * More cleanup * Make it a real prop * No need for client code * Don't move keyboard focus with selection * Update tags * Fix flow errors * Update colors, make ScrollView focusable * prettier * undo change * Fix flow errors * Clean up code + handle page up/down with new prop Co-authored-by: pull[bot] <39814207+pull[bot]@users.noreply.github.com> Co-authored-by: Ryan Linton <ryanlntn@gmail.com> Co-authored-by: Nick Trescases <42704557+ntre@users.noreply.github.com> Co-authored-by: Anandraj <anandrag@microsoft.com> Co-authored-by: Muhammad Hamza Zaman <mh.zaman.4069@gmail.com> * yarn lint --fix Co-authored-by: pull[bot] <39814207+pull[bot]@users.noreply.github.com> Co-authored-by: Ryan Linton <ryanlntn@gmail.com> Co-authored-by: Nick Trescases <42704557+ntre@users.noreply.github.com> Co-authored-by: Anandraj <anandrag@microsoft.com> Co-authored-by: Muhammad Hamza Zaman <mh.zaman.4069@gmail.com>
1 parent 577248c commit 3942b6f

File tree

6 files changed

+166
-111
lines changed

6 files changed

+166
-111
lines changed

Libraries/Components/ScrollView/ScrollView.js

Lines changed: 4 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1206,42 +1206,10 @@ class ScrollView extends React.Component<Props, State> {
12061206
nativeEvent.contentOffset.y +
12071207
nativeEvent.layoutMeasurement.height,
12081208
});
1209-
} else if (key === 'LEFT_ARROW') {
1210-
this._handleScrollByKeyDown(event, {
1211-
x:
1212-
nativeEvent.contentOffset.x +
1213-
-(this.props.horizontalLineScroll !== undefined
1214-
? this.props.horizontalLineScroll
1215-
: kMinScrollOffset),
1216-
y: nativeEvent.contentOffset.y,
1217-
});
1218-
} else if (key === 'RIGHT_ARROW') {
1219-
this._handleScrollByKeyDown(event, {
1220-
x:
1221-
nativeEvent.contentOffset.x +
1222-
(this.props.horizontalLineScroll !== undefined
1223-
? this.props.horizontalLineScroll
1224-
: kMinScrollOffset),
1225-
y: nativeEvent.contentOffset.y,
1226-
});
1227-
} else if (key === 'DOWN_ARROW') {
1228-
this._handleScrollByKeyDown(event, {
1229-
x: nativeEvent.contentOffset.x,
1230-
y:
1231-
nativeEvent.contentOffset.y +
1232-
(this.props.verticalLineScroll !== undefined
1233-
? this.props.verticalLineScroll
1234-
: kMinScrollOffset),
1235-
});
1236-
} else if (key === 'UP_ARROW') {
1237-
this._handleScrollByKeyDown(event, {
1238-
x: nativeEvent.contentOffset.x,
1239-
y:
1240-
nativeEvent.contentOffset.y +
1241-
-(this.props.verticalLineScroll !== undefined
1242-
? this.props.verticalLineScroll
1243-
: kMinScrollOffset),
1244-
});
1209+
} else if (key === 'HOME') {
1210+
this.scrollTo({x: 0, y: 0});
1211+
} else if (key === 'END') {
1212+
this.scrollToEnd({animated: true});
12451213
}
12461214
}
12471215
}

Libraries/Lists/VirtualizedList.js

Lines changed: 78 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -588,7 +588,7 @@ class VirtualizedList extends React.PureComponent<Props, State> {
588588
const newOffset = Math.min(contentLength, visTop + (frameEnd - visEnd));
589589
this.scrollToOffset({offset: newOffset});
590590
} else if (frame.offset < visTop) {
591-
const newOffset = Math.max(0, visTop - frame.length);
591+
const newOffset = Math.min(frame.offset, visTop - frame.length);
592592
this.scrollToOffset({offset: newOffset});
593593
}
594594
}
@@ -884,7 +884,13 @@ class VirtualizedList extends React.PureComponent<Props, State> {
884884
index={ii}
885885
inversionStyle={inversionStyle}
886886
item={item}
887-
isSelected={this.state.selectedRowIndex === ii ? true : false} // TODO(macOS GH#774)
887+
// [TODO(macOS GH#774)
888+
isSelected={
889+
this.props.enableSelectionOnKeyPress &&
890+
this.state.selectedRowIndex === ii
891+
? true
892+
: false
893+
} // TODO(macOS GH#774)]
888894
key={key}
889895
prevCellKey={prevCellKey}
890896
onUpdateSeparators={this._onUpdateSeparators}
@@ -1322,10 +1328,12 @@ class VirtualizedList extends React.PureComponent<Props, State> {
13221328
// $FlowFixMe[prop-missing] Invalid prop usage
13231329
<ScrollView
13241330
{...props}
1325-
onScrollKeyDown={keyEventHandler} // TODO(macOS GH#774)
1331+
// [TODO(macOS GH#774)
1332+
{...(props.enableSelectionOnKeyPress && {focusable: true})}
1333+
onScrollKeyDown={keyEventHandler}
13261334
onPreferredScrollerStyleDidChange={
13271335
preferredScrollerStyleDidChangeHandler
1328-
} // TODO(macOS GH#774)
1336+
} // TODO(macOS GH#774)]
13291337
refreshControl={
13301338
props.refreshControl == null ? (
13311339
<RefreshControl
@@ -1344,11 +1352,11 @@ class VirtualizedList extends React.PureComponent<Props, State> {
13441352
// $FlowFixMe Invalid prop usage
13451353
<ScrollView
13461354
{...props}
1347-
onScrollKeyDown={keyEventHandler} // TODO(macOS GH#774)
1355+
{...(props.enableSelectionOnKeyPress && {focusable: true})} // [TODO(macOS GH#774)
1356+
onScrollKeyDown={keyEventHandler}
13481357
onPreferredScrollerStyleDidChange={
1349-
// TODO(macOS GH#774)
1350-
preferredScrollerStyleDidChangeHandler // TODO(macOS GH#774)
1351-
}
1358+
preferredScrollerStyleDidChangeHandler
1359+
} // TODO(macOS GH#774)]
13521360
/>
13531361
);
13541362
}
@@ -1506,6 +1514,13 @@ class VirtualizedList extends React.PureComponent<Props, State> {
15061514
return rowAbove;
15071515
};
15081516

1517+
_selectRowAtIndex = rowIndex => {
1518+
this.setState(state => {
1519+
return {selectedRowIndex: rowIndex};
1520+
});
1521+
return rowIndex;
1522+
};
1523+
15091524
_selectRowBelowIndex = rowIndex => {
15101525
if (this.props.getItemCount) {
15111526
const {data} = this.props;
@@ -1520,61 +1535,81 @@ class VirtualizedList extends React.PureComponent<Props, State> {
15201535
}
15211536
};
15221537

1523-
_handleKeyDown = (e: ScrollEvent) => {
1538+
_handleKeyDown = (event: ScrollEvent) => {
15241539
if (this.props.onScrollKeyDown) {
1525-
this.props.onScrollKeyDown(e);
1540+
this.props.onScrollKeyDown(event);
15261541
} else {
15271542
if (Platform.OS === 'macos') {
15281543
// $FlowFixMe Cannot get e.nativeEvent because property nativeEvent is missing in Event
1529-
const event = e.nativeEvent;
1530-
const key = event.key;
1544+
const nativeEvent = event.nativeEvent;
1545+
const key = nativeEvent.key;
15311546

15321547
let prevIndex = -1;
15331548
let newIndex = -1;
15341549
if ('selectedRowIndex' in this.state) {
15351550
prevIndex = this.state.selectedRowIndex;
15361551
}
15371552

1538-
const {data, getItem} = this.props;
1539-
if (key === 'DOWN_ARROW') {
1540-
newIndex = this._selectRowBelowIndex(prevIndex);
1541-
this.ensureItemAtIndexIsVisible(newIndex);
1542-
1543-
if (prevIndex !== newIndex) {
1544-
const item = getItem(data, newIndex);
1545-
if (this.props.onSelectionChanged) {
1546-
this.props.onSelectionChanged({
1547-
previousSelection: prevIndex,
1548-
newSelection: newIndex,
1549-
item: item,
1550-
});
1551-
}
1552-
}
1553-
} else if (key === 'UP_ARROW') {
1553+
// const {data, getItem} = this.props;
1554+
if (key === 'UP_ARROW') {
15541555
newIndex = this._selectRowAboveIndex(prevIndex);
1555-
this.ensureItemAtIndexIsVisible(newIndex);
1556-
1557-
if (prevIndex !== newIndex) {
1558-
const item = getItem(data, newIndex);
1559-
if (this.props.onSelectionChanged) {
1560-
this.props.onSelectionChanged({
1561-
previousSelection: prevIndex,
1562-
newSelection: newIndex,
1563-
item: item,
1564-
});
1565-
}
1566-
}
1556+
this._handleSelectionChange(prevIndex, newIndex);
1557+
} else if (key === 'DOWN_ARROW') {
1558+
newIndex = this._selectRowBelowIndex(prevIndex);
1559+
this._handleSelectionChange(prevIndex, newIndex);
15671560
} else if (key === 'ENTER') {
15681561
if (this.props.onSelectionEntered) {
1569-
const item = getItem(data, prevIndex);
1562+
const item = this.props.getItem(this.props.data, prevIndex);
15701563
if (this.props.onSelectionEntered) {
15711564
this.props.onSelectionEntered(item);
15721565
}
15731566
}
1567+
} else if (key === 'OPTION_UP') {
1568+
newIndex = this._selectRowAtIndex(0);
1569+
this._handleSelectionChange(prevIndex, newIndex);
1570+
} else if (key === 'OPTION_DOWN') {
1571+
newIndex = this._selectRowAtIndex(this.state.last);
1572+
this._handleSelectionChange(prevIndex, newIndex);
1573+
} else if (key === 'PAGE_UP') {
1574+
const maxY =
1575+
event.nativeEvent.contentSize.height -
1576+
event.nativeEvent.layoutMeasurement.height;
1577+
const newOffset = Math.min(
1578+
maxY,
1579+
nativeEvent.contentOffset.y + -nativeEvent.layoutMeasurement.height,
1580+
);
1581+
this.scrollToOffset({animated: true, offset: newOffset});
1582+
} else if (key === 'PAGE_DOWN') {
1583+
const maxY =
1584+
event.nativeEvent.contentSize.height -
1585+
event.nativeEvent.layoutMeasurement.height;
1586+
const newOffset = Math.min(
1587+
maxY,
1588+
nativeEvent.contentOffset.y + nativeEvent.layoutMeasurement.height,
1589+
);
1590+
this.scrollToOffset({animated: true, offset: newOffset});
1591+
} else if (key === 'HOME') {
1592+
this.scrollToOffset({animated: true, offset: 0});
1593+
} else if (key === 'END') {
1594+
this.scrollToEnd({animated: true});
15741595
}
15751596
}
15761597
}
15771598
};
1599+
1600+
_handleSelectionChange = (prevIndex, newIndex) => {
1601+
this.ensureItemAtIndexIsVisible(newIndex);
1602+
if (prevIndex !== newIndex) {
1603+
const item = this.props.getItem(this.props.data, newIndex);
1604+
if (this.props.onSelectionChanged) {
1605+
this.props.onSelectionChanged({
1606+
previousSelection: prevIndex,
1607+
newSelection: newIndex,
1608+
item: item,
1609+
});
1610+
}
1611+
}
1612+
};
15781613
// ]TODO(macOS GH#774)
15791614

15801615
_renderDebugOverlay() {
@@ -2172,6 +2207,7 @@ class CellRenderer extends React.Component<
21722207
return React.createElement(ListItemComponent, {
21732208
item,
21742209
index,
2210+
isSelected,
21752211
separators: this._separators,
21762212
});
21772213
}
@@ -2248,6 +2284,7 @@ class CellRenderer extends React.Component<
22482284
{itemSeparator}
22492285
</CellRendererComponent>
22502286
);
2287+
// TODO(macOS GH#774)]
22512288

22522289
return (
22532290
<VirtualizedListCellContextProvider cellKey={this.props.cellKey}>

React/Views/ScrollView/RCTScrollView.m

Lines changed: 36 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1258,16 +1258,22 @@ - (void)uiManagerWillPerformMounting:(RCTUIManager *)manager
12581258

12591259
#if TARGET_OS_OSX // [TODO(macOS GH#774)
12601260

1261-
- (NSString*)keyCommandFromKeyCode:(NSInteger)keyCode
1261+
- (NSString*)keyCommandFromKeyCode:(NSInteger)keyCode modifierFlags:(NSEventModifierFlags)modifierFlags
12621262
{
12631263
switch (keyCode)
12641264
{
12651265
case 36:
12661266
return @"ENTER";
12671267

1268+
case 115:
1269+
return @"HOME";
1270+
12681271
case 116:
12691272
return @"PAGE_UP";
12701273

1274+
case 119:
1275+
return @"END";
1276+
12711277
case 121:
12721278
return @"PAGE_DOWN";
12731279

@@ -1278,35 +1284,44 @@ - (NSString*)keyCommandFromKeyCode:(NSInteger)keyCode
12781284
return @"RIGHT_ARROW";
12791285

12801286
case 125:
1281-
return @"DOWN_ARROW";
1287+
if (modifierFlags & NSEventModifierFlagOption) {
1288+
return @"OPTION_DOWN";
1289+
} else {
1290+
return @"DOWN_ARROW";
1291+
}
12821292

12831293
case 126:
1284-
return @"UP_ARROW";
1294+
if (modifierFlags & NSEventModifierFlagOption) {
1295+
return @"OPTION_UP";
1296+
} else {
1297+
return @"UP_ARROW";
1298+
}
12851299
}
12861300
return @"";
12871301
}
12881302

12891303
- (void)keyDown:(UIEvent*)theEvent
12901304
{
12911305
// Don't emit a scroll event if tab was pressed while the scrollview is first responder
1292-
if (self == [[self window] firstResponder] &&
1293-
theEvent.keyCode != 48) {
1294-
NSString *keyCommand = [self keyCommandFromKeyCode:theEvent.keyCode];
1295-
RCT_SEND_SCROLL_EVENT(onScrollKeyDown, (@{ @"key": keyCommand}));
1296-
} else {
1297-
[super keyDown:theEvent];
1298-
1299-
// AX: if a tab key was pressed and the first responder is currently clipped by the scroll view,
1300-
// automatically scroll to make the view visible to make it navigable via keyboard.
1301-
if ([theEvent keyCode] == 48) { //tab key
1302-
id firstResponder = [[self window] firstResponder];
1303-
if ([firstResponder isKindOfClass:[NSView class]] &&
1304-
[firstResponder isDescendantOf:[_scrollView documentView]]) {
1305-
NSView *view = (NSView*)firstResponder;
1306-
NSRect visibleRect = ([view superview] == [_scrollView documentView]) ? NSInsetRect(view.frame, -1, -1) :
1307-
[view convertRect:view.frame toView:_scrollView.documentView];
1308-
[[_scrollView documentView] scrollRectToVisible:visibleRect];
1309-
}
1306+
if (!(self == [[self window] firstResponder] && theEvent.keyCode == 48)) {
1307+
NSString *keyCommand = [self keyCommandFromKeyCode:theEvent.keyCode modifierFlags:theEvent.modifierFlags];
1308+
if (![keyCommand isEqual: @""]) {
1309+
RCT_SEND_SCROLL_EVENT(onScrollKeyDown, (@{ @"key": keyCommand}));
1310+
} else {
1311+
[super keyDown:theEvent];
1312+
}
1313+
}
1314+
1315+
// AX: if a tab key was pressed and the first responder is currently clipped by the scroll view,
1316+
// automatically scroll to make the view visible to make it navigable via keyboard.
1317+
if ([theEvent keyCode] == 48) { //tab key
1318+
id firstResponder = [[self window] firstResponder];
1319+
if ([firstResponder isKindOfClass:[NSView class]] &&
1320+
[firstResponder isDescendantOf:[_scrollView documentView]]) {
1321+
NSView *view = (NSView*)firstResponder;
1322+
NSRect visibleRect = ([view superview] == [_scrollView documentView]) ? NSInsetRect(view.frame, -1, -1) :
1323+
[view convertRect:view.frame toView:_scrollView.documentView];
1324+
[[_scrollView documentView] scrollRectToVisible:visibleRect];
13101325
}
13111326
}
13121327
}

React/Views/UIView+React.m

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -282,29 +282,37 @@ - (void)setReactIsFocusNeeded:(BOOL)isFocusNeeded
282282

283283
- (void)reactFocus
284284
{
285-
if (![self becomeFirstResponder]) {
286-
self.reactIsFocusNeeded = YES;
287-
}
285+
#if TARGET_OS_OSX // [TODO(macOS GH#774)
286+
if (![[self window] makeFirstResponder:self]) {
287+
#else
288+
if (![self becomeFirstResponder]) {
289+
#endif //// TODO(macOS GH#774)]
290+
self.reactIsFocusNeeded = YES;
291+
}
288292
}
289293

290294
- (void)reactFocusIfNeeded
291295
{
292-
if (self.reactIsFocusNeeded) {
293-
if ([self becomeFirstResponder]) {
294-
self.reactIsFocusNeeded = NO;
295-
}
296-
}
296+
if (self.reactIsFocusNeeded) {
297+
#if TARGET_OS_OSX // [TODO(macOS GH#774)
298+
if ([[self window] makeFirstResponder:self]) {
299+
#else
300+
if ([self becomeFirstResponder]) {
301+
#endif // TODO(macOS GH#774)]
302+
self.reactIsFocusNeeded = NO;
303+
}
304+
}
297305
}
298306

299307
- (void)reactBlur
300308
{
301-
#if TARGET_OS_OSX // TODO(macOS GH#774)
309+
#if TARGET_OS_OSX // [TODO(macOS GH#774)
302310
if (self == [[self window] firstResponder]) {
303311
[[self window] makeFirstResponder:[[self window] nextResponder]];
304312
}
305313
#else
306314
[self resignFirstResponder];
307-
#endif
315+
#endif // TODO(macOS GH#774)]
308316
}
309317

310318
#pragma mark - Layout

0 commit comments

Comments
 (0)