Skip to content
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

Fix scrolling on execution and switching notebook mode #15702

Merged

Conversation

krassowski
Copy link
Member

@krassowski krassowski commented Jan 26, 2024

References

Fixes #15693, solving two regressions introduced in 4.1 cycle:

  • fixes erratic scrolling after Shift+Enter in non-windowing modes (defer, none),
  • fixes too much scrolling after Ctrl+Enter in both windowing and non-windowing modes (Chromium-based browsers, Firefox not affected).

Code changes

  • corrects the node used for scrolling and padding computation - fixing a regression due to change in DOM structure introduced by Add virtual scrollbar component to windowed lists. #15533 (see the use of _outerElement)
  • fixes behaviour of setting command mode which was causing a spurious scroll before running cells due to focus triggering a browser scroll - regression introduced by Fix tab trap notebook cells #14115 (see preventScrollOnFocus)
  • the logic for scroll offset computation is now unified between windowing modes; when windowing is disabled the actual dimensions of the notebook and cell are used (see precomputed argument).
  • repeats scrolling if notebook area resizes within 100ms of scroll request in non-windowing mode; this ensures that if a very brief change in dimensions due to output of the previously active cell hiding and re-appearing (in Shift + Enter scenario) shifts the newly active cell out of the viewport, that cell will be scrolled to again. This was previously used (with 50ms) in windowed mode which is why Shift + Enter worked there.

The last point might be better solved by changing when scroll gets called when a cell is run. Currently the sequence is:

  • schedule cell execution (clears output)
  • switch to next cell
  • scroll if needed
  • return a promise which resolves when cell(s) executed

This does not work great when executing a cell which has long output in the following scenario:

  • schedule cell execution
  • switch to next cell
  • scroll if needed: decides no scrolling is needed
  • execution completes, output is populated
  • the newly added output shifts the next cell out of view (bad)

Therefore, it might be tempting to change this sequence to:

  • schedule cell execution
  • switch to next cell
  • await execution to complete (and output to be populated)
  • scroll to the output

But this would lead to surprising results if execution takes longer than ~100ms because a user could manually scroll away and be surprised that once the cell(s) completed execution, they are snapped back to the cell after the last executed cell.

Thus we would also need to cancel the pending scroll to next cell if user has already scrolled the view.

User-facing changes

First, spurious scrolling is eliminated if the following cell (for shift + enter) or current cell (for ctrl + enter) is significantly visible (see examples below)

Shift + Enter: short cell → long cell

4.1.0b2 (defer mode) This PR (defer mode) 3.6.x (reference)
shift-enter-short-long-defer-mode-4 1 shift-enter-short-long-defer-mode-PR shift-enter-short-long-3 6

Note: this already worked in full mode since 4.0.9+ thanks to #15288 (see details).

4.1.0b2 (full mode) This PR (full mode) 4.0.10 (full mode)
shift-enter-short-long-full-mode-4 1 shift-enter-short-long-full-mode-PR shift-enter-short-long-full-mode-4 0

Ctrl + Enter: long cell (partially out of view), Chrome

4.1.0b2 (defer mode) This PR (defer mode) 3.6.x (reference)
ctrl-enter-long-defer-mode-4 1-chrome ctrl-enter-long-defer-mode-PR-chrome ctrl-enter-long-3 6-chrome

Note: this was also broken in full, and this PR also fixes it in the full mode (see details).

4.1.0b2 (full mode) This PR (full mode)
ctrl-enter-long-full-mode-4 1-chrome ctrl-enter-long-full-mode-PR-chrome

Second, when execution with Shift + Enter invokes scrolling to next cell which was out of viewport, that cell will be positioned so that its top is in the middle of the viewport.

Backwards-incompatible changes

None

@krassowski krassowski added this to the 4.1.0 milestone Jan 26, 2024
Copy link

Thanks for making a pull request to jupyterlab!
To try out this branch on binder, follow this link: Binder

