Skip to content

Commit

Permalink
allow custom StickyHeader in ScrollView-based components (#25428)
Browse files Browse the repository at this point in the history
Summary:
This PR adds support for custom `StickyHeaderComponent` to be used in ScrollView (and by extension in FlatList, SectionList..).

Motivation: I've been working on a FlatList with hidable header that has a search field in it. Something like https://medium.com/appandflow/react-native-collapsible-navbar-e51a049b560a but using a FlatList w/ pull-to-refresh. The implementation can be found at https://snack.expo.io/vonovak/hidable-header-flatlist .

I used the `ListHeaderComponent` prop to render the custom header - as opposed to absolute positioning which is used in the linked article because I also need the loading indicator (I added `refreshing` and `onRefresh` for that) to show up above the header.
I proceeded by adding `stickyHeaderIndices={[0]}` to keep the header at the top, which seems to be the idiomatic way to do so. Then I added `Animated.View` with custom translation logic to the rendered header.

All appears to be working fine at the first sight - when you tap any item, you'll see it react to touch (red underlay). You'll also see the header becomes hidden if I scroll far enough and appears again after scrolling up. BUT - when you scroll down so that the header becomes hidden and tap the first visible item in the list, it will not react to touches! The reason is that `ScrollViewStickyHeader`

https://github.com/facebook/react-native/blob/9a84970c35d22b68fb3d8eac019c7f415a14c888/Libraries/Components/ScrollView/ScrollView.js#L984

has its own translation logic and when I tap onto the item at the top of the list, it seems like I'm tapping the item but I'm in fact tapping that `ScrollViewStickyHeader`.

I tried working around this by not specifying `stickyHeaderIndices={[0]}` and using `ListHeaderComponentStyle` prop (this needed some additional changes in https://github.com/facebook/react-native/blob/9a84970c35d22b68fb3d8eac019c7f415a14c888/Libraries/Lists/VirtualizedList.js#L786, and the animation is junky for some reason - as if the header always needed to "catch up" with the scroll offset, causing jitter) and `CellRendererComponent` (junky animations too), but concluded that allowing to specify custom `StickyHeaderComponent` is the cleanest way to make something like this work. I'm slightly surprised I needed to do all this to make such a usual pattern work - am I missing something?

## Changelog

[GENERAL] [ADDED] - allow custom StickyHeader in ScrollView-based components
Pull Request resolved: #25428

Test Plan: This is a minor change that should not break anything; tested locally.

Differential Revision: D16073016

Pulled By: cpojer

fbshipit-source-id: cdb878d12a426068dbaa9a54367c1190a6c55328
  • Loading branch information
vonovak authored and facebook-github-bot committed Jul 1, 2019
1 parent e7371b2 commit e6c7846
Show file tree
Hide file tree
Showing 2 changed files with 23 additions and 6 deletions.
27 changes: 22 additions & 5 deletions Libraries/Components/ScrollView/ScrollView.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import type {NativeMethodsMixinType} from '../../Renderer/shims/ReactNativeTypes
import type {ViewStyleProp} from '../../StyleSheet/StyleSheet';
import type {ViewProps} from '../View/ViewPropTypes';
import type {PointProp} from '../../StyleSheet/PointPropType';
import type {Props as ScrollViewStickyHeaderProps} from './ScrollViewStickyHeader';

import type {ColorValue} from '../../StyleSheet/StyleSheetTypes';
import type {State as ScrollResponderState} from '../ScrollResponder';
Expand Down Expand Up @@ -353,6 +354,10 @@ type VRProps = $ReadOnly<{|
scrollBarThumbImage?: ?($ReadOnly<{||}> | number), // Opaque type returned by import IMAGE from './image.jpg'
|}>;

type StickyHeaderComponentType = React.ComponentType<ScrollViewStickyHeaderProps> & {
setNextHeaderY: number => void,
};

export type Props = $ReadOnly<{|
...ViewProps,
...TouchableProps,
Expand Down Expand Up @@ -501,6 +506,13 @@ export type Props = $ReadOnly<{|
* with `horizontal={true}`.
*/
stickyHeaderIndices?: ?$ReadOnlyArray<number>,
/**
* A React Component that will be used to render sticky headers.
* To be used together with `stickyHeaderIndices` or with `SectionList`, defaults to `ScrollViewStickyHeader`.
* You may need to set this if your sticky header uses custom transforms (eg. translation),
* for example when you want your list to have an animated hidable header.
*/
StickyHeaderComponent?: StickyHeaderComponentType,
/**
* When set, causes the scroll view to stop at multiples of the value of
* `snapToInterval`. This can be used for paginating through children
Expand Down Expand Up @@ -670,7 +682,7 @@ class ScrollView extends React.Component<Props, State> {
0,
);
_scrollAnimatedValueAttachment: ?{detach: () => void} = null;
_stickyHeaderRefs: Map<number, ScrollViewStickyHeader> = new Map();
_stickyHeaderRefs: Map<string, StickyHeaderComponentType> = new Map();
_headerLayoutYs: Map<string, number> = new Map();

state = {
Expand Down Expand Up @@ -840,7 +852,7 @@ class ScrollView extends React.Component<Props, State> {
}
}

_setStickyHeaderRef(key, ref) {
_setStickyHeaderRef(key: string, ref: ?StickyHeaderComponentType) {
if (ref) {
this._stickyHeaderRefs.set(key, ref);
} else {
Expand Down Expand Up @@ -868,7 +880,9 @@ class ScrollView extends React.Component<Props, State> {
const previousHeader = this._stickyHeaderRefs.get(
this._getKeyForIndex(previousHeaderIndex, childArray),
);
previousHeader && previousHeader.setNextHeaderY(layoutY);
previousHeader &&
previousHeader.setNextHeaderY &&
previousHeader.setNextHeaderY(layoutY);
}
}

Expand Down Expand Up @@ -985,9 +999,12 @@ class ScrollView extends React.Component<Props, State> {
if (indexOfIndex > -1) {
const key = child.key;
const nextIndex = stickyHeaderIndices[indexOfIndex + 1];
const StickyHeaderComponent =
this.props.StickyHeaderComponent || ScrollViewStickyHeader;
return (
<ScrollViewStickyHeader
<StickyHeaderComponent
key={key}
// $FlowFixMe - inexact mixed is incompatible with exact React.Element
ref={ref => this._setStickyHeaderRef(key, ref)}
nextHeaderLayoutY={this._headerLayoutYs.get(
this._getKeyForIndex(nextIndex, childArray),
Expand All @@ -997,7 +1014,7 @@ class ScrollView extends React.Component<Props, State> {
inverted={this.props.invertStickyHeaders}
scrollViewHeight={this.state.layoutHeight}>
{child}
</ScrollViewStickyHeader>
</StickyHeaderComponent>
);
} else {
return child;
Expand Down
2 changes: 1 addition & 1 deletion Libraries/Components/ScrollView/ScrollViewStickyHeader.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import type {LayoutEvent} from '../../Types/CoreEventTypes';

const AnimatedView = AnimatedImplementation.createAnimatedComponent(View);

type Props = {
export type Props = {
children?: React.Element<any>,
nextHeaderLayoutY: ?number,
onLayout: (event: LayoutEvent) => void,
Expand Down

0 comments on commit e6c7846

Please sign in to comment.