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
Add support for sticky header and fix static header issues with numColumns #129
Conversation
type: "InvariantViolation", | ||
}, | ||
stickyWhileHorizontalNotSupported: { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
FlatList does the same, these features don't make much sense when in horizontal mode
[other: string]: unknown; | ||
} | ||
|
||
export class PureComponentWrapper extends React.PureComponent<PureComponentWrapperProps> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Any props can be passed to this wrapper and it rerenders if any prop changes. This view can also show or hide the view on request.
renderAheadOffset: renderAheadOffset, | ||
windowSize: windowSize, | ||
isHorizontal: horizontal | ||
) | ||
else { return } | ||
|
||
let isNextCellVisible = isWithinBounds( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is only to make blank events more accurate. Existing compute is not impacted
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can we add this to the tests, too?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see that tests are not setup for iOS clearGaps method. It's not very straightforward, I'll track it in another issue. I've added the tests for Android. Good that you pointed this somehow I forgot.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Tracking here: #133
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yeah, let's not increase the scope of this PR, thanks!
@@ -16,6 +16,7 @@ module.exports = { | |||
"@shopify/jsx-no-hardcoded-content": "off", | |||
"@shopify/jest/no-vague-titles": "off", | |||
"@shopify/jsx-no-complex-expressions": "off", | |||
"@shopify/react-prefer-private-members": "off", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can have certain public APIs that are not used internally. This rule didn't make sense for a library like this.
android/src/main/kotlin/com/shopify/reactnative/recycler_flat_list/AutoLayoutShadow.kt
Show resolved
Hide resolved
blankOffsetAtStart = lastMinBound - actualScrollOffset | ||
blankOffsetAtEnd = actualScrollOffset + windowSize - renderOffset - lastMaxBound | ||
fun computeBlankFromGivenOffset(actualScrollOffset: Int, distanceFromWindowStart: Int, distanceFromWindowEnd: Int): Int { | ||
val actualScrollOffset = actualScrollOffset - offsetFromStart; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
By subtracting minY of autolayout from scroll offset we can fix most of the compute
val startOffset = if (alShadow.horizontal) left else top | ||
val endOffset = if (alShadow.horizontal) right else bottom | ||
|
||
val distanceFromWindowStart = kotlin.math.max(startOffset - scrollOffset, 0) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Visible portions of header and footer will be removed from the blank area visible on the screen.
ios/Sources/AutoLayoutView.swift
Outdated
/// Checks for overlaps or gaps between adjacent items and then applies a correction. | ||
/// Performance: RecyclerListView renders very small number of views and this is not going to trigger multiple layouts on the iOS side. | ||
private func clearGaps(for cellContainers: [CellContainer]) { | ||
var maxBound: CGFloat = 0 | ||
var minBound: CGFloat = CGFloat(Int.max) | ||
|
||
var maxBoundNextCell: CGFloat = 0 | ||
let correctedScrollOffset = scrollOffset - (horizontal ? self.frame.minX : self.frame.minY) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Accounting for offset from top in compute. I've tested the accuracy in iOS as well.
src/RecyclerFlatList.tsx
Outdated
* You can also include any padding that might have been added to the start of the list. RecyclerFlatList needs to this to determine | ||
* where the first item in the list starts from. | ||
*/ | ||
estimatedHeaderSize?: number; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is only required if someone wants to use initialScrollIndex and expect a lot of accuracy. It's rare but enabling that use case.
render() { | ||
if (this.state.dataProvider.getSize() === 0) { | ||
return this.props.ListEmptyComponent || null; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
cleaned up
}; | ||
} | ||
const { | ||
drawDistance, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
preventing props being passed to scrollview unless we want some of them to
{children} | ||
</AutoLayoutView> | ||
<> | ||
<PureComponentWrapper |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
header with no unnecessary renders using the new wrapper
> | ||
{children} | ||
</AutoLayoutView> | ||
<PureComponentWrapper |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
footer with no unnecessary renders using the new wrapper
); | ||
}; | ||
|
||
private itemContainer = (props, parentProps, children) => { | ||
return ( | ||
<ItemContainer {...props} index={parentProps.index}> | ||
<ItemContainer |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
styling fixes to support flex:1 in grid mode without breaking other layouts. Consistent with Flatlist now
public scrollToEnd(params?: { animated?: boolean | null | undefined }) { | ||
this.rlvRef?.scrollToEnd(Boolean(params?.animated)); | ||
} | ||
|
||
// eslint-disable-next-line @shopify/react-prefer-private-members | ||
// TODO: Improve accuracy with headers |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Will do this later #131
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As of now scrollToIndex doesn't account for header. I'll improve this as a separate PR.
android/src/main/kotlin/com/shopify/reactnative/recycler_flat_list/AutoLayoutShadow.kt
Show resolved
Hide resolved
renderAheadOffset: renderAheadOffset, | ||
windowSize: windowSize, | ||
isHorizontal: horizontal | ||
) | ||
else { return } | ||
|
||
let isNextCellVisible = isWithinBounds( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can we add this to the tests, too?
src/RecyclerFlatList.tsx
Outdated
@@ -34,6 +39,13 @@ export interface RecyclerFlatListProps<T> extends FlatListProps<T> { | |||
*/ | |||
estimatedListSize?: { height: number; width: number }; | |||
|
|||
/** | |||
* Provide estimated size of the header that is going to be rendered. This can help make initialScrollIndex prop more accurate. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In this case, how exactly will the inaccuracy manifest?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If will fall short by this exact value. Let's say you want to start at index 5 and each item is 100. So we want to start at 400px but if there's a header we need to start at header size + 400px.
This only impacts initial render. We can make it better however, right now I've just provided a way to handle it. In future we can try getting rid of this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree - could you create an issue for this if there already isn't one? I'm ok with the limitation for now.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
src/RecyclerFlatList.tsx
Outdated
@@ -117,6 +132,9 @@ class RecyclerFlatList<T> extends React.PureComponent< | |||
this.listFixedDimensionSize = props.estimatedListSize.width; | |||
} | |||
} | |||
this.distanceFromWindow = props.estimatedHeaderSize; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd rather keep the RecyclerFlatList
named estimatedHeaderSize
and do the translation to how recycler-list-view
calls it as late as possible. To me, it's then more obvious why we rename it. But subjective for sure)
Co-authored-by: Marek Fořt <marek.fort@shopify.com>
Co-authored-by: Marek Fořt <marek.fort@shopify.com>
@fortmarek I've addressed the comments. Regarding header size I've renamed it. I want to keep it as a workaround but I do want to explore alternative solutions. It's just not a big priority right now. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is great, thanks Talha!
import GridLayoutProviderWithProps from "./GridLayoutProviderWithProps"; | ||
import CustomError from "./errors/CustomError"; | ||
import ExceptionList from "./errors/ExceptionList"; | ||
|
||
interface StickyProps extends StickyContainerProps { | ||
children: any; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
could this be JSX.Element
?
Thanks for the review @fortmarek. I've created another issue to track |
Fixes header implementation and adds Sticky headers.
Changes
Tophatting
I've tested the changes in Android/iOS. You can add header or pass sticky indices to existing samples.
resolves #95
resolves #44
resolves #96