Skip to content

Commit

Permalink
[0.68] Deprecate onScrollKeyDown, refactor Flatlist selection logic (f…
Browse files Browse the repository at this point in the history
…acebook#1374)

* Refactor handling of keyDown/keyUp (facebook#1338)

This refactors / simplifies certain keyUp|Down event handling.
It will make a later change (adding textInput handling for textInput fields) easier (to review)

Co-authored-by: Scott Kyle <skyle@fb.com>

* Deprecate onScrollKeyDown, refactor Flatlist selection logic (facebook#1365)

* Deprecate onScrollKeyDown

remove pressable diff

Remove JS handling for PageUp/Down, fix flow errors

Add back "autoscroll to focused view" behavior

remove commented code

remove change to pressable

Update documentation

fix flow error

fix lint issue

Fix 'selectRowAtIndex'

More simplification

lock

* Make method public again

* Add initialSelectedIndex

* macOS tags

* fix lint

* RNTester: only show the Flatlist keyboard navigation switch on macOS

Co-authored-by: Christoph Purrer <christophpurrer@outlook.com>
Co-authored-by: Scott Kyle <skyle@fb.com>
  • Loading branch information
3 people authored Aug 25, 2022
1 parent 23d7d88 commit 247f915
Show file tree
Hide file tree
Showing 22 changed files with 293 additions and 565 deletions.
45 changes: 0 additions & 45 deletions Libraries/Components/ScrollView/ScrollView.js
Original file line number Diff line number Diff line change
Expand Up @@ -1184,50 +1184,6 @@ class ScrollView extends React.Component<Props, State> {
}

// [TODO(macOS GH#774)
_handleKeyDown = (event: ScrollEvent) => {
if (this.props.onScrollKeyDown) {
this.props.onScrollKeyDown(event);
} else {
if (Platform.OS === 'macos') {
const nativeEvent = event.nativeEvent;
const key = nativeEvent.key;
const kMinScrollOffset = 10;
if (key === 'PAGE_UP') {
this._handleScrollByKeyDown(event, {
x: nativeEvent.contentOffset.x,
y:
nativeEvent.contentOffset.y +
-nativeEvent.layoutMeasurement.height,
});
} else if (key === 'PAGE_DOWN') {
this._handleScrollByKeyDown(event, {
x: nativeEvent.contentOffset.x,
y:
nativeEvent.contentOffset.y +
nativeEvent.layoutMeasurement.height,
});
} else if (key === 'HOME') {
this.scrollTo({x: 0, y: 0});
} else if (key === 'END') {
this.scrollToEnd({animated: true});
}
}
}
};

_handleScrollByKeyDown = (event: ScrollEvent, newOffset) => {
const maxX =
event.nativeEvent.contentSize.width -
event.nativeEvent.layoutMeasurement.width;
const maxY =
event.nativeEvent.contentSize.height -
event.nativeEvent.layoutMeasurement.height;
this.scrollTo({
x: Math.max(0, Math.min(maxX, newOffset.x)),
y: Math.max(0, Math.min(maxY, newOffset.y)),
});
};

_handlePreferredScrollerStyleDidChange = (event: ScrollEvent) => {
this.setState({contentKey: this.state.contentKey + 1});
}; // ]TODO(macOS GH#774)
Expand Down Expand Up @@ -1783,7 +1739,6 @@ class ScrollView extends React.Component<Props, State> {
// Override the onContentSizeChange from props, since this event can
// bubble up from TextInputs
onContentSizeChange: null,
onScrollKeyDown: this._handleKeyDown, // TODO(macOS GH#774)
onPreferredScrollerStyleDidChange:
this._handlePreferredScrollerStyleDidChange, // TODO(macOS GH#774)
onLayout: this._handleLayout,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ exports[`<ScrollView /> should render as expected: should deep render when not m
onScroll={[Function]}
onScrollBeginDrag={[Function]}
onScrollEndDrag={[Function]}
onScrollKeyDown={[Function]}
onScrollShouldSetResponder={[Function]}
onStartShouldSetResponder={[Function]}
onStartShouldSetResponderCapture={[Function]}
Expand Down
6 changes: 0 additions & 6 deletions Libraries/Components/View/ViewPropTypes.js
Original file line number Diff line number Diff line change
Expand Up @@ -76,12 +76,6 @@ type DirectEventProps = $ReadOnly<{|
*/
onPreferredScrollerStyleDidChange?: ?(event: ScrollEvent) => mixed, // TODO(macOS GH#774)

/**
* When `focusable` is true, the system will try to invoke this function
* when the user performs accessibility key down gesture.
*/
onScrollKeyDown?: ?(event: ScrollEvent) => mixed, // TODO(macOS GH#774)

/**
* Invoked on mount and layout changes with:
*
Expand Down
24 changes: 24 additions & 0 deletions Libraries/Lists/FlatList.js
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,24 @@ type OptionalProps<ItemT> = {|
* Optional custom style for multi-item rows generated when numColumns > 1.
*/
columnWrapperStyle?: ViewStyleProp,
// [TODO(macOS GH#774)
/**
* Allows you to 'select' a row using arrow keys. The selected row will have the prop `isSelected`
* passed in as true to it's renderItem / ListItemComponent. You can also imperatively select a row
* using the `selectRowAtIndex` method. You can set the initially selected row using the
* `initialSelectedIndex` prop.
* Keyboard Behavior:
* - ArrowUp: Select row above current selected row
* - ArrowDown: Select row below current selected row
* - Option+ArrowUp: Select the first row
* - Opton+ArrowDown: Select the last 'realized' row
* - Home: Scroll to top of list
* - End: Scroll to end of list
*
* @platform macos
*/
enableSelectionOnKeyPress?: ?boolean,
// ]TODO(macOS GH#774)
/**
* A marker property for telling the list to re-render (since it implements `PureComponent`). If
* any of your `renderItem`, Header, Footer, etc. functions depend on anything outside of the
Expand Down Expand Up @@ -111,6 +129,12 @@ type OptionalProps<ItemT> = {|
* `getItemLayout` to be implemented.
*/
initialScrollIndex?: ?number,
// [TODO(macOS GH#774)
/**
* The initially selected row, if `enableSelectionOnKeyPress` is set.
*/
initialSelectedIndex?: ?number,
// ]TODO(macOS GH#774)
/**
* Reverses the direction of scroll. Uses scale transforms of -1.
*/
Expand Down
200 changes: 94 additions & 106 deletions Libraries/Lists/VirtualizedList.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ import type {
ViewToken,
ViewabilityConfigCallbackPair,
} from './ViewabilityHelper';
import type {ScrollEvent} from '../Types/CoreEventTypes'; // TODO(macOS GH#774)
import type {KeyEvent} from '../Types/CoreEventTypes'; // TODO(macOS GH#774)
import {
VirtualizedListCellContextProvider,
VirtualizedListContext,
Expand Down Expand Up @@ -109,12 +109,24 @@ type OptionalProps = {|
* this for debugging purposes. Defaults to false.
*/
disableVirtualization?: ?boolean,
// [TODO(macOS GH#774)
/**
* Handles key down events and updates selection based on the key event
* Allows you to 'select' a row using arrow keys. The selected row will have the prop `isSelected`
* passed in as true to it's renderItem / ListItemComponent. You can also imperatively select a row
* using the `selectRowAtIndex` method. You can set the initially selected row using the
* `initialSelectedIndex` prop.
* Keyboard Behavior:
* - ArrowUp: Select row above current selected row
* - ArrowDown: Select row below current selected row
* - Option+ArrowUp: Select the first row
* - Opton+ArrowDown: Select the last 'realized' row
* - Home: Scroll to top of list
* - End: Scroll to end of list
*
* @platform macos
*/
enableSelectionOnKeyPress?: ?boolean, // TODO(macOS GH#774)
enableSelectionOnKeyPress?: ?boolean,
// ]TODO(macOS GH#774)
/**
* A marker property for telling the list to re-render (since it implements `PureComponent`). If
* any of your `renderItem`, Header, Footer, etc. functions depend on anything outside of the
Expand Down Expand Up @@ -145,6 +157,12 @@ type OptionalProps = {|
* `getItemLayout` to be implemented.
*/
initialScrollIndex?: ?number,
// [TODO(macOS GH#774)
/**
* The initially selected row, if `enableSelectionOnKeyPress` is set.
*/
initialSelectedIndex?: ?number,
// ]TODO(macOS GH#774)
/**
* Reverses the direction of scroll. Uses scale transforms of -1.
*/
Expand Down Expand Up @@ -782,7 +800,7 @@ class VirtualizedList extends React.PureComponent<Props, State> {
(this.props.initialScrollIndex || 0) +
initialNumToRenderOrDefault(this.props.initialNumToRender),
) - 1,
selectedRowIndex: 0, // TODO(macOS GH#774)
selectedRowIndex: this.props.initialSelectedIndex || -1, // TODO(macOS GH#774)
};

if (this._isNestedWithSameOrientation()) {
Expand Down Expand Up @@ -845,7 +863,7 @@ class VirtualizedList extends React.PureComponent<Props, State> {
),
last: Math.max(0, Math.min(prevState.last, getItemCount(data) - 1)),
selectedRowIndex: Math.max(
0,
-1, // Used to indicate no row is selected
Math.min(prevState.selectedRowIndex, getItemCount(data)),
), // TODO(macOS GH#774)
};
Expand Down Expand Up @@ -1309,14 +1327,16 @@ class VirtualizedList extends React.PureComponent<Props, State> {
}

_defaultRenderScrollComponent = props => {
let keyEventHandler = this.props.onScrollKeyDown; // [TODO(macOS GH#774)
if (!keyEventHandler) {
keyEventHandler = this.props.enableSelectionOnKeyPress
? this._handleKeyDown
: null;
}
// [TODO(macOS GH#774)
const preferredScrollerStyleDidChangeHandler =
this.props.onPreferredScrollerStyleDidChange; // ]TODO(macOS GH#774)
this.props.onPreferredScrollerStyleDidChange;

const keyboardNavigationProps = {
focusable: true,
validKeysDown: ['ArrowUp', 'ArrowDown', 'Home', 'End'],
onKeyDown: this._handleKeyDown,
};
// ]TODO(macOS GH#774)
const onRefresh = props.onRefresh;
if (this._isNestedWithSameOrientation()) {
// $FlowFixMe[prop-missing] - Typing ReactNativeComponent revealed errors
Expand All @@ -1333,8 +1353,7 @@ class VirtualizedList extends React.PureComponent<Props, State> {
<ScrollView
{...props}
// [TODO(macOS GH#774)
{...(props.enableSelectionOnKeyPress && {focusable: true})}
onScrollKeyDown={keyEventHandler}
{...(props.enableSelectionOnKeyPress && keyboardNavigationProps)}
onPreferredScrollerStyleDidChange={
preferredScrollerStyleDidChangeHandler
} // TODO(macOS GH#774)]
Expand All @@ -1356,8 +1375,8 @@ class VirtualizedList extends React.PureComponent<Props, State> {
// $FlowFixMe Invalid prop usage
<ScrollView
{...props}
{...(props.enableSelectionOnKeyPress && {focusable: true})} // [TODO(macOS GH#774)
onScrollKeyDown={keyEventHandler}
// [TODO(macOS GH#774)
{...(props.enableSelectionOnKeyPress && keyboardNavigationProps)}
onPreferredScrollerStyleDidChange={
preferredScrollerStyleDidChangeHandler
} // TODO(macOS GH#774)]
Expand Down Expand Up @@ -1510,98 +1529,11 @@ class VirtualizedList extends React.PureComponent<Props, State> {
};

// [TODO(macOS GH#774)
_selectRowAboveIndex = rowIndex => {
const rowAbove = rowIndex > 0 ? rowIndex - 1 : rowIndex;
this.setState(state => {
return {selectedRowIndex: rowAbove};
});
return rowAbove;
};

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

_selectRowBelowIndex = rowIndex => {
if (this.props.getItemCount) {
const {data} = this.props;
const itemCount = this.props.getItemCount(data);
const rowBelow = rowIndex < itemCount - 1 ? rowIndex + 1 : rowIndex;
this.setState(state => {
return {selectedRowIndex: rowBelow};
});
return rowBelow;
} else {
return rowIndex;
}
};

_handleKeyDown = (event: ScrollEvent) => {
if (this.props.onScrollKeyDown) {
this.props.onScrollKeyDown(event);
} else {
if (Platform.OS === 'macos') {
// $FlowFixMe Cannot get e.nativeEvent because property nativeEvent is missing in Event
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 === 'UP_ARROW') {
newIndex = this._selectRowAboveIndex(prevIndex);
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 = 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});
}
}
}
};
const prevIndex = this.state.selectedRowIndex;
const newIndex = rowIndex;
this.setState({selectedRowIndex: newIndex});

_handleSelectionChange = (prevIndex, newIndex) => {
this.ensureItemAtIndexIsVisible(newIndex);
if (prevIndex !== newIndex) {
const item = this.props.getItem(this.props.data, newIndex);
Expand All @@ -1613,6 +1545,62 @@ class VirtualizedList extends React.PureComponent<Props, State> {
});
}
}

return newIndex;
};

_selectRowAboveIndex = rowIndex => {
const rowAbove = rowIndex > 0 ? rowIndex - 1 : rowIndex;
this._selectRowAtIndex(rowAbove);
};

_selectRowBelowIndex = rowIndex => {
const rowBelow = rowIndex < this.state.last ? rowIndex + 1 : rowIndex;
this._selectRowAtIndex(rowBelow);
};

_handleKeyDown = (event: KeyEvent) => {
if (Platform.OS === 'macos') {
this.props.onKeyDown?.(event);
if (event.defaultPrevented) {
return;
}

const nativeEvent = event.nativeEvent;
const key = nativeEvent.key;

let selectedIndex = -1;
if (this.state.selectedRowIndex >= 0) {
selectedIndex = this.state.selectedRowIndex;
}

if (key === 'ArrowUp') {
if (nativeEvent.altKey) {
// Option+Up selects the first element
this._selectRowAtIndex(0);
} else {
this._selectRowAboveIndex(selectedIndex);
}
} else if (key === 'ArrowDown') {
if (nativeEvent.altKey) {
// Option+Down selects the last element
this._selectRowAtIndex(this.state.last);
} else {
this._selectRowBelowIndex(selectedIndex);
}
} else if (key === 'Enter') {
if (this.props.onSelectionEntered) {
const item = this.props.getItem(this.props.data, selectedIndex);
if (this.props.onSelectionEntered) {
this.props.onSelectionEntered(item);
}
}
} else if (key === 'Home') {
this.scrollToOffset({animated: true, offset: 0});
} else if (key === 'End') {
this.scrollToEnd({animated: true});
}
}
};
// ]TODO(macOS GH#774)

Expand Down
Loading

0 comments on commit 247f915

Please sign in to comment.