Skip to content

Commit 84c0863

Browse files
Saadnajmipull[bot]ryanlntnntremganandraj
authored
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>
1 parent 573eab0 commit 84c0863

File tree

6 files changed

+171
-111
lines changed

6 files changed

+171
-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}
@@ -1330,10 +1336,12 @@ class VirtualizedList extends React.PureComponent<Props, State> {
13301336
// $FlowFixMe[prop-missing] Invalid prop usage
13311337
<ScrollView
13321338
{...props}
1333-
onScrollKeyDown={keyEventHandler} // TODO(macOS GH#774)
1339+
// [TODO(macOS GH#774)
1340+
{...(props.enableSelectionOnKeyPress && {focusable: true})}
1341+
onScrollKeyDown={keyEventHandler}
13341342
onPreferredScrollerStyleDidChange={
13351343
preferredScrollerStyleDidChangeHandler
1336-
} // TODO(macOS GH#774)
1344+
} // TODO(macOS GH#774)]
13371345
refreshControl={
13381346
props.refreshControl == null ? (
13391347
<RefreshControl
@@ -1352,11 +1360,11 @@ class VirtualizedList extends React.PureComponent<Props, State> {
13521360
// $FlowFixMe Invalid prop usage
13531361
<ScrollView
13541362
{...props}
1355-
onScrollKeyDown={keyEventHandler} // TODO(macOS GH#774)
1363+
{...(props.enableSelectionOnKeyPress && {focusable: true})} // [TODO(macOS GH#774)
1364+
onScrollKeyDown={keyEventHandler}
13561365
onPreferredScrollerStyleDidChange={
1357-
// TODO(macOS GH#774)
1358-
preferredScrollerStyleDidChangeHandler // TODO(macOS GH#774)
1359-
}
1366+
preferredScrollerStyleDidChangeHandler
1367+
} // TODO(macOS GH#774)]
13601368
/>
13611369
);
13621370
}
@@ -1514,6 +1522,13 @@ class VirtualizedList extends React.PureComponent<Props, State> {
15141522
return rowAbove;
15151523
};
15161524