@krassowski krassowski added the bug label Jan 26, 2024
@krassowski krassowski changed the title Fix scroll in non windowed mode Fix scrolling on execution Jan 26, 2024
Comment on lines 662 to 665
// If a cell is outside of viewport and scrolling is needed,
// prefer to align the cell to the top viewport edge,
// rather than to the bottom (so that editor is visible)
void Private.handleRunState(notebook, state, true, 'start');
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is leading to the should scroll as little as possible on markdown rendering test failing, which highlights trade-offs of different approaches, compare:

image

with alternative:

image

and with:

image

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems to me that we would want to restore the JupyterLab 3 behaviour here, maybe a variation of it (say "top edge in the 1/4 of the viewport from the top").

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So the problem with the earlier implementation (before #15288) was not that auto alignment differed from Lab 3.6, just that "center" was implemented as "align the middles of the cell and the viewport". This obviously led to very annoying behaviour for cells longer than viewport as seemingly random part of the output was shown:

image

I will push a commit to fix that behaviour, which should simplify the code too.

@krassowski
Copy link
Member Author

I now see that the fault of cell scrolling on switching to command mode affects many more actions than just running cells; for example pressing Esc in edit mode for a cell partially out of view would scroll, opening context menu over such a cell would scroll, etc. I will try to switch scroll on focus behaviour off by default.

@krassowski krassowski force-pushed the fix-scroll-in-non-windowed-mode branch from 6b37a7c to 133a55c Compare January 29, 2024 12:37
if (this.viewModel.windowingActive) {
if (!this._itemsResizeObserver) {
this._itemsResizeObserver = new ResizeObserver(
this._onItemResize.bind(this)

Check warning

Code scanning / CodeQL

Superfluous trailing arguments

Superfluous argument passed to [default constructor of class ResizeObserverMock](1).
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

False positive from testing code, resolved in 0870ffe

this._outerElement.addEventListener('scroll', this, passiveIfSupported);

this._scrollbarResizeObserver = new ResizeObserver(
this._adjustDimensionsForScrollbar.bind(this)

Check warning

Code scanning / CodeQL

Superfluous trailing arguments

Superfluous argument passed to [default constructor of class ResizeObserverMock](1).
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

False positive from testing code, resolved in 0870ffe

} else {
if (!this._areaResizeObserver) {
this._areaResizeObserver = new ResizeObserver(
this._onAreaResize.bind(this)

Check warning

Code scanning / CodeQL

Superfluous trailing arguments

Superfluous argument passed to [default constructor of class ResizeObserverMock](1).
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

False positive from testing code, resolved in 0870ffe

@krassowski krassowski marked this pull request as ready for review January 29, 2024 15:44
@krassowski krassowski changed the title Fix scrolling on execution Fix scrolling on execution and switching notebook mode Jan 29, 2024
@krassowski
Copy link
Member Author

Note for reviewer: failures of Visual Regression Tests are unrelated:

Copy link
Contributor

@gabalafou gabalafou left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not done reviewing, but I thought I would go ahead and leave the review comments I have so far.

I'm not sure I feel confident about my ability to review the changes to the windowed list classes.

galata/playwright.config.js Show resolved Hide resolved
* If the item is less than one viewport away, scroll so that it becomes fully visible (following the `auto` heuristics).
* If the item is more than one viewport away, scroll so that it is centered within the viewport (`center` if smaller than viewport, `top-center` otherwise).
* center - Align the middle of the item with the middle of the viewport (it only works well for items smaller than the viewport).
* top-center - Align the top of the item with the middle of the viewport (works well for items larger than the viewport).
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice

packages/notebook/src/actions.tsx Outdated Show resolved Hide resolved
// If a cell is outside of viewport and scrolling is needed,
// prefer to align the cell to the top viewport edge,
// rather than to the bottom (so that editor is visible)
void Private.handleRunState(notebook, state, true, 'center');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why 'center' and not 'top-center'? I thought the idea was to keep the cell that we just left in view, which we cannot guarantee if we scroll the center of the next cell into the center of the viewport.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great question! This is because it only applies to auto strategy and handleRunState uses the smart strategy, which will:

  • (a) not scroll if significantly visible, otherwise
  • (b) scroll using auto heuristics if less than one viewport away
    this point is important because if an item is less than one viewport and not significantly visible it implies that it fits in the viewport hence we can apply center rather than top-center.
  • (c) choose between top-center and center depending on item size.

So in (b) we can use center over top-center; but should we? For small cells it is nicer, and in a rare case of item size being equal to viewport we loose past context (future context would not be visible either way), so maybe we could choose between center and top-center depending on item size, but I did not want to make the logic any more complicated; in fact I was considering removing this fallback to auto heuristic here but was also cautious not to modify the logic too much in case if downstream already depends on smart falling back to auto in this circumstance. In either case, yes we could also use top-center here. What do you think?

This also highlights that the comment above the line you commented on is outdated because I had not updated it since the previous solution was in place. I will update it now.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment was updated in 5b02962

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can't help but wondering if the added code complexity outweighs the UX benefit. Seems to me that if we don't introduce the alignPreference parameter then the consequence is that in the scenario labeled (b), in which the cell fits within the viewport and is less than one viewport away, then the cell will be scrolled to in such a way that either its top edge will align with the top edge of the scrollable container or its bottom edge with the bottom edge. I'm probably missing something but I don't see how that's significantly worse than scrolling the cell's top edge (or center) to the center of the scroll-container.

Copy link
Contributor

@gabalafou gabalafou left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can see that a lot of thought, care, and hard work has gone into this PR, so I hesitate to ask this question, but I was wondering if it would make sense to split up this PR. I may be misunderstanding the code, but it seems that only some of the code changes in this PR strictly address fixing the regressions that caused the bug reported in issue #15693. The other code changes seem like enhancements. Would it make sense to separate the two?

Marking request changes for my question about the test code.

galata/test/jupyterlab/notebook-scroll.test.ts Outdated Show resolved Hide resolved
packages/notebook/src/actions.tsx Outdated Show resolved Hide resolved
galata/test/jupyterlab/utils.ts Outdated Show resolved Hide resolved
galata/test/jupyterlab/utils.ts Show resolved Hide resolved
galata/test/jupyterlab/utils.ts Outdated Show resolved Hide resolved
// If a cell is outside of viewport and scrolling is needed,
// prefer to align the cell to the top viewport edge,
// rather than to the bottom (so that editor is visible)
void Private.handleRunState(notebook, state, true, 'center');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can't help but wondering if the added code complexity outweighs the UX benefit. Seems to me that if we don't introduce the alignPreference parameter then the consequence is that in the scenario labeled (b), in which the cell fits within the viewport and is less than one viewport away, then the cell will be scrolled to in such a way that either its top edge will align with the top edge of the scrollable container or its bottom edge with the bottom edge. I'm probably missing something but I don't see how that's significantly worse than scrolling the cell's top edge (or center) to the center of the scroll-container.

@krassowski
Copy link
Member Author

krassowski commented Jan 31, 2024

I may be misunderstanding the code, but it seems that only some of the code changes in this PR strictly address fixing the regressions that caused the bug reported in issue #15693. The other code changes seem like enhancements. Would it make sense to separate the two?

No, this is incorrect. As soon as we change preventScrollOnFocus we need to perform manual scrolling because browser no longer handles it, hence we need to:

  • update the logic for scrolling implemented in windowed list module to align with user expectations (which has a nice side effect of making it consistent between windowed and non-windowed mode)
  • fix the regression of wrong node being scrolled (_outerElement) because otherwise the above does nor work in defer/none mode
  • if we stopped at this we would then have shift + enter in inconsistent state as a result of this PR hence the update to scrollback logic

I do not see any changes in this PR which are not directly addressing the erractic jumps reported in #15693 (and while demonstrated on one example - ctrl + enter, also appearing in scenario of shift + enter as described in the issue in detail in course of investigation; further, given the above resolution approach there is not even a way to fix ctrl + enter without fixing shift + enter).

Copy link
Contributor

@gabalafou gabalafou left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay thanks for the clarification.

Apart from some reservations I have about increasing the complexity of the code with the alignPreference parameter, this code looks good to me!

However, I still think there's something amiss with those test files. Could you diff them to make sure?

@@ -131,15 +124,53 @@ test.describe('Notebook scroll on execution', () => {
// The third cell should be positioned at the bottom, revealing between 10 to 20% of its content.
await expect(thirdCellLocator).toBeInViewport({ ratio: 0.1 });
await expect(thirdCellLocator).not.toBeInViewport({ ratio: 0.2 });
await expect(thirdCellLocator).not.toBeInViewport({ ratio: 0.2 });
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This line is duplicated

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great catch, thanks! It now looks good for me:

20c20
<       windowingMode: 'full'
---
>       windowingMode: 'none'
25c25
< test.describe('Notebook scroll on navigation (with windowing)', () => {
---
> test.describe('Notebook scroll on navigation (no windowing)', () => {
65c65
< test.describe('Notebook scroll on execution (with windowing)', () => {
---
> test.describe('Notebook scroll on execution (no windowing)', () => {
172c172
< test.describe('Notebook scroll over long outputs (with windowing)', () => {
---
> test.describe('Notebook scroll over long outputs (no windowing)', () => {

@krassowski
Copy link
Member Author

@gabalafou does it look good to you now?

Copy link
Contributor

@gabalafou gabalafou left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, I'm done reviewing and I approve with one reservation, which is that I'm still not sure whether the UX benefit of alignPreference is worth the added complexity (not sure if you saw my comment asking about that). But this is not blocking for me. I also left some comments in this last round of review, but these are also not blocking.

Great job unifying the windowing and no-windowing UX!

packages/ui-components/src/components/windowedlist.ts Outdated Show resolved Hide resolved
* Handle viewport area resize.
*/
private _onAreaResize(_entries: ResizeObserverEntry[]): void {
this._scrollBackToItemOnResize();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just checking my own understanding here.

Is this line more for code symmetry and to cover edge cases than to cover a commonly occurring scenario? I mean, it's generally unlikely that an area resize will happen within 100 ms of the app scrolling to some item, right?

Copy link
Member Author

@krassowski krassowski Feb 1, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, it is actually quite common when user executes a cell which produces output and advances to the next cell. See:

image

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, for some reason I thought the area resize observer was observing the outer (scrollable) element (for e.g. window resize). Thanks for the clarification.

packages/notebook/src/actions.tsx Outdated Show resolved Hide resolved
packages/notebook/src/actions.tsx Outdated Show resolved Hide resolved
@@ -610,7 +610,8 @@ export class NotebookToCFactory extends TableOfContentsFactory<NotebookPanel> {
console.debug('scrolling to heading: using fallback strategy');
await widget.content.scrollToItem(
widget.content.activeCellIndex,
this.scrollToTop ? 'start' : undefined
this.scrollToTop ? 'start' : undefined,
0
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, I never asked about this change (margin=0) and I'm not sure I understand why it's here

Copy link
Member Author

@krassowski krassowski Feb 1, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good question. First, this is the fallback strategy which is unlikely to be used. Before this PR it was broken in defer mode because of wrong node being used for scrolling further down the line. Currently both defer and full mode will work, invoking a logic which considers the margin. Now, the default visibility margin for scrolling on action is 25%. But when user uses ToC they often want to find the heading and if it was partially revealed and the clicked either way it means that they expect actions anyways (hence it should be zero).

}
}

window.ResizeObserver = ResizeObserverMock;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also checking my understanding here. If the ResizeObserver is basically just a no-op mock, that means that none of the tests are hitting code paths that would lead to _scrollBackToItemOnResize() being called, correct?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that means that none of the tests are hitting code paths

More precisely none of the unit tests get to call _scrollBackToItemOnResize(), but the integration tests do. In fact if one was to remove it the visual tests would fail.

krassowski and others added 2 commits February 1, 2024 09:17
@krassowski
Copy link
Member Author

Thank you for the review @gabalafou!

with one reservation, which is that I'm still not sure whether the UX benefit of alignPreference is worth the added complexity (not sure if you saw my comment asking about that).

I just want to acknowledge that I saw your comment and take responsibility of dealing with the added complexity arising from having this parameter if it ever becomes a problem (and I believe this is worth the - small - UX benefit).

@krassowski krassowski merged commit 5ba76a8 into jupyterlab:main Feb 1, 2024
78 of 79 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Erratic scroll jumps when performing actions in notebook
2 participants