github quasarframework/quasar quasar-v2.31.0

3 hours ago

Important

Behavior changes worth checking before you upgrade:

  • A readonly QInput/QSelect/QFile/QField now reflects focus: it shows the focused styling (highlight, floated label, hide-hint hint), emits focus/blur and validates on blur with lazy-rules. Only disable now exempts a field from counting as focused, matching the existing rule that only disable exempts it from validation.
  • config.dark: 'auto' resolves again on SSR/SSG apps, on the client once it takes over. It has silently been a plain light false since v2.18.2. The first paint stays light (the server cannot know the preference), so the documented cookie approach remains the way to a flash-free page.
  • The fa and fa-IR language packs render Persian digits in QDate, QTime and QPagination, through the new root-level formatNumber pack key.
  • QStepper's horizontal DOM lost its .q-panel.scroll wrapper element; its classes moved onto .q-stepper__step-content, now a direct descendant of .q-stepper__step.
  • The q-transition--field-message-* classes no longer exist. Restyle a QField message swap through .q-field__messages--animated instead; the leave half of the animation (the old message fading out under the new one) is gone.

New

  • feat(QColor): palette slot, and the picker is fully keyboard-accessible -> the palette view had no way to be customized: swatches were plain divs, laid out 10 per row, reachable by pointer only. The new scoped palette slot replaces the default swatches while keeping the other views and the view switcher, handing over the resolved colors (palette), a select(color) function and an editable flag. The default swatches become buttons following the roving tabindex pattern (arrows move, Up/Down by one visual row, Home/End jump to the edges, Enter/Space picks), and the spectrum panel, the last pointer-only part of the picker, becomes a WAI-ARIA slider over its two axes: arrows step the saturation and the brightness by one, ten with Shift, Home/End and PageUp/PageDown jump. Both follow the visual direction in RTL, both draw a two-tone inset focus ring that stays visible on any color, and the spectrum spells out its value through two new lang-pack keys, colorPicker.saturation and colorPicker.brightness, translated across every pack (#8613)
  • feat(ui): shadow colors are CSS custom properties -> since v2.11 the elevation shadows are drawn with a light color while in Dark Mode, but the color was baked into the built CSS through the $shadow-color/$dark-shadow-color Sass variables, so the UMD build, the Vite plugin and every other prebuilt-CSS flavour had no way to revert or tune it short of restyling each component. Both colors are now declared on :root as --q-shadow-color and --q-dark-shadow-color, with the Sass variables as their defaults so existing Sass overrides keep working. Nothing moves by a pixel out of the box, and the pre-v2.11 look is a one-liner (body.body--dark { --q-dark-shadow-color: #000 }); transparent removes the shadows altogether. The tints are derived on body, so the tokens are read at the :root/body level (a body.body--dark rule, setCssVar()), not on a deeper element (#15144)
  • feat(Lang): formatNumber pack key, localized digits in QDate, QTime and QPagination -> a Persian calendar coupled with the fa-IR pack showed Persian month names next to Western day numbers, with no way to change that short of a font that remaps the ASCII digit glyphs. Language packs (and the locale prop) can now define an optional root-level formatNumber(value), which receives the ASCII digit string the component would display, zero-padded where it is padded ("5", "05", "1403"), and returns the localized rendering. QDate routes its day cells, navigation year, years grid, header title and subtitle, the "N days" count and the day buttons' aria-label through it, QTime its header units and clock face, QPagination its page buttons and their aria-label. Model strings, date hashes and QTime's stringModel stay ASCII. The fa and fa-IR packs opt in; every other pack is unchanged (#12735)
  • feat(QTable): autoWidth in the column definition -> shrinking a column to its content required a per-column header-cell and body-cell slot override with <q-th auto-width>/<q-td auto-width>, cumbersome for dynamically built columns, the only alternative being an undocumented class through classes and headerClasses. A column can now simply declare autoWidth: true, which applies to the auto-rendered cells as well as to QTh/QTd fed with the slot props (#8206)

Fixes

  • fix(QMenu/QDialog): the portal opening over a closing one takes the focus -> a QMenu item or a QDialog button carrying v-close-popup that opens a dialog made the two portals compete for focus, and the closing one won on both counts: the dialog opened without focus in its autofocus field (on Android the soft keyboard never showed), and it had captured, as the element to return focus to, an element inside the closing portal, destroyed with it, so closing the dialog dropped focus altogether. A portal closing while another one sits on top of it in its opening transition now hands its own anchor to the opener instead of restoring focus itself; portals that take no focus (no-focus, hover-shown menus) or that are already fully open decline the hand-off, so everything else is unchanged (#8211)
  • fix(QField): a readonly field reflects focus like a native readonly control -> the focused state was gated on the field being editable, so a focus event landing while readonly was still true got dropped and nothing re-synced afterwards: a handler flipping readonly off and calling focus() in the same tick left the input holding DOM focus with the field unfocused, label resting over the caret. The gate itself was the problem, not its timing, since a readonly control stays in the tab order and a keyboard user tabbing onto it got no visible focus indication at all. The focused state now mirrors DOM focus, only disable keeping a field from counting as focused. See the note at the top (#16056)
  • fix(QField): forward focus from the control slot wrapper to the control -> with a control slot, autofocus, focus() and every other way of focusing the field (QForm/QDialog/QMenu autofocus, clearValue, a click around the control) landed on the q-field__native wrapper: the field looked focused and emitted focus, but the slotted control never received the caret. The wrapper now forwards any focus it receives to the element bound to the slot scope id, else to the first focusable element of the slot, and keeps it only when the slot holds nothing focusable (#12648)
  • fix(QField): no more one compositor layer per field on Chromium -> every QField label carried backface-visibility: hidden, which Chromium answers with a permanent compositor layer per label. With a few hundred fields on a page each hover repaint re-runs the layer assignment pass against all of those interleaved layers and it goes quadratic: on the reported layout (100 rows x 5 labelled number inputs) headless Chromium held ~22 fps with 50-67 ms frames while native inputs held 60. The rule dates from the Qv1 stylesheets with no recorded purpose and the settled label renders pixel-identical without it. Measured after: 6 composited layers instead of 461, ~65 ms of Layerize per hover sweep instead of ~1.7 s (#16024)
  • fix(QSelect): typed text wins over the model's automatic highlight on Enter -> opening the list (and every filter completion) highlights the option mirroring the current value. With new-value-mode/@new-value, Enter or Tab then re-selected that option and silently dropped the text the user had just typed, @new-value never being emitted. The highlight now remembers whether it was placed automatically or by the user, and only the automatic one yields to typed text under new-value mode: navigated or hovered options still win, fill-input's filled-in label is not user text so Enter right after opening still re-selects, and selects without new-value mode never consult it (#16514)
  • fix(QStepper): the steps stay mounted across an orientation switch -> a stepper bound to $q.screen (:vertical="$q.screen.lt.sm") lost the active step's content, form state included, whenever the viewport crossed the breakpoint, and keep-alive could not bridge it either, the two orientations rendering different trees with two different caches. Both orientations now render every QStep under .q-stepper__content, with one plain Transition per step driving the vertical height slide and the horizontal slide alike. keep-alive-max, which the vertical mode never honoured, is now counted by the stepper itself, and the stepper's transition-duration also drives the vertical slides (default unchanged). See the note at the top (#9703)
  • fix(QStepper): a nested stepper keeps its own orientation -> the horizontal and vertical layout rules hung off the stepper root with descendant selectors, so a stepper nested inside a step of the other orientation matched both rule sets and source order decided: a vertical stepper inside a horizontal one drew a horizontal connector after its active label, lost its vertical connectors and got clipped by the tab overflow; a horizontal one inside a vertical stepper lost the connector before each dot and received the vertical content padding. Every orientation rule now hangs off a per-element modifier class instead of the root; no selector gained a class and several lost one, so existing overrides keep their targets and their precedence (#16516)
  • fix(QStepper): show the prefix on an active erroring step with active-icon="none" -> the header decided the dot's content with two computeds that disagreed on which state wins, so an active step that also had error, with active-icon set to none, got neither the icon nor the prefix and the dot was left empty; a step-level active/error/done-icon of none hit the same blank. The state icon is now resolved once, with the step prop taking precedence over the stepper one, and both the prefix decision and the rendered icon derive from it (#15259)
  • fix(QDrawer): mini-mode items stay put when the standard layout takes over -> in mini mode the item's side section was flex-centered inside the drawer's content box, which only agrees with the standard layout's 16px anchor when that box is exactly 56px wide. An unbordered drawer gives 57px, and on a display whose scale factor is not a whole number Chromium and WebKit snap the 1px border to a fraction of a pixel, so the centered icon landed on a fractional position and visibly jumped once the mini animation restored the standard layout (Mac Retina and 1x displays are exact, which is why it could not be reproduced there). The drawer now hands its current width to CSS as the --q-drawer-width custom property, documented as usable by user CSS, and the mini rule sizes the item from it, so 24px icons land on the standard inset at any scale factor, bordered or not (#18320)
  • fix(QDrawer): iOS Safari's edge navigation swipe stays out of the drawer gestures -> on iOS Safari, swiping in from the screen edge to open a drawer also ran the browser's back/forward navigation gesture: the page slid towards the previous history entry while the drawer followed the finger. The same happened when the backdrop of a right-side drawer was swiped from the left edge to close it. The opener strip and the backdrop now cancel the touch sequence once the pan direction is detected, which is what leaves the page in Safari's hands, while a vertical move (and so the page scroll) is left alone
  • fix(prevent-scroll): the page background keeps painting under the iOS scroll lock -> the iOS lock pins the body with position: fixed, which collapses the root element to zero height, and a body background propagates to the canvas with the root box as its positioning area: a gradient or a cover-sized image stopped painting the moment a popup locked the page, showing a blank canvas behind the dialog. A solid color needs no positioning area, which is why the default dark page never showed it, and desktop and Android use the viewport clip lock, so only iOS was affected. The lock class now gives the root element min-height: 100%, min-height so user html { height } rules still win (#16799)
  • fix(QResizeObserver): track padding and border changes of the observed element -> the component emits offsetWidth/offsetHeight, the border box, but observed the default content box, so a padding or border change on the observed element itself grew the border box without touching the content box and no notification fired. QHeader, QFooter and QDrawer size the layout through it, so a padding-top: env(safe-area-inset-top) rule placed on .q-header (the iOS PWA status-bar pattern) left QPageContainer with a page padding that ignored the inset until the device rotated. Padding on the toolbar child, which is what Quasar's own q-ios-padding/q-safe-area-padding rules use, was already tracked (#7526)
  • fix(QPage): the children's vertical margins stay inside the page -> QPage was a plain block with no padding or border, so a first child's top margin (a q-mt-md card, say) collapsed through it and pushed the page down inside QPageContainer; together with the page's min-height the layout then exceeded the viewport and the window grew a scrollbar even when the content fit the screen. A last child's bottom margin did the same at the other end. The page now establishes a block formatting context, through a zero-specificity rule so display utilities on the page and user overrides keep winning (#9536)
  • fix(Dark): 'auto' resolves on SSR/SSG clients once they take over -> since v2.18.2 the ssr-client build reads its initial dark state from the server-emitted body class, and the server renders 'auto' as light, so config.dark: 'auto' ended up as a plain false on every SSR/SSG app: no media query listener, light forever. The server now flags an 'auto' render with a body attribute, and the client resolves it on takeover unless the app already set an explicit value meanwhile. A SSR+PWA shell boot, which has no server markup to read, starts from the config like an SPA does; the config was silently dropped there too. See the note at the top (#7530)
  • fix(debounce): guard reentrant immediate calls -> a debounced function created with the immediate option could re-enter itself from within its own invocation and fire again instead of being held back for the wait period (#18544)
  • fix(morph): give up on a destination detached before the first frame -> morph() defers its work to a requestAnimationFrame callback, and when the destination's subtree was removed in between (a v-if or an unmount in the same tick as the model change) the validity check still accepted it, because a detached subtree keeps its parentNode, and the fixed-position parent walk then reached null and threw inside getComputedStyle

Other

  • perf(QField): message swaps animate with a CSS keyframe instead of a Transition -> every field with a hint, rules, a counter or an explicit error prop rendered a Transition component around its messages element, only to animate the hint-to-error swap. With Vue 3.5 that instance is about a quarter of a QInput's mount JS (2000 labelled inputs with hints: 148 ms to 113 ms on Chromium), and it was the single biggest item in the reporter's own devtools timeline. The messages element is keyed by its content, so a swap already re-creates it, and a keyframe on the new element reproduces the enter half of the old transition; nothing animates on page load and no field gets a composited layer at mount. See the note at the top (#12369)
  • perf(ui): 2D transforms are written as 2D instead of the 3D variants -> Chromium gives any 3D transform function its own compositor layer, permanently, and with hundreds of instances on a page every repaint re-runs the layer assignment against all of them. The production stylesheet never shipped these (the minifier flattens them), but quasar dev compiles the Sass verbatim, so dev servers paid for a layer per radio, checkbox, toggle, slider ring, linear progress, date indicator, tree arrow, spinner and notification badge; the JS-built strings (QLinearProgress, QCircularProgress and through it QKnob, QAjaxBar, Ripple, QTabs' indicator) reached production as written, so those are a production win too. Every value is rewritten as its exact 2D equivalent and the minified quasar.prod.css/quasar.rtl.prod.css are byte-identical before and after. QLayout's layer hack and QParallax's media transform stay 3D on purpose

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.