1525+
_selectRowAtIndex = rowIndex => {
1526+
this.setState(state => {
1527+
return {selectedRowIndex: rowIndex};
1528+
});
1529+
return rowIndex;
1530+
};
1531+
15171532
_selectRowBelowIndex = rowIndex => {
15181533
if (this.props.getItemCount) {
15191534
const {data} = this.props;
@@ -1528,61 +1543,81 @@ class VirtualizedList extends React.PureComponent<Props, State> {
15281543
}
15291544
};
15301545

1531-
_handleKeyDown = (e: ScrollEvent) => {
1546+
_handleKeyDown = (event: ScrollEvent) => {
15321547
if (this.props.onScrollKeyDown) {
1533-
this.props.onScrollKeyDown(e);
1548+
this.props.onScrollKeyDown(event);
15341549
} else {
15351550
if (Platform.OS === 'macos') {
15361551
// $FlowFixMe Cannot get e.nativeEvent because property nativeEvent is missing in Event
1537-
const event = e.nativeEvent;
1538-
const key = event.key;
1552+
const nativeEvent = event.nativeEvent;
1553+
const key = nativeEvent.key;
15391554

15401555
let prevIndex = -1;
15411556
let newIndex = -1;
15421557
if ('selectedRowIndex' in this.state) {
15431558
prevIndex = this.state.selectedRowIndex;
15441559
}
15451560

1546-
const {data, getItem} = this.props;
1547-
if (key === 'DOWN_ARROW') {
1548-
newIndex = this._selectRowBelowIndex(prevIndex);
1549-
this.ensureItemAtIndexIsVisible(newIndex);
1550-
1551-
if (prevIndex !== newIndex) {
1552-
const item = getItem(data, newIndex);
1553-
if (this.props.onSelectionChanged) {
1554-
this.props.onSelectionChanged({
1555-
previousSelection: prevIndex,
1556-
newSelection: newIndex,
1557-
item: item,
1558-
});
1559-
}
1560-
}
1561-
} else if (key === 'UP_ARROW') {
1561+
// const {data, getItem} = this.props;
1562+
if (key === 'UP_ARROW') {
15621563
newIndex = this._selectRowAboveIndex(prevIndex);
1563-
this.ensureItemAtIndexIsVisible(newIndex);
1564-
1565-
if (prevIndex !== newIndex) {
1566-
const item = getItem(data, newIndex);
1567-
if (this.props.onSelectionChanged) {
1568-
this.props.onSelectionChanged({
1569-
previousSelection: prevIndex,
1570-
newSelection: newIndex,
1571-
item: item,
1572-
});
1573-
}
1574-
}
1564+
this._handleSelectionChange(prevIndex, newIndex);
1565+
} else if (key === 'DOWN_ARROW') {
1566+
newIndex = this._selectRowBelowIndex(prevIndex);
1567+
this._handleSelectionChange(prevIndex, newIndex);
15751568
} else if (key === 'ENTER') {
15761569
if (this.props.onSelectionEntered) {
1577-
const item = getItem(data, prevIndex);
1570+
const item = this.props.getItem(this.props.data, prevIndex);
15781571
if (this.props.onSelectionEntered) {
15791572
this.props.onSelectionEntered(item);
15801573
}
15811574
}
1575+
} else if (key === 'OPTION_UP') {
1576+
newIndex = this._selectRowAtIndex(0);
1577+
this._handleSelectionChange(prevIndex, newIndex);
1578+
} else if (key === 'OPTION_DOWN') {
1579+
newIndex = this._selectRowAtIndex(this.state.last);
1580+
this._handleSelectionChange(prevIndex, newIndex);
1581+
} else if (key === 'PAGE_UP') {
1582+
const maxY =
1583+
event.nativeEvent.contentSize.height -
1584+
event.nativeEvent.layoutMeasurement.height;
1585+
const newOffset = Math.min(
1586+
maxY,
1587+
nativeEvent.contentOffset.y + -nativeEvent.layoutMeasurement.height,
1588+
);
1589+
this.scrollToOffset({animated: true, offset: newOffset});
1590+
} else if (key === 'PAGE_DOWN') {
1591+
const maxY =
1592+
event.nativeEvent.contentSize.height -
1593+
event.nativeEvent.layoutMeasurement.height;
1594+
const newOffset = Math.min(
1595+
maxY,
1596+
nativeEvent.contentOffset.y + nativeEvent.layoutMeasurement.height,
1597+
);
1598+
this.scrollToOffset({animated: true, offset: newOffset});
1599+
} else if (key === 'HOME') {
1600+
this.scrollToOffset({animated: true, offset: 0});
1601+
} else if (key === 'END') {
1602+
this.scrollToEnd({animated: true});
15821603
}
15831604
}
15841605
}
15851606
};
1607+
1608+
_handleSelectionChange = (prevIndex, newIndex) => {
1609+
this.ensureItemAtIndexIsVisible(newIndex);
1610+
if (prevIndex !== newIndex) {
1611+
const item = this.props.getItem(this.props.data, newIndex);
1612+
if (this.props.onSelectionChanged) {
1613+
this.props.onSelectionChanged({
1614+
previousSelection: prevIndex,
1615+
newSelection: newIndex,
1616+
item: item,
1617+
});
1618+
}
1619+
}
1620+
};
15861621
// ]TODO(macOS GH#774)
15871622

15881623
_renderDebugOverlay() {
@@ -2188,6 +2223,7 @@ class CellRenderer extends React.Component<
21882223
return React.createElement(ListItemComponent, {
21892224
item,
21902225
index,
2226+
isSelected,
21912227
separators: this._separators,
21922228
});
21932229
}
@@ -2265,6 +2301,7 @@ class CellRenderer extends React.Component<
22652301
{itemSeparator}
22662302
</CellRendererComponent>
22672303
);
2304+
// TODO(macOS GH#774)]
22682305

22692306
return (
22702307
<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)