Important
Behavior changes worth checking before you upgrade:
- QInfiniteScroll no longer watches the scroll position; it watches the end of its content.
offsetis 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,debouncecounts 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 ascroll-targetoutside the component's ancestry no longer clips it. v-scroll-firefires 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:1argument 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-intersectiondispatches 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 containingddnow match instead of never matching.v-touch-holdignores a press while another one is tracked, so a two-finger hold fires the handler once instead of twice.- QTooltip's
cursor-positionand QMenu'stouch-position/context-menure-decide their placement from the intended side after a resize or anupdatePosition(), instead of building on the correction of the first paint, and a point-modeoffsetnow 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 fromsetup()with a plain object, a ref or a getter (target,root,rootMargin,threshold,once,disabled,onIntersect), getting back a reactiveisIntersectingand astop(). The observer pool behind it is now shared by the composable,v-intersectionand QIntersection (rebuilt on top of the composable), so a page mixing the three with the same options runs a single IntersectionObserver - feat(QPullToRefresh):
sideprop, pull from any edge of the content to refresh -> the pull can start from thetop(the default, unchanged),bottom,leftorrightof the content, as an inward pan from that edge while the scroll target sits at it.bottomis the messenger case,left/rightserve horizontally scrolling content (#7599) - feat(QTooltip):
cursor-positionprop, 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 nativetitleattribute opens at the cursor. The pointer becomes the anchor point (anchorno longer applies,selfandoffsetkeep 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: hiddenon 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
staggerprop -> 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.staggersets the milliseconds between consecutive actions (default 40; 0 animates them all at once); the delays are derived in CSS withsibling-index(), so no number of actions is special-cased and browsers predating those functions simply animate the actions together (#6413) - feat(QPagination):
ellipsisslot 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 }(plustowhento-fnis set);btnPropsreplicates 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-delayanddropdown-hover-hide-delayprops 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 aGGGG-[W]ww-Emask resolved to Jan 1st 1900 instead of the day it described and twelve formatting tokens could not survive aformatDate()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 anduseIntersection, 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
.mouseon a device without touch) can be armed later. The notes are gone from the four pages - feat(ui): the
scroll-targetprop 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.configspinner of Loading and Notify is typed by name ->framework.config.loading.spinnerandframework.config.notify.spinnertake 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.quasarnow also exports theQuasarSpinnersunion of the spinner component names
Fixes
- fix(ui): intersection observing works in cross-origin iframes (Codepen and the like) -> Chromium and WebKit withhold
rootBoundsfrom 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 nov-intersectionor QIntersection handler ever ran, since 2020 (the docs advised therootprop 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
mousedownandtouchstartonly, so a page cancellingpointerdown(Cesium does it on every mouse press with a registered action) suppressed the compatibilitymousedownand the popup stayed open, in every engine. QSelect's dropdown got stuck along with it. A mouse or penpointerdownnow arms a 0ms fallback that themousedownof 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-widthprops, 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/middlehad 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/readonlyin 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.readonlykeeps the read-only commands (fullscreen, print, viewsource),disabletakes the whole toolbar out of play. The global.disabledrule 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/readonlytoggle, QTabPanels/QStepper/QCarousel destroyed every panel (KeepAlive state included) on aswipeabletoggle, QDrawer re-created its content on ano-swipe-closetoggle and QSplitter its separator slot ondisable. 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
isFinalpayload never arrived: QSplitter kept its--activeclass and dropped theupdate: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-intersectionaccepted Booleanfalseonly and threw onundefined, andv-mutationtolerated 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-mutationdropping 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$faband 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.bufferproperty, so a plain{ buffer: ArrayBuffer }(and DataView) spun forever. Binary views are recognised throughArrayBuffer.isView(), which also covers views over a SharedArrayBuffer, and both sides'byteLengthis compared before anything is allocated (#18539) - fix(ui/api): a Vue reference in an event or method payload is a
ComponentInstance, not aComponent-> QFile, QForm, QPopupProxy, QScrollArea, QSelect, QTable, QTree, QUploader, QVirtualScroll anduseVirtualScrollhand over mounted component instances that the generated typings declared with Vue's definition type.Componentstays where a definition really is passed: the plugins'spinneroptions and Dialog'scomponent
Other
- perf(ui): a sweep over the directives and the scroll-driven components ->
v-ripple,v-scroll,v-close-popup,v-intersection,v-morphand the four touch directives now share module-level listeners instead of allocating a set of closures per element,v-intersectionshares one IntersectionObserver per distinct config (per-scroll main-thread time on 3000 observed elements 4.1ms -> 2.5ms) andv-scrollone 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 ashallowRef() - 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: