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

Keyboard navigation in Flatlist #1258

Merged
merged 47 commits into from
Jul 19, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
c2b2e2f
add pull yml
Saadnajmi Mar 22, 2021
ab88c7d
Merge pull request #1 from microsoft/master
Saadnajmi Apr 2, 2021
7a9006b
match handleOpenURLNotification event payload with iOS (#755) (#2)
pull[bot] Apr 21, 2021
497aa72
Merge pull request #3 from microsoft/master
Saadnajmi Apr 28, 2021
1c81e5b
Merge pull request #4 from microsoft/master
Saadnajmi May 6, 2021
28aed35
Merge branch 'master' of github.com:microsoft/react-native-macos
Saadnajmi May 20, 2021
62dc473
Merge branch 'master' of github.com:Saadnajmi/react-native-macos
Saadnajmi May 20, 2021
93c7296
Merge branch 'master' of github.com:microsoft/react-native-macos
Saadnajmi May 21, 2021
780b2b7
Merge branch 'master' of github.com:microsoft/react-native-macos
Saadnajmi Jun 11, 2021
99d5182
Merge branch 'master' of github.com:microsoft/react-native-macos
Saadnajmi Aug 22, 2021
09e872d
Merge branch 'master' of github.com:microsoft/react-native-macos
Saadnajmi Sep 3, 2021
9a25530
Merge branch 'master' of github.com:microsoft/react-native-macos
Saadnajmi Oct 6, 2021
d82a01b
Merge branch 'master' of github.com:microsoft/react-native-macos
Saadnajmi Oct 12, 2021
d9faa3a
[pull] master from microsoft:master (#11)
pull[bot] Oct 14, 2021
39bd488
Merge branch 'master' of github.com:microsoft/react-native-macos
Saadnajmi Oct 21, 2021
c583ed3
Merge branch 'master' of github.com:microsoft/react-native-macos
Saadnajmi Oct 25, 2021
9cea270
Merge branch 'master' of github.com:Saadnajmi/react-native-macos
Saadnajmi Nov 7, 2021
7571092
Merge branch 'master' of github.com:microsoft/react-native-macos
Saadnajmi Nov 7, 2021
2153ef5
Merge branch 'master' of github.com:microsoft/react-native-macos
Saadnajmi Dec 8, 2021
47ee4ab
Merge branch 'microsoft:master' into master
Saadnajmi Jan 3, 2022
c74bf4d
Merge branch 'microsoft:main' into main
Saadnajmi Jan 30, 2022
5f905ef
Merge branch 'microsoft:main' into main
Saadnajmi Feb 24, 2022
2869642
Merge branch 'microsoft:main' into main
Saadnajmi Mar 7, 2022
b97b271
Merge branch 'microsoft:main' into main
Saadnajmi Apr 23, 2022
199621a
Merge branch 'main' of github.com:microsoft/react-native-macos
Saadnajmi Apr 29, 2022
5ec261f
Merge branch 'microsoft:main' into main
Saadnajmi Jun 20, 2022
61d88c3
wip
Saadnajmi Jun 24, 2022
276eb27
wip
Saadnajmi Jun 30, 2022
02849d2
more wip
Saadnajmi Jul 8, 2022
d7f2814
Home/End/OptionUp/OptionDown work
Saadnajmi Jul 11, 2022
85fd82b
ensureItemAtIndexIsVisible works
Saadnajmi Jul 13, 2022
747ce4d
Home/End work
Saadnajmi Jul 13, 2022
9d631f7
Initial cleanup for PR
Saadnajmi Jul 13, 2022
e313195
Merge branch 'main' of github.com:microsoft/react-native-macos into l…
Saadnajmi Jul 13, 2022
37c61b4
More cleanup
Saadnajmi Jul 13, 2022
f385886
More cleanup
Saadnajmi Jul 13, 2022
8fc9621
Make it a real prop
Saadnajmi Jul 14, 2022
b11f135
No need for client code
Saadnajmi Jul 15, 2022
2a2495b
Merge branch 'main' into lists
Saadnajmi Jul 15, 2022
a089eda
Don't move keyboard focus with selection
Saadnajmi Jul 17, 2022
f4ccfdc
Update tags
Saadnajmi Jul 17, 2022
4453e75
Fix flow errors
Saadnajmi Jul 17, 2022
ae04c16
Update colors, make ScrollView focusable
Saadnajmi Jul 19, 2022
d4bdb68
prettier
Saadnajmi Jul 19, 2022
34d51e1
undo change
Saadnajmi Jul 19, 2022
010db4e
Fix flow errors
Saadnajmi Jul 19, 2022
b96c95e
Clean up code + handle page up/down with new prop
Saadnajmi Jul 19, 2022
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') {
chiuam marked this conversation as resolved.
Show resolved Hide resolved
this.scrollTo({x: 0, y: 0});
} else if (key === 'END') {
this.scrollToEnd({animated: true});
Saadnajmi marked this conversation as resolved.
Show resolved Hide resolved
}
}
}
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 @@ -1330,10 +1336,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 @@ -1352,11 +1360,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 @@ -1514,6 +1522,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 @@ -1528,61 +1543,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 @@ -2188,6 +2223,7 @@ class CellRenderer extends React.Component<
return React.createElement(ListItemComponent, {
item,
index,
isSelected,
separators: this._separators,
});
}
Expand Down Expand Up @@ -2265,6 +2301,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] &&
Saadnajmi marked this conversation as resolved.
Show resolved Hide resolved
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