Skip to content

[0.68] Deprecate onScrollKeyDown, refactor Flatlist selection logic #1374

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

Merged
merged 3 commits into from
Aug 25, 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
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