quasarframework / quasar

Quasar Framework - Build high-performance VueJS user interfaces in record time
https://quasar.dev
MIT License
26.02k stars 3.54k forks source link

[QScrollArea] setScrollPercentage wrong offset after added element #17472

Closed KammererTob closed 2 months ago

KammererTob commented 3 months ago

What happened?

Using setScrollPercentage to scroll to the end of the scroll area after adding an element is not working correctly when using nextTick. If you use setTimeout(..., 1) it works as expected.

What did you expect to happen?

It should scroll to the end of the scroll area after adding an element and waiting for the next tick (i.e. after the DOM is updated).

Reproduction URL

https://stackblitz.com/edit/quasarframework-ecbkw4?file=src%2Fpages%2FIndexPage.vue

How to reproduce?

  1. Go to the reproduction
  2. Notice the difference between the two add buttons. One is using setTimeout (with 1ms delay), the other nextTick. The latter is always one element off.

Flavour

Quasar CLI with Vite (@quasar/cli | @quasar/app-vite)

Areas

Components (quasar)

Platforms/Browsers

Firefox

Quasar info output

Operating System - Windows_NT(10.0.19045) - win32/x64
NodeJs - 20.11.1

Global packages
  NPM - 10.2.4
  yarn - 1.22.19
  pnpm - 9.4.0
  bun - Not installed
  @quasar/cli - undefined
  @quasar/icongenie - Not installed
  cordova - Not installed

Important local packages
  quasar - 2.16.9 -- Build high-performance VueJS user interfaces (SPA, PWA, SSR, Mobile and Desktop) in record time
  @quasar/app-vite - 2.0.0-beta.20 -- Quasar Framework App CLI with Vite
  @quasar/extras - 1.16.12 -- Quasar Framework fonts, icons and animations
  eslint-plugin-quasar - Not installed
  vue - 3.4.38 -- The progressive JavaScript framework for building modern web UI.
  vue-router - 4.4.3
  pinia - Not installed
  vuex - Not installed
  vite - 5.4.2 -- Native-ESM powered web dev build tool
  vite-plugin-checker - Not installed
  eslint - 9.9.1 -- An AST-based pattern checker for JavaScript.
  esbuild - 0.23.1 -- An extremely fast JavaScript and CSS bundler and minifier.
  typescript - 5.5.4 -- TypeScript is a language for application scale JavaScript development
  workbox-build - Not installed
  register-service-worker - Not installed
  electron - Not installed
  @electron/packager - Not installed
  electron-builder - Not installed
  @capacitor/core - Not installed
  @capacitor/cli - Not installed
  @capacitor/android - Not installed
  @capacitor/ios - Not installed

Quasar App Extensions
  *None installed*

Relevant log output

No response

Additional context

No response

yusufkandemir commented 2 months ago

I've recently experienced the same behavior. In my case, the elements were added after calling an Apollo mutation and updating the cache, so wasn't sure where the problem was. But, this issue explains the situation pretty well, thanks.

In your reproduction, setTimeout(..., 1) works better but even that isn't working perfectly. Increasing the timeout makes it work better, e.g. to 200.

thexeos commented 2 months ago

nextTick is about reactivity and not layout/CSS (which is what the scroll sizes/offsets depend on). What you actually need is to wait for reflow. You can guarantee that reflow had happened if you wait for after the next frame is drawn (aka call requestAnimationFrame twice).

  1. Make reactive change
  2. Wait for reactive change to be flushes to DOM by Vue by waiting for nextTick
  3. Wait for browser to render the frame naturally (you could also trash the layout if you want) by waiting for requestAnimationFrame to trigger
  4. Now you can setup the next requestAnimationFrame callback that would guarantee the layout was recalculated for the previous frame rendering
  5. Finally when callback triggers, browser is ready to accept your changes based on the current state of DOM/layout. ResizeObserver that Quasar uses had definitely triggered and the internal state of Vue/Quasar matches what's in DOM and on the screen - you can call setScrollPosition or setScrollPercentage safely

Arguably Quasar could wait for requestAnimationFrame on your behalf, but Quasar is not aware you've made changes that would result in resize of Scroll Area content, so it's probably best that developers implement this logic in their own code.

function addElement() {
  elements.value.push(++i);
  nextTick(() => {
    // DOM was updated by Vue
    requestAnimationFrame(() => {
      // Browser is about to handle the reflow - the CSS/boundary boxes state is still prior to DOM insertion
      requestAnimationFrame(() => {
        // Browser had definitely recomputed everything and all had notified Quasar of the new scroll content dimensions
        scrollAreaRef.value.setScrollPercentage('horizontal', 1.0, 150);
      });
    });
  });
}
KammererTob commented 2 months ago

@thexeos Thanks for the explanation and your code example. If there is nothing to "improve" on the Quasar side (maybe apart from mentioning it in the documentation) this can probably be closed.