Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[0.68] Keyboard navigation in Flatlist (#1258) #1262

Merged
merged 2 commits into from
Jul 20, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 4 additions & 36 deletions Libraries/Components/ScrollView/ScrollView.js
Original file line number Diff line number Diff line change
Expand Up @@ -1206,42 +1206,10 @@ class ScrollView extends React.Component<Props, State> {
nativeEvent.contentOffset.y +
nativeEvent.layoutMeasurement.height,
});
} else if (key === 'LEFT_ARROW') {
this._handleScrollByKeyDown(event, {
x:
nativeEvent.contentOffset.x +
-(this.props.horizontalLineScroll !== undefined
? this.props.horizontalLineScroll
: kMinScrollOffset),
y: nativeEvent.contentOffset.y,
});
} else if (key === 'RIGHT_ARROW') {
this._handleScrollByKeyDown(event, {
x:
nativeEvent.contentOffset.x +
(this.props.horizontalLineScroll !== undefined
? this.props.horizontalLineScroll
: kMinScrollOffset),
y: nativeEvent.contentOffset.y,
});
} else if (key === 'DOWN_ARROW') {
this._handleScrollByKeyDown(event, {
x: nativeEvent.contentOffset.x,
y:
nativeEvent.contentOffset.y +
(this.props.verticalLineScroll !== undefined
? this.props.verticalLineScroll
: kMinScrollOffset),
});
} else if (key === 'UP_ARROW') {
this._handleScrollByKeyDown(event, {
x: nativeEvent.contentOffset.x,
y:
nativeEvent.contentOffset.y +
-(this.props.verticalLineScroll !== undefined
? this.props.verticalLineScroll
: kMinScrollOffset),
});
} else if (key === 'HOME') {
this.scrollTo({x: 0, y: 0});
} else if (key === 'END') {
this.scrollToEnd({animated: true});
}
}
}
Expand Down
119 changes: 78 additions & 41 deletions Libraries/Lists/VirtualizedList.js
Original file line number Diff line number Diff line change
Expand Up @@ -588,7 +588,7 @@ class VirtualizedList extends React.PureComponent<Props, State> {
const newOffset = Math.min(contentLength, visTop + (frameEnd - visEnd));
this.scrollToOffset({offset: newOffset});
} else if (frame.offset < visTop) {
const newOffset = Math.max(0, visTop - frame.length);
const newOffset = Math.min(frame.offset, visTop - frame.length);
this.scrollToOffset({offset: newOffset});
}
}
Expand Down Expand Up @@ -884,7 +884,13 @@ class VirtualizedList extends React.PureComponent<Props, State> {
index={ii}
inversionStyle={inversionStyle}
item={item}
isSelected={this.state.selectedRowIndex === ii ? true : false} // TODO(macOS GH#774)
// [TODO(macOS GH#774)
isSelected={
this.props.enableSelectionOnKeyPress &&
this.state.selectedRowIndex === ii
? true
: false
} // TODO(macOS GH#774)]
key={key}
prevCellKey={prevCellKey}
onUpdateSeparators={this._onUpdateSeparators}
Expand Down Expand Up @@ -1322,10 +1328,12 @@ class VirtualizedList extends React.PureComponent<Props, State> {
// $FlowFixMe[prop-missing] Invalid prop usage
<ScrollView
{...props}
onScrollKeyDown={keyEventHandler} // TODO(macOS GH#774)
// [TODO(macOS GH#774)
{...(props.enableSelectionOnKeyPress && {focusable: true})}
onScrollKeyDown={keyEventHandler}
onPreferredScrollerStyleDidChange={
preferredScrollerStyleDidChangeHandler
} // TODO(macOS GH#774)
} // TODO(macOS GH#774)]
refreshControl={
props.refreshControl == null ? (
<RefreshControl
Expand All @@ -1344,11 +1352,11 @@ class VirtualizedList extends React.PureComponent<Props, State> {
// $FlowFixMe Invalid prop usage
<ScrollView
{...props}
onScrollKeyDown={keyEventHandler} // TODO(macOS GH#774)
{...(props.enableSelectionOnKeyPress && {focusable: true})} // [TODO(macOS GH#774)
onScrollKeyDown={keyEventHandler}
onPreferredScrollerStyleDidChange={
// TODO(macOS GH#774)
preferredScrollerStyleDidChangeHandler // TODO(macOS GH#774)
}
preferredScrollerStyleDidChangeHandler
} // TODO(macOS GH#774)]
/>
);
}
Expand Down Expand Up @@ -1506,6 +1514,13 @@ class VirtualizedList extends React.PureComponent<Props, State> {
return rowAbove;
};

_selectRowAtIndex = rowIndex => {
this.setState(state => {
return {selectedRowIndex: rowIndex};
});
return rowIndex;
};

_selectRowBelowIndex = rowIndex => {
if (this.props.getItemCount) {
const {data} = this.props;
Expand All @@ -1520,61 +1535,81 @@ class VirtualizedList extends React.PureComponent<Props, State> {
}
};

_handleKeyDown = (e: ScrollEvent) => {
_handleKeyDown = (event: ScrollEvent) => {
if (this.props.onScrollKeyDown) {
this.props.onScrollKeyDown(e);
this.props.onScrollKeyDown(event);
} else {
if (Platform.OS === 'macos') {
// $FlowFixMe Cannot get e.nativeEvent because property nativeEvent is missing in Event
const event = e.nativeEvent;
const key = event.key;
const nativeEvent = event.nativeEvent;
const key = nativeEvent.key;

let prevIndex = -1;
let newIndex = -1;
if ('selectedRowIndex' in this.state) {
prevIndex = this.state.selectedRowIndex;
}

const {data, getItem} = this.props;
if (key === 'DOWN_ARROW') {
newIndex = this._selectRowBelowIndex(prevIndex);
this.ensureItemAtIndexIsVisible(newIndex);

if (prevIndex !== newIndex) {
const item = getItem(data, newIndex);
if (this.props.onSelectionChanged) {
this.props.onSelectionChanged({
previousSelection: prevIndex,
newSelection: newIndex,
item: item,
});
}
}
} else if (key === 'UP_ARROW') {
// const {data, getItem} = this.props;
if (key === 'UP_ARROW') {
newIndex = this._selectRowAboveIndex(prevIndex);
this.ensureItemAtIndexIsVisible(newIndex);

if (prevIndex !== newIndex) {
const item = getItem(data, newIndex);
if (this.props.onSelectionChanged) {
this.props.onSelectionChanged({
previousSelection: prevIndex,
newSelection: newIndex,
item: item,
});
}
}
this._handleSelectionChange(prevIndex, newIndex);
} else if (key === 'DOWN_ARROW') {
newIndex = this._selectRowBelowIndex(prevIndex);
this._handleSelectionChange(prevIndex, newIndex);
} else if (key === 'ENTER') {
if (this.props.onSelectionEntered) {
const item = getItem(data, prevIndex);
const item = this.props.getItem(this.props.data, prevIndex);
if (this.props.onSelectionEntered) {
this.props.onSelectionEntered(item);
}
}
} else if (key === 'OPTION_UP') {
newIndex = this._selectRowAtIndex(0);
this._handleSelectionChange(prevIndex, newIndex);
} else if (key === 'OPTION_DOWN') {
newIndex = this._selectRowAtIndex(this.state.last);
this._handleSelectionChange(prevIndex, newIndex);
} else if (key === 'PAGE_UP') {
const maxY =
event.nativeEvent.contentSize.height -
event.nativeEvent.layoutMeasurement.height;
const newOffset = Math.min(
maxY,
nativeEvent.contentOffset.y + -nativeEvent.layoutMeasurement.height,
);
this.scrollToOffset({animated: true, offset: newOffset});
} else if (key === 'PAGE_DOWN') {
const maxY =
event.nativeEvent.contentSize.height -
event.nativeEvent.layoutMeasurement.height;
const newOffset = Math.min(
maxY,
nativeEvent.contentOffset.y + nativeEvent.layoutMeasurement.height,
);
this.scrollToOffset({animated: true, offset: newOffset});
} else if (key === 'HOME') {
this.scrollToOffset({animated: true, offset: 0});
} else if (key === 'END') {
this.scrollToEnd({animated: true});
}
}
}
};

_handleSelectionChange = (prevIndex, newIndex) => {
this.ensureItemAtIndexIsVisible(newIndex);
if (prevIndex !== newIndex) {
const item = this.props.getItem(this.props.data, newIndex);
if (this.props.onSelectionChanged) {
this.props.onSelectionChanged({
previousSelection: prevIndex,
newSelection: newIndex,
item: item,
});
}
}
};
// ]TODO(macOS GH#774)

_renderDebugOverlay() {
Expand Down Expand Up @@ -2172,6 +2207,7 @@ class CellRenderer extends React.Component<
return React.createElement(ListItemComponent, {
item,
index,
isSelected,
separators: this._separators,
});
}
Expand Down Expand Up @@ -2248,6 +2284,7 @@ class CellRenderer extends React.Component<
{itemSeparator}
</CellRendererComponent>
);
// TODO(macOS GH#774)]

return (
<VirtualizedListCellContextProvider cellKey={this.props.cellKey}>
Expand Down
57 changes: 36 additions & 21 deletions React/Views/ScrollView/RCTScrollView.m
Original file line number Diff line number Diff line change
Expand Up @@ -1258,16 +1258,22 @@ - (void)uiManagerWillPerformMounting:(RCTUIManager *)manager

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

- (NSString*)keyCommandFromKeyCode:(NSInteger)keyCode
- (NSString*)keyCommandFromKeyCode:(NSInteger)keyCode modifierFlags:(NSEventModifierFlags)modifierFlags
{
switch (keyCode)
{
case 36:
return @"ENTER";

case 115:
return @"HOME";

case 116:
return @"PAGE_UP";

case 119:
return @"END";

case 121:
return @"PAGE_DOWN";

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

case 125:
return @"DOWN_ARROW";
if (modifierFlags & NSEventModifierFlagOption) {
return @"OPTION_DOWN";
} else {
return @"DOWN_ARROW";
}

case 126:
return @"UP_ARROW";
if (modifierFlags & NSEventModifierFlagOption) {
return @"OPTION_UP";
} else {
return @"UP_ARROW";
}
}
return @"";
}

- (void)keyDown:(UIEvent*)theEvent
{
// Don't emit a scroll event if tab was pressed while the scrollview is first responder
if (self == [[self window] firstResponder] &&
theEvent.keyCode != 48) {
NSString *keyCommand = [self keyCommandFromKeyCode:theEvent.keyCode];
RCT_SEND_SCROLL_EVENT(onScrollKeyDown, (@{ @"key": keyCommand}));
} else {
[super keyDown:theEvent];

// AX: if a tab key was pressed and the first responder is currently clipped by the scroll view,
// automatically scroll to make the view visible to make it navigable via keyboard.
if ([theEvent keyCode] == 48) { //tab key
id firstResponder = [[self window] firstResponder];
if ([firstResponder isKindOfClass:[NSView class]] &&
[firstResponder isDescendantOf:[_scrollView documentView]]) {
NSView *view = (NSView*)firstResponder;
NSRect visibleRect = ([view superview] == [_scrollView documentView]) ? NSInsetRect(view.frame, -1, -1) :
[view convertRect:view.frame toView:_scrollView.documentView];
[[_scrollView documentView] scrollRectToVisible:visibleRect];
}
if (!(self == [[self window] firstResponder] && theEvent.keyCode == 48)) {
NSString *keyCommand = [self keyCommandFromKeyCode:theEvent.keyCode modifierFlags:theEvent.modifierFlags];
if (![keyCommand isEqual: @""]) {
RCT_SEND_SCROLL_EVENT(onScrollKeyDown, (@{ @"key": keyCommand}));
} else {
[super keyDown:theEvent];
}
}

// AX: if a tab key was pressed and the first responder is currently clipped by the scroll view,
// automatically scroll to make the view visible to make it navigable via keyboard.
if ([theEvent keyCode] == 48) { //tab key
id firstResponder = [[self window] firstResponder];
if ([firstResponder isKindOfClass:[NSView class]] &&
[firstResponder isDescendantOf:[_scrollView documentView]]) {
NSView *view = (NSView*)firstResponder;
NSRect visibleRect = ([view superview] == [_scrollView documentView]) ? NSInsetRect(view.frame, -1, -1) :
[view convertRect:view.frame toView:_scrollView.documentView];
[[_scrollView documentView] scrollRectToVisible:visibleRect];
}
}
}
Expand Down
28 changes: 18 additions & 10 deletions React/Views/UIView+React.m
Original file line number Diff line number Diff line change
Expand Up @@ -282,29 +282,37 @@ - (void)setReactIsFocusNeeded:(BOOL)isFocusNeeded

- (void)reactFocus
{
if (![self becomeFirstResponder]) {
self.reactIsFocusNeeded = YES;
}
#if TARGET_OS_OSX // [TODO(macOS GH#774)
if (![[self window] makeFirstResponder:self]) {
#else
if (![self becomeFirstResponder]) {
#endif //// TODO(macOS GH#774)]
self.reactIsFocusNeeded = YES;
}
}

- (void)reactFocusIfNeeded
{
if (self.reactIsFocusNeeded) {
if ([self becomeFirstResponder]) {
self.reactIsFocusNeeded = NO;
}
}
if (self.reactIsFocusNeeded) {
#if TARGET_OS_OSX // [TODO(macOS GH#774)
if ([[self window] makeFirstResponder:self]) {
#else
if ([self becomeFirstResponder]) {
#endif // TODO(macOS GH#774)]
self.reactIsFocusNeeded = NO;
}
}
}

- (void)reactBlur
{
#if TARGET_OS_OSX // TODO(macOS GH#774)
#if TARGET_OS_OSX // [TODO(macOS GH#774)
if (self == [[self window] firstResponder]) {
[[self window] makeFirstResponder:[[self window] nextResponder]];
}
#else
[self resignFirstResponder];
#endif
#endif // TODO(macOS GH#774)]
}

#pragma mark - Layout
Expand Down
Loading