github quasarframework/quasar quasar-v2.30.0

3 hours ago

Important

Behavior changes worth checking before you upgrade:

  • QInfiniteScroll no longer watches the scroll position; it watches the end of its content. offset is now measured from the end of the content instead of the end of the scroll target (identical whenever the component is the last thing in its scroller), nothing loads once you scrolled past the component, debounce counts from the moment the end comes within the offset rather than from the last scroll event, the mount-time load lands one frame later instead of synchronously, and a scroll-target outside the component's ancestry no longer clips it.
  • v-scroll-fire fires as soon as any part of the element is visible, where it used to wait for the element's bottom edge to enter the container (pass the new :1 argument for the old timing; elements taller than the viewport never reach it). It also fires when the element becomes visible without a scroll, and the 25ms debounce is gone.
  • v-intersection dispatches every entry of a callback batch (previously only the first per callback, which could swallow a fast in-then-out), no longer passes the observer as an undocumented second argument, and ignores entries queued by an element's former observer.
  • Every QFab animates its Fab Actions individually rather than as a single block, cascading out of the trigger.
  • extractDate() returns an Invalid Date for a week mask that cannot resolve (week 0, week 54, week 53 of a 52-week year, or no year at all) where it used to return a date in 1900; masks containing dd now match instead of never matching.
  • v-touch-hold ignores a press while another one is tracked, so a two-finger hold fires the handler once instead of twice.
  • QTooltip's cursor-position and QMenu's touch-position/context-menu re-decide their placement from the intended side after a resize or an updatePosition(), instead of building on the correction of the first paint, and a point-mode offset now carries the sign of the direction the popup grows.
  • A disabled QEditor dims its toolbars and content instead of its whole root, so its border and background are no longer dimmed and its plain toolbar buttons now dim like its dropdowns already did.

