Skip to content

Commit 01a0de8

Browse files
Saadnajmipull[bot]ryanlntnntremganandraj
authored
[0.66] Keyboard navigation in Flatlist (#1258) (#1263)
* Keyboard navigation in Flatlist (#1258) * add pull yml * match handleOpenURLNotification event payload with iOS (#755) (#2) Co-authored-by: Ryan Linton <[email protected]> * [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 <[email protected]> * 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 <[email protected]> * Grammar fixes. (#856) Updates simple grammar issues. Co-authored-by: Nick Trescases <[email protected]> Co-authored-by: Anandraj <[email protected]> Co-authored-by: Saad Najmi <[email protected]> Co-authored-by: pull[bot] <39814207+pull[bot]@users.noreply.github.com> Co-authored-by: Ryan Linton <[email protected]> Co-authored-by: Muhammad Hamza Zaman <[email protected]> * 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 <[email protected]> Co-authored-by: Nick Trescases <[email protected]> Co-authored-by: Anandraj <[email protected]> Co-authored-by: Muhammad Hamza Zaman <[email protected]> * yarn lint --fix Co-authored-by: pull[bot] <39814207+pull[bot]@users.noreply.github.com> Co-authored-by: Ryan Linton <[email protected]> Co-authored-by: Nick Trescases <[email protected]> Co-authored-by: Anandraj <[email protected]> Co-authored-by: Muhammad Hamza Zaman <[email protected]>
1 parent e430c98 commit 01a0de8

File tree

6 files changed

+178
-111
lines changed

6 files changed

+178
-111
lines changed

Libraries/Components/ScrollView/ScrollView.js

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

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
}
@@ -882,7 +882,13 @@ class VirtualizedList extends React.PureComponent<Props, State> {
882882
index={ii}
883883
inversionStyle={inversionStyle}
884884
item={item}
885-
isSelected={this.state.selectedRowIndex === ii ? true : false} // TODO(macOS GH#774)
885+
// [TODO(macOS GH#774)
886+
isSelected={
887+
this.props.enableSelectionOnKeyPress &&
888+
this.state.selectedRowIndex === ii
889+
? true
890+
: false
891+
} // TODO(macOS GH#774)]
886892
key={key}
887893
prevCellKey={prevCellKey}
888894
onUpdateSeparators={this._onUpdateSeparators}
@@ -1323,10 +1329,12 @@ class VirtualizedList extends React.PureComponent<Props, State> {
13231329
// $FlowFixMe[prop-missing] Invalid prop usage
13241330
<ScrollView
13251331
{...props}
1326-
onScrollKeyDown={keyEventHandler} // TODO(macOS GH#774)
1332+
// [TODO(macOS GH#774)
1333+
{...(props.enableSelectionOnKeyPress && {focusable: true})}
1334+
onScrollKeyDown={keyEventHandler}
13271335
onPreferredScrollerStyleDidChange={
13281336
preferredScrollerStyleDidChangeHandler
1329-
} // TODO(macOS GH#774)
1337+
} // TODO(macOS GH#774)]
13301338
refreshControl={
13311339
props.refreshControl == null ? (
13321340
<RefreshControl
@@ -1345,11 +1353,11 @@ class VirtualizedList extends React.PureComponent<Props, State> {
13451353
// $FlowFixMe Invalid prop usage
13461354
<ScrollView
13471355
{...props}
1348-
onScrollKeyDown={keyEventHandler} // TODO(macOS GH#774)
1356+
{...(props.enableSelectionOnKeyPress && {focusable: true})} // [TODO(macOS GH#774)
1357+
onScrollKeyDown={keyEventHandler}
13491358
onPreferredScrollerStyleDidChange={
1350-
// TODO(macOS GH#774)
1351-
preferredScrollerStyleDidChangeHandler // TODO(macOS GH#774)
1352-
}
1359+
preferredScrollerStyleDidChangeHandler
1360+
} // TODO(macOS GH#774)]
13531361
/>
13541362
);
13551363
}
@@ -1507,6 +1515,13 @@ class VirtualizedList extends React.PureComponent<Props, State> {
15071515
return rowAbove;
15081516
};
15091517

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

1524-
_handleKeyDown = (e: ScrollEvent) => {
1539+
_handleKeyDown = (event: ScrollEvent) => {
15251540
if (this.props.onScrollKeyDown) {
1526-
this.props.onScrollKeyDown(e);
1541+
this.props.onScrollKeyDown(event);
15271542
} else {
15281543
if (Platform.OS === 'macos') {
15291544
// $FlowFixMe Cannot get e.nativeEvent because property nativeEvent is missing in Event
1530-
const event = e.nativeEvent;
1531-
const key = event.key;
1545+
const nativeEvent = event.nativeEvent;
1546+
const key = nativeEvent.key;
15321547

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

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

15811616
_renderDebugOverlay() {
@@ -2182,6 +2217,7 @@ class CellRenderer extends React.Component<
21822217
return React.createElement(ListItemComponent, {
21832218
item,
21842219
index,
2220+
isSelected,
21852221
separators: this._separators,
21862222
});
21872223
}
@@ -2258,6 +2294,7 @@ class CellRenderer extends React.Component<
22582294
{itemSeparator}
22592295
</CellRendererComponent>
22602296
);
2297+
// TODO(macOS GH#774)]
22612298

22622299
return (
22632300
<VirtualizedListCellContextProvider cellKey={this.props.cellKey}>

React/Views/ScrollView/RCTScrollView.m

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

11811181
#if TARGET_OS_OSX // [TODO(macOS GH#774)
11821182

1183-
- (NSString*)keyCommandFromKeyCode:(NSInteger)keyCode
1183+
- (NSString*)keyCommandFromKeyCode:(NSInteger)keyCode modifierFlags:(NSEventModifierFlags)modifierFlags
11841184
{
11851185
switch (keyCode)
11861186
{
11871187
case 36:
11881188
return @"ENTER";
11891189

1190+
case 115:
1191+
return @"HOME";
1192+
11901193
case 116:
11911194
return @"PAGE_UP";
11921195

1196+
case 119:
1197+
return @"END";
1198+
11931199
case 121:
11941200
return @"PAGE_DOWN";
11951201

@@ -1200,35 +1206,44 @@ - (NSString*)keyCommandFromKeyCode:(NSInteger)keyCode
12001206
return @"RIGHT_ARROW";
12011207

12021208
case 125:
1203-
return @"DOWN_ARROW";
1209+
if (modifierFlags & NSEventModifierFlagOption) {
1210+
return @"OPTION_DOWN";
1211+
} else {
1212+
return @"DOWN_ARROW";
1213+
}
12041214

12051215
case 126:
1206-
return @"UP_ARROW";
1216+
if (modifierFlags & NSEventModifierFlagOption) {
1217+
return @"OPTION_UP";
1218+
} else {
1219+
return @"UP_ARROW";
1220+
}
12071221
}
12081222
return @"";
12091223
}
12101224

12111225
- (void)keyDown:(UIEvent*)theEvent
12121226
{
12131227
// Don't emit a scroll event if tab was pressed while the scrollview is first responder
1214-
if (self == [[self window] firstResponder] &&
1215-
theEvent.keyCode != 48) {
1216-
NSString *keyCommand = [self keyCommandFromKeyCode:theEvent.keyCode];
1217-
RCT_SEND_SCROLL_EVENT(onScrollKeyDown, (@{ @"key": keyCommand}));
1218-
} else {
1219-
[super keyDown:theEvent];
1220-
1221-
// AX: if a tab key was pressed and the first responder is currently clipped by the scroll view,
1222-
// automatically scroll to make the view visible to make it navigable via keyboard.
1223-
if ([theEvent keyCode] == 48) { //tab key
1224-
id firstResponder = [[self window] firstResponder];
1225-
if ([firstResponder isKindOfClass:[NSView class]] &&
1226-
[firstResponder isDescendantOf:[_scrollView documentView]]) {
1227-
NSView *view = (NSView*)firstResponder;
1228-
NSRect visibleRect = ([view superview] == [_scrollView documentView]) ? NSInsetRect(view.frame, -1, -1) :
1229-
[view convertRect:view.frame toView:_scrollView.documentView];
1230-
[[_scrollView documentView] scrollRectToVisible:visibleRect];
1231-
}
1228+
if (!(self == [[self window] firstResponder] && theEvent.keyCode == 48)) {
1229+
NSString *keyCommand = [self keyCommandFromKeyCode:theEvent.keyCode modifierFlags:theEvent.modifierFlags];
1230+
if (![keyCommand isEqual: @""]) {
1231+
RCT_SEND_SCROLL_EVENT(onScrollKeyDown, (@{ @"key": keyCommand}));
1232+
} else {
1233+
[super keyDown:theEvent];
1234+
}
1235+
}
1236+
1237+
// AX: if a tab key was pressed and the first responder is currently clipped by the scroll view,
1238+
// automatically scroll to make the view visible to make it navigable via keyboard.
1239+
if ([theEvent keyCode] == 48) { //tab key
1240+
id firstResponder = [[self window] firstResponder];
1241+
if ([firstResponder isKindOfClass:[NSView class]] &&
1242+
[firstResponder isDescendantOf:[_scrollView documentView]]) {
1243+
NSView *view = (NSView*)firstResponder;
1244+
NSRect visibleRect = ([view superview] == [_scrollView documentView]) ? NSInsetRect(view.frame, -1, -1) :
1245+
[view convertRect:view.frame toView:_scrollView.documentView];
1246+
[[_scrollView documentView] scrollRectToVisible:visibleRect];
12321247
}
12331248
}
12341249
}

React/Views/UIView+React.m

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

286286
- (void)reactFocus
287287
{
288-
if (![self becomeFirstResponder]) {
289-
self.reactIsFocusNeeded = YES;
290-
}
288+
#if TARGET_OS_OSX // [TODO(macOS GH#774)
289+
if (![[self window] makeFirstResponder:self]) {
290+
#else
291+
if (![self becomeFirstResponder]) {
292+
#endif //// TODO(macOS GH#774)]
293+
self.reactIsFocusNeeded = YES;
294+
}
291295
}
292296

293297
- (void)reactFocusIfNeeded
294298
{
295-
if (self.reactIsFocusNeeded) {
296-
if ([self becomeFirstResponder]) {
297-
self.reactIsFocusNeeded = NO;
298-
}
299-
}
299+
if (self.reactIsFocusNeeded) {
300+
#if TARGET_OS_OSX // [TODO(macOS GH#774)
301+
if ([[self window] makeFirstResponder:self]) {
302+
#else
303+
if ([self becomeFirstResponder]) {
304+
#endif // TODO(macOS GH#774)]
305+
self.reactIsFocusNeeded = NO;
306+
}
307+
}
300308
}
301309

302310
- (void)reactBlur
303311
{
304-
#if TARGET_OS_OSX // TODO(macOS GH#774)
312+
#if TARGET_OS_OSX // [TODO(macOS GH#774)
305313
if (self == [[self window] firstResponder]) {
306314
[[self window] makeFirstResponder:[[self window] nextResponder]];
307315
}
308316
#else
309317
[self resignFirstResponder];
310-
#endif
318+
#endif // TODO(macOS GH#774)]
311319
}
312320

313321
#pragma mark - Layout

0 commit comments

Comments
 (0)