-
-
Notifications
You must be signed in to change notification settings - Fork 3.1k
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 custom element option to WindowScroller #481
Add custom element option to WindowScroller #481
Conversation
@@ -0,0 +1,3 @@ | |||
.disabled > * { | |||
pointer-events: none !important; |
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’m using this instead of inline styles because a custom scroll container may have many children that need pointer-events: none
, whereas scrolling the window only needs to apply that style to body
. So, I wasn’t sure whether to add !important
or not. It ought to work pretty similarly to the way it used to (replacing inline styles), and it is still overrideable (higher-specificity selectors with !important
, or inline styles with !important
). On the other hand, I hate it when libraries use !important
.
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.
Do you actually mean .disabled *
?
Hm... either way, > *
is not a very performant selector so I'm pretty reluctant to use it, and definitely not with such a generic class name as disabled
. RV classes are named so as to (hopefully) not conflict with any other global CSS names.
Also related- RV has also been shifting away from any CSS-based styles (with the exception of Table
). Basically anything that's functional is defined via an inline style; anything that's presentational (only Table
) is defined via CSS to make it easier to override in a variety of ways (without requiring use of !important
).
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 I understand correctly, the intention is to disable pointer events on the contents of whatever element is scrolling, without disabling them on the scrolling element itself. So if the contents can now be N number of nodes, either we use CSS to select all children, or we iterate over all N nodes to add/remove inline styles.
To the initial question, .disabled > *
and .disabled *
should have the same effect, and I didn't expect the latter to be more efficient, but that’s fine.
And as far as the name goes, we can definitely change it—since the demo was using CSS Modules, and I'm using CSS Modules in my own stuff, I totally didn't think about it. Another option would be to dynamically generate/insert a stylesheet, but that would be a lot more magic; harder for developers to track down if they have the need to customize behavior.
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.
To the initial question,
.disabled > *
and.disabled *
should have the same effect, and I didn't expect the latter to be more efficient, but that’s fine.
No. The first disables pointer events only for immediate children. The latter disables for all descendants. I wasn't sure which you were going for. (The second is more expensive, so I wasn't recommending it- just asking for clarification.) First would only work if the Grid
(or List
or whatever) was an immediate child of the new scrollElement
.
And as far as the name goes, we can definitely change it—since the demo was using CSS Modules, and I'm using CSS Modules in my own stuff, I totally didn't think about it.
Yup, the demo uses CSS modules (love 'em) but the lib itself doesn't in order to be as flexible and compatible with the various approaches to styling as possible.
Another option would be to dynamically generate/insert a stylesheet, but that would be a lot more magic; harder for developers to track down if they have the need to customize behavior.
Agreed. I don't think this is a good path to go down.
Hmm...it's been a long day and I'm tired, but...I wonder if it would be more appropriate to approach this by passing a prop (eg disablePointerEvents
) to Grid
telling it to disable pointer events. That would be more performant than this CSS approach. Basically this line would change to something like:
pointerEvents: this.state.isScrolling || this.props.disablePointerEvents ? 'none' : '',
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.
The first disables pointer events only for immediate children. The latter disables for all descendants.
Well, yeah, but pointer-events
is inherited, which I guess is what I was counting on there—analogous to WindowScroller in master counting on elements to inherit it from document.body
.
I wonder if it would be more appropriate to approach this by passing a prop (eg disablePointerEvents) to Grid telling it to disable pointer events.
That works, but it is different than the existing behavior. WindowScroller in current master
disables pointer-events on document.body
, e.g. all the stuff inside the scroll container. My CSS (while obviously not being feasible) accomplishes exactly that for arbitrary scroll containers. What you’re suggesting would narrow the scope of what gets pointer-events: none
down to just the grid/list, not the whole scroll container. That seems like a reasonable trade-off to me, but I don’t have the same prior knowledge as you about how pointer-events
effects performance of this whole system. If you’re ok with that trade-off, I can move forward with implementing it.
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.
pointer-evetns
is an Inherited style, yes, but it can be overridden by children. And Grid
would override, since it sets the property on the ReactVirtualized__Grid__innerScrollContainer
, which is not the direct child of WindowScroller
but- at best- a child of its child.
Also using !important
is kind of a last resort, so I like to avoid it.
static contextTypes = { | ||
list: PropTypes.instanceOf(Immutable.List).isRequired | ||
list: PropTypes.instanceOf(Immutable.List).isRequired, | ||
customElement: PropTypes.object, |
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 an earlier commit, I had PropTypes.instanceOf(Element)
, but the linter complained about that, and I realized that it’s probably for good reason: Element
won’t be defined on servers, and this is a static property. So, not sure if there’s something better than object
here? I guess we could make a custom validator function which, when invoked, looks at window.Element
but gracefully does nothing on server-side render?
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
type is probably okay here. It's not great, but even the official Flow types use any
for ref
type.
* Handles edge-case where a user is navigating back (history) from an already-scrolled page. | ||
* In this case the body’s top position will be a negative number and this element’s top will be increased (by that amount). | ||
*/ | ||
export function getPositionFromTop (element, container) { |
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 was having trouble wrapping my mind around this (previously here), so extra care in making sure this satisfies the original intention is appreciated.
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.
Okay. When I do a real review, I'll read this one carefully.
@@ -1,5 +1,5 @@ | |||
.ContentBox { | |||
flex: 1; | |||
flex: 1 0 auto; |
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 obviously not directly related to the source changes, but something to this effect was required to make my addition to the demo work (a ContentBox was shrinking and scrolling, rather than .Body
). I spot checked the other demo changes and didn't see anything obviously broken. Removing overflow: auto
has the same effect, if that is preferable to you.
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.
No strong feelings here. That's fine. 😄
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.
Don't have time to do a full code review now but thought I'd share some quick thoughts.
By the way, thanks a ton for contributing. Very exciting. 😁
@@ -0,0 +1,3 @@ | |||
.disabled > * { | |||
pointer-events: none !important; |
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.
Do you actually mean .disabled *
?
Hm... either way, > *
is not a very performant selector so I'm pretty reluctant to use it, and definitely not with such a generic class name as disabled
. RV classes are named so as to (hopefully) not conflict with any other global CSS names.
Also related- RV has also been shifting away from any CSS-based styles (with the exception of Table
). Basically anything that's functional is defined via an inline style; anything that's presentational (only Table
) is defined via CSS to make it easier to override in a variety of ways (without requiring use of !important
).
static contextTypes = { | ||
list: PropTypes.instanceOf(Immutable.List).isRequired | ||
list: PropTypes.instanceOf(Immutable.List).isRequired, | ||
customElement: PropTypes.object, |
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
type is probably okay here. It's not great, but even the official Flow types use any
for ref
type.
* Handles edge-case where a user is navigating back (history) from an already-scrolled page. | ||
* In this case the body’s top position will be a negative number and this element’s top will be increased (by that amount). | ||
*/ | ||
export function getPositionFromTop (element, container) { |
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.
Okay. When I do a real review, I'll read this one carefully.
@@ -1,5 +1,5 @@ | |||
.ContentBox { | |||
flex: 1; | |||
flex: 1 0 auto; |
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.
No strong feelings here. That's fine. 😄
Tests were failing due to missing |
Back to the CSS issue, I see we definitely have to do something different. I totally wasn't thinking and did |
@@ -1,5 +1,5 @@ | |||
import styles from '../WindowScroller.css' |
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.
Not using CSS modules in RV proper
I'm intentionally not using CSS modules within react-virtualized because I don't want to impose that requirement on lib users. Things started failing b'c you are importing a stylesheet to use it this way and the build isn't setup to. (Static style names are used.) |
* When the scroll position is updated internally (i.e. the inner container is scrolled), it previously updated the internal scroll state for both axes, which overwrote the scroll position that was passed in from props * If one axis was controlled externally (e.g. by WindowScroller), that axis's scroll position was being overwritten * This fix stops the internal state of an axis from being modified if an external scroll position is supplied for that axis * Additionally, when the external scroll position is modified (props are updated), the scroll direction is now updated, so that overscanning works for an axis that is controlled, e.g. by a WindowScroller
…it's supported by default
@bvaughn don't want to be a bother, but any thoughts on my last comments? |
No worries, I'll look into it tomorrow! Definitely need to get that fixed. Thanks for reviewing! |
❤️ thank you for understanding. |
@bvaughn fixed the bug you were seeing—good catch. When WindowScroller’s Above, I fixed the issue by making sure that scroll container always grows to fill the window, regardless of its contents. However, do you think this is a good case for using |
…er handles window resize events.
…the behavior regarding controlled/uncontrolled and props and in a way that was not backwards compatible. (See issue #490 for more.)
Nice work on fixing that bug. I resumed the code review and left some more thoughts. I also merged in To save you a little time, I've pushed the merged result here: https://github.com/bvaughn/react-virtualized/tree/andrewbranch-window-scroller-custom-element |
} else if (!nextProps.scrollElement && this.scrollElement !== window) { | ||
this._updateDimensions(window) | ||
unregisterScrollListener(this, this.scrollElement) | ||
registerScrollListener(this, window) |
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.
Am I reading this wrong, or could we reduce the complexity of this a bit?
if (nextProps.scrollElement !== this.scrollElement) {
this._updateDimensions(nextProps.scrollElement || window)
unregisterScrollListener(this, this.scrollElement)
registerScrollListener(this, nextProps.scrollElement || window)
}
@@ -92,7 +113,7 @@ export default class WindowScroller extends Component { | |||
_onResizeWindow (event) { | |||
const { onResize } = this.props | |||
|
|||
const height = window.innerHeight || 0 | |||
const height = getHeight(this.scrollElement) |
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 should probably call this._updateDimensions()
here instead? That will handle cases where a resize affects top position (due to things like text wrapping) and it will also only call setState
if the height
has actually changed (eg not just a horizontal resize).
The onResize
call should also be moved into this helper method for similar reasons (and to avoid duplication).
_onResizeWindow (event) {
this._updateDimensions()
}
} | ||
|
||
render () { | ||
const { list } = this.context | ||
const { list, isScrollingCustomElement, customElement } = this.context |
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 know this is probably necessary to test the change within the demo site, but I'm not a fan of the complexity this adds to the WindowScroller.example
or Application
components. I wonder if it wouldn't be better to leave this particular functionality out of the demo and just cover it via unit tests instead?
Speaking of which 😁 this change set should be accompanied by a couple of new unit tests so others (including myself) don't break it in the future.
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.
Totally. I was hoping to get your thoughts on what/how to test. Was wondering if some of the existing WindowScroller tests could be extracted into a function and be called once for each "mode" (scrolling window
and scrolling scrollElement
). Does that make sense or should each new test be written out distinctly? Do you want to duplicate some of the basic assertions to make sure WindowScroller as a whole doesn’t mess up with scrollElement
, or should the new tests only address the specific added functionality?
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.
One way I test things like this is by iterating over a set of options and creating my tests (the describe
block) inside of a loop. eg
for (let i = 0; i < 2; i++) {
describe('WindowScroller', () => {
const useCustomElement = !!i;
// Tests here ...
});
}
Rather than extracting tests into helper functions. I'm not convinced that's the appropriate way to handle this case though. But new functionality like this absolutely needs new test coverage. 😄
if (height !== newHeight) { | ||
this.setState({ | ||
height: newHeight | ||
}) |
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 probably the place to call the onResize
prop too.
Current coverage is 98.20% (diff: 94.54%)@@ master #481 diff @@
==========================================
Files 61 62 +1
Lines 4338 4404 +66
Methods 916 928 +12
Messages 0 0
Branches 331 339 +8
==========================================
+ Hits 4263 4325 +62
- Misses 75 79 +4
Partials 0 0
|
@@ -26,5 +26,5 @@ export function getHeight (element) { | |||
*/ | |||
export function getPositionFromTop (element, container) { | |||
const containerElement = container === window ? document.documentElement : container | |||
return element.getBoundingClientRect().top - containerElement.getBoundingClientRect().top | |||
return element.getBoundingClientRect().top + getVerticalScroll(container) - containerElement.getBoundingClientRect().top |
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 fixes a bug which can be reproduced in the demo, before this commit, by
- Check the "use custom element for scrolling" box
- Press Shift + Tab twice so that the "Hide header text" button is focused
- Scroll down a ways
- Press the space bar so as to press "Hide header text"
- Scroll again
I also made sure that the back-button navigation edge case still works.
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.
@bvaughn Can you check this out, because it ostensibly fixes bugs, but breaks some tests 😭
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.
@bvaughn Any chance you could look at this sometime soonish? Too long in PRgatory makes me anxious 😅
Previously, given two mounted WindowScrollers with different scrollElements, when either scrollElement was scrolled, both WindowScrollers were updated with { isScrolling: true }.
I'm so sorry. Promise I'll try to make time to look this weekend. Please ping me again if you haven't heard back from my by Saturday. |
PS Thanks for keeping on top of this! |
No worries! I didn't want to bother you until after the holidays. Maintaining popular open source things sounds hard 😁 |
That's very understanding of you 😄 Let's get this done this weekend. We'll talk more soon! |
Looks like this branch has diverged from master a bit, unfortunately. There are changes showing up in the diffs that aren't related, and some things committed to master haven't made it into this branch either. My fault for taking so long to review it but it is what it is. If I checkout this branch as-is and merge in master, 4 tests fail:
These tests were added in 2e91ec7 which was made after your branch and did not merged in. I'll take a stab at reconciling them this morning because I know we'd both really like this PR to get merged. 😄 |
Merged in 5cc2ccb, after conflicts resolved. Thanks! |
Okay @andrewbranch. This was just released in 8.10.0. Thanks again for your work! If you'd be willing, I'd still like to get some test coverage for this new feature maybe as a follow-up PR. If you can't get to it I'll find some time to add them. Cheers! |
🎉 Thanks for all the help! I can probably add some tests in the near-ish future. Cheers! |
That would be great. Thanks! :) |
In reference to #474
Adds an optional prop
scrollElement
to WindowScroller which, if provided, attaches scroll handlers to the given element instead of towindow
.@bvaughn I “tested” this via adding an option to the demo (although maybe you’d like to change the way I did it—added a couple things to Application’s
context
—I’m not familiar with HashRouter, but maybe it can be done through props instead?) and it seems to work nicely. I’m happy to help add tests and docs, but wanted to let you take a look at it as soon as possible, and also get your feedback on the best way to add some tests for this. It almost seems like a big group of the tests could just be run twice, once forwindow
and once for some custom element.There are a couple things I wasn’t sure about, so I’ll start adding a few inline comments to ask about specific things.