New

  • feat(ui): useIntersection() composable -> observe any element's visibility from setup() with a plain object, a ref or a getter (target, root, rootMargin, threshold, once, disabled, onIntersect), getting back a reactive isIntersecting and a stop(). The observer pool behind it is now shared by the composable, v-intersection and QIntersection (rebuilt on top of the composable), so a page mixing the three with the same options runs a single IntersectionObserver
  • feat(QPullToRefresh): side prop, pull from any edge of the content to refresh -> the pull can start from the top (the default, unchanged), bottom, left or right of the content, as an inward pan from that edge while the scroll target sits at it. bottom is the messenger case, left/right serve horizontally scrolling content (#7599)
  • feat(QTooltip): cursor-position prop, position the tooltip at the pointer -> on a large trigger a tooltip placed against the anchor's box ends up far from what the user is pointing at, which is why the native title attribute opens at the cursor. The pointer becomes the anchor point (anchor no longer applies, self and offset keep working), after waiting for the pointer to settle so a tooltip opening mid-sweep can't freeze at a point already left behind; a touch or stylus press opens at the contact point without waiting, and a keyboard focus or a bare model toggle keeps the regular anchor-relative placement (#6019)
  • feat(QTabs): let the tab strip scroll natively along its axis -> the strip was overflow: hidden on desktop, so the only way to reach offscreen tabs was the arrow icons and a horizontal trackpad swipe or a wheel tilt did nothing. It now scrolls on its own axis on every platform, the cross axis staying hidden, the scrollbar staying invisible and the arrows still tracking the position. Vertical wheel over horizontal tabs is deliberately not remapped, so a tab bar in a page never captures the page scroll (#7404)
  • feat(QFab): animate Fab Actions individually via the new stagger prop -> each action fades and scales on its own, cascading outwards from the trigger when opening and back towards it when closing, which is the Material speed dial behavior. stagger sets the milliseconds between consecutive actions (default 40; 0 animates them all at once); the delays are derived in CSS with sibling-index(), so no number of actions is special-cased and browsers predating those functions simply animate the actions together (#6413)
  • feat(QPagination): ellipsis slot to replace the "..." buttons -> the ellipsis buttons were hard-wired to jump to the first hidden page on their side and there was no slot to attach anything else. The slot replaces the button entirely and hands over { side, page, btnProps, onClick } (plus to when to-fn is set); btnProps replicates the default button but carries neither the click handler nor the router target, so spreading it onto a QBtn hosting a QPopupEdit does not also trigger the jump. Without the slot the rendered output is unchanged (#11341)
  • feat(QEditor): open toolbar dropdowns on hover -> the new dropdown-hover, dropdown-hover-delay and dropdown-hover-hide-delay props are forwarded to the QBtnDropdown of every toolbar dropdown. Click, tap and keyboard interactions keep toggling as before, and touch devices fall back to them
  • feat(date): extractDate() parses ISO week dates -> the week (w/wo/ww), ISO week year (GG/GGGG) and day of week (d/do/E/ddd/dddd) tokens were emitted by the parser but never mapped, so a GGGG-[W]ww-E mask resolved to Jan 1st 1900 instead of the day it described and twelve formatting tokens could not survive a formatDate() round trip. A mask carrying a week number now resolves as an ISO week date, an explicit calendar day still winning. See the note at the top (#7732)
  • feat(ScrollFire): watch through the shared IntersectionObserver, new threshold argument -> the directive registered a debounced scroll listener per element on the scroll target and only ever checked at mount and on scroll, so an element that became visible through a layout change, a resize or an already scrolled container never fired. It now subscribes to the pooled observer shared with v-intersection, QIntersection and useIntersection, clipping through every scrollable ancestor up to the viewport, and takes the fraction of the element that must be visible as an argument (v-scroll-fire:0.5, default 0). See the note at the top
  • feat(TouchHold/TouchPan/TouchRepeat/TouchSwipe): the argument and every modifier follow runtime changes -> the four docs pages carried a "Note on HMR" since 2019 saying not all modifiers were reactive. Now every directive re-derives its listener options and settings whenever the modifiers object changes, swapping only the listeners whose options changed, re-parsing the argument when its string differs and always creating its context, so a directive that starts inert (no .mouse on a device without touch) can be armed later. The notes are gone from the four pages
  • feat(ui): the scroll-target prop accepts a Vue component reference, standing for its root element -> QInfiniteScroll, QScrollObserver, QVirtualScroll, QParallax, QPullToRefresh, QTree and QTable share the prop, and the resolver behind it has always unwrapped a component instance to its root element, but the prop type rejected it: in production it worked, in development Vue's prop validation refused it and building the warning threw ("Cannot convert object to primitive value"), aborting the update. A value without a root element now falls back to auto detection with a warning naming the cause
  • feat(ui/app-vite): the quasar.config spinner of Loading and Notify is typed by name -> framework.config.loading.spinner and framework.config.notify.spinner take a spinner's name ('QSpinnerGears'), which the CLI imports for you, but the typing dropped both keys altogether, so naming a spinner there was a type error. quasar now also exports the QuasarSpinners union of the spinner component names

Fixes

  • fix(ui): intersection observing works in cross-origin iframes (Codepen and the like) -> Chromium and WebKit withhold rootBounds from a cross-origin iframe for every entry of the viewport root, and a 2020 guard read that null as the element having been caught out of the DOM by a Vue transition, re-observing forever. So inside Codepen, jsFiddle and StackBlitz previews no v-intersection or QIntersection handler ever ran, since 2020 (the docs advised the root prop as a workaround and Codepen was disabled on the examples). Firefox was unaffected. The guard is dropped, the docs' workaround paragraph with it, and the Intersection examples get their Codepen link back
  • fix(QMenu): close on a press whose compatibility mousedown got suppressed -> the outside-click handler listened for mousedown and touchstart only, so a page cancelling pointerdown (Cesium does it on every mouse press with a registered action) suppressed the compatibility mousedown and the popup stayed open, in every engine. QSelect's dropdown got stuck along with it. A mouse or pen pointerdown now arms a 0ms fallback that the mousedown of a normal press cancels in the same task, so every existing path is unchanged (#12575)
  • fix(QMenu/QTooltip): a growing popup grows away from its anchor -> the JS fallback engine expressed every placement as a pixel top/left, so a popup with a bottom or right self origin kept its top edge frozen: content growing after the decision extended it over its own anchor until the next pass re-derived the position and it jumped. The engine now writes its inset from the edge the self origin anchors, like the native one
  • fix(QMenu/QTooltip): keep a flipped popup capped so growth can't escape the viewport, without losing the bound it already has -> a flipped popup is now capped at its side's space whether or not it fits there, and that cap is the smaller of the space and the popup's own computed max size, which resolves the max-height/max-width props, the stylesheet ceilings ($menu-max-height, ...) and any app CSS at once. Before the amendment a flip towards a roomier side could widen the popup past those bounds (#18536, #18537)
  • fix(position-engine): correct point-mode self origins and offsets -> a popup positioned from a coordinate got two things wrong. An axis whose self origin is center/middle had no viewport correction at all, and now shifts back inside the viewport, frozen at decision time. The offset was applied with a fixed positive sign whatever the self origin, so a popup growing up or left opened onto the coordinate instead of clearing it; it now carries the sign of the direction the popup grows. Visible today for a QMenu opened at a coordinate. See the note at the top
  • fix(QEditor): honor disable/readonly in the toolbar, keep the fullscreen root opaque -> getBtn() overwrote the editor-wide disabled state with the per-button definition, so every plain toolbar button of a disabled or readonly editor stayed actionable (only the dropdowns behaved), which is how a disabled fullscreen editor's fullscreen button could be clicked. readonly keeps the read-only commands (fullscreen, print, viewsource), disable takes the whole toolbar out of play. The global .disabled rule also sat on the root, which in fullscreen is a viewport-sized fixed element, so the page composited straight through it. See the note at the top (#15486)
  • fix(QKnob/QTabPanels/QStepper/QCarousel/QDrawer/QSplitter): don't remount the content when a gesture prop is toggled -> all of them baked a prop-derived condition into the vnode key, so flipping it destroyed and rebuilt the whole subtree: QKnob re-created its QCircularProgress and default slot on a disable/readonly toggle, QTabPanels/QStepper/QCarousel destroyed every panel (KeepAlive state included) on a swipeable toggle, QDrawer re-created its content on a no-swipe-close toggle and QSplitter its separator slot on disable. The directive now stays attached and is disarmed in place. QColor's spectrum, QSlider/QRange's track and QDrawer's backdrop got the same treatment internally (#12668, #18535)
  • fix(TouchPan): deliver the final payload when the value is unset mid-pan -> the mouse variant defers the final handler call by 50ms and read the handler at call time, so a value that turned undefined during an active pan threw and the isFinal payload never arrived: QSplitter kept its --active class and dropped the update:modelValue, QPullToRefresh stayed in its pulling state, QDrawer's close pan lost its end on a breakpoint crossing
  • fix(v-intersection/v-mutation): disable in place on a non-function value -> v-intersection accepted Boolean false only and threw on undefined, and v-mutation tolerated a non-function value only by accident, keeping the observer connected and rebuilding it on every handler change (an inline arrow function on each render). Both now disarm cleanly, v-mutation dropping its observer and only swapping the callback target on a handler change (#18531)
  • fix(QFabAction): restore the fallback state when used outside a QFab -> the inject() fallback factory was never flagged as one, so the function itself landed in $fab and a QFabAction rendered outside a QFab crashed instead of showing itself as an enabled, standalone button (#18538)
  • fix(Morph): an object value no longer resets the directive modifiers it does not name -> the modifier pass also ran on the object form, so v-morph.tween="{ name, group, model }" lost the tween. The directive modifiers are the base now and the object overrides only the keys it defines
  • fix(Ripple): a keyboard ripple within 300ms of re-enabling is no longer swallowed -> the throttle window opened on any key press, even while the element was disabled; it now only advances when a ripple actually fires
  • fix(utils/is): deepEqual() compares binary data by content -> ArrayBuffers, DataViews and typed arrays were compared through the generic object path, and the typed-array branch was duck-typed on a .buffer property, so a plain { buffer: ArrayBuffer } (and DataView) spun forever. Binary views are recognised through ArrayBuffer.isView(), which also covers views over a SharedArrayBuffer, and both sides' byteLength is compared before anything is allocated (#18539)
  • fix(ui/api): a Vue reference in an event or method payload is a ComponentInstance, not a Component -> QFile, QForm, QPopupProxy, QScrollArea, QSelect, QTable, QTree, QUploader, QVirtualScroll and useVirtualScroll hand over mounted component instances that the generated typings declared with Vue's definition type. Component stays where a definition really is passed: the plugins' spinner options and Dialog's component

Other

  • perf(ui): a sweep over the directives and the scroll-driven components -> v-ripple, v-scroll, v-close-popup, v-intersection, v-morph and the four touch directives now share module-level listeners instead of allocating a set of closures per element, v-intersection shares one IntersectionObserver per distinct config (per-scroll main-thread time on 3000 observed elements 4.1ms -> 2.5ms) and v-scroll one listener per target (2000 elements on window: mount 8.9ms -> 2.0ms, unmount 8.3ms -> 0.8ms, one scroll event 1.74ms -> 0.08ms). QInfiniteScroll and QPullToRefresh watch the content through the intersection pool rather than a scroll listener and render their indicator in a sub-component, so a load or a pull no longer re-renders the slot content, and QPullToRefresh arms TouchPan only while the pulled edge is on screen, keeping JS out of the touch path of every other scroll gesture. Every template ref in the ui package is a shallowRef()
  • refactor(QMenu/QTooltip): share the positioning lifecycle through a single internal composable -> the two components each carried their own copy of both positioning engines and the show-time glue, which had drifted apart. Two side effects on QTooltip: the scroll offset of content that fits is restored after a boundary pass like QMenu's, and the JS fallback re-tracks the anchor after a useFullscreen() relocation (#18513)

Donations

Quasar Framework is an open-source MIT-licensed project made possible due to the generous contributions by sponsors and backers. If Quasar is useful in your workflow and you want to support ongoing maintenance, please consider the following:

Don't miss a new quasar release

NewReleases is sending notifications on new releases.