Skip to content

Commit

Permalink
Keyboard navigation in Flatlist (#1258)
Browse files Browse the repository at this point in the history
* 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>
  • Loading branch information
6 people authored Jul 19, 2022
1 parent 573eab0 commit 84c0863
Show file tree
Hide file tree
Showing 6 changed files with 171 additions and 111 deletions.
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 @@ -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] &&
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

0 comments on commit 84c0863

Please sign in to comment.